Compare commits

..

No commits in common. "forgejo" and "v12.0.0-dev" have entirely different histories.

3320 changed files with 42918 additions and 86484 deletions

View file

@ -1,7 +1,7 @@
forgejo.org/cmd
code.gitea.io/gitea/cmd
NoMainListener
forgejo.org/cmd/forgejo
code.gitea.io/gitea/cmd/forgejo
ContextSetNoInit
ContextSetNoExit
ContextSetStderr
@ -9,95 +9,94 @@ forgejo.org/cmd/forgejo
ContextSetStdout
ContextSetStdin
forgejo.org/models
code.gitea.io/gitea/models
IsErrSHANotFound
IsErrMergeDivergingFastForwardOnly
forgejo.org/models/auth
code.gitea.io/gitea/models/auth
WebAuthnCredentials
forgejo.org/models/db
code.gitea.io/gitea/models/db
TruncateBeans
InTransaction
DumpTables
GetTableNames
forgejo.org/models/dbfs
code.gitea.io/gitea/models/dbfs
file.renameTo
Create
Rename
forgejo.org/models/forgejo/semver
code.gitea.io/gitea/models/forgefed
GetFederationHost
code.gitea.io/gitea/models/forgejo/semver
GetVersion
SetVersionString
SetVersion
forgejo.org/models/git
code.gitea.io/gitea/models/git
RemoveDeletedBranchByID
forgejo.org/models/issues
code.gitea.io/gitea/models/issues
IsErrUnknownDependencyType
IsErrIssueWasClosed
forgejo.org/models/organization
code.gitea.io/gitea/models/organization
SearchMembersOptions.ToConds
forgejo.org/models/perm/access
code.gitea.io/gitea/models/perm/access
GetRepoWriters
forgejo.org/models/repo
code.gitea.io/gitea/models/repo
WatchRepoMode
forgejo.org/models/user
code.gitea.io/gitea/models/user
IsErrExternalLoginUserAlreadyExist
IsErrExternalLoginUserNotExist
NewFederatedUser
IsErrUserSettingIsNotExist
GetUserAllSettings
DeleteUserSetting
GetFederatedUser
forgejo.org/modules/activitypub
code.gitea.io/gitea/modules/activitypub
NewContext
Context.APClientFactory
forgejo.org/modules/assetfs
code.gitea.io/gitea/modules/assetfs
Bindata
forgejo.org/modules/auth/password/hash
code.gitea.io/gitea/modules/auth/password/hash
DummyHasher.HashWithSaltBytes
NewDummyHasher
forgejo.org/modules/auth/password/pwn
code.gitea.io/gitea/modules/auth/password/pwn
WithHTTP
forgejo.org/modules/base
code.gitea.io/gitea/modules/base
SetupGiteaRoot
forgejo.org/modules/cache
code.gitea.io/gitea/modules/cache
GetInt
WithNoCacheContext
RemoveContextData
forgejo.org/modules/emoji
code.gitea.io/gitea/modules/emoji
ReplaceCodes
forgejo.org/modules/eventsource
code.gitea.io/gitea/modules/eventsource
Event.String
forgejo.org/modules/forgefed
NewForgeFollow
code.gitea.io/gitea/modules/forgefed
NewForgeUndoLike
ForgeUndoLike.UnmarshalJSON
ForgeUndoLike.Validate
NewPersonIDFromModel
GetItemByType
JSONUnmarshalerFn
NotEmpty
ToRepository
OnRepository
forgejo.org/modules/git
code.gitea.io/gitea/modules/git
AllowLFSFiltersArgs
AddChanges
AddChangesWithArgs
@ -107,55 +106,55 @@ forgejo.org/modules/git
openRepositoryWithDefaultContext
ToEntryMode
forgejo.org/modules/gitrepo
code.gitea.io/gitea/modules/gitrepo
GetBranchCommitID
GetWikiDefaultBranch
forgejo.org/modules/graceful
code.gitea.io/gitea/modules/graceful
Manager.TerminateContext
Manager.Err
Manager.Value
Manager.Deadline
forgejo.org/modules/hcaptcha
code.gitea.io/gitea/modules/hcaptcha
WithHTTP
forgejo.org/modules/hostmatcher
code.gitea.io/gitea/modules/hostmatcher
HostMatchList.AppendPattern
forgejo.org/modules/json
code.gitea.io/gitea/modules/json
StdJSON.Marshal
StdJSON.Unmarshal
StdJSON.NewEncoder
StdJSON.NewDecoder
StdJSON.Indent
forgejo.org/modules/log
code.gitea.io/gitea/modules/log
NewEventWriterBuffer
forgejo.org/modules/markup
code.gitea.io/gitea/modules/markup
GetRendererByType
RenderString
IsMarkupFile
forgejo.org/modules/markup/console
code.gitea.io/gitea/modules/markup/console
Render
RenderString
forgejo.org/modules/markup/markdown
code.gitea.io/gitea/modules/markup/markdown
RenderRawString
forgejo.org/modules/markup/mdstripper
code.gitea.io/gitea/modules/markup/mdstripper
stripRenderer.AddOptions
StripMarkdown
forgejo.org/modules/markup/orgmode
code.gitea.io/gitea/modules/markup/orgmode
RenderString
forgejo.org/modules/process
code.gitea.io/gitea/modules/process
Manager.ExecTimeout
forgejo.org/modules/queue
code.gitea.io/gitea/modules/queue
newBaseChannelSimple
newBaseChannelUnique
newBaseRedisSimple
@ -164,75 +163,71 @@ forgejo.org/modules/queue
testStateRecorder.Reset
newWorkerPoolQueueForTest
forgejo.org/modules/queue/lqinternal
code.gitea.io/gitea/modules/queue/lqinternal
QueueItemIDBytes
QueueItemKeyBytes
ListLevelQueueKeys
forgejo.org/modules/setting
code.gitea.io/gitea/modules/setting
NewConfigProviderFromData
GitConfigType.GetOption
InitLoggersForTest
forgejo.org/modules/sync
code.gitea.io/gitea/modules/sync
StatusTable.Start
StatusTable.IsRunning
forgejo.org/modules/timeutil
code.gitea.io/gitea/modules/timeutil
GetExecutableModTime
MockSet
MockUnset
forgejo.org/modules/translation
code.gitea.io/gitea/modules/translation
MockLocale.Language
MockLocale.TrString
MockLocale.Tr
MockLocale.TrN
MockLocale.TrPluralString
MockLocale.TrPluralStringAllForms
MockLocale.TrSize
MockLocale.HasKey
MockLocale.PrettyNumber
forgejo.org/modules/translation/localeiter
IterateMessagesContent
forgejo.org/modules/util
code.gitea.io/gitea/modules/util
OptionalArg
forgejo.org/modules/util/filebuffer
code.gitea.io/gitea/modules/util/filebuffer
CreateFromReader
forgejo.org/modules/validation
code.gitea.io/gitea/modules/validation
IsErrNotValid
forgejo.org/modules/web
code.gitea.io/gitea/modules/web
RouteMock
RouteMockReset
forgejo.org/modules/zstd
code.gitea.io/gitea/modules/zstd
NewWriter
Writer.Write
Writer.Close
forgejo.org/routers/web/org
code.gitea.io/gitea/routers/web
NotFound
code.gitea.io/gitea/routers/web/org
MustEnableProjects
forgejo.org/services/context
code.gitea.io/gitea/services/context
GetPrivateContext
forgejo.org/services/federation
FollowRemoteActor
forgejo.org/services/repository
code.gitea.io/gitea/services/repository
IsErrForkAlreadyExist
forgejo.org/services/repository/files
code.gitea.io/gitea/services/repository/files
ContentType.String
forgejo.org/services/repository/gitgraph
code.gitea.io/gitea/services/repository/gitgraph
Parser.Reset
forgejo.org/services/webhook
code.gitea.io/gitea/services/webhook
NewNotifier

View file

@ -4,9 +4,13 @@
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
"version": "22"
"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": {

View file

@ -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

View file

@ -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

View file

@ -6,7 +6,7 @@ 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

View file

@ -6,7 +6,7 @@ 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

View file

@ -1,4 +1,4 @@
FROM data.forgejo.org/oci/alpine:3.22
FROM data.forgejo.org/oci/alpine:3.21
ARG RELEASE_VERSION=unkown
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.version="${RELEASE_VERSION}"

View file

@ -1,3 +1,3 @@
module forgejo.org
module code.gitea.io/gitea
go 1.23.3

View file

@ -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

View file

@ -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 http://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

View file

@ -47,7 +47,7 @@ jobs:
cat <<'EOF'
${{ toJSON(github) }}
EOF
- uses: https://data.forgejo.org/actions/git-backporting@v4.8.5
- uses: https://data.forgejo.org/actions/git-backporting@v4.8.4
with:
target-branch-pattern: "^backport/(?<target>(v.*))$"
strategy: ort

View file

@ -28,7 +28,7 @@ jobs:
- uses: https://data.forgejo.org/actions/checkout@v4
- id: forgejo
uses: https://data.forgejo.org/actions/setup-forgejo@v3.0.3
uses: https://data.forgejo.org/actions/setup-forgejo@v2.0.4
with:
user: root
password: admin1234

View file

@ -164,7 +164,7 @@ jobs:
- name: build container & release
if: ${{ secrets.TOKEN != '' }}
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.4.1
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
@ -183,7 +183,7 @@ jobs:
- name: build rootless container
if: ${{ secrets.TOKEN != '' }}
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.4.1
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
@ -201,7 +201,7 @@ jobs:
- name: end-to-end tests
if: ${{ secrets.TOKEN != '' && vars.ROLE == 'forgejo-integration' && vars.SKIP_END_TO_END != 'true' }}
uses: https://data.forgejo.org/actions/cascading-pr@v2.3.0
uses: https://data.forgejo.org/actions/cascading-pr@v2.2.0
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: ${{ github.repository }}
@ -212,7 +212,6 @@ jobs:
destination-repo: forgejo/end-to-end
destination-branch: main
destination-token: ${{ secrets.CASCADE_DESTINATION_TOKEN }}
close: true
update: .forgejo/cascading-release-end-to-end
- name: copy to experimental

View file

@ -41,7 +41,7 @@ jobs:
with:
fetch-depth: '0'
show-progress: 'false'
- uses: https://data.forgejo.org/actions/cascading-pr@v2.3.0
- uses: https://data.forgejo.org/actions/cascading-pr@v2.2.0
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: ${{ github.repository }}
@ -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

View file

@ -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/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.5-scratch
options: --tmpfs /data:noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
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://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: coverage
path: ${{ forge.workspace }}/coverage/merged

View file

@ -1,4 +1,4 @@
# Copyright 2025 The Forgejo Authors
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
name: requirements
@ -13,8 +13,7 @@ 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:22-bookworm'
@ -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:"

View file

@ -2,8 +2,6 @@
#
# See also https://forgejo.org/docs/next/contributor/release/#stable-release-process
#
# TOKEN_NEXT_DIGEST is a token with write repository access to https://invisible.forgejo.org/infrastructure/next-digest issued by https://invisible.forgejo.org/forgejo-next-digest
#
# https://codeberg.org/forgejo-experimental/forgejo
#
# Copies a release from codeberg.org/forgejo-integration to codeberg.org/forgejo-experimental
@ -16,7 +14,7 @@
# vars.DOER: forgejo-experimental-ci
# secrets.TOKEN: <generated from codeberg.org/forgejo-experimental-ci>
#
# http://invisible.forgejo.org/forgejo/forgejo
# http://private.forgejo.org/forgejo/forgejo
#
# Copies & sign a release from codeberg.org/forgejo-integration to codeberg.org/forgejo
#
@ -44,7 +42,7 @@ jobs:
- uses: https://data.forgejo.org/actions/checkout@v4
- name: copy & sign
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.4.1
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.4
with:
from-forgejo: ${{ vars.FORGEJO }}
to-forgejo: ${{ vars.FORGEJO }}
@ -80,9 +78,9 @@ jobs:
label: trigger
- name: upgrade v*.next.forgejo.org
uses: https://data.forgejo.org/infrastructure/next-digest@v1.2.2
uses: https://data.forgejo.org/infrastructure/next-digest@v1.1.0
with:
url: https://placeholder:${{ secrets.TOKEN_NEXT_DIGEST }}@invisible.forgejo.org/infrastructure/next-digest
url: https://placeholder:${{ secrets.TOKEN_NEXT_DIGEST }}@code.forgejo.org/infrastructure/next-digest
ref_name: '${{ github.ref_name }}'
image: 'codeberg.org/forgejo-experimental/forgejo'
tag_suffix: '-rootless'

View file

@ -4,35 +4,30 @@ on:
schedule:
- cron: '@daily'
env:
RNA_WORKDIR: /srv/rna
RNA_VERSION: v1.4.1 # renovate: datasource=gitea-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@v4
- uses: https://data.forgejo.org/actions/cache@v4
- 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

View file

@ -7,9 +7,6 @@ on:
- synchronize
- labeled
env:
RNA_VERSION: v1.4.1 # renovate: datasource=gitea-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')
@ -41,4 +38,4 @@ jobs:
- name: release-notes-assistant preview
run: |
go run code.forgejo.org/forgejo/release-notes-assistant@$RNA_VERSION --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 }}

View file

@ -20,7 +20,7 @@ env:
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 currently not needed
RENOVATE_X_PLATFORM_VERSION: 10.0.0+gitea-1.22.0
jobs:
renovate:
@ -28,7 +28,7 @@ jobs:
runs-on: docker
container:
image: data.forgejo.org/renovate/renovate:41.76.0
image: data.forgejo.org/renovate/renovate:39.212.0
steps:
- name: Load renovate repo cache
@ -49,7 +49,7 @@ jobs:
LOG_LEVEL: debug
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
RENOVATE_ENDPOINT: ${{ github.server_url }}
RENOVATE_PLATFORM: forgejo
RENOVATE_PLATFORM: gitea
RENOVATE_REPOSITORY_CACHE: 'enabled'
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
@ -63,10 +63,6 @@ jobs:
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
# use direct connection for these domains for renovate go datasource instead of the go proxy
# allows faster lookups
GONOPROXY: code.forgejo.org
- name: Save renovate repo cache
if: always() && env.RENOVATE_DRY_RUN != 'full'
uses: https://data.forgejo.org/actions/cache/save@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1

View file

@ -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:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- 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:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- 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:22-bookworm'
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@v4
- uses: ./.forgejo/workflows-composite/setup-env
- name: install dependencies & git >= 2.42
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git git-lfs
- 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

View file

@ -37,11 +37,7 @@ jobs:
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- 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
@ -95,7 +91,6 @@ jobs:
RACE_ENABLED: 'true'
TAGS: bindata
TEST_ELASTICSEARCH_URL: http://elasticsearch:9200
TEST_MINIO_ENDPOINT: minio:9000
test-e2e:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
@ -119,14 +114,9 @@ jobs:
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@v46
uses: https://data.forgejo.org/tj-actions/changed-files@v45
with:
separator: '\n'
- run: |
@ -136,7 +126,6 @@ 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@v4

6
.gitignore vendored
View file

@ -37,8 +37,6 @@ _testmain.go
*coverage.out
coverage.all
coverage.html
coverage.html.gz
coverage/
cpu.out
@ -55,13 +53,10 @@ cpu.out
*.log
*.log.*.gz
/build/lint-locale/lint-locale
/build/lint-locale-usage/lint-locale-usage
/gitea
/gitea-vet
/debug
/integrations.test
/forgejo
/bin
/dist
@ -133,4 +128,3 @@ prime/
# Manpage
/man
tests/integration/api_activitypub_person_inbox_useractivity_test.go

View file

@ -1,9 +1,7 @@
version: "2"
output:
sort-order:
- file
linters:
default: none
enable-all: false
disable-all: true
fast: false
enable:
- bidichk
- depguard
@ -11,41 +9,37 @@ linters:
- errcheck
- forbidigo
- gocritic
- gofmt
- gofumpt
- gosimple
- govet
- ineffassign
- nakedret
- nolintlint
- revive
- staticcheck
- stylecheck
- testifylint
- typecheck
- unconvert
- unparam
- unused
- unparam
- usetesting
- wastedassign
settings:
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
- 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
run:
timeout: 10m
output:
sort-results: true
sort-order: [file]
show-stats: true
linters-settings:
stylecheck:
checks: ["all", "-ST1005", "-ST1003"]
nakedret:
max-func-lines: 0
gocritic:
disabled-checks:
- ifElseChain
@ -83,91 +77,74 @@ linters:
- name: unreachable-code
- name: var-declaration
- name: var-naming
arguments:
- []
- []
- - skip-package-name-checks: true
- name: redefines-builtin-id
disabled: true
staticcheck:
checks:
- all
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: code.gitea.io/gitea/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
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
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/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):"
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

View file

@ -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)

View file

@ -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
@ -39,5 +38,4 @@ options/locale/.* @0ko
options/locale_next/.* @0ko
# Personal interest
build/lint-locale-usage/.* @fogti
.*/webhook.* @oliverpool

View file

@ -1,9 +1,9 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 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"
@ -30,13 +30,13 @@ RUN cp /*-alpine-linux-musl*/lib/ld-musl-*.so.1 /lib || true
RUN apk --no-cache add build-base git nodejs npm
COPY . ${GOPATH}/src/forgejo.org
WORKDIR ${GOPATH}/src/forgejo.org
COPY . ${GOPATH}/src/code.gitea.io/gitea
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
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
@ -47,11 +47,11 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/gitea/* \
/tmp/local/etc/s6/openssh/* \
/tmp/local/etc/s6/.s6-svscan/* \
/go/src/forgejo.org/gitea \
/go/src/forgejo.org/environment-to-ini
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
/go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM data.forgejo.org/oci/alpine:3.22
FROM data.forgejo.org/oci/alpine:3.21
ARG RELEASE_VERSION
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.authors="Forgejo" \
@ -102,7 +102,7 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"]
COPY --from=build-env /tmp/local /
RUN cd /usr/local/bin ; ln -s gitea forgejo
COPY --from=build-env /go/src/forgejo.org/gitea /app/gitea/gitea
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
RUN ln -s /app/gitea/gitea /app/gitea/forgejo-cli
COPY --from=build-env /go/src/forgejo.org/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh

View file

@ -1,9 +1,9 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 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"
@ -30,13 +30,13 @@ RUN cp /*-alpine-linux-musl*/lib/ld-musl-*.so.1 /lib || true
RUN apk --no-cache add build-base git nodejs npm
COPY . ${GOPATH}/src/forgejo.org
WORKDIR ${GOPATH}/src/forgejo.org
COPY . ${GOPATH}/src/code.gitea.io/gitea
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
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
@ -45,11 +45,11 @@ COPY docker/rootless /tmp/local
RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
/tmp/local/usr/local/bin/docker-setup.sh \
/tmp/local/usr/local/bin/gitea \
/go/src/forgejo.org/gitea \
/go/src/forgejo.org/environment-to-ini
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
/go/src/code.gitea.io/gitea/gitea \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM data.forgejo.org/oci/alpine:3.22
FROM data.forgejo.org/oci/alpine:3.21
ARG RELEASE_VERSION
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.authors="Forgejo" \
@ -91,10 +91,10 @@ RUN chown git:git /var/lib/gitea /etc/gitea
COPY --from=build-env /tmp/local /
RUN cd /usr/local/bin ; ln -s gitea forgejo
COPY --from=build-env --chown=root:root /go/src/forgejo.org/gitea /app/gitea/gitea
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
RUN ln -s /app/gitea/gitea /app/gitea/forgejo-cli
COPY --from=build-env --chown=root:root /go/src/forgejo.org/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
#git:git
USER 1000:1000

278
Makefile
View file

@ -16,7 +16,7 @@ else
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := forgejo.org
IMPORT := code.gitea.io/gitea
GO ?= $(shell go env GOROOT)/bin/go
SHASUM ?= shasum -a 256
@ -37,17 +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.3.0 # renovate: datasource=go
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.3.1 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # 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.31.0 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
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.36.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.6.0 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@41.76.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
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.18.1 # 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: ...
@ -90,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)
@ -115,8 +124,11 @@ 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 code.gitea.io/gitea/models/migrations/...) $(shell $(GO) list code.gitea.io/gitea/models/forgejo_migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/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))
GO_TEST_REMOTE_CACHER_PACKAGES ?= $(addprefix code.gitea.io/gitea/modules/,$(REMOTE_CACHER_MODULES))
FOMANTIC_WORK_DIR := web_src/fomantic
@ -125,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
@ -156,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 code.gitea.io/gitea/models/migrations/... code.gitea.io/gitea/models/forgejo_migrations/...)
endif
ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
GO_SOURCES += $(BINDATA_DEST)
@ -206,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"
@ -232,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"
@ -255,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/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
# 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/migrations/... forgejo.org/models/forgejo_migrations/...))
endif
###
# Check system and environment requirements
###
@ -338,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-* \
@ -426,10 +401,10 @@ 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 lint-css
@ -443,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
@ -465,8 +452,8 @@ lint-swagger: node_modules
.PHONY: lint-renovate
lint-renovate: node_modules
npx --yes --package $(RENOVATE_NPM_PACKAGE) -- renovate-config-validator > .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
@ -475,13 +462,21 @@ lint-locale:
.PHONY: lint-locale-usage
lint-locale-usage:
$(GO) run ./build/lint-locale-usage --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
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-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 code.gitea.io/gitea
.PHONY: lint-go
lint-go:
@ -500,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
@ -512,9 +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 ./...
go run $(GOVULNCHECK_PACKAGE) ./...
###
# Development and testing targets
@ -537,9 +546,9 @@ 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 $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
@$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-remote-cacher
test-remote-cacher:
@ -567,39 +576,20 @@ test-check:
fi
.PHONY: test\#%
test\#%: | compute-go-test-packages
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
@TZ=UTC $(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:
@ -620,7 +610,7 @@ tidy-check: tidy
go-licenses: $(GO_LICENSE_FILE)
$(GO_LICENSE_FILE): go.mod go.sum
-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --ignore forgejo.org --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --ignore code.gitea.io/gitea --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
@rm -rf $(GO_LICENSE_TMP_DIR)
@ -672,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
@ -719,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\#%
@ -742,68 +731,82 @@ 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
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test
integrations.pgsql.test: git-check $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -o integrations.pgsql.test
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test
integrations.sqlite.test: git-check $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
integrations.cover.test: git-check $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/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
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GOTESTCOMPILEDRUNPREFIX) ./migrations.mysql.test $(GOTESTCOMPILEDRUNSUFFIX)
.PHONY: migrations.pgsql.test
migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration/migration-test -o migrations.pgsql.test
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTESTCOMPILEDRUNPREFIX) ./migrations.pgsql.test $(GOTESTCOMPILEDRUNSUFFIX)
.PHONY: migrations.sqlite.test
migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
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/migrations/$*
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/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/migrations/$*
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/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/migrations/$*
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
e2e.mysql.test: $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/e2e -o e2e.mysql.test
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test
e2e.pgsql.test: $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/e2e -o e2e.pgsql.test
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test
e2e.sqlite.test: $(GO_SOURCES)
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
$(GOTEST) $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: check
check: test
@ -813,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
@ -841,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
@ -860,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
@ -925,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
@ -941,11 +947,13 @@ 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 $(GOMOCK_PACKAGE)
$(GO) install $(GOPLS_PACKAGE)
node_modules: package-lock.json
npm install --no-save
@ -961,7 +969,6 @@ fomantic:
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
@ -1006,11 +1013,12 @@ generate-gitignore:
.PHONY: generate-gomock
generate-gomock:
$(GO) run $(GOMOCK_PACKAGE) -package mock -destination ./modules/queue/mock/redisuniversalclient.go forgejo.org/modules/nosql RedisClient
$(GO) run $(GOMOCK_PACKAGE) -package mock -destination ./modules/queue/mock/redisuniversalclient.go code.gitea.io/gitea/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:

View file

@ -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&#865;ʒ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.

View file

@ -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

282
assets/go-licenses.json generated

File diff suppressed because one or more lines are too long

14
build.go Normal file
View 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"
)

View file

@ -12,8 +12,8 @@ import (
"path/filepath"
"strings"
"forgejo.org/modules/container"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
)
func main() {

View file

@ -15,7 +15,7 @@ import (
"strconv"
"strings"
"forgejo.org/build/codeformat"
"code.gitea.io/gitea/build/codeformat"
)
// Windows has a limitation for command line arguments, the size can not exceed 32KB.

View file

@ -14,7 +14,7 @@ import (
var importPackageGroupOrders = map[string]int{
"": 1, // internal
"forgejo.org/": 2,
"code.gitea.io/gitea/": 2,
}
var errInvalidCommentBetweenImports = errors.New("comments between imported packages are invalid, please move comments to the end of the package line")

View file

@ -58,8 +58,8 @@ import (
"code.gitea.io/other/package"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/the/package"
@ -82,8 +82,8 @@ import (
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/other/package"
"github.com/issue9/identicon"

View file

@ -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 }
`))

View file

@ -20,7 +20,7 @@ import (
"strings"
"unicode/utf8"
"forgejo.org/modules/json"
"code.gitea.io/gitea/modules/json"
)
const (

View file

@ -15,7 +15,7 @@ import (
"path/filepath"
"strings"
"forgejo.org/modules/util"
"code.gitea.io/gitea/modules/util"
)
func main() {

View file

@ -16,7 +16,7 @@ import (
"sort"
"strings"
"forgejo.org/modules/container"
"code.gitea.io/gitea/modules/container"
)
// regexp is based on go-license, excluding README and NOTICE
@ -102,9 +102,9 @@ func main() {
pkgName := path.Dir(pkgPath)
// There might be a bug somewhere in go-licenses that sometimes interprets the
// root package as "." and sometimes as "forgejo.org". Workaround by
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
// removing both of them for the sake of stable output.
if pkgName == "." || pkgName == "forgejo.org" {
if pkgName == "." || pkgName == "code.gitea.io/gitea" {
continue
}

View file

@ -15,7 +15,7 @@ import (
"path/filepath"
"strings"
"forgejo.org/modules/util"
"code.gitea.io/gitea/modules/util"
)
func main() {

View file

@ -1,54 +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
# models/asymkey/gpg_key_object_verification.go: $ObjectVerification.Reason
# unfortunately, it is non-trivial to parse all the occurences
gpg.error.extract_sign
gpg.error.failed_retrieval_gpg_keys
gpg.error.generate_hash
gpg.error.no_committer_account
# models/system/notice.go: func (n *Notice) TrStr() string
admin.notices.type_1
admin.notices.type_2
# modules/setting/ui.go
themes.names.
# services/context/context.go
relativetime.
# templates/mail/issue/default.tmpl: $.locale.Tr
mail.issue.in_tree_path
# templates/package/metadata/arch.tmpl: $.locale.Tr
packages.details.license
# templates/repo/issue/view_content.tmpl: indirection via $closeTranslationKey
repo.issues.close
repo.pulls.close
# templates/repo/issue/view_content/comments.tmpl: indirection via $refTr
repo.issues.ref_closing_from
repo.issues.ref_issue_from
repo.issues.ref_pull_from
repo.issues.ref_reopening_from
# templates/repo/issue/view_content/comments.tmpl: ctx.Locale.Tr (printf "projects.type-%d.display_name" .OldProject.Type)
projects.
projects.type-1.display_name
projects.type-2.display_name
projects.type-3.display_name
# 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.
# services/migrations/migrate.go: messenger calls
# ToDo: give them a unique prefix
repo.migrate.

View file

@ -1,217 +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"
"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)
}
}
}
func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) {
if argLit, ok := n.(*ast.BasicLit); ok {
handler.handleGoTrBasicLit(fset, argLit, prefix)
} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok {
if argBinExpr.Op != token.ADD {
// pass
} else if argLit, ok := argBinExpr.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)
}
}
}
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
}
}
// 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) HandleGoFile(fname string, src any) error {
fset := token.NewFileSet()
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
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, ...)`
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:
ident, ok := n2.Type.(*ast.Ident)
if !ok {
return true
}
// special case: models/unit/unit.go
if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" {
if len(n2.Elts) != 6 {
handler.OnWarning(fset, n2.Pos(), "unexpected initialization of 'Unit' (unexpected number of arguments)")
}
// NameKey has index 2
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
nameKey, ok := n2.Elts[2].(*ast.BasicLit)
if !ok || nameKey.Kind != token.STRING {
handler.OnWarning(fset, n2.Elts[2].Pos(), "unexpected initialization of 'Unit' (expected string literal as NameKey)")
return true
}
// extract string content
arg, err := strconv.Unquote(nameKey.Value)
if err == nil {
// found interesting strings
handler.OnMsgid(fset, nameKey.ValuePos, arg)
}
}
case *ast.FuncDecl:
matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey")
if matchInsPrefix == nil {
return true
}
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
})
return true
case *ast.GenDecl:
if !(n2.Tok == token.CONST || n2.Tok == token.VAR) {
return true
}
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
})
}
}
return true
})
return nil
}

View file

@ -1,233 +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/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 := ""
if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
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]
}
} else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok {
if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
return
}
funcname = nodeField.Ident[1]
}
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)
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)
} else {
if nodeIdent.Ident == "printf" {
parts := strings.SplitN(msgidPrefix, "%", 2)
if len(parts) != 2 {
handler.OnWarning(
fset,
stringPos,
fmt.Sprintf("unsupported invocation of locate function (format string doesn't match \"prefix%%smth\" pattern): %s", nodeString.String()),
)
return
}
msgidPrefix = parts[0]
}
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
}

View file

@ -5,24 +5,29 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"go/ast"
goParser "go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
tmplParser "text/template/parse"
"forgejo.org/modules/container"
"forgejo.org/modules/translation/localeiter"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/locale"
fjTemplates "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/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
@ -44,198 +49,219 @@ func (e LocatedError) Error() string {
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
func isLocaleTrFunction(funcname string) bool {
return funcname == "Tr" || funcname == "TrN"
}
type Handler struct {
OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string)
OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool)
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
}
// 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`}
type StringTrie interface {
Matches(key []string) bool
}
type StringTrieMap map[string]StringTrie
func (m StringTrieMap) Matches(key []string) bool {
if len(key) == 0 || m == nil {
return true
}
value, ok := m[key[0]]
if !ok {
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)
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: "Open",
Kind: "Go parser",
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
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
}
if linePrefix, found := strings.CutSuffix(line, "."); found {
allowedMaskedPrefixes.Insert(strings.Split(linePrefix, "."))
} else {
if !chkMsgid(line) {
return LocatedError{
Location: fmt.Sprintf("%s: line %d", fname, lno),
Kind: "undefined msgid",
Err: errors.New(line),
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
}
usedMsgids.Add(line)
}
}
if err := scanner.Err(); err != nil {
return LocatedError{
Location: fname,
Kind: "Scanner",
Err: err,
}
// 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
}
// 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
}
// 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)
func Usage() {
outp := flag.CommandLine.Output()
fmt.Fprintf(outp, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
case tmplParser.NodeCommand:
nodeCommand := node.(*tmplParser.CommandNode)
fmt.Fprintf(outp, "\nThis command assumes that it gets started from the project root directory.\n")
omh.handleTemplateFileNodes(fset, nodeCommand.Args)
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)
if len(nodeCommand.Args) != 2 {
return
}
fmt.Fprintf(outp, "\nSpecial Go doc comments:\n")
for _, i := range []string{
"//llu:returnsTrKey",
"\tcan be used in front of functions to indicate",
"\tthat the function returns message IDs",
"\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)
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
allowUnusedMsgids := false
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)
for _, arg := range os.Args[1:] {
if arg == "--allow-missing-msgids" {
allowMissingMsgids = true
}
}
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(
&allowUnusedMsgids,
"allow-unused-msgids",
false,
"don't return an error code if unused message IDs are found",
)
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)
@ -244,10 +270,7 @@ func main() {
os.Exit(2)
}
if err = localeiter.IterateMessagesContent(localeContent, func(trKey, trValue string) error {
msgids[trKey] = struct{}{}
return nil
}); err != nil {
if err = locale.IterateMessagesContent(localeContent, onMsgid); err != nil {
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
os.Exit(2)
}
@ -259,63 +282,19 @@ func main() {
os.Exit(2)
}
if err := localeiter.IterateMessagesNextContent(localeContent, func(trKey, pluralForm, trValue string) error {
// ignore plural form
msgids[trKey] = struct{}{}
return nil
}); err != nil {
if err := locale.IterateMessagesNextContent(localeContent, onMsgid); 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 := Handler{
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) {
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)
} 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: InitLocaleTrFunctions(),
}
})
if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error {
if err != nil {
@ -329,15 +308,15 @@ func main() {
if name == "docker" || name == ".git" || name == "node_modules" {
return fs.SkipDir
}
} else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" {
} else if name == "bindata.go" {
// skip false positives
} else if strings.HasSuffix(name, ".go") {
onError(handler.HandleGoFile(fpath, nil))
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(handler.HandleTemplateFile(fpath, nil))
onError(omh.HandleTemplateFile(fpath, nil))
}
}
return nil
@ -346,27 +325,7 @@ func main() {
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)
}
}

View file

@ -11,39 +11,33 @@ import (
"github.com/stretchr/testify/require"
)
func buildHandler(ret *[]string) Handler {
return Handler{
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) {
*ret = append(*ret, msgid)
},
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {},
LocaleTrFunctions: InitLocaleTrFunctions(),
}
}
func HandleGoFileWrapped(t *testing.T, fname, src string) []string {
var ret []string
handler := buildHandler(&ret)
require.NoError(t, handler.HandleGoFile(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"))
})

View file

@ -14,7 +14,7 @@ import (
"slices"
"strings"
"forgejo.org/modules/translation/localeiter"
"code.gitea.io/gitea/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)

View file

@ -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&nbsp;\x1b[0m\x1b[32m\u00a0\x1b[0m"}, checkLocaleContent([]byte(`key = و&nbsp;`)))
assert.EqualValues(t, []string{"key: و\x1b[31m&nbsp\x1b[0m\x1b[32m\u00a0\x1b[0m"}, checkLocaleContent([]byte(`key = و&nbsp;`)))
})
}
@ -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
View 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/

View file

@ -4,28 +4,25 @@
package cmd
import (
"context"
"fmt"
"forgejo.org/modules/private"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/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",
Action: runGenerateActionsRunnerToken,
@ -39,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()

View file

@ -8,71 +8,63 @@ import (
"context"
"fmt"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/log"
repo_module "forgejo.org/modules/repository"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/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",
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(),
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",
Action: runSendMail,
@ -94,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, _ *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRepoSyncReleases(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {

View file

@ -4,39 +4,26 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"text/tabwriter"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
auth_service "forgejo.org/services/auth"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/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()},
Flags: []cli.Flag{idFlag},
Action: runDeleteAuth,
}
}
func microcmdAuthList() *cli.Command {
return &cli.Command{
microcmdAuthList = &cli.Command{
Name: "list",
Usage: "List auth sources",
Action: runListAuth,
@ -67,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 {
@ -104,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)
}
@ -113,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 {

View file

@ -8,14 +8,23 @@ import (
"fmt"
"strings"
"forgejo.org/models/auth"
"forgejo.org/services/auth/source/ldap"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/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 users LDAP record containing the users 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,62 +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{
microcmdAuthAddLdapBindDn = &cli.Command{
Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source",
Action: func(ctx context.Context, cli *cli.Command) error {
return newAuthService().addLdapBindDn(ctx, cli)
Action: func(c *cli.Context) error {
return newAuthService().addLdapBindDn(c)
},
Flags: ldapBindDnCLIFlags(),
Flags: ldapBindDnCLIFlags,
}
}
func microcmdAuthUpdateLdapBindDn() *cli.Command {
return &cli.Command{
microcmdAuthUpdateLdapBindDn = &cli.Command{
Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source",
Action: func(ctx context.Context, cli *cli.Command) error {
return newAuthService().updateLdapBindDn(ctx, cli)
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{
microcmdAuthAddLdapSimpleAuth = &cli.Command{
Name: "add-ldap-simple",
Usage: "Add new LDAP (simple auth) authentication source",
Action: func(ctx context.Context, cli *cli.Command) error {
return newAuthService().addLdapSimpleAuth(ctx, cli)
Action: func(c *cli.Context) error {
return newAuthService().addLdapSimpleAuth(c)
},
Flags: ldapSimpleAuthCLIFlags(),
Flags: ldapSimpleAuthCLIFlags,
}
}
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
return &cli.Command{
microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source",
Action: func(ctx context.Context, cli *cli.Command) error {
return newAuthService().updateLdapSimpleAuth(ctx, cli)
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")
}
@ -195,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")
}
@ -282,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
}
@ -300,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 {
@ -329,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 {
@ -351,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 {
@ -380,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 {

View file

@ -7,18 +7,19 @@ import (
"context"
"testing"
"forgejo.org/models/auth"
"forgejo.org/modules/test"
"forgejo.org/services/auth/source/ldap"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/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 {

View file

@ -4,19 +4,18 @@
package cmd
import (
"context"
"errors"
"fmt"
"net/url"
auth_model "forgejo.org/models/auth"
"forgejo.org/services/auth/source/oauth2"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/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,32 +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",
},
}
}
func microcmdAuthAddOauth() *cli.Command {
return &cli.Command{
microcmdAuthAddOauth = &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
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",
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{
@ -172,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"),
@ -180,19 +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"),
}
}
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") {
@ -200,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,
@ -208,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
}
@ -255,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")
}
@ -282,10 +262,6 @@ func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
if c.IsSet("allow-username-change") {
oAuth2Config.AllowUsernameChange = c.Bool("allow-username-change")
}
// update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{}
@ -319,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)
}

View file

@ -1,706 +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",
},
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,
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://)",
},
}
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",
},
}
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)
}
}
}

View file

@ -4,19 +4,18 @@
package cmd
import (
"context"
"errors"
"strings"
auth_model "forgejo.org/models/auth"
"forgejo.org/modules/util"
"forgejo.org/services/auth/source/smtp"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/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,27 +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",
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",
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"}
@ -128,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 {
@ -168,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 {

View file

@ -4,13 +4,11 @@
package cmd
import (
"context"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/modules/graceful"
repo_service "code.gitea.io/gitea/services/repository"
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 (
@ -27,8 +25,8 @@ var (
}
)
func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRegenerateHooks(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
@ -37,8 +35,8 @@ func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
}
func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRegenerateKeys(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {

View file

@ -4,21 +4,18 @@
package cmd
import (
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func subcmdUser() *cli.Command {
return &cli.Command{
var subcmdUser = &cli.Command{
Name: "user",
Usage: "Modify users",
Commands: []*cli.Command{
microcmdUserCreate(),
microcmdUserList(),
microcmdUserChangePassword(),
microcmdUserDelete(),
microcmdUserGenerateAccessToken(),
microcmdUserMustChangePassword(),
microcmdUserResetMFA(),
Subcommands: []*cli.Command{
microcmdUserCreate,
microcmdUserList,
microcmdUserChangePassword,
microcmdUserDelete,
microcmdUserGenerateAccessToken,
microcmdUserMustChangePassword,
},
}
}

View file

@ -4,21 +4,19 @@
package cmd
import (
"context"
"errors"
"fmt"
user_model "forgejo.org/models/user"
"forgejo.org/modules/auth/password"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
user_service "forgejo.org/services/user"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserChangePassword() *cli.Command {
return &cli.Command{
var microcmdUserChangePassword = &cli.Command{
Name: "change-password",
Usage: "Change a user's password",
Action: runChangePassword,
@ -41,15 +39,14 @@ func microcmdUserChangePassword() *cli.Command {
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 {

View file

@ -4,23 +4,20 @@
package cmd
import (
"context"
"errors"
"fmt"
"strings"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
user_model "forgejo.org/models/user"
pwd "forgejo.org/modules/auth/password"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
pwd "code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserCreate() *cli.Command {
return &cli.Command{
var microcmdUserCreate = &cli.Command{
Name: "create",
Usage: "Create a new user in database",
Action: runCreateUser,
@ -53,6 +50,7 @@ func microcmdUserCreate() *cli.Command {
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",
@ -63,29 +61,14 @@ func microcmdUserCreate() *cli.Command {
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`,
},
},
}
}
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()
@ -110,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 {
@ -167,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{
@ -175,40 +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 != "" {
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
if c.Bool("access-token") {
t := &auth_model.AccessToken{
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
}

View file

@ -4,20 +4,18 @@
package cmd
import (
"context"
"errors"
"fmt"
"strings"
user_model "forgejo.org/models/user"
"forgejo.org/modules/storage"
user_service "forgejo.org/services/user"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/storage"
user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserDelete() *cli.Command {
return &cli.Command{
var microcmdUserDelete = &cli.Command{
Name: "delete",
Usage: "Delete specific user by id, name or email",
Flags: []cli.Flag{
@ -41,15 +39,14 @@ func microcmdUserDelete() *cli.Command {
},
},
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 {

View file

@ -4,18 +4,16 @@
package cmd
import (
"context"
"errors"
"fmt"
auth_model "forgejo.org/models/auth"
user_model "forgejo.org/models/user"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserGenerateAccessToken() *cli.Command {
return &cli.Command{
var microcmdUserGenerateAccessToken = &cli.Command{
Name: "generate-access-token",
Usage: "Generate an access token for a specific user",
Flags: []cli.Flag{
@ -36,20 +34,19 @@ func microcmdUserGenerateAccessToken() *cli.Command {
},
&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"`,
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 {
@ -80,9 +77,6 @@ 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
// create the token

View file

@ -4,18 +4,16 @@
package cmd
import (
"context"
"fmt"
"os"
"text/tabwriter"
user_model "forgejo.org/models/user"
user_model "code.gitea.io/gitea/models/user"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserList() *cli.Command {
return &cli.Command{
var microcmdUserList = &cli.Command{
Name: "list",
Usage: "List users",
Action: runListUsers,
@ -25,11 +23,10 @@ func microcmdUserList() *cli.Command {
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 {
@ -44,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)
@ -52,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])
}

View file

@ -4,17 +4,15 @@
package cmd
import (
"context"
"errors"
"fmt"
user_model "forgejo.org/models/user"
user_model "code.gitea.io/gitea/models/user"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func microcmdUserMustChangePassword() *cli.Command {
return &cli.Command{
var microcmdUserMustChangePassword = &cli.Command{
Name: "must-change-password",
Usage: "Set the must change password flag for the provided users or all users",
Action: runMustChangePassword,
@ -34,11 +32,10 @@ func microcmdUserMustChangePassword() *cli.Command {
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") {

View file

@ -1,73 +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",
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
}

View file

@ -6,7 +6,6 @@
package cmd
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@ -21,12 +20,11 @@ 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{
var CmdCert = &cli.Command{
Name: "cert",
Usage: "Generate self-signed certificate",
Description: `Generate a self-signed X.509 certificate for a TLS server.
@ -63,7 +61,6 @@ Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
Usage: "whether this cert should be its own Certificate Authority",
},
},
}
}
func publicKey(priv any) any {
@ -92,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
}

View file

@ -15,28 +15,26 @@ import (
"strings"
"syscall"
"forgejo.org/models/db"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/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) {
if util.IsEmptyString(c.String(a)) {
return errors.New(a + " is required")
}
}
}
return nil
}
@ -75,8 +73,8 @@ If this is the intended configuration file complete the [database] section.`, se
return nil
}
func installSignals(ctx context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
func installSignals() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// install notify
signalChannel := make(chan os.Signal, 1)
@ -111,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
@ -122,16 +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
return nil
}
}

65
cmd/docs.go Normal file
View 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
}

View file

@ -4,7 +4,6 @@
package cmd
import (
"context"
"fmt"
golog "log"
"os"
@ -12,34 +11,32 @@ import (
"strings"
"text/tabwriter"
"forgejo.org/models/db"
"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/services/doctor"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
migrate_base "code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/doctor"
"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{
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(),
Subcommands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
},
}
}
func cmdDoctorCheck() *cli.Command {
return &cli.Command{
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.",
@ -75,11 +72,9 @@ func cmdDoctorCheck() *cli.Command {
Usage: "Use color for outputted information",
},
},
}
}
func cmdRecreateTable() *cli.Command {
return &cli.Command{
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)",
@ -95,11 +90,10 @@ This command will cause Xorm to recreate tables, copying over the data and delet
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
Action: runRecreateTable,
}
}
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
@ -126,7 +120,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))
}
@ -136,31 +130,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 := 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)
@ -172,8 +159,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

View file

@ -4,28 +4,25 @@
package cmd
import (
"context"
"fmt"
"forgejo.org/models/db"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/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{
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 {

View file

@ -7,11 +7,11 @@ import (
"context"
"testing"
"forgejo.org/modules/log"
"forgejo.org/services/doctor"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/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"`)
}

View file

@ -5,62 +5,52 @@
package cmd
import (
"context"
"errors"
"fmt"
"io/fs"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"forgejo.org/models/db"
"forgejo.org/modules/json"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/storage"
"forgejo.org/modules/util"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.forgejo.org/go-chi/session"
"github.com/mholt/archives"
"github.com/urfave/cli/v3"
"github.com/mholt/archiver/v3"
"github.com/urfave/cli/v2"
)
func addObject(archiveJobs chan archives.ArchiveAsyncJob, object fs.File, customName string, verbose bool) error {
func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
if verbose {
log.Info("Adding file %s", customName)
}
info, err := object.Stat()
if err != nil {
return err
}
ch := make(chan error)
archiveJobs <- archives.ArchiveAsyncJob{
File: archives.FileInfo{
return w.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
NameInArchive: customName,
Open: func() (fs.File, error) {
return object, nil
CustomName: customName,
},
},
Result: ch,
}
return <-ch
ReadCloser: r,
})
}
func addFile(archiveJobs chan archives.ArchiveAsyncJob, filePath, absPath string, verbose bool) error {
file, err := os.Open(absPath) // Closed by archiver
func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
file, err := os.Open(absPath)
if err != nil {
return err
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return err
}
return addObject(archiveJobs, file, filePath, verbose)
return addReader(w, file, fileInfo, filePath, verbose)
}
func isSubdir(upper, lower string) (bool, error) {
@ -93,10 +83,6 @@ func (o *outputType) Set(value string) error {
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
@ -109,57 +95,8 @@ var outputTypeEnum = &outputType{
Default: "zip",
}
func getArchiverByType(outType string) (archives.ArchiverAsync, error) {
var archiver archives.ArchiverAsync
switch outType {
case "zip":
archiver = archives.Zip{}
case "tar":
archiver = archives.Tar{}
case "tar.sz":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Sz{},
}
case "tar.gz":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Gz{},
}
case "tar.xz":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Xz{},
}
case "tar.bz2":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Bz2{},
}
case "tar.br":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Brotli{},
}
case "tar.lz4":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Lz4{},
}
case "tar.zst":
archiver = archives.CompressedArchive{
Archival: archives.Tar{},
Compression: archives.Zstd{},
}
default:
return nil, fmt.Errorf("unsupported output type: %s", outType)
}
return archiver, nil
}
// CmdDump represents the available dump sub-command.
func cmdDump() *cli.Command {
return &cli.Command{
var CmdDump = &cli.Command{
Name: "dump",
Usage: "Dump Forgejo files and database",
Description: `Dump compresses all related files and database into zip file.
@ -185,6 +122,7 @@ It can be used for backup and capture Forgejo server image to send to maintainer
&cli.StringFlag{
Name: "tempdir",
Aliases: []string{"t"},
Value: os.TempDir(),
Usage: "Temporary dir path",
},
&cli.StringFlag{
@ -232,7 +170,6 @@ It can be used for backup and capture Forgejo server image to send to maintainer
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
},
},
}
}
func fatal(format string, args ...any) {
@ -240,7 +177,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")
@ -276,16 +213,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)
@ -310,40 +247,99 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
return err
}
archiveJobs := make(chan archives.ArchiveAsyncJob)
wg := sync.WaitGroup{}
archiver, err := getArchiverByType(outType)
var iface any
if fileName == "-" {
iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
} else {
iface, err = archiver.ByExtension(fileName)
}
if err != nil {
fatal("Failed to get archiver for extension: %v", err)
}
w, _ := iface.(archiver.Writer)
if err := w.Create(file); err != nil {
fatal("Creating archiver.Writer failed: %v", err)
}
defer w.Close()
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
log.Info("Skipping local repositories")
} else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
wg.Add(1)
go dumpRepos(ctx, archiveJobs, &wg, absFileName, verbose)
if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
fatal("Failed to include repositories: %v", err)
}
wg.Add(1)
go dumpDatabase(ctx, archiveJobs, &wg, verbose)
if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
log.Info("Skipping LFS data")
} else if !setting.LFS.StartServer {
log.Info("LFS not enabled - skipping")
} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
}
if len(setting.CustomConf) > 0 {
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)
return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
}); err != nil {
fatal("Failed to dump LFS objects: %v", err)
}
}
tmpDir := ctx.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
dbDump, err := os.CreateTemp(tmpDir, "forgejo-db.sql")
if err != nil {
fatal("Failed to create tmp file: %v", err)
}
defer func() {
_ = dbDump.Close()
if err := util.Remove(dbDump.Name()); err != nil {
log.Warn("Failed to remove temporary file: %s: Error: %v", dbDump.Name(), err)
}
}()
targetDBType := ctx.String("database")
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
log.Info("Dumping database...")
}
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
fatal("Failed to dump database: %v", err)
}
if err := addFile(w, "forgejo-db.sql", dbDump.Name(), verbose); err != nil {
fatal("Failed to include forgejo-db.sql: %v", err)
}
if len(setting.CustomConf) > 0 {
log.Info("Adding custom configuration file from %s", setting.CustomConf)
if err := addFile(w, "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") {
log.Info("Skipping custom directory")
} else {
wg.Add(1)
go dumpCustom(archiveJobs, &wg, absFileName, verbose)
customDir, err := os.Stat(setting.CustomPath)
if err == nil && customDir.IsDir() {
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
fatal("Failed to include custom: %v", err)
}
} else {
log.Info("Custom dir %s is inside data dir %s, skipping", setting.CustomPath, setting.AppDataPath)
}
} else {
log.Info("Custom dir %s does not exist, skipping", setting.CustomPath)
}
}
isExist, err := util.IsExist(setting.AppDataPath)
@ -353,95 +349,11 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
if isExist {
log.Info("Packing data directory...%s", setting.AppDataPath)
wg.Add(1)
go dumpData(ctx, archiveJobs, &wg, absFileName, verbose)
}
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
log.Info("Skipping attachment data")
} else {
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") {
log.Info("Skipping package data")
} else if !setting.Packages.Enabled {
log.Info("Package registry not enabled - skipping")
} else {
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,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
log.Info("Skipping log files")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
if err != nil {
log.Error("Failed to check if %s exists: %v", setting.Log.RootPath, err)
}
if isExist {
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)
}
}()
}
}
// Wait for all jobs to finish before closing the channel
// ArchiveAsync will only return after the channel is closed
go func() {
wg.Wait()
close(archiveJobs)
}()
if err := archiver.ArchiveAsync(stdCtx, file, archiveJobs); err != nil {
_ = util.Remove(fileName)
fatal("Archiving failed: %v", err)
}
if fileName != "-" {
if err := os.Chmod(fileName, 0o600); err != nil {
log.Info("Can't change file access permissions mask to 0600: %v", err)
}
log.Info("Finished dumping in file %s", fileName)
} else {
log.Info("Finished dumping to stdout")
}
return nil
}
func dumpData(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
defer wg.Done()
var excludes []string
if setting.SessionConfig.OriginalProvider == "file" {
var opts session.Options
if err := json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
fatal("Failed to parse session config: %v", err)
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
return err
}
excludes = append(excludes, opts.ProviderConfig)
}
@ -463,98 +375,78 @@ func dumpData(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *s
excludes = append(excludes, setting.Packages.Storage.Path)
excludes = append(excludes, setting.Log.RootPath)
excludes = append(excludes, absFileName)
if err := addRecursiveExclude(archiveJobs, "data", setting.AppDataPath, excludes, verbose); err != nil {
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
fatal("Failed to include data directory: %v", err)
}
}
func dumpCustom(archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
defer wg.Done()
customDir, err := os.Stat(setting.CustomPath)
if err == nil && customDir.IsDir() {
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
if err := addRecursiveExclude(archiveJobs, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
fatal("Failed to include custom: %v", err)
}
} else {
log.Info("Custom dir %s is inside data dir %s, skipping", setting.CustomPath, setting.AppDataPath)
}
} else {
log.Info("Custom dir %s does not exist, skipping", setting.CustomPath)
}
}
func dumpDatabase(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, verbose bool) {
defer wg.Done()
var err error
tmpDir := ctx.String("tempdir")
if tmpDir == "" {
tmpDir, err = os.MkdirTemp("", "forgejo-dump-*")
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
log.Info("Skipping attachment data")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
fatal("Failed to create temporary directory: %v", err)
return err
}
defer func() {
if err := util.Remove(tmpDir); err != nil {
log.Warn("Failed to remove temporary directory: %s: Error: %v", tmpDir, err)
}
}()
}
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
dbDump, err := os.CreateTemp(tmpDir, "forgejo-db.sql")
if err != nil {
fatal("Failed to create temporary file: %v", err)
}
defer func() {
_ = dbDump.Close()
if err := util.Remove(dbDump.Name()); err != nil {
log.Warn("Failed to remove temporary database file: %s: Error: %v", dbDump.Name(), err)
}
}()
targetDBType := ctx.String("database")
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
log.Info("Dumping database...")
}
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
fatal("Failed to dump database: %v", err)
}
if err := addFile(archiveJobs, "forgejo-db.sql", dbDump.Name(), verbose); err != nil {
fatal("Failed to include forgejo-db.sql: %v", err)
}
}
func dumpRepos(ctx *cli.Command, 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 {
fatal("Failed to include repositories: %v", err)
}
if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
log.Info("Skipping LFS data")
} else if !setting.LFS.StartServer {
log.Info("LFS not enabled - skipping")
} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
return addObject(archiveJobs, object, path.Join("data", "lfs", objPath), verbose)
return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
}); err != nil {
fatal("Failed to dump LFS objects: %v", err)
fatal("Failed to dump attachments: %v", err)
}
if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
log.Info("Skipping package data")
} else if !setting.Packages.Enabled {
log.Info("Package registry not enabled - skipping")
} else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
}
return addReader(w, object, info, 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,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
log.Info("Skipping log files")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
if err != nil {
log.Error("Failed to check if %s exists: %v", setting.Log.RootPath, err)
}
if isExist {
if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
fatal("Failed to include log: %v", err)
}
}
}
if fileName != "-" {
if err = w.Close(); err != nil {
_ = util.Remove(fileName)
fatal("Failed to save %s: %v", fileName, err)
}
if err := os.Chmod(fileName, 0o600); err != nil {
log.Info("Can't change file access permissions mask to 0600: %v", err)
}
}
if fileName != "-" {
log.Info("Finish dumping in file %s", fileName)
} else {
log.Info("Finish dumping to stdout")
}
return nil
}
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
// archives.FilesFromDisk doesn't support excluding files, so we have to do it manually
func addRecursiveExclude(archiveJobs chan archives.ArchiveAsyncJob, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
absPath, err := filepath.Abs(absPath)
if err != nil {
return err
@ -579,11 +471,10 @@ func addRecursiveExclude(archiveJobs chan archives.ArchiveAsyncJob, insidePath,
}
if file.IsDir() {
if err := addFile(archiveJobs, currentInsidePath, currentAbsPath, false); err != nil {
if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
return err
}
if err := addRecursiveExclude(archiveJobs, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
return err
}
} else {
@ -601,7 +492,7 @@ func addRecursiveExclude(archiveJobs chan archives.ArchiveAsyncJob, insidePath,
shouldAdd = targetStat.Mode().IsRegular()
}
if shouldAdd {
if err := addFile(archiveJobs, currentInsidePath, currentAbsPath, verbose); err != nil {
if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
return err
}
}

View file

@ -10,21 +10,20 @@ import (
"os"
"strings"
"forgejo.org/modules/git"
"forgejo.org/modules/log"
base "forgejo.org/modules/migration"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
"forgejo.org/modules/util"
"forgejo.org/services/convert"
"forgejo.org/services/migrations"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/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{
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.",
@ -78,16 +77,10 @@ func cmdDumpRepository() *cli.Command {
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 {

View file

@ -4,32 +4,40 @@
package cmd
import (
"io"
"os"
"testing"
"github.com/mholt/archives"
"github.com/mholt/archiver/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func mockArchiverAsync(ch chan archives.ArchiveAsyncJob, files *[]string) {
for job := range ch {
*files = append(*files, job.File.NameInArchive)
job.Result <- nil
}
type mockArchiver struct {
addedFiles []string
}
func (mockArchiver) Create(out io.Writer) error {
return nil
}
func (m *mockArchiver) Write(f archiver.File) error {
m.addedFiles = append(m.addedFiles, f.Name())
return nil
}
func (mockArchiver) Close() error {
return nil
}
func TestAddRecursiveExclude(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
dir := t.TempDir()
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, []string{}, false)
err := addRecursiveExclude(archiver, "", dir, []string{}, false)
require.NoError(t, err)
assert.Empty(t, files)
assert.Empty(t, archiver.addedFiles)
})
t.Run("Single file", func(t *testing.T) {
@ -38,25 +46,20 @@ func TestAddRecursiveExclude(t *testing.T) {
require.NoError(t, err)
t.Run("No exclude", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, nil, false)
err = addRecursiveExclude(archiver, "", dir, nil, false)
require.NoError(t, err)
assert.Len(t, files, 1)
assert.Contains(t, files, "example")
assert.Len(t, archiver.addedFiles, 1)
assert.Contains(t, archiver.addedFiles, "example")
})
t.Run("With exclude", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, []string{dir + "/example"}, false)
err = addRecursiveExclude(archiver, "", dir, []string{dir + "/example"}, false)
require.NoError(t, err)
assert.Empty(t, files)
assert.Empty(t, archiver.addedFiles)
})
})
@ -70,57 +73,46 @@ func TestAddRecursiveExclude(t *testing.T) {
require.NoError(t, err)
t.Run("No exclude", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, nil, false)
err = addRecursiveExclude(archiver, "", dir, nil, false)
require.NoError(t, err)
assert.Len(t, files, 5)
assert.Contains(t, files, "deep")
assert.Contains(t, files, "deep/nested")
assert.Contains(t, files, "deep/nested/folder")
assert.Contains(t, files, "deep/nested/folder/example")
assert.Contains(t, files, "deep/nested/folder/another-file")
assert.Len(t, archiver.addedFiles, 5)
assert.Contains(t, archiver.addedFiles, "deep")
assert.Contains(t, archiver.addedFiles, "deep/nested")
assert.Contains(t, archiver.addedFiles, "deep/nested/folder")
assert.Contains(t, archiver.addedFiles, "deep/nested/folder/example")
assert.Contains(t, archiver.addedFiles, "deep/nested/folder/another-file")
})
t.Run("Exclude first directory", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, []string{dir + "/deep"}, false)
err = addRecursiveExclude(archiver, "", dir, []string{dir + "/deep"}, false)
require.NoError(t, err)
assert.Empty(t, files)
assert.Empty(t, archiver.addedFiles)
})
t.Run("Exclude nested directory", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, []string{dir + "/deep/nested/folder"}, false)
err = addRecursiveExclude(archiver, "", dir, []string{dir + "/deep/nested/folder"}, false)
require.NoError(t, err)
assert.Len(t, files, 2)
assert.Contains(t, files, "deep")
assert.Contains(t, files, "deep/nested")
assert.Len(t, archiver.addedFiles, 2)
assert.Contains(t, archiver.addedFiles, "deep")
assert.Contains(t, archiver.addedFiles, "deep/nested")
})
t.Run("Exclude file", func(t *testing.T) {
ch := make(chan archives.ArchiveAsyncJob)
var files []string
go mockArchiverAsync(ch, &files)
archiver := &mockArchiver{}
err := addRecursiveExclude(ch, "", dir, []string{dir + "/deep/nested/folder/example"}, false)
err = addRecursiveExclude(archiver, "", dir, []string{dir + "/deep/nested/folder/example"}, false)
require.NoError(t, err)
assert.Len(t, files, 4)
assert.Contains(t, files, "deep")
assert.Contains(t, files, "deep/nested")
assert.Contains(t, files, "deep/nested/folder")
assert.Contains(t, files, "deep/nested/folder/another-file")
assert.Len(t, archiver.addedFiles, 4)
assert.Contains(t, archiver.addedFiles, "deep")
assert.Contains(t, archiver.addedFiles, "deep/nested")
assert.Contains(t, archiver.addedFiles, "deep/nested/folder")
assert.Contains(t, archiver.addedFiles, "deep/nested/folder/another-file")
})
})
}

View file

@ -4,41 +4,38 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"forgejo.org/modules/assetfs"
"forgejo.org/modules/log"
"forgejo.org/modules/options"
"forgejo.org/modules/public"
"forgejo.org/modules/setting"
"forgejo.org/modules/templates"
"forgejo.org/modules/util"
"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/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 {

View file

@ -6,25 +6,24 @@ package forgejo
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strings"
actions_model "forgejo.org/models/actions"
"forgejo.org/modules/private"
"forgejo.org/modules/setting"
private_routers "forgejo.org/routers/private"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
private_routers "code.gitea.io/gitea/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",
@ -106,26 +105,26 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command {
}
}
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 {
@ -139,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)
@ -162,17 +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")
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
}
@ -210,7 +209,7 @@ 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{}
if err := runner.GenerateToken(); err != nil {
return err
@ -221,7 +220,7 @@ func RunGenerateSecret(ctx context.Context, cli *cli.Command) error {
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)
@ -230,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() {

View file

@ -4,13 +4,14 @@
package forgejo
import (
"context"
"fmt"
"testing"
"code.gitea.io/gitea/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)

View file

@ -8,19 +8,19 @@ import (
"context"
"errors"
"forgejo.org/models"
"forgejo.org/modules/git"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/storage"
"forgejo.org/services/f3/util"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/f3/util"
_ "forgejo.org/services/f3/driver" // register the driver
_ "code.gitea.io/gitea/services/f3/driver" // register the driver
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 {
@ -28,21 +28,21 @@ func CmdF3(ctx context.Context) *cli.Command {
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")
@ -69,7 +69,7 @@ func runMirror(ctx context.Context, c *cli.Command, action cli.ActionFunc) error
}
}
err := action(ctx, c)
err := action(c)
if panicError, ok := err.(f3_util.PanicError); ok {
log.Debug("F3 Stack trace\n%s", panicError.Stack())
}

View file

@ -11,12 +11,12 @@ import (
"os/signal"
"syscall"
"forgejo.org/models/db"
"forgejo.org/modules/log"
"forgejo.org/modules/private"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
type key int
@ -34,7 +34,7 @@ func CmdForgejo(ctx context.Context) *cli.Command {
Name: "forgejo-cli",
Usage: "Forgejo CLI",
Flags: []cli.Flag{},
Commands: []*cli.Command{
Subcommands: []*cli.Command{
CmdActions(ctx),
CmdF3(ctx),
},
@ -147,12 +147,12 @@ func handleCliResponseExtra(ctx context.Context, extra private.ResponseExtra) er
return cli.Exit(extra.Error, 1)
}
func prepareWorkPathAndCustomConf(ctx context.Context) func(ctx context.Context, cli *cli.Command) (context.Context, error) {
return func(ctx context.Context, cli *cli.Command) (context.Context, error) {
func prepareWorkPathAndCustomConf(ctx context.Context) func(c *cli.Context) error {
return func(c *cli.Context) error {
if !ContextGetNoInit(ctx) {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
for _, curCtx := range cli.Lineage() {
for _, curCtx := range c.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
}
@ -165,6 +165,6 @@ func prepareWorkPathAndCustomConf(ctx context.Context) func(ctx context.Context,
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
}
return ctx, nil
return nil
}
}

View file

@ -5,65 +5,56 @@
package cmd
import (
"context"
"fmt"
"os"
"forgejo.org/modules/generate"
"code.gitea.io/gitea/modules/generate"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// CmdGenerate represents the available generate sub-command.
func cmdGenerate() *cli.Command {
return &cli.Command{
var (
// CmdGenerate represents the available generate sub-command.
CmdGenerate = &cli.Command{
Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens",
Commands: []*cli.Command{
subcmdSecret(),
Subcommands: []*cli.Command{
subcmdSecret,
},
}
}
func subcmdSecret() *cli.Command {
return &cli.Command{
subcmdSecret = &cli.Command{
Name: "secret",
Usage: "Generate a secret token",
Commands: []*cli.Command{
microcmdGenerateInternalToken(),
microcmdGenerateLfsJwtSecret(),
microcmdGenerateSecretKey(),
Subcommands: []*cli.Command{
microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret,
microcmdGenerateSecretKey,
},
}
}
func microcmdGenerateInternalToken() *cli.Command {
return &cli.Command{
microcmdGenerateInternalToken = &cli.Command{
Name: "INTERNAL_TOKEN",
Usage: "Generate a new INTERNAL_TOKEN",
Action: runGenerateInternalToken,
}
}
func microcmdGenerateLfsJwtSecret() *cli.Command {
return &cli.Command{
microcmdGenerateLfsJwtSecret = &cli.Command{
Name: "JWT_SECRET",
Aliases: []string{"LFS_JWT_SECRET"},
Usage: "Generate a new JWT_SECRET",
Action: runGenerateLfsJwtSecret,
}
}
func microcmdGenerateSecretKey() *cli.Command {
return &cli.Command{
microcmdGenerateSecretKey = &cli.Command{
Name: "SECRET_KEY",
Usage: "Generate a new SECRET_KEY",
Action: runGenerateSecretKey,
}
}
)
func runGenerateInternalToken(ctx context.Context, c *cli.Command) error {
func runGenerateInternalToken(c *cli.Context) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
return err
@ -72,25 +63,28 @@ func runGenerateInternalToken(ctx context.Context, c *cli.Command) error {
fmt.Printf("%s", internalToken)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println()
fmt.Printf("\n")
}
return nil
}
func runGenerateLfsJwtSecret(ctx context.Context, c *cli.Command) error {
_, jwtSecretBase64 := generate.NewJwtSecret()
func runGenerateLfsJwtSecret(c *cli.Context) error {
_, jwtSecretBase64, err := generate.NewJwtSecret()
if err != nil {
return err
}
fmt.Printf("%s", jwtSecretBase64)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Print("\n")
fmt.Printf("\n")
}
return nil
}
func runGenerateSecretKey(ctx context.Context, c *cli.Command) error {
func runGenerateSecretKey(c *cli.Context) error {
secretKey, err := generate.NewSecretKey()
if err != nil {
return err
@ -99,7 +93,7 @@ func runGenerateSecretKey(ctx context.Context, c *cli.Command) error {
fmt.Printf("%s", secretKey)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Print("\n")
fmt.Printf("\n")
}
return nil

View file

@ -14,38 +14,36 @@ import (
"strings"
"time"
"forgejo.org/modules/git"
"forgejo.org/modules/git/pushoptions"
"forgejo.org/modules/log"
"forgejo.org/modules/private"
repo_module "forgejo.org/modules/repository"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/pushoptions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
const (
hookBatchSize = 30
)
// CmdHook represents the available hooks sub-command.
func cmdHook() *cli.Command {
return &cli.Command{
var (
// CmdHook represents the available hooks sub-command.
CmdHook = &cli.Command{
Name: "hook",
Usage: "(internal) Should only be called by Git",
Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Commands: []*cli.Command{
subcmdHookPreReceive(),
subcmdHookUpdate(),
subcmdHookPostReceive(),
subcmdHookProcReceive(),
Subcommands: []*cli.Command{
subcmdHookPreReceive,
subcmdHookUpdate,
subcmdHookPostReceive,
subcmdHookProcReceive,
},
}
}
func subcmdHookPreReceive() *cli.Command {
return &cli.Command{
subcmdHookPreReceive = &cli.Command{
Name: "pre-receive",
Usage: "Delegate pre-receive Git hook",
Description: "This command should only be called by Git",
@ -56,10 +54,7 @@ func subcmdHookPreReceive() *cli.Command {
},
},
}
}
func subcmdHookUpdate() *cli.Command {
return &cli.Command{
subcmdHookUpdate = &cli.Command{
Name: "update",
Usage: "Delegate update Git hook",
Description: "This command should only be called by Git",
@ -70,10 +65,7 @@ func subcmdHookUpdate() *cli.Command {
},
},
}
}
func subcmdHookPostReceive() *cli.Command {
return &cli.Command{
subcmdHookPostReceive = &cli.Command{
Name: "post-receive",
Usage: "Delegate post-receive Git hook",
Description: "This command should only be called by Git",
@ -84,11 +76,8 @@ func subcmdHookPostReceive() *cli.Command {
},
},
}
}
// Note: new hook since git 2.29
func subcmdHookProcReceive() *cli.Command {
return &cli.Command{
// Note: new hook since git 2.29
subcmdHookProcReceive = &cli.Command{
Name: "proc-receive",
Usage: "Delegate proc-receive Git hook",
Description: "This command should only be called by Git",
@ -99,7 +88,7 @@ func subcmdHookProcReceive() *cli.Command {
},
},
}
}
)
type delayWriter struct {
internal io.Writer
@ -172,14 +161,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil
}
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
func runHookPreReceive(c *cli.Context) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
ctx, cancel := installSignals(ctx)
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), true)
setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
@ -231,6 +220,11 @@ Forgejo or set your environment appropriately.`, "")
}
}
supportProcReceive := false
if git.CheckGitVersionAtLeast("2.29") == nil {
supportProcReceive = true
}
for scanner.Scan() {
// TODO: support news feeds for wiki
if isWiki {
@ -248,12 +242,15 @@ Forgejo or set your environment appropriately.`, "")
total++
lastline++
// All references should be checked because permission check was delayed.
// If the ref is a branch or tag, check if it's protected
// if supportProcReceive all ref should be checked because
// permission check was delayed
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
count++
fmt.Fprint(out, "*")
fmt.Fprintf(out, "*")
if count >= hookBatchSize {
fmt.Fprintf(out, " Checking %d references\n", count)
@ -268,8 +265,11 @@ Forgejo or set your environment appropriately.`, "")
count = 0
lastline = 0
}
} else {
fmt.Fprintf(out, ".")
}
if lastline >= hookBatchSize {
fmt.Fprint(out, "\n")
fmt.Fprintf(out, "\n")
lastline = 0
}
}
@ -286,7 +286,7 @@ Forgejo or set your environment appropriately.`, "")
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
}
} else if lastline > 0 {
fmt.Fprint(out, "\n")
fmt.Fprintf(out, "\n")
}
fmt.Fprintf(out, "Checked %d references in total\n", total)
@ -294,13 +294,13 @@ Forgejo or set your environment appropriately.`, "")
}
// runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update
func runHookUpdate(ctx context.Context, c *cli.Command) error {
func runHookUpdate(c *cli.Context) error {
// Now if we're an internal don't do anything else
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
ctx, cancel := installSignals(ctx)
ctx, cancel := installSignals()
defer cancel()
if c.NArg() != 3 {
@ -326,11 +326,11 @@ func runHookUpdate(ctx context.Context, c *cli.Command) error {
return fail(ctx, fmt.Sprintf("The modification of %s is skipped as it's an internal reference.", refFullName), "")
}
func runHookPostReceive(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runHookPostReceive(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), true)
setup(ctx, c.Bool("debug"))
// First of all run update-server-info no matter what
if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
@ -402,7 +402,7 @@ Forgejo or set your environment appropriately.`, "")
continue
}
fmt.Fprint(out, ".")
fmt.Fprintf(out, ".")
oldCommitIDs[count] = string(fields[0])
newCommitIDs[count] = string(fields[1])
refFullNames[count] = git.RefName(fields[2])
@ -490,11 +490,11 @@ func hookPrintResults(results []private.HookPostReceiveBranchResult) {
}
}
func runHookProcReceive(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runHookProcReceive(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), true)
setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
@ -505,6 +505,10 @@ Forgejo or set your environment appropriately.`, "")
return nil
}
if git.CheckGitVersionAtLeast("2.29") != nil {
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
}
reader := bufio.NewReader(os.Stdin)
repoUser := os.Getenv(repo_module.EnvRepoUsername)
repoName := os.Getenv(repo_module.EnvRepoName)

View file

@ -14,12 +14,12 @@ import (
"testing"
"time"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// Capture what's being written into a standard file descriptor.
@ -134,14 +134,14 @@ func TestDelayWriter(t *testing.T) {
defer ts.Close()
defer test.MockVariableValue(&setting.LocalURL, ts.URL+"/")()
app := cli.Command{}
app.Commands = []*cli.Command{subcmdHookPreReceive()}
app := cli.NewApp()
app.Commands = []*cli.Command{subcmdHookPreReceive}
t.Run("Should delay", func(t *testing.T) {
defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Millisecond*500)()
finish := captureOutput(t, os.Stdout)
err = app.Run(t.Context(), []string{"./forgejo", "pre-receive"})
err = app.Run([]string{"./forgejo", "pre-receive"})
require.NoError(t, err)
out := finish()
@ -153,7 +153,7 @@ func TestDelayWriter(t *testing.T) {
defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Second*5)()
finish := captureOutput(t, os.Stdout)
err = app.Run(t.Context(), []string{"./forgejo", "pre-receive"})
err = app.Run([]string{"./forgejo", "pre-receive"})
require.NoError(t, err)
out := finish()
@ -163,15 +163,15 @@ func TestDelayWriter(t *testing.T) {
}
func TestRunHookUpdate(t *testing.T) {
app := cli.Command{}
app.Commands = []*cli.Command{subcmdHookUpdate()}
app := cli.NewApp()
app.Commands = []*cli.Command{subcmdHookUpdate}
t.Run("Removal of internal reference", func(t *testing.T) {
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
defer test.MockVariableValue(&setting.IsProd, false)()
finish := captureOutput(t, os.Stderr)
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
err := app.Run([]string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
out := finish()
require.Error(t, err)
@ -183,7 +183,7 @@ func TestRunHookUpdate(t *testing.T) {
defer test.MockVariableValue(&setting.IsProd, false)()
finish := captureOutput(t, os.Stderr)
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000001"})
err := app.Run([]string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000001"})
out := finish()
require.Error(t, err)
@ -191,12 +191,12 @@ func TestRunHookUpdate(t *testing.T) {
})
t.Run("Removal of branch", func(t *testing.T) {
err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/head/main", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
err := app.Run([]string{"./forgejo", "update", "refs/head/main", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"})
require.NoError(t, err)
})
t.Run("Not enough arguments", func(t *testing.T) {
err := app.Run(t.Context(), []string{"./forgejo", "update"})
err := app.Run([]string{"./forgejo", "update"})
require.NoError(t, err)
})
}

View file

@ -4,20 +4,18 @@
package cmd
import (
"context"
"errors"
"fmt"
"strings"
"forgejo.org/modules/log"
"forgejo.org/modules/private"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// CmdKeys represents the available keys sub-command
func cmdKeys() *cli.Command {
return &cli.Command{
var CmdKeys = &cli.Command{
Name: "keys",
Usage: "(internal) Should only be called by SSH server",
Description: "Queries the Forgejo database to get the authorized command for a given ssh key fingerprint",
@ -49,10 +47,9 @@ func cmdKeys() *cli.Command {
Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)",
},
},
}
}
func runKeys(ctx context.Context, c *cli.Command) error {
func runKeys(c *cli.Context) error {
if !c.IsSet("username") {
return errors.New("No username provided")
}
@ -71,16 +68,16 @@ func runKeys(ctx context.Context, c *cli.Command) error {
return errors.New("No key type and content provided")
}
ctx, cancel := installSignals(ctx)
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), true)
setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
if extra.Error != nil {
return extra.Error
}
_, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text))
_, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text))
return nil
}

View file

@ -4,17 +4,16 @@
package cmd
import (
"context"
"fmt"
"forgejo.org/modules/private"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func runSendMail(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runSendMail(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setting.MustInstalled()

View file

@ -10,11 +10,11 @@ import (
"path/filepath"
"strings"
"forgejo.org/cmd/forgejo"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/cmd/forgejo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// cmdHelp is our own help subcommand with more information
@ -25,18 +25,18 @@ func cmdHelp() *cli.Command {
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(ctx context.Context, c *cli.Command) (err error) {
lineage := c.Lineage() // The order is from child to parent: help, doctor, Forgejo
Action: func(c *cli.Context) (err error) {
lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil}
targetCmdIdx := 0
if c.Name == "help" {
if c.Command.Name == "help" {
targetCmdIdx = 1
}
if targetCmdIdx+1 < len(lineage) {
err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1], lineage[targetCmdIdx].Name)
if lineage[targetCmdIdx+1].Command != nil {
err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name)
} else {
err = cli.ShowAppHelp(c)
}
_, _ = fmt.Fprintf(c.Root().Writer, `
_, _ = fmt.Fprintf(c.App.Writer, `
DEFAULT CONFIGURATION:
AppPath: %s
WorkPath: %s
@ -77,25 +77,25 @@ func appGlobalFlags() []cli.Flag {
}
}
func prepareSubcommandWithConfig(command *cli.Command, globalFlags func() []cli.Flag) {
command.Flags = append(globalFlags(), command.Flags...)
func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) {
command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...)
command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true
if command.Name != "help" {
command.Commands = append(command.Commands, cmdHelp())
command.Subcommands = append(command.Subcommands, cmdHelp())
}
for i := range command.Commands {
prepareSubcommandWithConfig(command.Commands[i], globalFlags)
for i := range command.Subcommands {
prepareSubcommandWithConfig(command.Subcommands[i], globalFlags)
}
}
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(_ context.Context, _ *cli.Command) error {
return func(ctx context.Context, cli *cli.Command) error {
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error {
return func(ctx *cli.Context) error {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
for _, curCtx := range cli.Lineage() {
for _, curCtx := range ctx.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
}
@ -107,24 +107,24 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(_ context.Context,
}
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
if cli.Bool("help") || action == nil {
if ctx.Bool("help") || action == nil {
// the default behavior of "urfave/cli": "nil action" means "show help"
return cmdHelp().Action(ctx, cli)
return cmdHelp().Action(ctx)
}
return action(ctx, cli)
return action(ctx)
}
}
func NewMainApp(version, versionExtra string) *cli.Command {
func NewMainApp(version, versionExtra string) *cli.App {
path, err := os.Executable()
if err != nil {
panic(err)
}
executable := filepath.Base(path)
subCmdsStandalone := make([]*cli.Command, 0, 10)
subCmdWithConfig := make([]*cli.Command, 0, 10)
globalFlags := func() []cli.Flag { return []cli.Flag{} }
var subCmdsStandalone []*cli.Command = make([]*cli.Command, 0, 10)
var subCmdWithConfig []*cli.Command = make([]*cli.Command, 0, 10)
var globalFlags []cli.Flag = make([]cli.Flag, 0, 10)
//
// If the executable is forgejo-cli, provide a Forgejo specific CLI
@ -133,16 +133,14 @@ func NewMainApp(version, versionExtra string) *cli.Command {
if executable == "forgejo-cli" {
subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdActions(context.Background()))
subCmdWithConfig = append(subCmdWithConfig, forgejo.CmdF3(context.Background()))
globalFlags = func() []cli.Flag {
return []cli.Flag{
globalFlags = append(globalFlags, []cli.Flag{
&cli.BoolFlag{
Name: "quiet",
},
&cli.BoolFlag{
Name: "verbose",
},
}
}
}...)
} else {
//
// Otherwise provide a Gitea compatible CLI which includes Forgejo
@ -151,54 +149,55 @@ func NewMainApp(version, versionExtra string) *cli.Command {
// binary and rename it to forgejo if they want.
//
subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdForgejo(context.Background()))
subCmdWithConfig = append(subCmdWithConfig, cmdActions())
subCmdWithConfig = append(subCmdWithConfig, CmdActions)
}
return innerNewMainApp(version, versionExtra, subCmdsStandalone, subCmdWithConfig, globalFlags)
}
func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmdWithConfigArgs []*cli.Command, globalFlagsArgs func() []cli.Flag) *cli.Command {
app := &cli.Command{}
app.Name = "forgejo"
func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmdWithConfigArgs []*cli.Command, globalFlagsArgs []cli.Flag) *cli.App {
app := cli.NewApp()
app.HelpName = "forgejo"
app.Name = "Forgejo"
app.Usage = "Beyond coding. We forge."
app.Description = `By default, forgejo will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".`
app.Version = version + versionExtra
app.EnableShellCompletion = true
app.EnableBashCompletion = true
// these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
cmdWeb(),
cmdServ(),
cmdHook(),
cmdKeys(),
cmdDump(),
cmdAdmin(),
cmdMigrate(),
cmdDoctor(),
cmdManager(),
cmdEmbedded(),
cmdMigrateStorage(),
cmdDumpRepository(),
cmdRestoreRepository(),
CmdWeb,
CmdServ,
CmdHook,
CmdKeys,
CmdDump,
CmdAdmin,
CmdMigrate,
CmdDoctor,
CmdManager,
CmdEmbedded,
CmdMigrateStorage,
CmdDumpRepository,
CmdRestoreRepository,
}
subCmdWithConfig = append(subCmdWithConfig, subCmdWithConfigArgs...)
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{
cmdCert(),
cmdGenerate(),
CmdCert,
CmdGenerate,
CmdDocs,
}
subCmdStandalone = append(subCmdStandalone, subCmdsStandaloneArgs...)
app.DefaultCommand = cmdWeb().Name
app.DefaultCommand = CmdWeb.Name
globalFlags := func() []cli.Flag {
return append(appGlobalFlags(), globalFlagsArgs()...)
}
globalFlags := appGlobalFlags()
globalFlags = append(globalFlags, globalFlagsArgs...)
app.Flags = append(app.Flags, cli.VersionFlag)
app.Flags = append(app.Flags, globalFlags()...)
app.Flags = append(app.Flags, globalFlags...)
app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig {
@ -211,8 +210,8 @@ func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmd
return app
}
func RunMainApp(app *cli.Command, args ...string) error {
err := app.Run(context.Background(), args)
func RunMainApp(app *cli.App, args ...string) error {
err := app.Run(args)
if err == nil {
return nil
}
@ -221,7 +220,7 @@ func RunMainApp(app *cli.Command, args ...string) error {
cli.OsExiter(1)
return err
}
_, _ = fmt.Fprintf(app.Root().ErrWriter, "Command error: %v\n", err)
_, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)
cli.OsExiter(1)
return err
}

View file

@ -4,21 +4,19 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"testing"
"forgejo.org/models/unittest"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func TestMain(m *testing.M) {
@ -29,10 +27,10 @@ func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
}
func newTestApp(testCmdAction func(_ context.Context, ctx *cli.Command) error) *cli.Command {
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
app := NewMainApp("version", "version-extra")
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
prepareSubcommandWithConfig(testCmd, appGlobalFlags)
prepareSubcommandWithConfig(testCmd, appGlobalFlags())
app.Commands = append(app.Commands, testCmd)
app.DefaultCommand = testCmd.Name
return app
@ -44,7 +42,7 @@ type runResult struct {
ExitCode int
}
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
func runTestApp(app *cli.App, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
@ -67,6 +65,7 @@ func TestCliCmd(t *testing.T) {
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
cli.CommandHelpTemplate = "(command help template)"
cli.AppHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
@ -110,17 +109,12 @@ func TestCliCmd(t *testing.T) {
},
}
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
defer test.MockProtect(&setting.AppWorkPath)()
defer test.MockProtect(&setting.CustomPath)()
defer test.MockProtect(&setting.CustomConf)()
app := newTestApp(func(_ context.Context, ctx *cli.Command) error {
_, _ = fmt.Fprint(ctx.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
app := newTestApp(func(ctx *cli.Context) error {
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
})
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
for k, v := range c.env {
t.Setenv(k, v)
}
@ -128,37 +122,37 @@ func TestCliCmd(t *testing.T) {
r, err := runTestApp(app, args...)
require.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
assert.Contains(t, r.Stdout, c.exp, c.cmd+"\n"+r.Stdout)
assert.Contains(t, r.Stdout, c.exp, c.cmd)
})
}
}
func TestCliCmdError(t *testing.T) {
app := newTestApp(func(_ context.Context, ctx *cli.Command) error { return errors.New("normal error") })
app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") })
r, err := runTestApp(app, "./gitea", "test-cmd")
require.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "", r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return cli.Exit("exit error", 2) })
app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) })
r, err = runTestApp(app, "./gitea", "test-cmd")
require.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "", r.Stdout)
assert.Equal(t, "exit error\n", r.Stderr)
app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return nil })
app = newTestApp(func(ctx *cli.Context) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
require.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout)
assert.Equal(t, "", r.Stderr) // the cli package's strange behavior, the error message is not in stderr ....
app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return nil })
app = newTestApp(func(ctx *cli.Context) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd")
require.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
assert.Empty(t, r.Stdout)
assert.Empty(t, r.Stderr)
assert.Equal(t, "", r.Stdout)
assert.Equal(t, "", r.Stderr)
}

View file

@ -4,34 +4,30 @@
package cmd
import (
"context"
"os"
"time"
"forgejo.org/modules/private"
"code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// CmdManager represents the manager command
func cmdManager() *cli.Command {
return &cli.Command{
var (
// CmdManager represents the manager command
CmdManager = &cli.Command{
Name: "manager",
Usage: "Manage the running forgejo process",
Description: "This is a command for managing the running forgejo process",
Commands: []*cli.Command{
subcmdShutdown(),
subcmdRestart(),
subcmdReloadTemplates(),
subcmdFlushQueues(),
subcmdLogging(),
subCmdProcesses(),
Subcommands: []*cli.Command{
subcmdShutdown,
subcmdRestart,
subcmdReloadTemplates,
subcmdFlushQueues,
subcmdLogging,
subCmdProcesses,
},
}
}
func subcmdShutdown() *cli.Command {
return &cli.Command{
subcmdShutdown = &cli.Command{
Name: "shutdown",
Usage: "Gracefully shutdown the running process",
Flags: []cli.Flag{
@ -41,10 +37,7 @@ func subcmdShutdown() *cli.Command {
},
Action: runShutdown,
}
}
func subcmdRestart() *cli.Command {
return &cli.Command{
subcmdRestart = &cli.Command{
Name: "restart",
Usage: "Gracefully restart the running process - (not implemented for windows servers)",
Flags: []cli.Flag{
@ -54,10 +47,7 @@ func subcmdRestart() *cli.Command {
},
Action: runRestart,
}
}
func subcmdReloadTemplates() *cli.Command {
return &cli.Command{
subcmdReloadTemplates = &cli.Command{
Name: "reload-templates",
Usage: "Reload template files in the running process",
Flags: []cli.Flag{
@ -67,10 +57,7 @@ func subcmdReloadTemplates() *cli.Command {
},
Action: runReloadTemplates,
}
}
func subcmdFlushQueues() *cli.Command {
return &cli.Command{
subcmdFlushQueues = &cli.Command{
Name: "flush-queues",
Usage: "Flush queues in the running process",
Action: runFlushQueues,
@ -89,10 +76,7 @@ func subcmdFlushQueues() *cli.Command {
},
},
}
}
func subCmdProcesses() *cli.Command {
return &cli.Command{
subCmdProcesses = &cli.Command{
Name: "processes",
Usage: "Display running processes within the current process",
Action: runProcesses,
@ -122,49 +106,49 @@ func subCmdProcesses() *cli.Command {
},
},
}
}
)
func runShutdown(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runShutdown(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.Shutdown(ctx)
return handleCliResponseExtra(extra)
}
func runRestart(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRestart(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.Restart(ctx)
return handleCliResponseExtra(extra)
}
func runReloadTemplates(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runReloadTemplates(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.ReloadTemplates(ctx)
return handleCliResponseExtra(extra)
}
func runFlushQueues(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runFlushQueues(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
return handleCliResponseExtra(extra)
}
func runProcesses(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runProcesses(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
return handleCliResponseExtra(extra)
}

View file

@ -4,19 +4,18 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"forgejo.org/modules/log"
"forgejo.org/modules/private"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
func defaultLoggingFlags() []cli.Flag {
return []cli.Flag{
var (
defaultLoggingFlags = []cli.Flag{
&cli.StringFlag{
Name: "logger",
Usage: `Logger name - will default to "default"`,
@ -44,11 +43,6 @@ func defaultLoggingFlags() []cli.Flag {
Aliases: []string{"e"},
Usage: "Matching expression for the logger",
},
&cli.StringFlag{
Name: "exclusion",
Aliases: []string{"x"},
Usage: "Exclusion for the logger",
},
&cli.StringFlag{
Name: "prefix",
Aliases: []string{"p"},
@ -62,13 +56,11 @@ func defaultLoggingFlags() []cli.Flag {
Name: "debug",
},
}
}
func subcmdLogging() *cli.Command {
return &cli.Command{
subcmdLogging = &cli.Command{
Name: "logging",
Usage: "Adjust logging commands",
Commands: []*cli.Command{
Subcommands: []*cli.Command{
{
Name: "pause",
Usage: "Pause logging (Forgejo will buffer logs up to a certain point and will drop them after that point)",
@ -112,11 +104,11 @@ func subcmdLogging() *cli.Command {
}, {
Name: "add",
Usage: "Add a logger",
Commands: []*cli.Command{
Subcommands: []*cli.Command{
{
Name: "file",
Usage: "Add a file logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
Flags: append(defaultLoggingFlags, []cli.Flag{
&cli.StringFlag{
Name: "filename",
Aliases: []string{"f"},
@ -160,7 +152,7 @@ func subcmdLogging() *cli.Command {
}, {
Name: "conn",
Usage: "Add a net conn logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
Flags: append(defaultLoggingFlags, []cli.Flag{
&cli.BoolFlag{
Name: "reconnect-on-message",
Aliases: []string{"R"},
@ -201,13 +193,13 @@ func subcmdLogging() *cli.Command {
},
},
}
}
)
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRemoveLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
logger := c.String("logger")
if len(logger) == 0 {
logger = log.DEFAULT
@ -218,11 +210,11 @@ func runRemoveLogger(ctx context.Context, c *cli.Command) error {
return handleCliResponseExtra(extra)
}
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runAddConnLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "conn"
vals["net"] = "tcp"
@ -245,14 +237,14 @@ func runAddConnLogger(ctx context.Context, c *cli.Command) error {
if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
}
return commonAddLogger(ctx, c, mode, vals)
return commonAddLogger(c, mode, vals)
}
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runAddFileLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "file"
if c.IsSet("filename") {
@ -278,10 +270,10 @@ func runAddFileLogger(ctx context.Context, c *cli.Command) error {
if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level")
}
return commonAddLogger(ctx, c, mode, vals)
return commonAddLogger(c, mode, vals)
}
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String()
}
@ -291,9 +283,6 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
if len(c.String("expression")) > 0 {
vals["expression"] = c.String("expression")
}
if len(c.String("exclusion")) > 0 {
vals["exclusion"] = c.String("exclusion")
}
if len(c.String("prefix")) > 0 {
vals["prefix"] = c.String("prefix")
}
@ -311,47 +300,47 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
if c.IsSet("writer") {
writer = c.String("writer")
}
ctx, cancel := installSignals(ctx)
ctx, cancel := installSignals()
defer cancel()
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}
func runPauseLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runPauseLogging(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
func runResumeLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runResumeLogging(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
userMsg := private.ResumeLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runReleaseReopenLogging(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
userMsg := private.ReleaseReopenLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
func runSetLogSQL(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runSetLogSQL(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"), false)
setup(ctx, c.Bool("debug"))
extra := private.SetLogSQL(ctx, !c.Bool("off"))
return handleCliResponseExtra(extra)

View file

@ -6,26 +6,24 @@ package cmd
import (
"context"
"forgejo.org/models/db"
"forgejo.org/models/migrations"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// CmdMigrate represents the available migrate sub-command.
func cmdMigrate() *cli.Command {
return &cli.Command{
var CmdMigrate = &cli.Command{
Name: "migrate",
Usage: "Migrate the database",
Description: "This is a command for migrating the database, so that you can run 'forgejo admin user create' before starting the server.",
Action: runMigrate,
}
}
func runMigrate(stdCtx context.Context, ctx *cli.Command) error {
stdCtx, cancel := installSignals(stdCtx)
func runMigrate(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
if err := initDB(stdCtx); err != nil {
@ -38,13 +36,7 @@ func runMigrate(stdCtx context.Context, ctx *cli.Command) error {
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
if err := db.InitEngineWithMigration(context.Background(), func(dbEngine db.Engine) error {
masterEngine, err := db.GetMasterEngine(dbEngine)
if err != nil {
return err
}
return migrations.Migrate(masterEngine)
}); err != nil {
if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil {
log.Fatal("Failed to initialize ORM engine: %v", err)
return err
}

View file

@ -10,25 +10,23 @@ import (
"io/fs"
"strings"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
"forgejo.org/models/migrations"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/log"
packages_module "forgejo.org/modules/packages"
"forgejo.org/modules/setting"
"forgejo.org/modules/storage"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/migrations"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"github.com/urfave/cli/v3"
"xorm.io/xorm"
"github.com/urfave/cli/v2"
)
// CmdMigrateStorage represents the available migrate storage sub-command.
func cmdMigrateStorage() *cli.Command {
return &cli.Command{
var CmdMigrateStorage = &cli.Command{
Name: "migrate-storage",
Usage: "Migrate the storage",
Description: "Copies stored files from storage configured in app.ini to parameter-configured storage",
@ -96,7 +94,6 @@ func cmdMigrateStorage() *cli.Command {
Usage: "Minio checksum algorithm (default/md5)",
},
},
}
}
func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error {
@ -184,8 +181,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
})
}
func runMigrateStorage(stdCtx context.Context, ctx *cli.Command) error {
stdCtx, cancel := installSignals(stdCtx)
func runMigrateStorage(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
if err := initDB(stdCtx); err != nil {
@ -198,9 +195,7 @@ func runMigrateStorage(stdCtx context.Context, ctx *cli.Command) error {
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
if err := db.InitEngineWithMigration(context.Background(), func(e db.Engine) error {
return migrations.Migrate(e.(*xorm.Engine))
}); err != nil {
if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil {
log.Fatal("Failed to initialize ORM engine: %v", err)
return err
}

View file

@ -9,16 +9,16 @@ import (
"strings"
"testing"
"forgejo.org/models/actions"
"forgejo.org/models/db"
"forgejo.org/models/packages"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
packages_module "forgejo.org/modules/packages"
"forgejo.org/modules/setting"
"forgejo.org/modules/storage"
"forgejo.org/modules/test"
packages_service "forgejo.org/services/packages"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -81,8 +81,8 @@ func TestMigratePackages(t *testing.T) {
entries, err := os.ReadDir(p)
require.NoError(t, err)
assert.Len(t, entries, 2)
assert.Equal(t, "01", entries[0].Name())
assert.Equal(t, "tmp", entries[1].Name())
assert.EqualValues(t, "01", entries[0].Name())
assert.EqualValues(t, "tmp", entries[1].Name())
}
func TestMigrateActionsArtifacts(t *testing.T) {

View file

@ -4,18 +4,16 @@
package cmd
import (
"context"
"strings"
"forgejo.org/modules/private"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// CmdRestoreRepository represents the available restore a repository sub-command.
func cmdRestoreRepository() *cli.Command {
return &cli.Command{
var CmdRestoreRepository = &cli.Command{
Name: "restore-repo",
Usage: "Restore the repository from disk",
Description: "This is a command for restoring the repository data.",
@ -48,11 +46,10 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
Usage: "Sanity check the content of the files before trying to load them",
},
},
}
}
func runRestoreRepository(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runRestoreRepository(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setting.MustInstalled()

View file

@ -18,22 +18,22 @@ import (
"time"
"unicode"
asymkey_model "forgejo.org/models/asymkey"
git_model "forgejo.org/models/git"
"forgejo.org/models/perm"
"forgejo.org/modules/git"
"forgejo.org/modules/json"
"forgejo.org/modules/log"
"forgejo.org/modules/pprof"
"forgejo.org/modules/private"
"forgejo.org/modules/process"
repo_module "forgejo.org/modules/repository"
"forgejo.org/modules/setting"
"forgejo.org/services/lfs"
asymkey_model "code.gitea.io/gitea/models/asymkey"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/pprof"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/process"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/lfs"
"github.com/golang-jwt/jwt/v5"
"github.com/kballard/go-shellquote"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
const (
@ -41,8 +41,7 @@ const (
)
// CmdServ represents the available serv sub-command.
func cmdServ() *cli.Command {
return &cli.Command{
var CmdServ = &cli.Command{
Name: "serv",
Usage: "(internal) Should only be called by SSH shell",
Description: "Serv provides access auth for repositories",
@ -56,26 +55,22 @@ func cmdServ() *cli.Command {
Name: "debug",
},
},
}
}
func setup(ctx context.Context, debug, gitNeeded bool) {
func setup(ctx context.Context, debug bool) {
if debug {
setupConsoleLogger(log.TRACE, false, os.Stderr)
} else {
setupConsoleLogger(log.FATAL, false, os.Stderr)
}
setting.MustInstalled()
// Sanity check to ensure path is not relative, see: https://github.com/go-gitea/gitea/pull/19317
if _, err := os.Stat(setting.RepoRootPath); err != nil {
_ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
return
}
if gitNeeded {
if err := git.InitSimple(context.Background()); err != nil {
_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
}
}
}
var (
@ -88,14 +83,6 @@ var (
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
func sshLog(ctx context.Context, level log.Level, message string) error {
if testing.Testing() || setting.InternalToken == "" {
return nil
}
return private.SSHLog(ctx, level, message)
}
// fail prints message to stdout, it's mainly used for git serv and git hook commands.
// The output will be passed to git client and shown to user.
func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error {
@ -120,7 +107,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
logMsg = userMessage + ". " + logMsg
}
}
_ = sshLog(ctx, log.ERROR, logMsg)
// Don't send an log if this is done in a test and no InternalToken is set.
if !testing.Testing() || setting.InternalToken != "" {
_ = private.SSHLog(ctx, true, logMsg)
}
}
return cli.Exit("", 1)
}
@ -138,12 +128,12 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
return nil
}
func runServ(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
func runServ(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
// FIXME: This needs to internationalised
setup(ctx, c.Bool("debug"), true)
setup(ctx, c.Bool("debug"))
if setting.SSH.Disabled {
fmt.Println("Forgejo: SSH has been disabled")
@ -198,11 +188,13 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
if len(words) < 2 {
if git.CheckGitVersionAtLeast("2.29") == nil {
// for AGit Flow
if cmd == "ssh_info" {
fmt.Print(`{"type":"agit","version":1}`)
fmt.Print(`{"type":"gitea","version":1}`)
return nil
}
}
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
}
@ -261,12 +253,11 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
if verb == lfsAuthenticateVerb {
switch lfsVerb {
case "upload":
if lfsVerb == "upload" {
requestedMode = perm.AccessModeWrite
case "download":
} else if lfsVerb == "download" {
requestedMode = perm.AccessModeRead
default:
} else {
return fail(ctx, "Unknown LFS verb", "Unknown lfs verb %s", lfsVerb)
}
}

View file

@ -16,25 +16,24 @@ import (
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
"forgejo.org/modules/container"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/process"
"forgejo.org/modules/public"
"forgejo.org/modules/setting"
"forgejo.org/routers"
"forgejo.org/routers/install"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"
"github.com/felixge/fgprof"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
)
// PIDFile could be set from build tag
var PIDFile = "/run/gitea.pid"
// CmdWeb represents the available web sub-command.
func cmdWeb() *cli.Command {
return &cli.Command{
var CmdWeb = &cli.Command{
Name: "web",
Usage: "Start the Forgejo web server",
Description: `The Forgejo web server is the only thing you need to run,
@ -69,7 +68,6 @@ and it takes care of all the other things for you`,
Usage: "Set initial logging to TRACE level until logging is properly set-up",
},
},
}
}
func runHTTPRedirector() {
@ -130,7 +128,7 @@ func showWebStartupMessage(msg string) {
}
}
func serveInstall(_ context.Context, ctx *cli.Command) error {
func serveInstall(ctx *cli.Context) error {
showWebStartupMessage("Prepare to run install page")
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
@ -163,7 +161,7 @@ func serveInstall(_ context.Context, ctx *cli.Command) error {
return nil
}
func serveInstalled(_ context.Context, ctx *cli.Command) error {
func serveInstalled(ctx *cli.Context) error {
setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings()
setting.MustInstalled()
@ -200,6 +198,9 @@ func serveInstalled(_ context.Context, ctx *cli.Command) error {
for fn := range publicFilesSet.Seq() {
log.Error("Found legacy public asset %q in CustomPath. Please move it to %s/public/assets/%s", fn, setting.CustomPath, fn)
}
if _, err := os.Stat(filepath.Join(setting.CustomPath, "robots.txt")); err == nil {
log.Error(`Found legacy public asset "robots.txt" in CustomPath. Please move it to %s/public/robots.txt`, setting.CustomPath)
}
routers.InitWebInstalled(graceful.GetManager().HammerContext())
@ -235,7 +236,7 @@ func servePprof() {
finished()
}
func runWeb(ctx context.Context, cli *cli.Command) error {
func runWeb(ctx *cli.Context) error {
defer func() {
if panicked := recover(); panicked != nil {
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
@ -253,12 +254,12 @@ func runWeb(ctx context.Context, cli *cli.Command) error {
}
// Set pid file setting
if cli.IsSet("pid") {
createPIDFile(cli.String("pid"))
if ctx.IsSet("pid") {
createPIDFile(ctx.String("pid"))
}
if !setting.InstallLock {
if err := serveInstall(ctx, cli); err != nil {
if err := serveInstall(ctx); err != nil {
return err
}
} else {
@ -269,7 +270,7 @@ func runWeb(ctx context.Context, cli *cli.Command) error {
go servePprof()
}
return serveInstalled(ctx, cli)
return serveInstalled(ctx)
}
func setPort(port string) error {

View file

@ -12,11 +12,10 @@ import (
"strconv"
"strings"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/process"
"forgejo.org/modules/proxy"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"github.com/caddyserver/certmagic"
)
@ -55,8 +54,8 @@ func runACME(listenAddr string, m http.Handler) error {
altTLSALPNPort = p
}
certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
magic := certmagic.NewDefault()
magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
// Try to use private CA root if provided, otherwise defaults to system's trust
var certPool *x509.CertPool
if setting.AcmeCARoot != "" {
@ -66,8 +65,7 @@ func runACME(listenAddr string, m http.Handler) error {
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
}
}
certmagic.DefaultACME = certmagic.ACMEIssuer{
myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
CA: setting.AcmeURL,
TrustedRoots: certPool,
Email: setting.AcmeEmail,
@ -77,17 +75,7 @@ func runACME(listenAddr string, m http.Handler) error {
ListenHost: setting.HTTPAddr,
AltTLSALPNPort: altTLSALPNPort,
AltHTTPPort: altHTTPPort,
HTTPProxy: proxy.Proxy(),
}
// Preserve behavior to use Let's encrypt test CA when Let's encrypt is CA.
if certmagic.DefaultACME.CA == certmagic.LetsEncryptProductionCA {
certmagic.DefaultACME.TestCA = certmagic.LetsEncryptStagingCA
}
magic := certmagic.NewDefault()
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
})
magic.Issuers = []certmagic.Issuer{myACME}

View file

@ -9,9 +9,9 @@ import (
"net/http/fcgi"
"strings"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
func runHTTP(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {

View file

@ -9,9 +9,9 @@ import (
"os"
"strings"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/klauspost/cpuid/v2"
)

View file

@ -1,3 +1,4 @@
#! /bin/bash
# Heavily inspired by https://github.com/urfave/cli
_cli_bash_autocomplete() {
@ -6,9 +7,9 @@ _cli_bash_autocomplete() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-shell-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-shell-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0

Some files were not shown because too many files have changed in this diff Show more