Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c6ea6728 | ||
|
|
67977f0b1c | ||
|
|
78fbcf35ad | ||
|
|
8f5b1d27d4 | ||
|
|
89c99a4dcb | ||
|
|
3c7e7a19dd | ||
|
|
8313b5d998 | ||
|
|
6ca73bf662 | ||
|
|
5e10def7f7 | ||
|
|
1b8efb6fc7 | ||
|
|
8f89e1e174 | ||
|
|
cbc595b9d9 | ||
|
|
cc5ccf44dc | ||
|
|
f91e35b8b7 | ||
|
|
f52ed422dc | ||
|
|
0266ee5de7 | ||
|
|
ac03e65cf4 | ||
|
|
f3e6672c09 | ||
|
|
136ec9ef81 | ||
|
|
79018ae726 | ||
|
|
e11176192a | ||
|
|
4e0269e890 | ||
|
|
04114c637a | ||
|
|
e5540bfa81 | ||
|
|
d22d6ca0d8 | ||
|
|
d49feab428 | ||
|
|
9162f4403a | ||
|
|
d05cf08fad | ||
|
|
f4b4b0bf98 | ||
|
|
99596044d7 | ||
|
|
693d26914f | ||
|
|
315f197790 | ||
|
|
76b8f0c3a7 | ||
|
|
f99bbd7f3f | ||
|
|
f7ef657b5a | ||
|
|
486d274be6 | ||
|
|
ab3d2a944c | ||
|
|
12bfa9e83d | ||
|
|
dd661e92df | ||
|
|
0b31272c7e | ||
|
|
ec0c418719 | ||
|
|
6dc19fc29a | ||
|
|
9f1baa7d18 | ||
|
|
e13deb7a16 | ||
|
|
e5c1b8b632 | ||
|
|
e931b62f33 | ||
|
|
81ee93e5bc | ||
|
|
053f9186bc | ||
|
|
68fcdb6122 | ||
|
|
14ca309c39 | ||
|
|
4aba42519d | ||
|
|
9adf175df0 | ||
|
|
c3fa2a8729 | ||
|
|
89dfed32e0 | ||
|
|
d5062d0c27 | ||
|
|
90e9e79232 | ||
|
|
c6467edcb1 | ||
|
|
5d5b695527 | ||
|
|
0af7a7b79f | ||
|
|
9339661078 | ||
|
|
1e69f085d6 | ||
|
|
0bfccd8ecf | ||
|
|
534b9b35dd | ||
|
|
dbadc59b56 | ||
|
|
a57e2c4bc3 | ||
|
|
acd4e10990 | ||
|
|
0a1df294c8 | ||
|
|
52a964d1fc | ||
|
|
d3dbe0d9ce | ||
|
|
cdbbdbef06 | ||
|
|
79f555d465 | ||
|
|
ae2b795693 | ||
|
|
d1fdbf46bd | ||
|
|
f27a75564a | ||
|
|
958d0db4f4 | ||
|
|
4c2441ba5d | ||
|
|
6f5f0be9e3 | ||
|
|
23d2d224c2 | ||
|
|
a43d829de8 | ||
|
|
8ab1363fef | ||
|
|
178fd90852 | ||
|
|
b39f7a37d1 | ||
|
|
b9ed8fceff | ||
|
|
e6ce72b14a | ||
|
|
2eecd58bbe | ||
|
|
64b9b21790 | ||
|
|
3290aff964 | ||
|
|
7ed1e8987e | ||
|
|
f10e909fce | ||
|
|
a3b25436f2 | ||
|
|
b947bc4363 | ||
|
|
18dc41d6f8 | ||
|
|
bf5d00074d | ||
|
|
fb4e9f92f9 | ||
|
|
468d1919b5 | ||
|
|
1b788946a7 | ||
|
|
e8646ad1d8 | ||
|
|
29dc9c784e | ||
|
|
b1cc4bf77f | ||
|
|
d35161ceb8 | ||
|
|
8defca6d39 | ||
|
|
fac434da0a | ||
|
|
e18eae7129 | ||
|
|
c60bc26fd3 | ||
|
|
bacc69db83 | ||
|
|
c5da032193 | ||
|
|
3ace45c118 | ||
|
|
5d6c5ce71a | ||
|
|
7baa6fa47c | ||
|
|
f9a0b077a7 | ||
|
|
d3317ebabe | ||
|
|
e9481e1da3 | ||
|
|
8965c068e9 | ||
|
|
eaaa158df3 | ||
|
|
f5498421c4 | ||
|
|
a6a14c9a92 | ||
|
|
d0ec1788b8 | ||
|
|
c1202f1b57 | ||
|
|
1162cbccc0 | ||
|
|
038990e0ff | ||
|
|
03ff09870d | ||
|
|
8bf4f2cc8f | ||
|
|
21731c1370 | ||
|
|
a0e272d95a | ||
|
|
47537a8361 | ||
|
|
d018c1b4b1 | ||
|
|
d2cbe2fba0 | ||
|
|
d6233c25b5 | ||
|
|
2bf2d00c8a | ||
|
|
9bd56a8ba0 | ||
|
|
a1dc3c9bd1 | ||
|
|
47ee84d1f3 | ||
|
|
89f1df033a | ||
|
|
94b67f1967 | ||
|
|
0a9a84df11 | ||
|
|
cdac263bb8 | ||
|
|
a5c7df7a4c | ||
|
|
6d738fecc4 | ||
|
|
38cc7453e2 | ||
|
|
b44175c071 | ||
|
|
947358dffe | ||
|
|
be1090cb2d | ||
|
|
c8f3402841 | ||
|
|
a3a95a0b67 | ||
|
|
ed527b664d | ||
|
|
e4717d426e | ||
|
|
16f15d2f7b | ||
|
|
b3f5196241 | ||
|
|
6c5f0af45d | ||
|
|
c95cb7c7e2 | ||
|
|
6747e3e0eb | ||
|
|
a12b5b3640 | ||
|
|
834dad8cef |
@@ -4,7 +4,7 @@ tmp_dir = ".air"
|
||||
[build]
|
||||
pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs
|
||||
cmd = "make --no-print-directory backend"
|
||||
entrypoint = ["./gitea"]
|
||||
bin = "gitea"
|
||||
delay = 2000
|
||||
include_ext = ["go", "tmpl"]
|
||||
include_file = ["main.go"]
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
{
|
||||
"name": "Gitea DevContainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"containerEnv": {
|
||||
// override "local" from packaged version
|
||||
"GOTOOLCHAIN": "auto"
|
||||
},
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm",
|
||||
"features": {
|
||||
// installs nodejs into container
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "latest"
|
||||
"version": "20"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
|
||||
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
|
||||
"ghcr.io/devcontainers-extra/features/poetry:2": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"version": "3.13"
|
||||
"version": "3.12"
|
||||
},
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
|
||||
@@ -36,6 +36,15 @@ _testmain.go
|
||||
coverage.all
|
||||
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
|
||||
*.log
|
||||
|
||||
@@ -65,7 +74,6 @@ cpu.out
|
||||
/yarn.lock
|
||||
/yarn-error.log
|
||||
/npm-debug.log*
|
||||
/pnpm-debug.log*
|
||||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
@@ -74,9 +82,6 @@ cpu.out
|
||||
/VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
/Dockerfile
|
||||
/Dockerfile.rootless
|
||||
/.venv
|
||||
|
||||
# Files and folders that were previously generated
|
||||
/public/assets/img/webpack
|
||||
|
||||
@@ -25,10 +25,6 @@ insert_final_newline = false
|
||||
[templates/user/auth/oidc_wellknown.tmpl]
|
||||
indent_style = space
|
||||
|
||||
[templates/shared/actions/runner_badge_*.tmpl]
|
||||
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
|
||||
charset = unset
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
|
||||
1004
.eslintrc.cjs
Normal file
1004
.eslintrc.cjs
Normal file
File diff suppressed because it is too large
Load Diff
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -8,4 +8,3 @@
|
||||
/vendor/** -text -eol linguist-vendored
|
||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||
Dockerfile.* linguist-language=Dockerfile
|
||||
Makefile.* linguist-language=Makefile
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Repository Health Report
|
||||
|
||||
## Overall Score: 77/100 (Good)
|
||||
|
||||
### Summary
|
||||
- Commits Analyzed: 1000
|
||||
- Branches: 1
|
||||
- Authors: 149
|
||||
- Merges: 0
|
||||
|
||||
### Component Scores
|
||||
| Component | Score |
|
||||
|-----------|-------|
|
||||
| Messages | 90% |
|
||||
| Merges | 97% |
|
||||
| Duplicates | 0% |
|
||||
| Branches | 100% |
|
||||
| Authorship | 100% |
|
||||
|
||||
### Issues (3)
|
||||
- **Merge fix commits detected**: Found 1 commits with messages like 'fix merge' detected after merges. (-3 pts)
|
||||
- **Duplicate commits with identical content**: Found 1 groups of commits with identical file content (1 redundant commits). These are safe to squash as they have the same tree SHA. (-7 pts)
|
||||
- **Commits with duplicate messages**: Found 1 groups of commits with identical messages but different code changes (135 commits). Consider using more descriptive messages to differentiate changes. (-1 pts)
|
||||
|
||||
---
|
||||
Generated by GitCleaner for GitCaddy dd
|
||||
@@ -1,308 +0,0 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org,direct
|
||||
GOPRIVATE: git.marketally.com
|
||||
GONOSUMDB: git.marketally.com
|
||||
GO_VERSION: "1.25"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
# Lint job - must pass
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
|
||||
- name: Run Go linter
|
||||
run: make lint-go
|
||||
|
||||
- name: Run frontend linter
|
||||
run: make lint-frontend
|
||||
continue-on-error: true
|
||||
|
||||
# Unit tests with SQLite (no external database needed)
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
# Skip tests that require external services (Redis, Elasticsearch, Meilisearch, Azure, SHA256 git)
|
||||
go test -tags="sqlite sqlite_unlock_notify" -race \
|
||||
-skip "TestRepoStatsIndex|TestRenderHelper|Sha256|SHA256|Redis|redis|Elasticsearch|Meilisearch|AzureBlob|TestLockAndDo|TestLocker|TestBaseRedis" \
|
||||
./modules/... \
|
||||
./services/...
|
||||
env:
|
||||
GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT: true
|
||||
|
||||
# Integration tests with PostgreSQL
|
||||
test-pgsql:
|
||||
name: Integration Tests (PostgreSQL)
|
||||
runs-on: linux-latest
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: testgitea
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
|
||||
- name: Build frontend
|
||||
run: make frontend
|
||||
|
||||
- name: Generate bindata
|
||||
run: make generate
|
||||
env:
|
||||
TAGS: bindata
|
||||
|
||||
- name: Build test binary
|
||||
run: |
|
||||
go build -tags="bindata sqlite sqlite_unlock_notify" -o gitea .
|
||||
|
||||
- name: Generate test config
|
||||
run: |
|
||||
make generate-ini-pgsql
|
||||
env:
|
||||
TEST_PGSQL_HOST: localhost:5432
|
||||
TEST_PGSQL_DBNAME: testgitea
|
||||
TEST_PGSQL_USERNAME: postgres
|
||||
TEST_PGSQL_PASSWORD: postgres
|
||||
TEST_PGSQL_SCHEMA: gtestschema
|
||||
|
||||
- name: Run PostgreSQL integration tests
|
||||
run: |
|
||||
make test-pgsql
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_PGSQL_HOST: localhost:5432
|
||||
TEST_PGSQL_DBNAME: testgitea
|
||||
TEST_PGSQL_USERNAME: postgres
|
||||
TEST_PGSQL_PASSWORD: postgres
|
||||
TEST_PGSQL_SCHEMA: gtestschema
|
||||
GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT: true
|
||||
|
||||
# Create release job - runs first to create the release before build jobs upload
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: linux-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
outputs:
|
||||
release_id: ${{ steps.create.outputs.release_id }}
|
||||
steps:
|
||||
- name: Create or get release
|
||||
id: create
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
echo "Creating/getting release for tag: $TAG"
|
||||
|
||||
# Try to get existing release first
|
||||
EXISTING=$(curl -sf \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases/tags/$TAG" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$EXISTING" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$EXISTING" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Found existing release: $RELEASE_ID"
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create new release
|
||||
echo "Creating new release..."
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag_name":"'"$TAG"'","name":"Gitea '"$TAG"'","body":"Official release of Gitea '"$TAG"'.","draft":false,"prerelease":false}' \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases" 2>&1)
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Created release: $RELEASE_ID"
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ERROR: Failed to create release: $RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build job for binaries
|
||||
build:
|
||||
name: Build Binaries
|
||||
runs-on: linux-latest
|
||||
needs: [lint, create-release]
|
||||
if: always() && needs.lint.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
|
||||
- name: Build frontend
|
||||
run: make frontend
|
||||
|
||||
- name: Generate bindata
|
||||
run: make generate
|
||||
env:
|
||||
TAGS: bindata
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
LDFLAGS="-X code.gitea.io/gitea/modules/setting.AppVer=${VERSION}"
|
||||
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
EXT=".exe"
|
||||
fi
|
||||
|
||||
OUTPUT="gitea-${VERSION}-${GOOS}-${GOARCH}${EXT}"
|
||||
|
||||
go build -v -trimpath -tags "${TAGS}" -ldflags "${LDFLAGS}" -o "dist/${OUTPUT}" .
|
||||
|
||||
# Create checksum
|
||||
cd dist && sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"
|
||||
|
||||
- name: Upload to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
run: |
|
||||
set -e
|
||||
echo "Uploading binaries to release ID: $RELEASE_ID"
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "ERROR: No release ID provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload files with retry
|
||||
for file in dist/*; do
|
||||
if [ -f "$file" ]; then
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading $filename..."
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
UPLOAD_RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@$file" \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=$filename" 2>&1 || echo "")
|
||||
|
||||
if echo "$UPLOAD_RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
echo "✓ Uploaded $filename successfully"
|
||||
break
|
||||
else
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "Attempt $attempt failed, retrying in 5s..."
|
||||
sleep 5
|
||||
else
|
||||
echo "✗ Failed to upload $filename after 3 attempts: $UPLOAD_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
echo "All uploads complete!"
|
||||
@@ -1,113 +0,0 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org,direct
|
||||
GOPRIVATE: git.marketally.com
|
||||
GONOSUMDB: git.marketally.com
|
||||
GO_VERSION: "1.25"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
# Quick lint checks - must pass
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install Go dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Check Go formatting
|
||||
run: |
|
||||
if [ -n "$(gofmt -l .)" ]; then
|
||||
echo "Go code is not formatted. Please run 'gofmt -w .'"
|
||||
gofmt -l .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Go linter
|
||||
run: |
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 run
|
||||
|
||||
- name: Check for build errors
|
||||
run: go build -v ./...
|
||||
|
||||
# Unit tests
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
# Skip tests that require external services (Redis, Elasticsearch, Meilisearch, Azure, SHA256 git)
|
||||
go test -tags="sqlite sqlite_unlock_notify" -race \
|
||||
-skip "TestRepoStatsIndex|TestRenderHelper|Sha256|SHA256|Redis|redis|Elasticsearch|Meilisearch|AzureBlob|TestLockAndDo|TestLocker|TestBaseRedis" \
|
||||
./modules/... \
|
||||
./services/...
|
||||
env:
|
||||
GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT: true
|
||||
|
||||
# Frontend checks
|
||||
frontend:
|
||||
name: Frontend Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
continue-on-error: true
|
||||
|
||||
- name: TypeScript check
|
||||
run: pnpm run tsc
|
||||
continue-on-error: true
|
||||
|
||||
- name: ESLint
|
||||
run: pnpm run eslint
|
||||
continue-on-error: true
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
labels: [modifies/dependencies]
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
cooldown:
|
||||
default-days: 5
|
||||
17
.github/labeler.yml
vendored
17
.github/labeler.yml
vendored
@@ -46,11 +46,12 @@ modifies/internal:
|
||||
- ".gitpod.yml"
|
||||
- ".markdownlint.yaml"
|
||||
- ".spectral.yaml"
|
||||
- "stylelint.config.ts"
|
||||
- "stylelint.config.js"
|
||||
- ".yamllint.yaml"
|
||||
- ".github/**"
|
||||
- ".gitea/**"
|
||||
- ".devcontainer/**"
|
||||
- "build.go"
|
||||
- "build/**"
|
||||
- "contrib/**"
|
||||
|
||||
@@ -58,9 +59,9 @@ modifies/dependencies:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "package-lock.json"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- "poetry.lock"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
|
||||
@@ -80,13 +81,3 @@ docs-update-needed:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "custom/conf/app.example.ini"
|
||||
|
||||
topic/code-linting:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- ".eslintrc.cjs"
|
||||
- ".golangci.yml"
|
||||
- ".markdownlint.yaml"
|
||||
- ".spectral.yaml"
|
||||
- ".yamllint.yaml"
|
||||
- "stylelint.config.ts"
|
||||
|
||||
8
.github/workflows/cron-licenses.yml
vendored
8
.github/workflows/cron-licenses.yml
vendored
@@ -9,18 +9,16 @@ jobs:
|
||||
cron-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- run: make generate-gitignore
|
||||
timeout-minutes: 40
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@v1.0.0
|
||||
uses: appleboy/git-push-action@v0.0.3
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
|
||||
8
.github/workflows/cron-translations.yml
vendored
8
.github/workflows/cron-translations.yml
vendored
@@ -9,11 +9,9 @@ jobs:
|
||||
crowdin-pull:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: crowdin/github-action@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crowdin/github-action@v1
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
@@ -29,7 +27,7 @@ jobs:
|
||||
- name: update locales
|
||||
run: ./build/update-locales.sh
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@v1.0.0
|
||||
uses: appleboy/git-push-action@v0.0.3
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
|
||||
21
.github/workflows/files-changed.yml
vendored
21
.github/workflows/files-changed.yml
vendored
@@ -19,15 +19,11 @@ on:
|
||||
value: ${{ jobs.detect.outputs.swagger }}
|
||||
yaml:
|
||||
value: ${{ jobs.detect.outputs.yaml }}
|
||||
json:
|
||||
value: ${{ jobs.detect.outputs.json }}
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
backend: ${{ steps.changes.outputs.backend }}
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
@@ -37,9 +33,8 @@ jobs:
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
swagger: ${{ steps.changes.outputs.swagger }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
@@ -53,7 +48,7 @@ jobs:
|
||||
- "Makefile"
|
||||
- ".golangci.yml"
|
||||
- ".editorconfig"
|
||||
- "options/locale/locale_en-US.json"
|
||||
- "options/locale/locale_en-US.ini"
|
||||
|
||||
frontend:
|
||||
- "*.js"
|
||||
@@ -63,7 +58,7 @@ jobs:
|
||||
- "tools/*.ts"
|
||||
- "assets/emoji.json"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "package-lock.json"
|
||||
- "Makefile"
|
||||
- ".eslintrc.cjs"
|
||||
- ".npmrc"
|
||||
@@ -72,7 +67,7 @@ jobs:
|
||||
- "**/*.md"
|
||||
- ".markdownlint.yaml"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "package-lock.json"
|
||||
|
||||
actions:
|
||||
- ".github/workflows/*"
|
||||
@@ -82,7 +77,7 @@ jobs:
|
||||
- "tools/lint-templates-*.js"
|
||||
- "templates/**/*.tmpl"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- "poetry.lock"
|
||||
|
||||
docker:
|
||||
- "Dockerfile"
|
||||
@@ -95,7 +90,7 @@ jobs:
|
||||
- "templates/swagger/v1_input.json"
|
||||
- "Makefile"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "package-lock.json"
|
||||
- ".spectral.yaml"
|
||||
|
||||
yaml:
|
||||
@@ -103,6 +98,4 @@ jobs:
|
||||
- "**/*.yaml"
|
||||
- ".yamllint.yaml"
|
||||
- "pyproject.toml"
|
||||
|
||||
json:
|
||||
- "**/*.json"
|
||||
- "poetry.lock"
|
||||
|
||||
127
.github/workflows/pull-compliance.yml
vendored
127
.github/workflows/pull-compliance.yml
vendored
@@ -10,18 +10,14 @@ concurrency:
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
lint-backend:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -34,18 +30,17 @@ jobs:
|
||||
if: needs.files-changed.outputs.templates == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv python install 3.12
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
python-version: "3.12"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make deps-frontend
|
||||
- run: make lint-templates
|
||||
@@ -54,44 +49,26 @@ jobs:
|
||||
if: needs.files-changed.outputs.yaml == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv python install 3.12
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make lint-yaml
|
||||
|
||||
lint-json:
|
||||
if: needs.files-changed.outputs.json == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
- run: make deps-frontend
|
||||
- run: make lint-json
|
||||
|
||||
lint-swagger:
|
||||
if: needs.files-changed.outputs.swagger == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend
|
||||
- run: make lint-swagger
|
||||
|
||||
@@ -99,11 +76,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -113,11 +88,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -132,11 +105,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -149,11 +120,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -164,16 +133,13 @@ jobs:
|
||||
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend
|
||||
- run: make lint-frontend
|
||||
- run: make checks-frontend
|
||||
@@ -184,11 +150,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -218,16 +182,13 @@ jobs:
|
||||
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend
|
||||
- run: make lint-md
|
||||
|
||||
@@ -235,11 +196,9 @@ jobs:
|
||||
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
38
.github/workflows/pull-db-tests.yml
vendored
38
.github/workflows/pull-db-tests.yml
vendored
@@ -10,15 +10,11 @@ concurrency:
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
test-pgsql:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:14
|
||||
@@ -42,8 +38,8 @@ jobs:
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -69,22 +65,20 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- run: make deps-backend
|
||||
- run: GOEXPERIMENT='' make backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
- name: run migration tests
|
||||
run: make test-sqlite-migration
|
||||
- name: run tests
|
||||
run: GOEXPERIMENT='' make test-sqlite
|
||||
run: make test-sqlite
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
@@ -96,8 +90,6 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
elasticsearch:
|
||||
image: elasticsearch:7.5.0
|
||||
@@ -132,8 +124,8 @@ jobs:
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -150,7 +142,7 @@ jobs:
|
||||
RACE_ENABLED: true
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: GOEXPERIMENT='' make unit-test-coverage test-check
|
||||
run: make unit-test-coverage test-check
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
RACE_ENABLED: true
|
||||
@@ -160,8 +152,6 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
mysql:
|
||||
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||
@@ -187,8 +177,8 @@ jobs:
|
||||
- "587:587"
|
||||
- "993:993"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -213,8 +203,6 @@ jobs:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
@@ -229,8 +217,8 @@ jobs:
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
22
.github/workflows/pull-docker-dryrun.yml
vendored
22
.github/workflows/pull-docker-dryrun.yml
vendored
@@ -10,28 +10,26 @@ concurrency:
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
container:
|
||||
regular:
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: gitea/gitea:linux-amd64
|
||||
- name: Build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
rootless:
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: Dockerfile.rootless
|
||||
tags: gitea/gitea:linux-amd64
|
||||
|
||||
36
.github/workflows/pull-e2e-tests.yml
vendored
Normal file
36
.github/workflows/pull-e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: e2e-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
test-e2e:
|
||||
# the "test-e2e" won't pass, and it seems that there is no useful test, so skip
|
||||
# if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
if: false
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend frontend deps-backend
|
||||
- run: npx playwright install --with-deps
|
||||
- run: make test-e2e-sqlite
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
USE_REPO_TEST_DIR: 1
|
||||
2
.github/workflows/pull-labeler.yml
vendored
2
.github/workflows/pull-labeler.yml
vendored
@@ -15,6 +15,6 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
113
.github/workflows/release-nightly.yml
vendored
113
.github/workflows/release-nightly.yml
vendored
@@ -11,23 +11,20 @@ concurrency:
|
||||
jobs:
|
||||
nightly-binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
@@ -52,7 +49,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -60,47 +57,31 @@ jobs:
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
|
||||
nightly-container:
|
||||
nightly-docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
# if main then say nightly otherwise cleanup name
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "branch=nightly" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
tags: |
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless
|
||||
tags: |
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -112,20 +93,62 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular docker image
|
||||
uses: docker/build-push-action@v6
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
nightly-docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
# if main then say nightly otherwise cleanup name
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "branch=nightly" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
|
||||
91
.github/workflows/release-tag-rc.yml
vendored
91
.github/workflows/release-tag-rc.yml
vendored
@@ -12,23 +12,20 @@ concurrency:
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
@@ -53,7 +50,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -62,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
@@ -70,14 +67,12 @@ jobs:
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
container:
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
@@ -94,23 +89,6 @@ jobs:
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -122,20 +100,55 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
74
.github/workflows/release-tag-version.yml
vendored
74
.github/workflows/release-tag-version.yml
vendored
@@ -15,23 +15,21 @@ jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
@@ -56,7 +54,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -65,7 +63,7 @@ jobs:
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
@@ -73,14 +71,12 @@ jobs:
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
container:
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
@@ -101,10 +97,36 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta_rootless
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
@@ -121,8 +143,6 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -134,20 +154,12 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -22,12 +22,6 @@ _test
|
||||
.vscode
|
||||
__debug_bin*
|
||||
|
||||
# Visual Studio
|
||||
/.vs/
|
||||
|
||||
# mise version managment tool
|
||||
mise.toml
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
@@ -81,7 +75,6 @@ cpu.out
|
||||
/yarn.lock
|
||||
/yarn-error.log
|
||||
/npm-debug.log*
|
||||
/.pnpm-store
|
||||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
@@ -113,20 +106,3 @@ prime/
|
||||
|
||||
# Manpage
|
||||
/man
|
||||
|
||||
# Ignore AI/LLM instruction files
|
||||
/.claude/
|
||||
/.cursorrules
|
||||
/.cursor/
|
||||
/.goosehints
|
||||
/.windsurfrules
|
||||
/.github/copilot-instructions.md
|
||||
/AGENT.md
|
||||
/CLAUDE.md
|
||||
/llms.txt
|
||||
|
||||
# Ignore worktrees when working on multiple branches
|
||||
.worktrees/
|
||||
|
||||
# A Makefile for custom make targets
|
||||
Makefile.local
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
version: "2"
|
||||
output:
|
||||
sort-results: true
|
||||
sort-order:
|
||||
- file
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dupl
|
||||
- errcheck
|
||||
- forbidigo
|
||||
- gocheckcompilerdirectives
|
||||
- gocritic
|
||||
- govet
|
||||
- ineffassign
|
||||
- mirror
|
||||
- modernize
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
@@ -49,17 +45,10 @@ linters:
|
||||
desc: do not use the ini package, use gitea's config system instead
|
||||
- pkg: gitea.com/go-chi/cache
|
||||
desc: do not use the go-chi cache package, use gitea's cache system
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
gocritic:
|
||||
enabled-checks:
|
||||
- equalFold
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
|
||||
- deprecatedComment # conflicts with go-swagger comments
|
||||
revive:
|
||||
severity: error
|
||||
rules:
|
||||
@@ -94,10 +83,6 @@ linters:
|
||||
- name: unreachable-code
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
|
||||
- [] # DenyList
|
||||
- - skip-package-name-checks: true # supress errors from underscore in migration packages
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
@@ -112,12 +97,6 @@ linters:
|
||||
- require-error
|
||||
usetesting:
|
||||
os-temp-dir: true
|
||||
perfsprint:
|
||||
concat-loop: false
|
||||
govet:
|
||||
enable:
|
||||
- nilness
|
||||
- unusedwrite
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
@@ -143,10 +122,6 @@ linters:
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd
|
||||
- linters:
|
||||
- depguard
|
||||
- gofumpt
|
||||
path: sdk/
|
||||
- linters:
|
||||
- dupl
|
||||
text: (?i)webhook
|
||||
@@ -168,7 +143,6 @@ linters:
|
||||
text: '(?i)exitAfterDefer:'
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
@@ -188,7 +162,6 @@ formatters:
|
||||
generated: lax
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
@@ -196,4 +169,4 @@ formatters:
|
||||
- examples$
|
||||
|
||||
run:
|
||||
timeout: 30m
|
||||
timeout: 10m
|
||||
|
||||
3
.ignore
3
.ignore
@@ -1,6 +1,9 @@
|
||||
*.min.css
|
||||
*.min.js
|
||||
/assets/*.json
|
||||
/modules/options/bindata.go
|
||||
/modules/public/bindata.go
|
||||
/modules/templates/bindata.go
|
||||
/options/gitignore
|
||||
/options/license
|
||||
/public/assets
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1,7 +1,6 @@
|
||||
audit=false
|
||||
fund=false
|
||||
update-notifier=false
|
||||
package-lock=true
|
||||
save-exact=true
|
||||
auto-install-peers=true
|
||||
dedupe-peer-dependents=false
|
||||
enable-pre-post-scripts=true
|
||||
lockfile-version=3
|
||||
|
||||
332
CHANGELOG.md
332
CHANGELOG.md
@@ -4,321 +4,7 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
|
||||
|
||||
* SECURITY
|
||||
* Bump toolchain to go1.25.5, misc fixes (#36082)
|
||||
* ENHANCEMENTS
|
||||
* Add strikethrough button to markdown editor (#36087) (#36104)
|
||||
* Add "site admin" back to profile menu (#36010) (#36013)
|
||||
* Improve math rendering (#36124) (#36125)
|
||||
* BUGFIXES
|
||||
* Check user visibility when redirecting to a renamed user (#36148) (#36159)
|
||||
* Fix various bugs (#36139) (#36151)
|
||||
* Fix bug when viewing the commit diff page with non-ANSI files (#36149) (#36150)
|
||||
* Hide RSS icon when viewing a file not under a branch (#36135) (#36141)
|
||||
* Fix SVG size calulation, only use `style` attribute (#36133) (#36134)
|
||||
* Make Golang correctly delete temp files during uploading (#36128) (#36129)
|
||||
* Fix the bug when ssh clone with redirect user or repository (#36039) (#36090)
|
||||
* Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
|
||||
* Fix edit user email bug in API (#36068) (#36081)
|
||||
* Fix bug when updating user email (#36058) (#36066)
|
||||
* Fix incorrect viewed files counter if file has changed (#36009) (#36047)
|
||||
* Fix container registry error handling (#36021) (#36037)
|
||||
* Fix webAuthn insecure error view (#36165) (#36179)
|
||||
* Fix some file icon ui (#36078) (#36088)
|
||||
* Fix Actions `pull_request.paths` being triggered incorrectly by rebase (#36045) (#36054)
|
||||
* Fix error handling in mailer and wiki services (#36041) (#36053)
|
||||
* Fix bugs when comparing and creating pull request (#36166) (#36144)
|
||||
|
||||
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
|
||||
|
||||
* SECURITY
|
||||
* Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988)
|
||||
* Fix various permission & login related bugs (#36002) (#36004)
|
||||
* ENHANCEMENTS
|
||||
* Display source code downloads last for release attachments (#35897) (#35903)
|
||||
* Change project default column icon to 'star' (#35967) (#35979)
|
||||
* BUGFIXES
|
||||
* Allow empty commit when merging pull request with squash style (#35989) (#36003)
|
||||
* Fix container push tag overwriting (#35936) (#35954)
|
||||
* Fix corrupted external render content (#35946) and upgrade golang.org/x packages (#35950)
|
||||
* Limit reading bytes instead of ReadAll (#35928) (#35934)
|
||||
* Use correct form field for allowed force push users in branch protection API (#35894) (#35908)
|
||||
* Fix team member access check (#35899) (#35905)
|
||||
* Fix conda null depend issue (#35900) (#35902)
|
||||
* Set the dates to now when not specified by the caller (#35861) (#35874)
|
||||
* Fix gogit ListEntriesRecursiveWithSize (#35862)
|
||||
* Misc CSS fixes (#35888) (#35981)
|
||||
* Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) (#35941)
|
||||
* Load jQuery as early as possible to support custom scripts (#35926) (#35929)
|
||||
* Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) (#35917)
|
||||
* Make OAuth2 issuer configurable (#35915) (#35916)
|
||||
* Fix #35763: Add proper page title for project pages (#35773) (#35909)
|
||||
* Fix avatar upload error handling (#35887) (#35890)
|
||||
* Contribution heatmap improvements (#35876) (#35880)
|
||||
* Remove padding override on `.ui .sha.label` (#35864) (#35873)
|
||||
* Fix pull description code label background (#35865) (#35870)
|
||||
|
||||
## [1.25.1](https://github.com/go-gitea/gitea/releases/tag/v1.25.1) - 2025-11-03
|
||||
|
||||
* BUGFIXES
|
||||
* Make ACME email optional (#35849) #35857
|
||||
* Add a doctor command to fix inconsistent run status (#35840) (#35845)
|
||||
* Remove wrong code (#35846)
|
||||
* Fix viewed files number is not right if not all files loaded (#35821) (#35844)
|
||||
* Fix incorrect pull request counter (#35819) (#35841)
|
||||
* Upgrade go mail to 0.7.2 and fix the bug (#35833) (#35837)
|
||||
* Revert gomail to v0.7.0 to fix sending mail failed (#35816) (#35824)
|
||||
* Fix clone mixed bug (#35810) (#35822)
|
||||
* Fix cli "Before" handling (#35797) (#35808)
|
||||
* Improve and fix markup code preview rendering (#35777) (#35787)
|
||||
* Fix actions rerun bug (#35783) (#35784)
|
||||
* Fix actions schedule update issue (#35767) (#35774)
|
||||
* Fix circular spin animation direction (#35785) (#35823)
|
||||
* Fix file extension on gogs.png (#35793) (#35799)
|
||||
* Add pnpm to Snapcraft (#35778)
|
||||
|
||||
## [1.25.0](https://github.com/go-gitea/gitea/releases/tag/v1.25.0) - 2025-10-30
|
||||
|
||||
* BREAKING
|
||||
* Return 201 Created for CreateVariable API responses (#34517)
|
||||
* Add label 'state' to metric 'gitea_users' (#34326)
|
||||
* SECURITY
|
||||
* Upgrade security public key (#34956)
|
||||
* Also include all security fixes in 1.24.x after 1.25.0-rc0
|
||||
* FEATURES
|
||||
* Stream repo zip/tar.gz/bundle achives by default (#35487)
|
||||
* Use configurable remote name for git commands (#35172)
|
||||
* Send email on Workflow Run Success/Failure (#34982)
|
||||
* Refactor OpenIDConnect to support SSH/FullName sync (#34978)
|
||||
* Refactor repo contents API and add "contents-ext" API (#34822)
|
||||
* Add support for 3D/CAD file formats preview (#34794)
|
||||
* Improve instance wide ssh commit signing (#34341)
|
||||
* Edit file workflow for creating a fork and proposing changes (#34240)
|
||||
* Follow file symlinks in the UI to their target (#28835)
|
||||
* Allow renaming/moving binary/LFS files in the UI (#34350)
|
||||
* PERFORMANCE
|
||||
* Improve the performance when detecting the file editable (#34653)
|
||||
* ENHANCEMENTS
|
||||
* Enable more markdown paste features in textarea editor (#35494)
|
||||
* Don't store repo archives on `gitea dump` (#35467)
|
||||
* Always return the relevant status information, even if no status exists. (#35335)
|
||||
* Add start time on perf trace because it seems some steps haven't been recorded. (#35282)
|
||||
* Remove deprecated auth sources (#35272)
|
||||
* When sorting issues by nearest due date, issues without due date should be sorted ascending (#35267)
|
||||
* Disable field count validation of CSV viewer (#35228)
|
||||
* Add `has_code` to repository REST API (#35214)
|
||||
* Display pull request in merged commit view (#35202)
|
||||
* Support Basic Authentication for archive downloads (#35087)
|
||||
* Add hover background to table rows in user and repo admin page (#35072)
|
||||
* Partially refresh notifications list (#35010)
|
||||
* Also display "recently pushed branch" alert on PR view (#35001)
|
||||
* Refactor time tracker UI (#34983)
|
||||
* Improve CLI commands (#34973)
|
||||
* Improve project & label color picker and image scroll (#34971)
|
||||
* Improve NuGet API Parity (#21291) (#34940)
|
||||
* Support getting last commit message using contents-ext API (#34904)
|
||||
* Adds title on branch commit counts (#34869)
|
||||
* Add "Cancel workflow run" button to Actions list page (#34817)
|
||||
* Improve img lazy loading (#34804)
|
||||
* Forks repository list page follow other repositories page (#34784)
|
||||
* Add ff_only parameter to POST /repos/{owner}/{repo}/merge-upstream (#34770)
|
||||
* Rework delete org and rename org UI (#34762)
|
||||
* Improve nuget/rubygems package registries (#34741)
|
||||
* Add repo file tree item link behavior (#34730)
|
||||
* Add issue delete notifier (#34592)
|
||||
* Improve Actions list (#34530)
|
||||
* Add a default tab on repo header when migrating (#34503)
|
||||
* Add post-installation redirect based on admin account status (#34493)
|
||||
* Trigger 'unlabeled' event when label is Deleted from PR (#34316)
|
||||
* Support annotated tags when using create release API (#31840)
|
||||
* Use lfs label for lfs file rather than a long description (#34363)
|
||||
* Add "View workflow file" to Actions list page (#34538)
|
||||
* Move organization's visibility change to danger zone. (#34814)
|
||||
* Don't block site admin's operation if SECRET_KEY is lost (#35721)
|
||||
* Make restricted users can access public repositories (#35693)
|
||||
* The status icon of the Action step is consistent with GitHub (#35618) #35621
|
||||
* BUGFIXES
|
||||
* Update tab title when navigating file tree (#35757) #35772
|
||||
* Fix "ref-issue" handling in markup (#35739) #35771
|
||||
* Fix webhook to prevent tag events from bypassing branch filters targets (#35567) #35577
|
||||
* Fix markup init after issue comment editing (#35536) #35537
|
||||
* Fix creating pull request failure when the target branch name is the same as some tag (#35552) #35582
|
||||
* Fix auto-expand and auto-scroll for actions logs (#35570) (#35583) #35586
|
||||
* Use inputs context when parsing workflows (#35590) #35595
|
||||
* Fix diffpatch API endpoint (#35610) #35613
|
||||
* Creating push comments before invoke pull request checking (#35647) #35668
|
||||
* Fix missing Close when error occurs and abused connection pool (#35658) #35670
|
||||
* Fix build (#35674)
|
||||
* Use LFS object size instead of blob size when viewing a LFS file (#35679)
|
||||
* Fix workflow run event status while rerunning a failed job (#35689)
|
||||
* Avoid emoji mismatch and allow to only enable chosen emojis (#35692)
|
||||
* Refactor legacy code, fix LFS auth bypass, fix symlink bypass (#35708)
|
||||
* Fix various trivial problems (#35714)
|
||||
* Fix attachment file size limit in server backend (#35519)
|
||||
* Honor delete branch on merge repo setting when using merge API (#35488)
|
||||
* Fix external render, make iframe render work (#35727, #35730)
|
||||
* Upgrade go mail to 0.7.2 (#35748)
|
||||
* Revert #18491, fix oauth2 client link account (#35745)
|
||||
* Fix different behavior in status check pattern matching with double stars (#35474)
|
||||
* Fix overflow in notifications list (#35446)
|
||||
* Fix package link setting can only list limited repositories (#35394)
|
||||
* Extend comment treepath length (#35389)
|
||||
* Fix font-size in inline code comment preview (#35209)
|
||||
* Move git config/remote to gitrepo package and add global lock to resolve possible conflict when updating repository git config file (#35151)
|
||||
* Change some columns from text to longtext and fix column wrong type caused by xorm (#35141)
|
||||
* Redirect to a presigned URL of HEAD for HEAD requests (#35088)
|
||||
* Fix git commit committer parsing and add some tests (#35007)
|
||||
* Fix OCI manifest parser (#34797)
|
||||
* Refactor FindOrgOptions to use enum instead of bool, fix membership visibility (#34629)
|
||||
* Fix notification count positioning for variable-width elements (#34597)
|
||||
* Keeping consistent between UI and API about combined commit status state and fix some bugs (#34562)
|
||||
* Fix possible panic (#34508)
|
||||
* Fix autofocus behavior (#34397)
|
||||
* Fix Actions API (#35204)
|
||||
* Fix ListWorkflowRuns OpenAPI response model. (#35026)
|
||||
* Small fix in Pull Requests page (#34612)
|
||||
* Fix http auth header parsing (#34936)
|
||||
* Fix modal + form abuse (#34921)
|
||||
* Fix PR toggle WIP (#34920)
|
||||
* Fix log fmt (#34810)
|
||||
* Replace stopwatch toggle with explicit start/stop actions (#34818)
|
||||
* Fix some package registry problems (#34759)
|
||||
* Fix RPM package download routing & missing package version count (#34909)
|
||||
* Fix repo search input height (#34330)
|
||||
* Fix "The sidebar of the repository file list does not have a fixed height #34298" (#34321)
|
||||
* Fix minor typos in two files #HSFDPMUW (#34944)
|
||||
* Fix actions skipped commit status indicator (#34507)
|
||||
* Fix job status aggregation logic (#35000)
|
||||
* Fix broken OneDev migration caused by various REST API changes in OneDev 7.8.0 and later (#35216)
|
||||
* Fix typo in oauth2_full_name_claim_name string (#35199)
|
||||
* Fix typo in locale_en-US.ini (#35196)
|
||||
* API
|
||||
* Exposing TimeEstimate field in the API (#35475)
|
||||
* UpdateBranch API supports renaming a branch (#35374)
|
||||
* Add `owner` and `parent` fields clarification to docs (#35023)
|
||||
* Improve OAuth2 provider (correct Issuer, respect ENABLED) (#34966)
|
||||
* Add a `login`/`login-name`/`username` disambiguation to affected endpoint parameters and response/request models (#34901)
|
||||
* Do not mutate incoming options to SearchRepositoryByName (#34553)
|
||||
* Do not mutate incoming options to RenderUserSearch and SearchUsers (#34544)
|
||||
* Export repo's manual merge settings (#34502)
|
||||
* Add date range filtering to commit retrieval endpoints (#34497)
|
||||
* Add endpoint deleting workflow run (#34337)
|
||||
* Add workflow_run api + webhook (#33964)
|
||||
* REFACTOR
|
||||
* Move updateref and removeref to gitrepo and remove unnecessary open repository (#35511)
|
||||
* Remove unused param `doer` (#34545)
|
||||
* Split GetLatestCommitStatus as two functions (#34535)
|
||||
* Use gitrepo.SetDefaultBranch when set default branch of wiki repository (#33911)
|
||||
* Refactor editor (#34780)
|
||||
* Refactor packages (#34777)
|
||||
* Refactor container package (#34877)
|
||||
* Refactor "change file" API (#34855)
|
||||
* Rename pull request GetGitRefName to GetGitHeadRefName to prepare introducing GetGitMergeRefName (#35093)
|
||||
* Move git command to git/gitcmd (#35483)
|
||||
* Use db.WithTx/WithTx2 instead of TxContext when possible (#35428)
|
||||
* Support Node.js 22.6 with type stripping (#35427)
|
||||
* Migrate tools and configs to typescript, require node.js >= 22.18.0 (#35421)
|
||||
* Check user and repo for redirects when using git via SSH transport (#35416)
|
||||
* Remove the duplicated function GetTags (#35375)
|
||||
* Refactor to use reflect.TypeFor (#35370)
|
||||
* Deleting branch could delete broken branch which has database record but git branch is missing (#35360)
|
||||
* Exit with success when already up to date (#35312)
|
||||
* Split admin config settings templates to make it maintain easier (#35294)
|
||||
* A small refactor to use context in the service layer (#35179)
|
||||
* Refactor and update mail templates (#35150)
|
||||
* Use db.WithTx/WithTx2 instead of TxContext when possible (#35130)
|
||||
* Align `issue-title-buttons` with `list-header` (#35018)
|
||||
* Add Notifications section in User Settings (#35008)
|
||||
* Tweak placement of diff file menu (#34999)
|
||||
* Refactor mail template and support preview (#34990)
|
||||
* Rerun job only when run is done (#34970)
|
||||
* Merge index.js (#34963)
|
||||
* Refactor "delete-button" to "link-action" (#34962)
|
||||
* Refactor webhook and fix feishu/lark secret (#34961)
|
||||
* Exclude devtest.ts from tailwindcss (#34935)
|
||||
* Refactor head navbar icons (#34922)
|
||||
* Improve html escape (#34911)
|
||||
* Improve tags list page (#34898)
|
||||
* Improve `labels-list` rendering (#34846)
|
||||
* Remove unused variable HUGO_VERSION (#34840)
|
||||
* Correct migration tab name (#34826)
|
||||
* Refactor template helper (#34819)
|
||||
* Use `shallowRef` instead of `ref` in `.vue` files where possible (#34813)
|
||||
* Use standalone function to update repository cols (#34811)
|
||||
* Refactor wiki (#34805)
|
||||
* Remove unnecessary duplicate code (#34733)
|
||||
* Refactor embedded assets and drop unnecessary dependencies (#34692)
|
||||
* Update x/crypto package and make builtin SSH use default parameters (#34667)
|
||||
* Add `--color-logo`, matching the logo's primary color (#34639)
|
||||
* Add openssh-keygen to rootless image (#34625)
|
||||
* Replace update repository function in some places (#34566)
|
||||
* Change "rejected" to "changes requested" in 3rd party PR review notification (#34481)
|
||||
* Remove legacy template helper functions (#34426)
|
||||
* Use run-name and evaluate workflow variables (#34301)
|
||||
* Move HasWiki to repository service package (#33912)
|
||||
* Move some functions from package git to gitrepo (#33910)
|
||||
* TESTING
|
||||
* Add webhook test for push event (#34442)
|
||||
* Add a webhook push test for dev branch (#34421)
|
||||
* Add migrations tests (#34456) (#34498)
|
||||
* STYLE
|
||||
* Enforce explanation for necessary nolints and fix bugs (#34883)
|
||||
* Fix remaining issues after `gopls modernize` formatting (#34771)
|
||||
* Update gofumpt, add go.mod ignore directive (#35434)
|
||||
* Enforce nolint scope (#34851)
|
||||
* Enable gocritic `equalFold` and fix issues (#34952)
|
||||
* Run `gopls modernize` on codebase (#34751)
|
||||
* Upgrade `gopls` to v0.19.0, add `make fix` (#34772)
|
||||
* BUILD
|
||||
* bump archives&rar dep (#35637) #35638
|
||||
* Use github.com/mholt/archives replace github.com/mholt/archiver (#35390)
|
||||
* Update JS and PY dependencies (#35444)
|
||||
* Upgrade devcontainer go version to 1.24.6 (#35298)
|
||||
* Upgrade golang to 1.25.1 and add descriptions for the swagger structs' fields (#35418)
|
||||
* Update JS and PY deps (#35191)
|
||||
* Update JS and PY dependencies (#34391)
|
||||
* Update go tool dependencies (#34845)
|
||||
* Update `uint8-to-base64`, remove type stub (#34844)
|
||||
* Switch to `@resvg/resvg-wasm` for `generate-images` (#35415)
|
||||
* Switch to pnpm (#35274)
|
||||
* Update chroma to v2.20.0 (#35220)
|
||||
* Migrate to urfave v3 (#34510)
|
||||
* Update JS deps, regenerate SVGs (#34640)
|
||||
* Upgrade dependencies (#35384)
|
||||
* Bump `@github/relative-time-element` to v4.4.8 (#34413)
|
||||
* Update JS dependencies (#34951)
|
||||
* Upgrade orgmode to v1.8.0 (#34721)
|
||||
* Raise minimum Node.js version to 20, test on 24 (#34713)
|
||||
* Update JS deps (#34701)
|
||||
* Upgrade htmx to 2.0.6 (#34887)
|
||||
* Update eslint to v9 (#35485)
|
||||
* Update js dependencies (#35429)
|
||||
* Clean up npm dependencies (#35508)
|
||||
* Clean up npm dependencies (#35484)
|
||||
* Bump setup-node to v5 (#35448)
|
||||
* MISC
|
||||
* Add gitignore rules to exclude LLM instruction files (#35076)
|
||||
* Gitignore: Visual Studio settings folder (#34375)
|
||||
* Improve language in en-US locale strings (#35124)
|
||||
* Fixed all grammatical errors in locale_en-US.ini (#35053)
|
||||
* Docs/fix typo and grammar in CONTRIBUTING.md (#35024)
|
||||
* Improve english grammar and readability in locale_en-US.ini (#35017)
|
||||
|
||||
## [1.24.7](https://github.com/go-gitea/gitea/releases/tag/v1.24.7) - 2025-10-24
|
||||
|
||||
* SECURITY
|
||||
* Refactor legacy code (#35708) (#35713)
|
||||
* Fixing issue #35530: Password Leak in Log Messages (#35584) (#35665)
|
||||
* Fix a bug missed return (#35655) (#35671)
|
||||
* BUGFIXES
|
||||
* Fix inputing review comment will remove reviewer (#35591) (#35664)
|
||||
* TESTING
|
||||
* Mock external service in hcaptcha TestCaptcha (#35604) (#35663)
|
||||
* Fix build (#35669)
|
||||
|
||||
## [1.24.6](https://github.com/go-gitea/gitea/releases/tag/v1.24.6) - 2025-09-10
|
||||
## [1.24.6](https://github.com/go-gitea/gitea/releases/tag/1.24.6) - 2025-09-10
|
||||
|
||||
* SECURITY
|
||||
* Upgrade xz to v0.5.15 (#35385)
|
||||
@@ -418,7 +104,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Hide href attribute of a tag if there is no target_url (#34556) (#34684)
|
||||
* Fix tag target (#34781) #34783
|
||||
|
||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
|
||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
|
||||
|
||||
* BREAKING
|
||||
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
|
||||
@@ -788,7 +474,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Bump x/net (#32896) (#32900)
|
||||
* Only activity tab needs heatmap data loading (#34652)
|
||||
|
||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
|
||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
|
||||
|
||||
* SECURITY
|
||||
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
||||
@@ -815,7 +501,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Bump go version in go.mod (#34160)
|
||||
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
|
||||
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
|
||||
|
||||
* Enhancements
|
||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||
@@ -913,7 +599,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* BUGFIXES
|
||||
* Fix a bug caused by status webhook template #33512
|
||||
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
|
||||
|
||||
* BREAKING
|
||||
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
|
||||
@@ -3443,7 +3129,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Improve decryption failure message (#24573) (#24575)
|
||||
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
|
||||
|
||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
|
||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
|
||||
|
||||
* SECURITY
|
||||
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
|
||||
@@ -3456,7 +3142,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Fix incorrect CurrentUser check for docker rootless (#24435)
|
||||
* Getting the tag list does not require being signed in (#24413) (#24416)
|
||||
|
||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
|
||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
|
||||
|
||||
* SECURITY
|
||||
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
|
||||
@@ -3955,7 +3641,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Display attachments of review comment when comment content is blank (#23035) (#23046)
|
||||
* Return empty url for submodule tree entries (#23043) (#23048)
|
||||
|
||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
|
||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
|
||||
|
||||
* SECURITY
|
||||
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
|
||||
@@ -4382,7 +4068,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
|
||||
* Fix UI mis-align for PR commit history (#20845) (#20859)
|
||||
|
||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
|
||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
|
||||
|
||||
* SECURITY
|
||||
* Correctly escape within tribute.js (#20831) (#20832)
|
||||
|
||||
@@ -30,7 +30,7 @@ These are the values to which people in the Gitea community should aspire.
|
||||
- **Be constructive.**
|
||||
- Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
|
||||
- Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved.
|
||||
- Avoid snarking (pithy, unproductive, sniping comments).
|
||||
- Avoid snarking (pithy, unproductive, sniping comments)
|
||||
- Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
|
||||
- Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
|
||||
- **Be responsible.**
|
||||
@@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta
|
||||
|
||||
### Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
### Our Standards
|
||||
|
||||
|
||||
@@ -166,19 +166,19 @@ Here's how to run the test suite:
|
||||
|
||||
- code lint
|
||||
|
||||
| | |
|
||||
| :-------------------- | :--------------------------------------------------------------------------- |
|
||||
| | |
|
||||
| :-------------------- | :---------------------------------------------------------------- |
|
||||
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
||||
|``make lint-frontend`` | lint frontend files |
|
||||
|``make lint-backend`` | lint backend files |
|
||||
|``make lint-frontend`` | lint frontend files |
|
||||
|``make lint-backend`` | lint backend files |
|
||||
|
||||
- run tests (we suggest running them on Linux)
|
||||
|
||||
| Command | Action | |
|
||||
| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- |
|
||||
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
||||
|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) |
|
||||
|``make test-e2e-sqlite[\#SpecificTestName]`` | run [end-to-end](tests/e2e) test(s) for SQLite | [More details](tests/e2e/README.md) |
|
||||
| Command | Action | |
|
||||
| :------------------------------------- | :----------------------------------------------- | ------------ |
|
||||
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
||||
|``make test-sqlite[\#SpecificTestName]``| run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) |
|
||||
|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) |
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -591,7 +591,7 @@ be reviewed by two maintainers and must pass the automatic tests.
|
||||
## Releasing Gitea
|
||||
|
||||
- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future.
|
||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
|
||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody against in about serval hours.
|
||||
- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
|
||||
- Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`.
|
||||
- When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
|
||||
|
||||
50
Dockerfile
50
Dockerfile
@@ -1,8 +1,8 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
|
||||
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY=direct
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
@@ -14,32 +14,35 @@ RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
nodejs \
|
||||
pnpm
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Setup repo
|
||||
COPY . ${GOPATH}/src/code.gitea.io/gitea
|
||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored.
|
||||
# ".git" directory will be mounted later separately for getting version data.
|
||||
# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed.
|
||||
COPY --exclude=.git/ . .
|
||||
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make
|
||||
# Checkout version if set
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
# Set permissions for builds that made under windows which strips the executable bit from file
|
||||
# Set permissions
|
||||
RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
||||
/tmp/local/usr/local/bin/* \
|
||||
/tmp/local/usr/local/bin/gitea \
|
||||
/tmp/local/etc/s6/gitea/* \
|
||||
/tmp/local/etc/s6/openssh/* \
|
||||
/tmp/local/etc/s6/.s6-svscan/* \
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
/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 docker.io/library/alpine:3.22 AS gitea
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
|
||||
EXPOSE 22 3000
|
||||
|
||||
@@ -54,7 +57,8 @@ RUN apk --no-cache add \
|
||||
s6 \
|
||||
sqlite \
|
||||
su-exec \
|
||||
gnupg
|
||||
gnupg \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
RUN addgroup \
|
||||
-S -g 1000 \
|
||||
@@ -68,9 +72,6 @@ RUN addgroup \
|
||||
git && \
|
||||
echo "git:*" | chpasswd -e
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
|
||||
ENV USER=git
|
||||
ENV GITEA_CUSTOM=/data/gitea
|
||||
|
||||
@@ -78,3 +79,8 @@ VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/entrypoint"]
|
||||
CMD ["/usr/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
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
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
|
||||
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY=direct
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS="bindata timetzdata $TAGS"
|
||||
ARG CGO_EXTRA_CFLAGS
|
||||
|
||||
# Build deps
|
||||
#Build deps
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
nodejs \
|
||||
pnpm
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Setup repo
|
||||
COPY . ${GOPATH}/src/code.gitea.io/gitea
|
||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
# See the comments in Dockerfile
|
||||
COPY --exclude=.git/ . .
|
||||
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make
|
||||
# Checkout version if set
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
# Set permissions for builds that made under windows which strips the executable bit from file
|
||||
RUN chmod 755 /tmp/local/usr/local/bin/* \
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
# Set permissions
|
||||
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/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 docker.io/library/alpine:3.22 AS gitea-rootless
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
|
||||
EXPOSE 2222 3000
|
||||
|
||||
@@ -45,7 +52,7 @@ RUN apk --no-cache add \
|
||||
git \
|
||||
curl \
|
||||
gnupg \
|
||||
openssh-keygen
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
RUN addgroup \
|
||||
-S -g 1000 \
|
||||
@@ -63,6 +70,8 @@ RUN chown git:git /var/lib/gitea /etc/gitea
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
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
|
||||
|
||||
@@ -36,7 +36,9 @@ a1012112796 <1012112796@qq.com> (@a1012112796)
|
||||
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
|
||||
Norwin Roosen <git@nroo.de> (@noerw)
|
||||
Kyle Dumont <kdumontnu@gmail.com> (@kdumontnu)
|
||||
Patrick Schratz <patrick.schratz@gmail.com> (@pat-s)
|
||||
Janis Estelmann <admin@oldschoolhack.me> (@KN4CK3R)
|
||||
Steven Kriegler <sk.bunsenbrenner@gmail.com> (@justusbunsi)
|
||||
Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
|
||||
Leon Hofmeister <dev.lh@web.de> (@delvh)
|
||||
Wim <wim@42.be> (@42wim)
|
||||
@@ -62,5 +64,3 @@ Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
|
||||
hiifong <i@hiif.ong> (@hiifong)
|
||||
metiftikci <metiftikci@hotmail.com> (@metiftikci)
|
||||
Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
|
||||
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
|
||||
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
|
||||
|
||||
187
Makefile
187
Makefile
@@ -18,28 +18,25 @@ DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
IMPORT := code.gitea.io/gitea
|
||||
|
||||
# By default use go's 1.25 experimental json v2 library when building
|
||||
# TODO: remove when no longer experimental
|
||||
export GOEXPERIMENT ?= jsonv2
|
||||
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
|
||||
COMMA := ,
|
||||
|
||||
XGO_VERSION := go-1.25.x
|
||||
XGO_VERSION := go-1.24.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_TAG ?= latest
|
||||
@@ -94,19 +91,11 @@ ifeq ($(RACE_ENABLED),true)
|
||||
endif
|
||||
|
||||
STORED_VERSION_FILE := VERSION
|
||||
HUGO_VERSION ?= 0.111.3
|
||||
|
||||
GITHUB_REF_TYPE ?= branch
|
||||
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Enable typescript support in Node.js before 22.18
|
||||
# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23)
|
||||
NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | tr '.' ' '))
|
||||
ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0)
|
||||
NODE_VARS := NODE_OPTIONS="--experimental-strip-types"
|
||||
else
|
||||
NODE_VARS :=
|
||||
endif
|
||||
|
||||
ifneq ($(GITHUB_REF_TYPE),branch)
|
||||
VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
|
||||
GITEA_VERSION ?= $(VERSION)
|
||||
@@ -138,11 +127,12 @@ GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/m
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
|
||||
|
||||
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||
WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts
|
||||
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_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
|
||||
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
|
||||
|
||||
@@ -161,17 +151,23 @@ TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
|
||||
|
||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
|
||||
|
||||
GO_DIRS := build cmd models modules routers services tests tools
|
||||
GO_DIRS := build cmd models modules routers services tests
|
||||
WEB_DIRS := web_src/js web_src/css
|
||||
|
||||
ESLINT_FILES := web_src/js tools *.ts tests/e2e
|
||||
ESLINT_FILES := web_src/js tools *.js *.ts *.cjs tests/e2e
|
||||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
|
||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) $(filter-out tools/misspellings.csv, $(wildcard tools/*))
|
||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
|
||||
|
||||
GO_SOURCES := $(wildcard *.go)
|
||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
|
||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
|
||||
GO_SOURCES += $(GENERATED_GO_DEST)
|
||||
GO_SOURCES_NO_BINDATA := $(GO_SOURCES)
|
||||
|
||||
ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
|
||||
GO_SOURCES += $(BINDATA_DEST)
|
||||
GENERATED_GO_DEST += $(BINDATA_DEST)
|
||||
endif
|
||||
|
||||
# Force installation of playwright dependencies by setting this flag
|
||||
ifdef DEPS_PLAYWRIGHT
|
||||
@@ -197,10 +193,6 @@ TEST_MSSQL_DBNAME ?= gitea
|
||||
TEST_MSSQL_USERNAME ?= sa
|
||||
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
||||
|
||||
# Include local Makefile
|
||||
# Makefile.local is listed in .gitignore
|
||||
sinclude Makefile.local
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
@@ -232,13 +224,10 @@ git-check:
|
||||
node-check:
|
||||
$(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p'))
|
||||
$(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval PNPM_MISSING := $(shell hash pnpm > /dev/null 2>&1 || echo 1))
|
||||
@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" ]; then \
|
||||
echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater to build. You can get it at https://nodejs.org/en/download/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ "$(PNPM_MISSING)" = "1" ]; then \
|
||||
echo "Gitea requires pnpm to build. You can install it at https://pnpm.io/installation"; \
|
||||
$(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | tr '.' ' ');))
|
||||
$(eval NPM_MISSING := $(shell hash npm > /dev/null 2>&1 || echo 1))
|
||||
@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" -o "$(NPM_MISSING)" = "1" ]; then \
|
||||
echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -248,7 +237,7 @@ clean-all: clean ## delete backend, frontend and integration files
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## delete backend and integration files
|
||||
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \
|
||||
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
|
||||
integrations*.test \
|
||||
e2e*.test \
|
||||
tests/integration/gitea-integration-* \
|
||||
@@ -259,8 +248,8 @@ clean: ## delete backend and integration files
|
||||
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## format the Go and template code
|
||||
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}'
|
||||
fmt: ## format the Go code
|
||||
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
|
||||
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
|
||||
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
|
||||
@# whitespace before it
|
||||
@@ -290,7 +279,7 @@ endif
|
||||
.PHONY: generate-swagger
|
||||
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
|
||||
|
||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
||||
$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC_INPUT)
|
||||
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
||||
|
||||
.PHONY: swagger-check
|
||||
@@ -332,48 +321,44 @@ lint-frontend: lint-js lint-css ## lint frontend files
|
||||
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
|
||||
|
||||
.PHONY: lint-backend
|
||||
lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
|
||||
lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files
|
||||
|
||||
.PHONY: lint-backend-fix
|
||||
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js: node_modules ## lint js files
|
||||
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
|
||||
$(NODE_VARS) pnpm exec vue-tsc
|
||||
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
|
||||
npx vue-tsc
|
||||
|
||||
.PHONY: lint-js-fix
|
||||
lint-js-fix: node_modules ## lint js files and fix issues
|
||||
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
|
||||
$(NODE_VARS) pnpm exec vue-tsc
|
||||
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
|
||||
npx vue-tsc
|
||||
|
||||
.PHONY: lint-css
|
||||
lint-css: node_modules ## lint css files
|
||||
$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES)
|
||||
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
|
||||
|
||||
.PHONY: lint-css-fix
|
||||
lint-css-fix: node_modules ## lint css files and fix issues
|
||||
$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
|
||||
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
|
||||
|
||||
.PHONY: lint-swagger
|
||||
lint-swagger: node_modules ## lint swagger files
|
||||
$(NODE_VARS) pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
|
||||
npx spectral lint -q -F hint $(SWAGGER_SPEC)
|
||||
|
||||
.PHONY: lint-md
|
||||
lint-md: node_modules ## lint markdown files
|
||||
$(NODE_VARS) pnpm exec markdownlint *.md
|
||||
|
||||
.PHONY: lint-md-fix
|
||||
lint-md-fix: node_modules ## lint markdown files and fix issues
|
||||
$(NODE_VARS) pnpm exec markdownlint --fix *.md
|
||||
npx markdownlint *.md
|
||||
|
||||
.PHONY: lint-spell
|
||||
lint-spell: ## lint spelling
|
||||
@go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error $(SPELLCHECK_FILES)
|
||||
@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
|
||||
|
||||
.PHONY: lint-spell-fix
|
||||
lint-spell-fix: ## lint spelling and fix issues
|
||||
@go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w $(SPELLCHECK_FILES)
|
||||
@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go: ## lint go files
|
||||
@@ -393,7 +378,13 @@ lint-go-windows:
|
||||
.PHONY: lint-go-gitea-vet
|
||||
lint-go-gitea-vet: ## lint go files with gitea-vet
|
||||
@echo "Running gitea-vet..."
|
||||
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
|
||||
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet ./...
|
||||
|
||||
.PHONY: lint-go-gopls
|
||||
lint-go-gopls: ## lint go files with 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:
|
||||
@@ -406,20 +397,12 @@ lint-actions: ## lint action workflow files
|
||||
|
||||
.PHONY: lint-templates
|
||||
lint-templates: .venv node_modules ## lint template files
|
||||
@node tools/lint-templates-svg.ts
|
||||
@uv run --frozen djlint $(shell find templates -type f -iname '*.tmpl')
|
||||
@node tools/lint-templates-svg.js
|
||||
@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
|
||||
|
||||
.PHONY: lint-yaml
|
||||
lint-yaml: .venv ## lint yaml files
|
||||
@uv run --frozen yamllint -s .
|
||||
|
||||
.PHONY: lint-json
|
||||
lint-json: node_modules ## lint json files
|
||||
$(NODE_VARS) pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0
|
||||
|
||||
.PHONY: lint-json-fix
|
||||
lint-json-fix: node_modules ## lint and fix json files
|
||||
$(NODE_VARS) pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --fix
|
||||
@poetry run yamllint -s .
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## watch everything and continuously rebuild
|
||||
@@ -428,7 +411,7 @@ watch: ## watch everything and continuously rebuild
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
|
||||
NODE_ENV=development npx webpack --watch --progress
|
||||
|
||||
.PHONY: watch-backend
|
||||
watch-backend: go-check ## watch backend files and continuously rebuild
|
||||
@@ -444,7 +427,7 @@ test-backend: ## test backend files
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: node_modules ## test frontend files
|
||||
$(NODE_VARS) pnpm exec vitest
|
||||
npx vitest
|
||||
|
||||
.PHONY: test-check
|
||||
test-check:
|
||||
@@ -467,7 +450,7 @@ test\#%:
|
||||
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 tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
|
||||
$(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
|
||||
|
||||
.PHONY: unit-test-coverage
|
||||
unit-test-coverage:
|
||||
@@ -587,7 +570,7 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
|
||||
|
||||
.PHONY: playwright
|
||||
playwright: deps-frontend
|
||||
$(NODE_VARS) pnpm exec playwright install $(PLAYWRIGHT_FLAGS)
|
||||
npx playwright install $(PLAYWRIGHT_FLAGS)
|
||||
|
||||
.PHONY: test-e2e%
|
||||
test-e2e%: TEST_TYPE ?= e2e
|
||||
@@ -765,7 +748,7 @@ generate-go: $(TAGS_PREREQ)
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||
@@ -846,14 +829,15 @@ deps-tools: ## install tool dependencies
|
||||
$(GO) install $(GO_LICENSES_PACKAGE) & \
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||
$(GO) install $(GOPLS_PACKAGE) & \
|
||||
wait
|
||||
|
||||
node_modules: pnpm-lock.yaml
|
||||
$(NODE_VARS) pnpm install --frozen-lockfile
|
||||
node_modules: package-lock.json
|
||||
npm install --no-save
|
||||
@touch node_modules
|
||||
|
||||
.venv: uv.lock
|
||||
uv sync
|
||||
.venv: poetry.lock
|
||||
poetry install
|
||||
@touch .venv
|
||||
|
||||
.PHONY: update
|
||||
@@ -861,34 +845,34 @@ update: update-js update-py ## update js and py dependencies
|
||||
|
||||
.PHONY: update-js
|
||||
update-js: node-check | node_modules ## update js dependencies
|
||||
$(NODE_VARS) pnpm exec updates -u -f package.json
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
$(NODE_VARS) pnpm install
|
||||
$(NODE_VARS) pnpm exec nolyfill install
|
||||
$(NODE_VARS) pnpm install
|
||||
npx updates -u -f package.json
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install --package-lock
|
||||
npx nolyfill install
|
||||
npm install --package-lock
|
||||
@touch node_modules
|
||||
|
||||
.PHONY: update-py
|
||||
update-py: node-check | node_modules ## update py dependencies
|
||||
$(NODE_VARS) pnpm exec updates -u -f pyproject.toml
|
||||
rm -rf .venv uv.lock
|
||||
uv sync
|
||||
npx updates -u -f pyproject.toml
|
||||
rm -rf .venv poetry.lock
|
||||
poetry install
|
||||
@touch .venv
|
||||
|
||||
.PHONY: webpack
|
||||
webpack: $(WEBPACK_DEST) ## build webpack files
|
||||
|
||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
|
||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
|
||||
@$(MAKE) -s node-check node_modules
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
@echo "Running webpack..."
|
||||
@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
|
||||
@BROWSERSLIST_IGNORE_OLD_DATA=true npx webpack
|
||||
@touch $(WEBPACK_DEST)
|
||||
|
||||
.PHONY: svg
|
||||
svg: node-check | node_modules ## build svg files
|
||||
rm -rf $(SVG_DEST_DIR)
|
||||
node tools/generate-svg.ts
|
||||
node tools/generate-svg.js
|
||||
|
||||
.PHONY: svg-check
|
||||
svg-check: svg
|
||||
@@ -902,22 +886,33 @@ svg-check: svg
|
||||
|
||||
.PHONY: lockfile-check
|
||||
lockfile-check:
|
||||
$(NODE_VARS) pnpm install --frozen-lockfile
|
||||
@diff=$$(git diff --color=always pnpm-lock.yaml); \
|
||||
npm install --package-lock-only
|
||||
@diff=$$(git diff --color=always package-lock.json); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "pnpm-lock.yaml is inconsistent with package.json"; \
|
||||
echo "Please run 'pnpm install --frozen-lockfile' and commit the result:"; \
|
||||
echo "package-lock.json is inconsistent with package.json"; \
|
||||
echo "Please run 'npm install --package-lock-only' and commit the result:"; \
|
||||
printf "%s" "$${diff}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: update-translations
|
||||
update-translations:
|
||||
mkdir -p ./translations
|
||||
cd ./translations && curl -L https://crowdin.com/download/project/gitea.zip > gitea.zip && unzip gitea.zip
|
||||
rm ./translations/gitea.zip
|
||||
$(SED_INPLACE) -e 's/="/=/g' -e 's/"$$//g' ./translations/*.ini
|
||||
$(SED_INPLACE) -e 's/\\"/"/g' ./translations/*.ini
|
||||
mv ./translations/*.ini ./options/locale/
|
||||
rmdir ./translations
|
||||
|
||||
.PHONY: generate-gitignore
|
||||
generate-gitignore: ## update gitignore files
|
||||
$(GO) run build/generate-gitignores.go
|
||||
|
||||
.PHONY: generate-images
|
||||
generate-images: | node_modules ## generate images
|
||||
cd tools && node generate-images.ts $(TAGS)
|
||||
generate-images: | node_modules
|
||||
npm install --no-save fabric@6 imagemin-zopfli@7
|
||||
node tools/generate-images.js $(TAGS)
|
||||
|
||||
.PHONY: generate-manpage
|
||||
generate-manpage: ## generate manpage
|
||||
|
||||
571
README.md
571
README.md
@@ -1,416 +1,213 @@
|
||||
# GitCaddy
|
||||
# Gitea
|
||||
|
||||
The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-assisted development.
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
## What is GitCaddy?
|
||||
[繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
GitCaddy transforms Git hosting into an AI-ready platform. While traditional Git servers treat AI tools as an afterthought, GitCaddy is built from the ground up with structured APIs, capability discovery, and intelligent context that AI assistants need to write correct code, generate valid CI/CD workflows, and understand your projects deeply.
|
||||
## Purpose
|
||||
|
||||
**Key differentiators:**
|
||||
The goal of this project is to make the easiest, fastest, and most
|
||||
painless way of setting up a self-hosted Git service.
|
||||
|
||||
- **V2 API** - Modern, AI-optimized endpoints with batch operations, streaming, and structured errors
|
||||
- **Runner Capability Discovery** - AI tools query runner capabilities before generating workflows
|
||||
- **Action Compatibility Database** - Curated compatibility matrix prevents workflow errors
|
||||
- **AI Context APIs** - Rich, structured repository and issue intelligence
|
||||
- **Workflow Validation** - Pre-flight checks for CI/CD workflows before commit
|
||||
As Gitea is written in Go, it works across **all** the platforms and
|
||||
architectures that are supported by Go, including Linux, macOS, and
|
||||
Windows on x86, amd64, ARM and PowerPC architectures.
|
||||
This project has been
|
||||
[forked](https://blog.gitea.com/welcome-to-gitea/) from
|
||||
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
|
||||
|
||||
## Features
|
||||
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
|
||||
|
||||
### V2 API - Modern, AI-Optimized Interface
|
||||
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
|
||||
|
||||
A complete API redesign focused on AI tool consumption:
|
||||
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Batch Operations** | Fetch up to 100 files in a single request |
|
||||
| **Streaming** | NDJSON streams for progressive processing |
|
||||
| **Idempotency** | Built-in support for safe request retries |
|
||||
| **Structured Errors** | Machine-readable error codes, not just HTTP status |
|
||||
| **Request Tracking** | Every request gets a unique ID for debugging |
|
||||
| **Health Checks** | Kubernetes-compatible liveness/readiness probes |
|
||||
| **Operation Progress** | Server-Sent Events for long-running operations |
|
||||
## Documentation
|
||||
|
||||
```
|
||||
GET /api/v2/batch/files # Bulk file retrieval
|
||||
POST /api/v2/stream/files # NDJSON streaming
|
||||
GET /api/v2/operations/{id} # Operation status
|
||||
GET /api/v2/health/ready # Readiness probe
|
||||
```
|
||||
You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/).
|
||||
|
||||
### AI Context APIs - Repository Intelligence
|
||||
It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively.
|
||||
|
||||
Purpose-built endpoints that give AI tools the context they need:
|
||||
|
||||
**Repository Summary** (`/api/v2/ai/repo/summary`)
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"primary_language": "Go",
|
||||
"project_type": "application",
|
||||
"build_system": "go modules",
|
||||
"test_framework": "go test",
|
||||
"suggested_entry_points": ["cmd/main.go", "internal/app/"],
|
||||
"config_files": ["go.mod", "Makefile", ".gitea/workflows/"],
|
||||
"language_stats": {"Go": 45000, "YAML": 2000}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Navigation** (`/api/v2/ai/repo/navigation`)
|
||||
- Directory tree with depth control
|
||||
- Important paths ranked by priority (entry points, tests, docs)
|
||||
- File type distribution
|
||||
|
||||
**Issue Context** (`/api/v2/ai/issue/context`)
|
||||
- Issue details with all comments
|
||||
- Related issues and code references
|
||||
- AI hints: category (bug/feature), complexity estimation, suggested files
|
||||
|
||||
### Runner Capability Discovery
|
||||
|
||||
Runners report their capabilities. AI tools query before generating workflows.
|
||||
|
||||
**Endpoint:** `GET /api/v2/repos/{owner}/{repo}/actions/runners/capabilities`
|
||||
|
||||
```json
|
||||
{
|
||||
"runners": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ubuntu-runner",
|
||||
"status": "online",
|
||||
"labels": ["ubuntu-latest", "docker"],
|
||||
"capabilities": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"docker": true,
|
||||
"docker_compose": true,
|
||||
"shell": ["bash", "sh"],
|
||||
"tools": {
|
||||
"node": ["18.19.0", "20.10.0"],
|
||||
"go": ["1.21.5", "1.22.0"],
|
||||
"python": ["3.11.6", "3.12.0"]
|
||||
},
|
||||
"features": {
|
||||
"cache": true,
|
||||
"services": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"platform": {
|
||||
"type": "gitea",
|
||||
"version": "1.26.0",
|
||||
"supported_actions": {
|
||||
"actions/checkout": {"versions": ["v3", "v4"]},
|
||||
"actions/setup-node": {"versions": ["v3", "v4"]},
|
||||
"actions/upload-artifact": {"versions": ["v3"], "notes": "v4 not supported"}
|
||||
},
|
||||
"unsupported_features": [
|
||||
"GitHub-hosted runners",
|
||||
"OIDC token authentication"
|
||||
]
|
||||
},
|
||||
"workflow_hints": {
|
||||
"preferred_checkout": "actions/checkout@v4",
|
||||
"artifact_upload_alternative": "Use Gitea API for artifacts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Workflow Validation
|
||||
|
||||
Validate workflows before committing. Catch incompatibilities early.
|
||||
|
||||
**Endpoint:** `POST /api/v2/repos/{owner}/{repo}/actions/workflows/validate`
|
||||
|
||||
```json
|
||||
// Request
|
||||
{
|
||||
"content": "name: Build\non: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/upload-artifact@v4"
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
"valid": false,
|
||||
"warnings": [
|
||||
{
|
||||
"line": 8,
|
||||
"action": "actions/upload-artifact@v4",
|
||||
"severity": "error",
|
||||
"message": "actions/upload-artifact@v4 is not supported",
|
||||
"suggestion": "Use actions/upload-artifact@v3"
|
||||
}
|
||||
],
|
||||
"runner_match": [
|
||||
{
|
||||
"job": "build",
|
||||
"runs_on": ["ubuntu-latest"],
|
||||
"matched_runners": ["ubuntu-runner-1"],
|
||||
"capabilities_met": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Action Compatibility Database
|
||||
|
||||
Built-in knowledge of GitHub Action compatibility:
|
||||
|
||||
| Action | Compatible Versions | Notes |
|
||||
|--------|-------------------|-------|
|
||||
| `actions/checkout` | v2, v3, v4 | Fully compatible |
|
||||
| `actions/setup-node` | v2, v3, v4 | Fully compatible |
|
||||
| `actions/setup-go` | v3, v4, v5 | Fully compatible |
|
||||
| `actions/setup-python` | v4, v5 | Fully compatible |
|
||||
| `actions/cache` | v3, v4 | Fully compatible |
|
||||
| `actions/upload-artifact` | v2, v3 | v4 not supported |
|
||||
| `actions/download-artifact` | v2, v3 | v4 not supported |
|
||||
|
||||
### Release Archive
|
||||
|
||||
Archive old releases without deleting them:
|
||||
|
||||
- Toggle archived status via UI or API
|
||||
- Filter releases by archived state
|
||||
- Archived releases hidden by default, toggle to show
|
||||
- Preserves release history for compliance
|
||||
|
||||
```
|
||||
POST /api/v1/repos/{owner}/{repo}/releases/{id}/archive
|
||||
DELETE /api/v1/repos/{owner}/{repo}/releases/{id}/archive
|
||||
GET /api/v1/repos/{owner}/{repo}/releases?archived=false
|
||||
```
|
||||
|
||||
### Public Landing Pages & Releases for Private Repos
|
||||
|
||||
Private repositories can expose a public landing page and/or public releases. Perfect for:
|
||||
- Commercial software with private source but public downloads
|
||||
- Open-core projects with public documentation
|
||||
- Electron/desktop apps needing public update endpoints
|
||||
|
||||
Configure in `.gitea/landing.yaml`:
|
||||
```yaml
|
||||
enabled: true
|
||||
public_landing: true # Allow unauthenticated access to landing page
|
||||
|
||||
hero:
|
||||
title: "My App"
|
||||
tagline: "The best app ever"
|
||||
|
||||
advanced:
|
||||
public_releases: true # Allow unauthenticated access to releases
|
||||
```
|
||||
|
||||
**API Endpoints (no auth required when enabled):**
|
||||
```
|
||||
GET /api/v2/repos/{owner}/{repo}/pages/config # Landing page config
|
||||
GET /api/v2/repos/{owner}/{repo}/pages/content # Landing page content
|
||||
GET /api/v2/repos/{owner}/{repo}/releases # List releases
|
||||
GET /api/v2/repos/{owner}/{repo}/releases/latest # Latest release
|
||||
```
|
||||
|
||||
### App Update API (Electron/Squirrel Compatible)
|
||||
|
||||
Purpose-built endpoint for desktop app auto-updates. Returns Squirrel-compatible JSON format.
|
||||
|
||||
**Endpoint:** `GET /api/v2/repos/{owner}/{repo}/releases/update`
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `version` | Current app version (semver) | Required |
|
||||
| `platform` | `darwin`, `windows`, `linux` | Runtime OS |
|
||||
| `arch` | `x64`, `arm64` | Runtime arch |
|
||||
| `channel` | `stable`, `beta`, `alpha` | `stable` |
|
||||
|
||||
**Response (200 OK - update available):**
|
||||
```json
|
||||
{
|
||||
"url": "https://git.example.com/owner/repo/releases/download/v1.2.0/App-darwin-arm64.zip",
|
||||
"name": "v1.2.0",
|
||||
"notes": "Release notes in markdown...",
|
||||
"pub_date": "2026-01-10T12:00:00Z",
|
||||
"platform": {
|
||||
"size": 45000000,
|
||||
"releases_url": "https://...", // Windows RELEASES file
|
||||
"nupkg_url": "https://..." // Windows nupkg
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (204 No Content):** No update available
|
||||
|
||||
**Electron Integration:**
|
||||
```typescript
|
||||
// In your Electron app
|
||||
import { autoUpdater } from 'electron'
|
||||
|
||||
const version = app.getVersion()
|
||||
const platform = process.platform
|
||||
const arch = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
url: `https://git.example.com/api/v2/repos/owner/repo/releases/update?version=${version}&platform=${platform}&arch=${arch}`
|
||||
})
|
||||
|
||||
autoUpdater.checkForUpdates()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### From Binary
|
||||
|
||||
Download from [Releases](https://git.marketally.com/gitcaddy/gitea/releases):
|
||||
|
||||
```bash
|
||||
# Linux (amd64)
|
||||
curl -L -o gitcaddy https://git.marketally.com/gitcaddy/gitea/releases/latest/download/gitea-linux-amd64
|
||||
chmod +x gitcaddy
|
||||
./gitcaddy web
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/gitea.git
|
||||
cd gitea
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
./gitea web
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name gitcaddy \
|
||||
-p 3000:3000 \
|
||||
-v ./data:/data \
|
||||
gitcaddy/gitea:latest
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
GitCaddy uses the same configuration as Gitea. Key settings for AI features:
|
||||
|
||||
```ini
|
||||
[server]
|
||||
ROOT_URL = https://your-instance.com/
|
||||
|
||||
[actions]
|
||||
ENABLED = true
|
||||
|
||||
[api]
|
||||
; Enable V2 API (enabled by default)
|
||||
ENABLE_V2_API = true
|
||||
|
||||
; Max files in batch request
|
||||
MAX_BATCH_SIZE = 100
|
||||
|
||||
; Enable AI context endpoints
|
||||
ENABLE_AI_CONTEXT = true
|
||||
```
|
||||
|
||||
## GitCaddy Runner
|
||||
|
||||
For full capability reporting, use the [GitCaddy act_runner](https://git.marketally.com/gitcaddy/act_runner):
|
||||
|
||||
```bash
|
||||
# Download
|
||||
curl -L -o act_runner https://git.marketally.com/gitcaddy/act_runner/releases/latest/download/act_runner-linux-amd64
|
||||
chmod +x act_runner
|
||||
|
||||
# Register
|
||||
./act_runner register \
|
||||
--instance https://your-instance.com \
|
||||
--token YOUR_TOKEN \
|
||||
--name my-runner
|
||||
|
||||
# Run (automatically detects and reports capabilities)
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
The runner automatically detects:
|
||||
- OS and architecture
|
||||
- Docker/Podman availability
|
||||
- Installed tools (Node.js, Go, Python, Java, .NET, Rust)
|
||||
- Available shells
|
||||
- Docker Compose support
|
||||
|
||||
## API Documentation
|
||||
|
||||
Interactive API documentation available at:
|
||||
- `/api/v2/docs` - Scalar API explorer
|
||||
- `/api/v2/swagger.json` - OpenAPI specification
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GitCaddy
|
||||
|
|
||||
+------------------------------+------------------------------+
|
||||
| | |
|
||||
V2 API Layer Actions Engine Web Interface
|
||||
| | |
|
||||
+----+----+ +----+----+ +----+----+
|
||||
| | | | | |
|
||||
Batch Streaming Runners Workflows Repos Releases
|
||||
Files (NDJSON) Capability Validation (Archive)
|
||||
| | Discovery |
|
||||
| | | |
|
||||
+----+----+--------------------+---------+
|
||||
|
|
||||
AI Context APIs
|
||||
|
|
||||
+----+----+----+
|
||||
| | | |
|
||||
Repo Issue Nav Summary
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
|
||||
| Project | Description |
|
||||
|---------|-------------|
|
||||
| [gitcaddy/act_runner](https://git.marketally.com/gitcaddy/act_runner) | Runner with capability detection |
|
||||
| [gitcaddy/actions-proto-go](https://git.marketally.com/gitcaddy/actions-proto-go) | Protocol definitions |
|
||||
If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs)
|
||||
|
||||
## Building
|
||||
|
||||
Requirements:
|
||||
- Go 1.24+ (see `go.mod`)
|
||||
- Node.js 22.6+ (for frontend)
|
||||
- Make
|
||||
From the root of the source tree, run:
|
||||
|
||||
```bash
|
||||
# Full build
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
TAGS="bindata" make build
|
||||
|
||||
# Backend only
|
||||
make backend
|
||||
or if SQLite support is required:
|
||||
|
||||
# Frontend only
|
||||
make frontend
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
```
|
||||
The `build` target is split into two sub-targets:
|
||||
|
||||
- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
|
||||
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater.
|
||||
|
||||
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
|
||||
|
||||
More info: https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## Using
|
||||
|
||||
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!NOTE]
|
||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run tests: `make test`
|
||||
5. Submit a pull request
|
||||
Expected workflow is: Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
|
||||
> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
|
||||
|
||||
## Translating
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there.
|
||||
|
||||
You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up.
|
||||
|
||||
Get more information from [documentation](https://docs.gitea.com/contributing/localization).
|
||||
|
||||
## Official and Third-Party Projects
|
||||
|
||||
We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action.
|
||||
|
||||
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more.
|
||||
|
||||
## Communication
|
||||
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
|
||||
If you have questions that are not covered by the [documentation](https://docs.gitea.com/), you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/).
|
||||
|
||||
## Authors
|
||||
|
||||
- [Maintainers](https://github.com/orgs/go-gitea/people)
|
||||
- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [Translators](options/locale/TRANSLATORS)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/gitea#backer)]
|
||||
|
||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/gitea#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## FAQ
|
||||
|
||||
**How do you pronounce Gitea?**
|
||||
|
||||
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
|
||||
|
||||
**Why is this not hosted on a Gitea instance?**
|
||||
|
||||
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
|
||||
|
||||
**Where can I find the security patches?**
|
||||
|
||||
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
This project is licensed under the MIT License.
|
||||
See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file
|
||||
for the full license text.
|
||||
|
||||
---
|
||||
## Further information
|
||||
|
||||
## Acknowledgments
|
||||
<details>
|
||||
<summary>Looking for an overview of the interface? Check it out!</summary>
|
||||
|
||||
GitCaddy is a fork of [Gitea](https://gitea.io), the open-source self-hosted Git service. We thank the Gitea team and all contributors for building the foundation that makes GitCaddy possible.
|
||||
### Login/Register Page
|
||||
|
||||
- [Gitea Project](https://gitea.io)
|
||||
- [Gitea Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||

|
||||

|
||||
|
||||
### User Dashboard
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### User Profile
|
||||
|
||||

|
||||
|
||||
### Explore
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### Repository
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### Repository Issue
|
||||
|
||||

|
||||

|
||||
|
||||
#### Repository Pull Requests
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### Repository Actions
|
||||
|
||||

|
||||

|
||||
|
||||
#### Repository Activity
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### Organization
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
54
SECURITY.md
54
SECURITY.md
@@ -14,12 +14,12 @@ Please **DO NOT** file a public issue, instead send your report privately to `se
|
||||
|
||||
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
|
||||
|
||||
The PGP key is valid until July 4, 2026.
|
||||
The PGP key is valid until July 9, 2025.
|
||||
|
||||
```
|
||||
Key ID: 6FCD2D5B
|
||||
Key Type: RSA
|
||||
Expires: 7/4/2026
|
||||
Expires: 7/9/2025
|
||||
Key Size: 4096/4096
|
||||
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
|
||||
```
|
||||
@@ -42,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
|
||||
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
|
||||
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
|
||||
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
|
||||
LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
|
||||
f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
|
||||
cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
|
||||
t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
|
||||
HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
|
||||
I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
|
||||
LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
|
||||
RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
|
||||
HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
|
||||
+ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
|
||||
ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
|
||||
Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
|
||||
LVsFAmaMse0FCQW4fW8ACgkQqv0jgW/NLVtXLg/+PF4G9Jhlui15BTNlEBJAV2P/
|
||||
1QlAV2krk0fP7tykn0FR9RfGIfVV/kwC1f+ouosYPQDDevl9LWdUIM+g94DtNo2o
|
||||
7ACpcL3morvt5lVGpIZHL8TbX0qmFRXL/pB/cB+K6IwYvh2mrbp2zH+r4SCRyFYq
|
||||
BjgXYFTI1MylJ1ShAjU6Z+m3oJ+2xs5LzHS0X6zkTjzA2Zl4zQzciQ9T+wJcE7Zi
|
||||
HXdM1+YMF8KGNP8J9Rpug5oNDJ98lgZirRY7c3A/1xmYBiPnULwuuymdqEZO7l70
|
||||
SeAlE1RWYX8kbOBnBb/KY4XwE3Vic1oEzc9DiPWVH1ElX86WNNsFzuyULiwoBoWg
|
||||
pqZGhL9x1p5+46RGQSDczsHM7YGVtfYOiDo2PAVrmwsT0BnXnK8Oe3YIkvmUPEJu
|
||||
OkLt0Z6A5n8pz8zhQzuApwBsK4ncJ8zTCpvz/pfKKqZC/Vnoh3gKGhDGvOZ+b5IJ
|
||||
0kUTe2JsbnwFixDUMDtacQ1op8XOyLoLVmgqLn0+Pws4XPBlMof2bioFir3yHKnP
|
||||
gNchsF1agrlSIo5GA8u4ga+IlCSfvFIKrl7+cxacKcJYt/vbOU5KcvVJI5EtHKCG
|
||||
xfHjHY2ah1Qww7SxW6IXiRZZzPpsL2mBM2CD7N3qh9bV2s27wxYCdUodsIZbiyHe
|
||||
oWPzfBnkmiAN8KlZxHm5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
|
||||
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
|
||||
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
|
||||
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
|
||||
@@ -65,19 +65,19 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
|
||||
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
|
||||
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
|
||||
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
|
||||
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
|
||||
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
|
||||
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
|
||||
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
|
||||
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
|
||||
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
|
||||
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
|
||||
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
|
||||
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
|
||||
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
|
||||
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
|
||||
9M2VbqL9C51z/wyHLg==
|
||||
=SfZA
|
||||
WwUCZoyyjQUJBbh+DwAKCRCq/SOBb80tW18XD/9MXztmf01MT+1kZdBouZ/7Rp/7
|
||||
9kuqo//B1G+RXau4oFtPqb67kNe2WaIc3u5B73PUHsMf3i6z4ib2KbMhZZerLn0O
|
||||
dRglcuPeNWmsASY3dH/XVG0cT0zvvWegagd12TJEl3Vs+7XNrOw4cwDj9L1+GH9m
|
||||
kSt4uaANWn/6a3RvMRhiVEYuNwhAzcKaactPmYqrLJgoVLbRSDkgyHaMQ2jKgLxk
|
||||
ifS/fvluGV0ub2Po6DJiqfRpd1tDvPhe9y1+r1WFDZsOcvTcZUfSt/7dXMGfqGu0
|
||||
2daVFlfeSXSALrDE5uc0UxodHCpP3sqRYDZevGLBRaaTkIjYXG/+N898+7K5WJF4
|
||||
xXOLWxM2cwGkG7eC9pugcDnBp9XlF7O+GBiZ05JUe5flXDQFZ+h3exjopu6KHF1B
|
||||
RnzNy8LC0UKb+AuvRIOLV92a9Q9wGWU/jaVDu6nZ0umAeuSzxiHoDsonm0Fl9QAz
|
||||
2/xCokebuoeLrEK7R2af3X86mqq3sVO4ax+HPYChzOaVQBiHUW/TAldWcldYYphR
|
||||
/e2WsbmQfvCRtz/bZfo+aUVnrHNjzVMtF2SszdVmA/04Y8pS28MqtuRqhm5DPOOd
|
||||
g1YeUywK5jRZ1twyo1kzJEFPLaoeaXaycsR1PMVBW0Urik5mrR/pOWq7PPoZoKb2
|
||||
lXYLE8bwkuQTmsyL1g==
|
||||
=9i7d
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
```
|
||||
|
||||
1328
ai_enhancements.md
1328
ai_enhancements.md
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
103
assets/go-licenses.json
generated
103
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
23
build.go
Normal file
23
build.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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"
|
||||
|
||||
// for cover merge
|
||||
_ "golang.org/x/tools/cover"
|
||||
|
||||
// for vet
|
||||
_ "code.gitea.io/gitea-vet"
|
||||
|
||||
// for swagger
|
||||
_ "github.com/go-swagger/go-swagger/cmd/swagger"
|
||||
)
|
||||
@@ -12,11 +12,10 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/tools/codeformat"
|
||||
"code.gitea.io/gitea/build/codeformat"
|
||||
)
|
||||
|
||||
// Windows has a limitation for command line arguments, the size can not exceed 32KB.
|
||||
@@ -182,7 +181,7 @@ func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string
|
||||
break
|
||||
}
|
||||
}
|
||||
return mainOptions, subCmd, subArgs
|
||||
return
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
@@ -218,6 +217,15 @@ func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCol
|
||||
return newFileCollector(fileFilter, batchSize)
|
||||
}
|
||||
|
||||
func containsString(a []string, s string) bool {
|
||||
for _, v := range a {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func giteaFormatGoImports(files []string, doWriteFile bool) error {
|
||||
for _, file := range files {
|
||||
if err := codeformat.FormatGoImports(file, doWriteFile); err != nil {
|
||||
@@ -256,10 +264,10 @@ func main() {
|
||||
logVerbose("batch cmd: %s %v", subCmd, substArgs)
|
||||
switch subCmd {
|
||||
case "gitea-fmt":
|
||||
if slices.Contains(subArgs, "-d") {
|
||||
if containsString(subArgs, "-d") {
|
||||
log.Print("the -d option is not supported by gitea-fmt")
|
||||
}
|
||||
cmdErrors = append(cmdErrors, giteaFormatGoImports(files, slices.Contains(subArgs, "-w")))
|
||||
cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w")))
|
||||
cmdErrors = append(cmdErrors, passThroughCmd("gofmt", append([]string{"-w", "-r", "interface{} -> any"}, substArgs...)))
|
||||
cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra"}, substArgs...)))
|
||||
default:
|
||||
@@ -6,22 +6,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}")
|
||||
os.Exit(1)
|
||||
func needsUpdate(dir, filename string) (bool, []byte) {
|
||||
needRegen := false
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
needRegen = true
|
||||
}
|
||||
|
||||
dir, filename := os.Args[1], os.Args[2]
|
||||
fmt.Printf("generating bindata for %s to %s\n", dir, filename)
|
||||
if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil {
|
||||
fmt.Printf("failed: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
oldHash, err := os.ReadFile(filename + ".hash")
|
||||
if err != nil {
|
||||
oldHash = []byte{}
|
||||
}
|
||||
|
||||
hasher := sha1.New()
|
||||
|
||||
err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = hasher.Write([]byte(d.Name()))
|
||||
_, _ = hasher.Write([]byte(info.ModTime().String()))
|
||||
_, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16)))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return true, oldHash
|
||||
}
|
||||
|
||||
newHash := hasher.Sum([]byte{})
|
||||
|
||||
if bytes.Compare(oldHash, newHash) != 0 {
|
||||
return true, newHash
|
||||
}
|
||||
|
||||
return needRegen, newHash
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
log.Fatal("Insufficient number of arguments. Need: directory packageName filename")
|
||||
}
|
||||
|
||||
dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3]
|
||||
var useGlobalModTime bool
|
||||
if len(os.Args) == 5 {
|
||||
useGlobalModTime, _ = strconv.ParseBool(os.Args[4])
|
||||
}
|
||||
|
||||
update, newHash := needsUpdate(dir, filename)
|
||||
|
||||
if !update {
|
||||
fmt.Printf("bindata for %s already up-to-date\n", packageName)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("generating bindata for %s\n", packageName)
|
||||
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.Fatalf("%v\n", err)
|
||||
}
|
||||
_ = os.WriteFile(filename+".hash", newHash, 0o666)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
#!/bin/sh
|
||||
|
||||
# this script runs in alpine image which only has `sh` shell
|
||||
if [ ! -f ./options/locale/locale_en-US.json ]; then
|
||||
|
||||
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.json ./options/
|
||||
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=$(cat "./options/locale_en-US.json" | wc -l)
|
||||
baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1)
|
||||
baselines=$((baselines / 4))
|
||||
for filename in ./options/locale/*.json; do
|
||||
lines=$(cat "$filename" | wc -l)
|
||||
if [ "$lines" -lt "$baselines" ]; then
|
||||
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.json ./options/locale/
|
||||
mv ./options/locale_en-US.ini ./options/locale/
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -18,7 +17,7 @@ var (
|
||||
CmdActions = &cli.Command{
|
||||
Name: "actions",
|
||||
Usage: "Manage Gitea Actions",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdActionsGenRunnerToken,
|
||||
},
|
||||
}
|
||||
@@ -39,7 +38,10 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
|
||||
func runGenerateActionsRunnerToken(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setting.MustInstalled()
|
||||
|
||||
scope := c.String("scope")
|
||||
|
||||
49
cmd/admin.go
49
cmd/admin.go
@@ -15,7 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
CmdAdmin = &cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Perform common administrative operations",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdUser,
|
||||
subcmdRepoSyncReleases,
|
||||
subcmdRegenerate,
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
subcmdRegenerate = &cli.Command{
|
||||
Name: "regenerate",
|
||||
Usage: "Regenerate specific files",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdRegenHooks,
|
||||
microcmdRegenKeys,
|
||||
},
|
||||
@@ -50,15 +50,15 @@ var (
|
||||
subcmdAuth = &cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "Modify external auth providers",
|
||||
Commands: []*cli.Command{
|
||||
microcmdAuthAddOauth(),
|
||||
microcmdAuthUpdateOauth(),
|
||||
microcmdAuthAddLdapBindDn(),
|
||||
microcmdAuthUpdateLdapBindDn(),
|
||||
microcmdAuthAddLdapSimpleAuth(),
|
||||
microcmdAuthUpdateLdapSimpleAuth(),
|
||||
microcmdAuthAddSMTP(),
|
||||
microcmdAuthUpdateSMTP(),
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdAuthAddOauth,
|
||||
microcmdAuthUpdateOauth,
|
||||
microcmdAuthAddLdapBindDn,
|
||||
microcmdAuthUpdateLdapBindDn,
|
||||
microcmdAuthAddLdapSimpleAuth,
|
||||
microcmdAuthUpdateLdapSimpleAuth,
|
||||
microcmdAuthAddSMTP,
|
||||
microcmdAuthUpdateSMTP,
|
||||
microcmdAuthList,
|
||||
microcmdAuthDelete,
|
||||
},
|
||||
@@ -70,9 +70,9 @@ var (
|
||||
Action: runSendMail,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "title",
|
||||
Usage: "a title of a message",
|
||||
Required: true,
|
||||
Name: "title",
|
||||
Usage: `a title of a message`,
|
||||
Value: "",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "content",
|
||||
@@ -86,27 +86,28 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func idFlag() *cli.Int64Flag {
|
||||
return &cli.Int64Flag{
|
||||
idFlag = &cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of authentication source",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func runRepoSyncReleases(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.InitSimple(); err != nil {
|
||||
if err := git.InitSimple(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace("Synchronizing repository releases (this may take a while)")
|
||||
for page := 1; ; page++ {
|
||||
repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{
|
||||
repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: repo_model.RepositoryListDefaultPageSize,
|
||||
Page: page,
|
||||
@@ -121,7 +122,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
|
||||
}
|
||||
log.Trace("Processing next %d repos of %d", len(repos), count)
|
||||
for _, repo := range repos {
|
||||
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath())
|
||||
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Warn("OpenRepository: %v", err)
|
||||
@@ -147,7 +148,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Trace("repo %s releases synchronized to tags: from %d to %d",
|
||||
log.Trace(" repo %s releases synchronized to tags: from %d to %d",
|
||||
repo.FullName(), oldnum, count)
|
||||
gitRepo.Close()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -14,14 +13,14 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
microcmdAuthDelete = &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific auth source",
|
||||
Flags: []cli.Flag{idFlag()},
|
||||
Flags: []cli.Flag{idFlag},
|
||||
Action: runDeleteAuth,
|
||||
}
|
||||
microcmdAuthList = &cli.Command{
|
||||
@@ -57,7 +56,10 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runListAuth(ctx context.Context, c *cli.Command) error {
|
||||
func runListAuth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -88,11 +90,14 @@ 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()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -24,8 +24,8 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func commonLdapCLIFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
var (
|
||||
commonLdapCLIFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Authentication name.",
|
||||
@@ -94,10 +94,6 @@ func commonLdapCLIFlags() []cli.Flag {
|
||||
Name: "public-ssh-key-attribute",
|
||||
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ssh-keys-are-verified",
|
||||
Usage: "Set to true to automatically flag SSH keys in LDAP as verified.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-local-2fa",
|
||||
Usage: "Set to true to skip local 2fa for users authenticated by this source",
|
||||
@@ -107,10 +103,8 @@ func commonLdapCLIFlags() []cli.Flag {
|
||||
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ldapBindDnCLIFlags() []cli.Flag {
|
||||
return append(commonLdapCLIFlags(),
|
||||
ldapBindDnCLIFlags = append(commonLdapCLIFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "bind-dn",
|
||||
Usage: "The DN to bind to the LDAP server with when searching for the user.",
|
||||
@@ -163,59 +157,49 @@ func ldapBindDnCLIFlags() []cli.Flag {
|
||||
Name: "group-team-map-removal",
|
||||
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
|
||||
})
|
||||
}
|
||||
|
||||
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, cmd *cli.Command) error {
|
||||
return newAuthService().addLdapBindDn(ctx, cmd)
|
||||
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, cmd *cli.Command) error {
|
||||
return newAuthService().updateLdapBindDn(ctx, cmd)
|
||||
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, cmd *cli.Command) error {
|
||||
return newAuthService().addLdapSimpleAuth(ctx, cmd)
|
||||
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, cmd *cli.Command) error {
|
||||
return newAuthService().updateLdapSimpleAuth(ctx, cmd)
|
||||
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 {
|
||||
@@ -228,7 +212,7 @@ func newAuthService() *authService {
|
||||
}
|
||||
|
||||
// parseAuthSourceLdap assigns values on authSource according to command line flags.
|
||||
func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
|
||||
func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
|
||||
if c.IsSet("name") {
|
||||
authSource.Name = c.String("name")
|
||||
}
|
||||
@@ -248,7 +232,7 @@ func parseAuthSourceLdap(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")
|
||||
}
|
||||
@@ -261,7 +245,7 @@ func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
|
||||
if c.IsSet("security-protocol") {
|
||||
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
|
||||
return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
|
||||
}
|
||||
config.SecurityProtocol = p
|
||||
}
|
||||
@@ -298,9 +282,6 @@ func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
|
||||
if c.IsSet("public-ssh-key-attribute") {
|
||||
config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
|
||||
}
|
||||
if c.IsSet("ssh-keys-are-verified") {
|
||||
config.SSHKeysAreVerified = c.Bool("ssh-keys-are-verified")
|
||||
}
|
||||
if c.IsSet("avatar-attribute") {
|
||||
config.AttributeAvatar = c.String("avatar-attribute")
|
||||
}
|
||||
@@ -356,27 +337,32 @@ 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
|
||||
}
|
||||
|
||||
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authSource.Type != authType {
|
||||
return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
|
||||
return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
|
||||
}
|
||||
|
||||
return authSource, nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -398,7 +384,10 @@ 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 {
|
||||
func (a *authService) updateLdapBindDn(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -417,11 +406,14 @@ 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()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -443,7 +435,10 @@ 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 {
|
||||
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,16 +8,17 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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 {
|
||||
@@ -134,7 +135,7 @@ func TestAddLdapBindDn(t *testing.T) {
|
||||
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
|
||||
"--email-attribute", "mail",
|
||||
},
|
||||
errMsg: "unknown security protocol name: zzzzz",
|
||||
errMsg: "Unknown security protocol name: zzzzz",
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
@@ -233,18 +234,17 @@ func TestAddLdapBindDn(t *testing.T) {
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "getAuthSourceByID called", "case %d: should not call getAuthSourceByID", n)
|
||||
return nil, nil //nolint:nilnil // mock function covering improper behavior
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{
|
||||
Flags: microcmdAuthAddLdapBindDn().Flags,
|
||||
Action: service.addLdapBindDn,
|
||||
}
|
||||
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 {
|
||||
@@ -256,7 +256,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 {
|
||||
@@ -346,12 +348,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
|
||||
"--name", "ldap (simple auth) source",
|
||||
"--security-protocol", "zzzzz",
|
||||
"--host", "ldap-server",
|
||||
"--port", "1234",
|
||||
"--port", "123",
|
||||
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
|
||||
"--email-attribute", "mail",
|
||||
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
errMsg: "unknown security protocol name: zzzzz",
|
||||
errMsg: "Unknown security protocol name: zzzzz",
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
@@ -463,18 +465,17 @@ func TestAddLdapSimpleAuth(t *testing.T) {
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "getAuthSourceById called", "case %d: should not call getAuthSourceByID", n)
|
||||
return nil, nil //nolint:nilnil // mock function covering improper behavior
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := &cli.Command{
|
||||
Flags: microcmdAuthAddLdapSimpleAuth().Flags,
|
||||
Action: service.addLdapSimpleAuth,
|
||||
}
|
||||
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 {
|
||||
@@ -486,7 +487,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 {
|
||||
@@ -861,7 +864,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
"--id", "1",
|
||||
"--security-protocol", "xxxxx",
|
||||
},
|
||||
errMsg: "unknown security protocol name: xxxxx",
|
||||
errMsg: "Unknown security protocol name: xxxxx",
|
||||
},
|
||||
// case 22
|
||||
{
|
||||
@@ -880,7 +883,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &ldap.Source{},
|
||||
},
|
||||
errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
|
||||
errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
|
||||
},
|
||||
// case 24
|
||||
{
|
||||
@@ -944,12 +947,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{
|
||||
Flags: microcmdAuthUpdateLdapBindDn().Flags,
|
||||
Action: service.updateLdapBindDn,
|
||||
}
|
||||
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 {
|
||||
@@ -961,7 +964,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 {
|
||||
@@ -1252,7 +1257,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
"--id", "1",
|
||||
"--security-protocol", "xxxxx",
|
||||
},
|
||||
errMsg: "unknown security protocol name: xxxxx",
|
||||
errMsg: "Unknown security protocol name: xxxxx",
|
||||
},
|
||||
// case 18
|
||||
{
|
||||
@@ -1271,7 +1276,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
Type: auth.PAM,
|
||||
Cfg: &ldap.Source{},
|
||||
},
|
||||
errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM",
|
||||
errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
|
||||
},
|
||||
// case 20
|
||||
{
|
||||
@@ -1332,12 +1337,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{
|
||||
Flags: microcmdAuthUpdateLdapSimpleAuth().Flags,
|
||||
Action: service.updateLdapSimpleAuth,
|
||||
}
|
||||
app := cli.NewApp()
|
||||
app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
|
||||
app.Action = service.updateLdapSimpleAuth
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
err := app.Run(c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -13,11 +12,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"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: "",
|
||||
@@ -87,14 +86,6 @@ func oauthCLIFlags() []cli.Flag {
|
||||
Value: nil,
|
||||
Usage: "Scopes to request when to authenticate against this OAuth2 source",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ssh-public-key-claim-name",
|
||||
Usage: "Claim name that provides SSH public keys",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "full-name-claim-name",
|
||||
Usage: "Claim name that provides user's full name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "required-claim-name",
|
||||
Value: "",
|
||||
@@ -130,34 +121,23 @@ func oauthCLIFlags() []cli.Flag {
|
||||
Usage: "Activate automatic team membership removal depending on groups",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddOauth() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add-oauth",
|
||||
Usage: "Add new Oauth authentication source",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return newAuthService().runAddOauth(ctx, cmd)
|
||||
},
|
||||
Flags: oauthCLIFlags(),
|
||||
microcmdAuthAddOauth = &cli.Command{
|
||||
Name: "add-oauth",
|
||||
Usage: "Add new Oauth authentication source",
|
||||
Action: runAddOauth,
|
||||
Flags: oauthCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateOauth() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update-oauth",
|
||||
Usage: "Update existing Oauth authentication source",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return newAuthService().runUpdateOauth(ctx, cmd)
|
||||
},
|
||||
Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of authentication source",
|
||||
}}, oauthCLIFlags()[1:]...)...),
|
||||
microcmdAuthUpdateOauth = &cli.Command{
|
||||
Name: "update-oauth",
|
||||
Usage: "Update existing Oauth authentication source",
|
||||
Action: runUpdateOauth,
|
||||
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func parseOAuth2Config(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{
|
||||
@@ -185,13 +165,14 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
|
||||
RestrictedGroup: c.String("restricted-group"),
|
||||
GroupTeamMap: c.String("group-team-map"),
|
||||
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
|
||||
SSHPublicKeyClaimName: c.String("ssh-public-key-claim-name"),
|
||||
FullNameClaimName: c.String("full-name-claim-name"),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
func runAddOauth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -203,7 +184,7 @@ func (a *authService) runAddOauth(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,
|
||||
@@ -212,16 +193,19 @@ func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
|
||||
func runUpdateOauth(c *cli.Context) error {
|
||||
if !c.IsSet("id") {
|
||||
return errors.New("--id flag is missing")
|
||||
}
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -278,12 +262,6 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
|
||||
if c.IsSet("group-team-map-removal") {
|
||||
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
|
||||
}
|
||||
if c.IsSet("ssh-public-key-claim-name") {
|
||||
oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
|
||||
}
|
||||
if c.IsSet("full-name-claim-name") {
|
||||
oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
|
||||
}
|
||||
|
||||
// update custom URL mapping
|
||||
customURLMapping := &oauth2.CustomURLMapping{}
|
||||
@@ -318,5 +296,5 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
|
||||
oAuth2Config.CustomURLMapping = customURLMapping
|
||||
source.Cfg = oAuth2Config
|
||||
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||
return a.updateAuthSource(ctx, source)
|
||||
return auth_model.UpdateSource(ctx, source)
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestAddOauth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
source *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--provider", "github",
|
||||
"--key", "some_key",
|
||||
"--secret", "some_secret",
|
||||
},
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "github",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config with openid connect",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--provider", "openidConnect",
|
||||
"--key", "some_key",
|
||||
"--secret", "some_secret",
|
||||
"--auto-discover-url", "https://example.com",
|
||||
},
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "openidConnect",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--provider", "gitlab",
|
||||
"--key", "some_key",
|
||||
"--secret", "some_secret",
|
||||
"--use-custom-urls", "true",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
"--custom-tenant-id", "some_tenant",
|
||||
"--icon-url", "https://example.com/icon",
|
||||
"--scopes", "scope1,scope2",
|
||||
"--skip-local-2fa", "true",
|
||||
"--required-claim-name", "claim_name",
|
||||
"--required-claim-value", "claim_value",
|
||||
"--group-claim-name", "group_name",
|
||||
"--admin-group", "admin",
|
||||
"--restricted-group", "restricted",
|
||||
"--group-team-map", `{"group1": [1,2]}`,
|
||||
"--group-team-map-removal=true",
|
||||
"--ssh-public-key-claim-name", "attr_ssh_pub_key",
|
||||
"--full-name-claim-name", "attr_full_name",
|
||||
},
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://example.com/token",
|
||||
AuthURL: "https://example.com/auth",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "some_tenant",
|
||||
},
|
||||
IconURL: "https://example.com/icon",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
RequiredClaimName: "claim_name",
|
||||
RequiredClaimValue: "claim_value",
|
||||
GroupClaimName: "group_name",
|
||||
AdminGroup: "admin",
|
||||
RestrictedGroup: "restricted",
|
||||
GroupTeamMap: `{"group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
SSHPublicKeyClaimName: "attr_ssh_pub_key",
|
||||
FullNameClaimName: "attr_full_name",
|
||||
},
|
||||
TwoFactorPolicy: "skip",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var createdSource *auth_model.Source
|
||||
a := &authService{
|
||||
initDB: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
|
||||
createdSource = source
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
app := &cli.Command{
|
||||
Flags: microcmdAuthAddOauth().Flags,
|
||||
Action: a.runAddOauth,
|
||||
}
|
||||
|
||||
args := []string{"oauth-test"}
|
||||
args = append(args, tc.args...)
|
||||
|
||||
err := app.Run(t.Context(), args)
|
||||
|
||||
if tc.errMsg != "" {
|
||||
assert.EqualError(t, err, tc.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.source, createdSource)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOauth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth_model.Source
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing id",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
},
|
||||
errMsg: "--id flag is missing",
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
id: 1,
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "github",
|
||||
ClientID: "old_key",
|
||||
ClientSecret: "old_secret",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
"--provider", "gitlab",
|
||||
"--key", "new_key",
|
||||
"--secret", "new_secret",
|
||||
},
|
||||
authSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "new_key",
|
||||
ClientSecret: "new_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
id: 1,
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "old_key",
|
||||
ClientSecret: "old_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://old.example.com/token",
|
||||
AuthURL: "https://old.example.com/auth",
|
||||
ProfileURL: "https://old.example.com/profile",
|
||||
EmailURL: "https://old.example.com/email",
|
||||
Tenant: "old_tenant",
|
||||
},
|
||||
IconURL: "https://old.example.com/icon",
|
||||
Scopes: []string{"old_scope1", "old_scope2"},
|
||||
RequiredClaimName: "old_claim_name",
|
||||
RequiredClaimValue: "old_claim_value",
|
||||
GroupClaimName: "old_group_name",
|
||||
AdminGroup: "old_admin",
|
||||
RestrictedGroup: "old_restricted",
|
||||
GroupTeamMap: `{"old_group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
SSHPublicKeyClaimName: "old_ssh_pub_key",
|
||||
FullNameClaimName: "old_full_name",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
"--provider", "github",
|
||||
"--key", "new_key",
|
||||
"--secret", "new_secret",
|
||||
"--use-custom-urls", "true",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
"--custom-tenant-id", "new_tenant",
|
||||
"--icon-url", "https://example.com/icon",
|
||||
"--scopes", "scope1,scope2",
|
||||
"--skip-local-2fa=true",
|
||||
"--required-claim-name", "claim_name",
|
||||
"--required-claim-value", "claim_value",
|
||||
"--group-claim-name", "group_name",
|
||||
"--admin-group", "admin",
|
||||
"--restricted-group", "restricted",
|
||||
"--group-team-map", `{"group1": [1,2]}`,
|
||||
"--group-team-map-removal=false",
|
||||
"--ssh-public-key-claim-name", "new_ssh_pub_key",
|
||||
"--full-name-claim-name", "new_full_name",
|
||||
},
|
||||
authSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "github",
|
||||
ClientID: "new_key",
|
||||
ClientSecret: "new_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://example.com/token",
|
||||
AuthURL: "https://example.com/auth",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "new_tenant",
|
||||
},
|
||||
IconURL: "https://example.com/icon",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
RequiredClaimName: "claim_name",
|
||||
RequiredClaimValue: "claim_value",
|
||||
GroupClaimName: "group_name",
|
||||
AdminGroup: "admin",
|
||||
RestrictedGroup: "restricted",
|
||||
GroupTeamMap: `{"group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: false,
|
||||
SSHPublicKeyClaimName: "new_ssh_pub_key",
|
||||
FullNameClaimName: "new_full_name",
|
||||
},
|
||||
TwoFactorPolicy: "skip",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := &authService{
|
||||
initDB: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
|
||||
return &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
TwoFactorPolicy: "skip",
|
||||
}, nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
|
||||
assert.Equal(t, tc.authSource, source)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
app := &cli.Command{
|
||||
Flags: microcmdAuthUpdateOauth().Flags,
|
||||
Action: a.runUpdateOauth,
|
||||
}
|
||||
|
||||
args := []string{"oauth-test"}
|
||||
args = append(args, tc.args...)
|
||||
|
||||
err := app.Run(t.Context(), args)
|
||||
|
||||
if tc.errMsg != "" {
|
||||
assert.EqualError(t, err, tc.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestAddSMTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
source *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing name",
|
||||
args: []string{
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
},
|
||||
errMsg: "name must be set",
|
||||
},
|
||||
{
|
||||
name: "missing host",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--port", "25",
|
||||
},
|
||||
errMsg: "host must be set",
|
||||
},
|
||||
{
|
||||
name: "missing port",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
},
|
||||
errMsg: "port must be set",
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
},
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.SMTP,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "localhost",
|
||||
Port: 25,
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
"--auth-type", "LOGIN",
|
||||
"--force-smtps",
|
||||
"--skip-verify",
|
||||
"--helo-hostname", "example.com",
|
||||
"--disable-helo=true",
|
||||
"--allowed-domains", "example.com,example.org",
|
||||
"--skip-local-2fa",
|
||||
"--active=false",
|
||||
},
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.SMTP,
|
||||
Name: "test",
|
||||
IsActive: false,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "LOGIN",
|
||||
Host: "localhost",
|
||||
Port: 25,
|
||||
ForceSMTPS: true,
|
||||
SkipVerify: true,
|
||||
HeloHostname: "example.com",
|
||||
DisableHelo: true,
|
||||
AllowedDomains: "example.com,example.org",
|
||||
},
|
||||
TwoFactorPolicy: "skip",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := &authService{
|
||||
initDB: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
|
||||
assert.Equal(t, tc.source, source)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
Flags: microcmdAuthAddSMTP().Flags,
|
||||
Action: a.runAddSMTP,
|
||||
}
|
||||
|
||||
args := []string{"smtp-test"}
|
||||
args = append(args, tc.args...)
|
||||
|
||||
t.Log(args)
|
||||
err := cmd.Run(t.Context(), args)
|
||||
|
||||
if tc.errMsg != "" {
|
||||
assert.EqualError(t, err, tc.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSMTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
existingAuthSource *auth_model.Source
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing id",
|
||||
args: []string{
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
},
|
||||
errMsg: "--id flag is missing",
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "old host",
|
||||
Port: 26,
|
||||
},
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
},
|
||||
authSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "localhost",
|
||||
Port: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "old host",
|
||||
Port: 26,
|
||||
HeloHostname: "old.example.com",
|
||||
AllowedDomains: "old.example.com",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
"--host", "localhost",
|
||||
"--port", "25",
|
||||
"--auth-type", "LOGIN",
|
||||
"--force-smtps",
|
||||
"--skip-verify",
|
||||
"--helo-hostname", "example.com",
|
||||
"--disable-helo",
|
||||
"--allowed-domains", "example.com,example.org",
|
||||
"--skip-local-2fa",
|
||||
"--active=false",
|
||||
},
|
||||
authSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "test",
|
||||
IsActive: false,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "LOGIN",
|
||||
Host: "localhost",
|
||||
Port: 25,
|
||||
ForceSMTPS: true,
|
||||
SkipVerify: true,
|
||||
HeloHostname: "example.com",
|
||||
DisableHelo: true,
|
||||
AllowedDomains: "example.com,example.org",
|
||||
},
|
||||
TwoFactorPolicy: "skip",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := &authService{
|
||||
initDB: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
|
||||
return &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
||||
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
|
||||
assert.Equal(t, tc.authSource, source)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
app := &cli.Command{
|
||||
Flags: microcmdAuthUpdateSMTP().Flags,
|
||||
Action: a.runUpdateSMTP,
|
||||
}
|
||||
args := []string{"smtp-tests"}
|
||||
args = append(args, tc.args...)
|
||||
|
||||
err := app.Run(t.Context(), args)
|
||||
|
||||
if tc.errMsg != "" {
|
||||
assert.EqualError(t, err, tc.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
@@ -12,11 +11,11 @@ import (
|
||||
"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: "",
|
||||
@@ -68,34 +67,23 @@ func smtpCLIFlags() []cli.Flag {
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateSMTP() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update-smtp",
|
||||
Usage: "Update existing SMTP authentication source",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return newAuthService().runUpdateSMTP(ctx, cmd)
|
||||
},
|
||||
Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of authentication source",
|
||||
}}, smtpCLIFlags()[1:]...)...),
|
||||
microcmdAuthAddSMTP = &cli.Command{
|
||||
Name: "add-smtp",
|
||||
Usage: "Add new SMTP authentication source",
|
||||
Action: runAddSMTP,
|
||||
Flags: smtpCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddSMTP() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add-smtp",
|
||||
Usage: "Add new SMTP authentication source",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return newAuthService().runAddSMTP(ctx, cmd)
|
||||
},
|
||||
Flags: smtpCLIFlags(),
|
||||
microcmdAuthUpdateSMTP = &cli.Command{
|
||||
Name: "update-smtp",
|
||||
Usage: "Update existing SMTP authentication source",
|
||||
Action: runUpdateSMTP,
|
||||
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 +116,11 @@ func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
func runAddSMTP(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -157,7 +148,7 @@ func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
|
||||
smtpConfig.Auth = "PLAIN"
|
||||
}
|
||||
|
||||
return a.createAuthSource(ctx, &auth_model.Source{
|
||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||
Type: auth_model.SMTP,
|
||||
Name: c.String("name"),
|
||||
IsActive: active,
|
||||
@@ -166,16 +157,19 @@ func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (a *authService) 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")
|
||||
}
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -196,5 +190,5 @@ func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
source.Cfg = smtpConfig
|
||||
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||
return a.updateAuthSource(ctx, source)
|
||||
return auth_model.UpdateSource(ctx, source)
|
||||
}
|
||||
@@ -4,13 +4,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,14 +25,20 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
|
||||
func runRegenerateHooks(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
|
||||
}
|
||||
|
||||
func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
|
||||
func runRegenerateKeys(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var subcmdUser = &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Commands: []*cli.Command{
|
||||
microcmdUserCreate(),
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdUserCreate,
|
||||
microcmdUserList,
|
||||
microcmdUserChangePassword(),
|
||||
microcmdUserDelete(),
|
||||
microcmdUserChangePassword,
|
||||
microcmdUserDelete,
|
||||
microcmdUserGenerateAccessToken,
|
||||
microcmdUserMustChangePassword(),
|
||||
microcmdUserMustChangePassword,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -14,41 +13,44 @@ import (
|
||||
"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{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "The user to change password for",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "New password to set for user",
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password (can be disabled by --must-change-password=false)",
|
||||
Value: true,
|
||||
},
|
||||
var microcmdUserChangePassword = &cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password (can be disabled by --must-change-password=false)",
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
if !setting.IsInTesting {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
func runChangePassword(c *cli.Context) error {
|
||||
if err := argsSet(c, "username", "password"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestChangePasswordCommand(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
}()
|
||||
|
||||
t.Run("change password successfully", func(t *testing.T) {
|
||||
// defer func() {
|
||||
// require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
// }()
|
||||
// Prepare test user
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// load test user
|
||||
userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
|
||||
// Change the password
|
||||
err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the password has been changed
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.NotEqual(t, userBase.Passwd, user.Passwd)
|
||||
assert.NotEqual(t, userBase.Salt, user.Salt)
|
||||
|
||||
// Additional check for must-change-password flag
|
||||
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, user.MustChangePassword)
|
||||
|
||||
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, user.MustChangePassword)
|
||||
})
|
||||
|
||||
t.Run("failure cases", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "user does not exist",
|
||||
args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
|
||||
expectedErr: "user does not exist",
|
||||
},
|
||||
{
|
||||
name: "missing username",
|
||||
args: []string{"change-password", "--password", "newpassword"},
|
||||
expectedErr: `"username" not set`,
|
||||
},
|
||||
{
|
||||
name: "missing password",
|
||||
args: []string{"change-password", "--username", "testuser"},
|
||||
expectedErr: `"password" not set`,
|
||||
},
|
||||
{
|
||||
name: "too short password",
|
||||
args: []string{"change-password", "--username", "testuser", "--password", "1"},
|
||||
expectedErr: "password is not long enough",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := microcmdUserChangePassword().Run(ctx, tc.args)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -16,95 +16,87 @@ import (
|
||||
"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{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Action: runCreateUser,
|
||||
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
|
||||
{
|
||||
Flags: [][]cli.Flag{
|
||||
{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
var microcmdUserCreate = &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Action: runCreateUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "user-type",
|
||||
Usage: "Set user's type: individual or bot",
|
||||
Value: "individual",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
|
||||
HideDefault: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "fullname",
|
||||
Usage: `The full, human-readable name of the user`,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "user-type",
|
||||
Usage: "Set user's type: individual or bot",
|
||||
Value: "individual",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
|
||||
DisableDefaultText: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.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()
|
||||
|
||||
if err := argsSet(c, "email"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userTypes := map[string]user_model.UserType{
|
||||
"individual": user_model.UserTypeIndividual,
|
||||
"bot": user_model.UserTypeBot,
|
||||
@@ -121,6 +113,12 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
return errors.New("password can only be set for individual users")
|
||||
}
|
||||
}
|
||||
if c.IsSet("name") && c.IsSet("username") {
|
||||
return errors.New("cannot set both --name and --username flags")
|
||||
}
|
||||
if !c.IsSet("name") && !c.IsSet("username") {
|
||||
return errors.New("one of --name or --username flags must be set")
|
||||
}
|
||||
|
||||
if c.IsSet("password") && c.IsSet("random-password") {
|
||||
return errors.New("cannot set both -random-password and -password flags")
|
||||
@@ -131,12 +129,16 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
username = c.String("username")
|
||||
} else {
|
||||
username = c.String("name")
|
||||
_, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
|
||||
_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
if !setting.IsInTesting {
|
||||
// FIXME: need to refactor the "initDB" related code later
|
||||
// FIXME: need to refactor the "installSignals/initDB" related code later
|
||||
// it doesn't make sense to call it in (almost) every command action function
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = installSignals()
|
||||
defer cancel()
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,7 +153,6 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
fmt.Printf("generated random password is '%s'\n", password)
|
||||
} else if userType == user_model.UserTypeIndividual {
|
||||
return errors.New("must set either password or random-password flag")
|
||||
|
||||
@@ -18,10 +18,12 @@ import (
|
||||
)
|
||||
|
||||
func TestAdminUserCreate(t *testing.T) {
|
||||
app := NewMainApp(AppVersion{})
|
||||
|
||||
reset := func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
|
||||
}
|
||||
|
||||
t.Run("MustChangePassword", func(t *testing.T) {
|
||||
@@ -29,9 +31,8 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
IsAdmin bool
|
||||
MustChangePassword bool
|
||||
}
|
||||
|
||||
createCheck := func(name, args string) check {
|
||||
require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
})
|
||||
|
||||
createUser := func(name string, args ...string) error {
|
||||
return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
|
||||
return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
|
||||
}
|
||||
|
||||
t.Run("UserType", func(t *testing.T) {
|
||||
|
||||
@@ -4,56 +4,53 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"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{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
var microcmdUserDelete = &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
},
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
|
||||
func runDeleteUser(ctx context.Context, c *cli.Command) error {
|
||||
func runDeleteUser(c *cli.Context) error {
|
||||
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
|
||||
return errors.New("You must provide the id, username or email of a user to delete")
|
||||
}
|
||||
|
||||
if !setting.IsInTesting {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
@@ -73,11 +70,11 @@ func runDeleteUser(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
|
||||
return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
|
||||
return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
|
||||
}
|
||||
|
||||
if c.IsSet("id") && user.ID != c.Int64("id") {
|
||||
return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
|
||||
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
|
||||
}
|
||||
|
||||
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminUserDelete(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
defer func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
|
||||
}()
|
||||
|
||||
setupTestUser := func(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("delete user by id", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by username", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by email", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by all 3 attributes", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminUserDeleteFailure(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no user to delete",
|
||||
args: []string{"delete", "--username", "nonexistentuser"},
|
||||
expectedErr: "user does not exist",
|
||||
},
|
||||
{
|
||||
name: "user exists but provided username does not match",
|
||||
args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
|
||||
expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
|
||||
},
|
||||
{
|
||||
name: "user exists but provided id does not match",
|
||||
args: []string{"delete", "--username", "testuser", "--id", "999"},
|
||||
expectedErr: "the user testuser does not match the provided id 999",
|
||||
},
|
||||
{
|
||||
name: "no required flags are provided",
|
||||
args: []string{"delete"},
|
||||
expectedErr: "You must provide the id, username or email of a user to delete",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
if strings.Contains(tc.name, "user exists") {
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := microcmdUserDelete().Run(ctx, tc.args)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
})
|
||||
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
@@ -42,11 +41,14 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
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")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var microcmdUserList = &cli.Command{
|
||||
@@ -26,7 +25,10 @@ var microcmdUserList = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runListUsers(ctx context.Context, c *cli.Command) error {
|
||||
func runListUsers(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,41 +4,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserMustChangePassword() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set the must change password flag for the provided users or all users",
|
||||
Action: runMustChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Aliases: []string{"A"},
|
||||
Usage: "All users must change password, except those explicitly excluded with --exclude",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "exclude",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Do not change the must-change-password flag for these users",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "unset",
|
||||
Usage: "Instead of setting the must-change-password flag, unset it",
|
||||
},
|
||||
var microcmdUserMustChangePassword = &cli.Command{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set the must change password flag for the provided users or all users",
|
||||
Action: runMustChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Aliases: []string{"A"},
|
||||
Usage: "All users must change password, except those explicitly excluded with --exclude",
|
||||
},
|
||||
}
|
||||
&cli.StringSliceFlag{
|
||||
Name: "exclude",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Do not change the must-change-password flag for these users",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "unset",
|
||||
Usage: "Instead of setting the must-change-password flag, unset it",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runMustChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
func runMustChangePassword(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if c.NArg() == 0 && !c.IsSet("all") {
|
||||
return errors.New("either usernames or --all must be provided")
|
||||
}
|
||||
@@ -47,10 +46,8 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
all := c.Bool("all")
|
||||
exclude := c.StringSlice("exclude")
|
||||
|
||||
if !setting.IsInTesting {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
|
||||
@@ -58,7 +55,6 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMustChangePassword(t *testing.T) {
|
||||
defer func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
}()
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
// Reset password change flag
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Make all users change password
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.True(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Reset password change flag but exclude all tested users
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.True(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Reset password change flag by listing multiple users
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Exclude a user from all user
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Unset a flag for single user
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
}
|
||||
128
cmd/cert.go
128
cmd/cert.go
@@ -6,7 +6,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -22,59 +20,47 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// cmdCert represents the available cert sub-command.
|
||||
func cmdCert() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cert",
|
||||
Usage: "Generate self-signed certificate",
|
||||
Description: `Generate a self-signed X.509 certificate for a TLS server.
|
||||
// CmdCert represents the available cert sub-command.
|
||||
var CmdCert = &cli.Command{
|
||||
Name: "cert",
|
||||
Usage: "Generate self-signed certificate",
|
||||
Description: `Generate a self-signed X.509 certificate for a TLS server.
|
||||
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
|
||||
Action: runCert,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ecdsa-curve",
|
||||
Value: "",
|
||||
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "rsa-bits",
|
||||
Value: 3072,
|
||||
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "start-date",
|
||||
Value: "",
|
||||
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "duration",
|
||||
Value: 365 * 24 * time.Hour,
|
||||
Usage: "Duration that certificate is valid for",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ca",
|
||||
Usage: "whether this cert should be its own Certificate Authority",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Value: "cert.pem",
|
||||
Usage: "Path to the file where there certificate will be saved",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "keyout",
|
||||
Value: "key.pem",
|
||||
Usage: "Path to the file where there certificate key will be saved",
|
||||
},
|
||||
Action: runCert,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "",
|
||||
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "ecdsa-curve",
|
||||
Value: "",
|
||||
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "rsa-bits",
|
||||
Value: 3072,
|
||||
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "start-date",
|
||||
Value: "",
|
||||
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "duration",
|
||||
Value: 365 * 24 * time.Hour,
|
||||
Usage: "Duration that certificate is valid for",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ca",
|
||||
Usage: "whether this cert should be its own Certificate Authority",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func publicKey(priv any) any {
|
||||
@@ -103,7 +89,11 @@ func pemBlockForKey(priv any) *pem.Block {
|
||||
}
|
||||
}
|
||||
|
||||
func runCert(_ context.Context, c *cli.Command) error {
|
||||
func runCert(c *cli.Context) error {
|
||||
if err := argsSet(c, "host"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var priv any
|
||||
var err error
|
||||
switch c.String("ecdsa-curve") {
|
||||
@@ -118,17 +108,17 @@ func runCert(_ context.Context, c *cli.Command) error {
|
||||
case "P521":
|
||||
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
default:
|
||||
err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
|
||||
log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate private key: %w", err)
|
||||
log.Fatalf("Failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
var notBefore time.Time
|
||||
if startDate := c.String("start-date"); startDate != "" {
|
||||
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse creation date %w", err)
|
||||
log.Fatalf("Failed to parse creation date: %v", err)
|
||||
}
|
||||
} else {
|
||||
notBefore = time.Now()
|
||||
@@ -139,7 +129,7 @@ func runCert(_ context.Context, c *cli.Command) error {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate serial number: %w", err)
|
||||
log.Fatalf("Failed to generate serial number: %v", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
@@ -156,8 +146,8 @@ func runCert(_ context.Context, c *cli.Command) error {
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
hosts := strings.SplitSeq(c.String("host"), ",")
|
||||
for h := range hosts {
|
||||
hosts := strings.Split(c.String("host"), ",")
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
@@ -172,35 +162,35 @@ func runCert(_ context.Context, c *cli.Command) error {
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create certificate: %w", err)
|
||||
log.Fatalf("Failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
certOut, err := os.Create(c.String("out"))
|
||||
certOut, err := os.Create("cert.pem")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
|
||||
log.Fatalf("Failed to open cert.pem for writing: %v", err)
|
||||
}
|
||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode certificate: %w", err)
|
||||
log.Fatalf("Failed to encode certificate: %v", err)
|
||||
}
|
||||
err = certOut.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write cert: %w", err)
|
||||
log.Fatalf("Failed to write cert: %v", err)
|
||||
}
|
||||
fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
|
||||
log.Println("Written cert.pem")
|
||||
|
||||
keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
|
||||
log.Fatalf("Failed to open key.pem for writing: %v", err)
|
||||
}
|
||||
err = pem.Encode(keyOut, pemBlockForKey(priv))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode key: %w", err)
|
||||
log.Fatalf("Failed to encode key: %v", err)
|
||||
}
|
||||
err = keyOut.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write key: %w", err)
|
||||
log.Fatalf("Failed to write key: %v", err)
|
||||
}
|
||||
fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
|
||||
log.Println("Written key.pem")
|
||||
return nil
|
||||
}
|
||||
|
||||
123
cmd/cert_test.go
123
cmd/cert_test.go
@@ -1,123 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCertCommand(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "RSA cert generation",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost",
|
||||
"--rsa-bits", "2048",
|
||||
"--duration", "1h",
|
||||
"--start-date", "Jan 1 00:00:00 2024",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ECDSA cert generation",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost",
|
||||
"--ecdsa-curve", "P256",
|
||||
"--duration", "1h",
|
||||
"--start-date", "Jan 1 00:00:00 2024",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed host, certificate authority",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost,127.0.0.1",
|
||||
"--duration", "1h",
|
||||
"--start-date", "Jan 1 00:00:00 2024",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
app := cmdCert()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
certFile := filepath.Join(tempDir, "cert.pem")
|
||||
keyFile := filepath.Join(tempDir, "key.pem")
|
||||
|
||||
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.FileExists(t, certFile)
|
||||
assert.FileExists(t, keyFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertCommandFailures(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "Start Date Parsing failure",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost",
|
||||
"--start-date", "invalid-date",
|
||||
},
|
||||
errMsg: "parsing time",
|
||||
},
|
||||
{
|
||||
name: "Unknown curve",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost",
|
||||
"--ecdsa-curve", "invalid-curve",
|
||||
},
|
||||
errMsg: "unrecognized elliptic curve",
|
||||
},
|
||||
{
|
||||
name: "Key generation failure",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
"--host", "localhost",
|
||||
"--rsa-bits", "invalid-bits",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Missing parameters",
|
||||
args: []string{
|
||||
"cert-test",
|
||||
},
|
||||
errMsg: `"host" not set`,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
app := cmdCert()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
certFile := filepath.Join(tempDir, "cert.pem")
|
||||
keyFile := filepath.Join(tempDir, "key.pem")
|
||||
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
|
||||
require.Error(t, err)
|
||||
if c.errMsg != "" {
|
||||
assert.ErrorContains(t, err, c.errMsg)
|
||||
}
|
||||
assert.NoFileExists(t, certFile)
|
||||
assert.NoFileExists(t, keyFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
31
cmd/cmd.go
31
cmd/cmd.go
@@ -18,19 +18,20 @@ import (
|
||||
"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 c.Value(a) == nil {
|
||||
if util.IsEmptyString(c.String(a)) {
|
||||
return errors.New(a + " is required")
|
||||
}
|
||||
}
|
||||
@@ -108,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
|
||||
@@ -119,14 +120,8 @@ 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(context.Context, *cli.Command) (context.Context, error) {
|
||||
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
if setting.InstallLock {
|
||||
// During config loading, there might also be logs (for example: deprecation warnings).
|
||||
// It must make sure that console logger is set up before config is loaded.
|
||||
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
|
||||
return nil, errors.New("console logger must be setup before config is loaded")
|
||||
}
|
||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
level := defaultLevel
|
||||
if globalBool(c, "quiet") {
|
||||
level = log.FATAL
|
||||
@@ -135,16 +130,6 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
|
||||
level = log.TRACE
|
||||
}
|
||||
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
|
||||
return ctx, nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
|
||||
// Dirty patch for urfave/cli's strange design.
|
||||
// "./gitea bad-cmd" should not start the web server.
|
||||
rootArgs := cmd.Root().Args().Slice()
|
||||
if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
|
||||
return rootArgs[0], false
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestDefaultCommand(t *testing.T) {
|
||||
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
|
||||
called := false
|
||||
cmd := &cli.Command{
|
||||
DefaultCommand: "test",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "test",
|
||||
Action: func(ctx context.Context, command *cli.Command) error {
|
||||
retName, retValid := isValidDefaultSubCommand(command)
|
||||
assert.Equal(t, expectedRetName, retName)
|
||||
assert.Equal(t, expectedRetValid, retValid)
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cmd.Run(t.Context(), args))
|
||||
assert.True(t, called)
|
||||
}
|
||||
test(t, []string{"./gitea"}, "", true)
|
||||
test(t, []string{"./gitea", "test"}, "", true)
|
||||
test(t, []string{"./gitea", "other"}, "other", false)
|
||||
}
|
||||
156
cmd/config.go
156
cmd/config.go
@@ -1,156 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func cmdConfig() *cli.Command {
|
||||
subcmdConfigEditIni := &cli.Command{
|
||||
Name: "edit-ini",
|
||||
Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.",
|
||||
Description: `
|
||||
Help users to edit the Gitea configuration INI file.
|
||||
|
||||
# Keep Specified Keys
|
||||
|
||||
If you need to re-create the configuration file with only a subset of keys,
|
||||
you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag.
|
||||
For example, if a helm chart needs to reset the settings and only keep SECRET_KEY,
|
||||
it can use a template file (only keys take effect, values are ignored):
|
||||
|
||||
[security]
|
||||
SECRET_KEY=
|
||||
|
||||
$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini
|
||||
|
||||
# Map Environment Variables to INI Configuration
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value as provided.
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME__FILE"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value loaded from the specified file.
|
||||
|
||||
Environment variable keys can only contain characters "0-9A-Z_",
|
||||
if a section or key name contains dot ".", it needs to be escaped as _0x2E_.
|
||||
For example, to apply this config:
|
||||
|
||||
[git.config]
|
||||
foo.bar=val
|
||||
|
||||
$ export GITEA__git_0x2E_config__foo_0x2E_bar=val
|
||||
|
||||
# Put All Together
|
||||
|
||||
$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini}
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
// "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper
|
||||
// "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file
|
||||
&cli.BoolFlag{
|
||||
Name: "in-place",
|
||||
Usage: "Output to the same config file as input. This flag will be ignored if --out is set.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config-keep-keys",
|
||||
Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "apply-env",
|
||||
Usage: "Apply all GITEA__* variables from the environment to the config.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Usage: "Destination config file to write to.",
|
||||
},
|
||||
},
|
||||
Action: runConfigEditIni,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Manage Gitea configuration",
|
||||
Commands: []*cli.Command{
|
||||
subcmdConfigEditIni,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigEditIni(_ context.Context, c *cli.Command) error {
|
||||
// the config system may change the environment variables, so get a copy first, to be used later
|
||||
env := append([]string{}, os.Environ()...)
|
||||
|
||||
// don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly
|
||||
if !c.IsSet("config") {
|
||||
return errors.New("flag is required but not set: --config")
|
||||
}
|
||||
configFileIn := c.String("config")
|
||||
|
||||
cfgIn, err := setting.NewConfigProviderFromFile(configFileIn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config file %q: %v", configFileIn, err)
|
||||
}
|
||||
|
||||
// determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file
|
||||
inPlace := c.Bool("in-place")
|
||||
configFileOut := c.String("out")
|
||||
if configFileOut == "" {
|
||||
if !inPlace {
|
||||
return errors.New("either --in-place or --out must be specified")
|
||||
}
|
||||
configFileOut = configFileIn // in-place edit
|
||||
}
|
||||
|
||||
needWriteOut := configFileOut != configFileIn
|
||||
|
||||
cfgOut := cfgIn
|
||||
configKeepKeys := c.String("config-keep-keys")
|
||||
if configKeepKeys != "" {
|
||||
needWriteOut = true
|
||||
cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err)
|
||||
}
|
||||
|
||||
for _, secOut := range cfgOut.Sections() {
|
||||
for _, keyOut := range secOut.Keys() {
|
||||
secIn := cfgIn.Section(secOut.Name())
|
||||
keyIn := setting.ConfigSectionKey(secIn, keyOut.Name())
|
||||
if keyIn != nil {
|
||||
keyOut.SetValue(keyIn.String())
|
||||
} else {
|
||||
secOut.DeleteKey(keyOut.Name())
|
||||
}
|
||||
}
|
||||
if len(secOut.Keys()) == 0 {
|
||||
cfgOut.DeleteSection(secOut.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("apply-env") {
|
||||
if setting.EnvironmentToConfig(cfgOut, env) {
|
||||
needWriteOut = true
|
||||
}
|
||||
}
|
||||
|
||||
if needWriteOut {
|
||||
err = cfgOut.SaveTo(configFileOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigEdit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configOld := tmpDir + "/app-old.ini"
|
||||
configTemplate := tmpDir + "/app-template.ini"
|
||||
_ = os.WriteFile(configOld, []byte(`
|
||||
[sec]
|
||||
k1=v1
|
||||
k2=v2
|
||||
`), os.ModePerm)
|
||||
|
||||
_ = os.WriteFile(configTemplate, []byte(`
|
||||
[sec]
|
||||
k1=in-template
|
||||
|
||||
[sec2]
|
||||
k3=v3
|
||||
`), os.ModePerm)
|
||||
|
||||
t.Setenv("GITEA__EnV__KeY", "val")
|
||||
|
||||
t.Run("OutputToNewWithEnv", func(t *testing.T) {
|
||||
configNew := tmpDir + "/app-new.ini"
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "--config", configOld,
|
||||
"config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config-keep-keys", configTemplate,
|
||||
"--out", configNew,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// "k1" old value is kept because its key is in the template
|
||||
// "k2" is removed because it isn't in the template
|
||||
// "k3" isn't in new config because it isn't in the old config
|
||||
// [env] is applied from environment variable
|
||||
data, _ := os.ReadFile(configNew)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
|
||||
t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) {
|
||||
// the legacy "environment-to-ini" (now a wrapper script) behavior:
|
||||
// if no "--out", then "--in-place" must be used to overwrite the existing "--config" file
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.ErrorContains(t, err, "either --in-place or --out must be specified")
|
||||
|
||||
// simulate the "environment-to-ini" behavior with "--in-place"
|
||||
err = NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--in-place",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, _ := os.ReadFile(configOld)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
k2 = v2
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
}
|
||||
18
cmd/docs.go
18
cmd/docs.go
@@ -4,13 +4,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cli_docs "github.com/urfave/cli-docs/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdDocs represents the available docs sub-command.
|
||||
@@ -32,16 +30,16 @@ var CmdDocs = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runDocs(_ context.Context, cmd *cli.Command) error {
|
||||
docs, err := cli_docs.ToMarkdown(cmd.Root())
|
||||
if cmd.Bool("man") {
|
||||
docs, err = cli_docs.ToMan(cmd.Root())
|
||||
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 !cmd.Bool("man") {
|
||||
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
|
||||
@@ -53,8 +51,8 @@ func runDocs(_ context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
out := os.Stdout
|
||||
if cmd.String("output") != "" {
|
||||
fi, err := os.Create(cmd.String("output"))
|
||||
if ctx.String("output") != "" {
|
||||
fi, err := os.Create(ctx.String("output"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{
|
||||
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
cmdDoctorCheck,
|
||||
cmdRecreateTable,
|
||||
cmdDoctorConvert,
|
||||
@@ -93,13 +93,16 @@ You should back-up your database before doing this and ensure that your database
|
||||
Action: runRecreateTable,
|
||||
}
|
||||
|
||||
func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
func runRecreateTable(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
// Redirect the default golog to here
|
||||
golog.SetFlags(0)
|
||||
golog.SetPrefix("")
|
||||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
|
||||
debug := cmd.Bool("debug")
|
||||
debug := ctx.Bool("debug")
|
||||
setting.MustInstalled()
|
||||
setting.LoadDBSetting()
|
||||
|
||||
@@ -110,15 +113,15 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
setting.Database.LogSQL = debug
|
||||
if err := db.InitEngine(ctx); err != nil {
|
||||
if err := db.InitEngine(stdCtx); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
|
||||
return nil
|
||||
}
|
||||
|
||||
args := cmd.Args()
|
||||
names := make([]string, 0, cmd.NArg())
|
||||
for i := 0; i < cmd.NArg(); i++ {
|
||||
args := ctx.Args()
|
||||
names := make([]string, 0, ctx.NArg())
|
||||
for i := 0; i < ctx.NArg(); i++ {
|
||||
names = append(names, args.Get(i))
|
||||
}
|
||||
|
||||
@@ -128,7 +131,7 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
recreateTables := migrate_base.RecreateTables(beans...)
|
||||
|
||||
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error {
|
||||
return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error {
|
||||
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,11 +139,11 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
})
|
||||
}
|
||||
|
||||
func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
|
||||
func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
|
||||
// Silence the default loggers
|
||||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
||||
|
||||
logFile := cmd.String("log-file")
|
||||
logFile := ctx.String("log-file")
|
||||
switch logFile {
|
||||
case "":
|
||||
return // if no doctor log-file is set, do not show any log from default logger
|
||||
@@ -158,20 +161,23 @@ func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
|
||||
func runDoctorCheck(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
colorize := log.CanColorStdout
|
||||
if cmd.IsSet("color") {
|
||||
colorize = cmd.Bool("color")
|
||||
if ctx.IsSet("color") {
|
||||
colorize = ctx.Bool("color")
|
||||
}
|
||||
|
||||
setupDoctorDefaultLogger(cmd, colorize)
|
||||
setupDoctorDefaultLogger(ctx, colorize)
|
||||
|
||||
// Finally redirect the default golang's log to here
|
||||
golog.SetFlags(0)
|
||||
golog.SetPrefix("")
|
||||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
|
||||
if cmd.IsSet("list") {
|
||||
if ctx.IsSet("list") {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
|
||||
doctor.SortChecks(doctor.Checks)
|
||||
@@ -189,12 +195,12 @@ func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
var checks []*doctor.Check
|
||||
if cmd.Bool("all") {
|
||||
if ctx.Bool("all") {
|
||||
checks = make([]*doctor.Check, len(doctor.Checks))
|
||||
copy(checks, doctor.Checks)
|
||||
} else if cmd.IsSet("run") {
|
||||
addDefault := cmd.Bool("default")
|
||||
runNamesSet := container.SetOf(cmd.StringSlice("run")...)
|
||||
} else if ctx.IsSet("run") {
|
||||
addDefault := ctx.Bool("default")
|
||||
runNamesSet := container.SetOf(ctx.StringSlice("run")...)
|
||||
for _, check := range doctor.Checks {
|
||||
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
|
||||
checks = append(checks, check)
|
||||
@@ -211,5 +217,5 @@ func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
|
||||
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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.
|
||||
@@ -22,8 +21,11 @@ var cmdDoctorConvert = &cli.Command{
|
||||
Action: runDoctorConvert,
|
||||
}
|
||||
|
||||
func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
func runDoctorConvert(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/services/doctor"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestDoctorRun(t *testing.T) {
|
||||
@@ -22,13 +22,12 @@ func TestDoctorRun(t *testing.T) {
|
||||
|
||||
SkipDatabaseInitialization: true,
|
||||
})
|
||||
app := &cli.Command{
|
||||
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"})
|
||||
assert.NoError(t, err)
|
||||
err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
|
||||
err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
|
||||
assert.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"})
|
||||
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
|
||||
}
|
||||
|
||||
76
cmd/dump.go
76
cmd/dump.go
@@ -5,7 +5,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -20,7 +19,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdDump represents the available dump sub-command.
|
||||
@@ -101,17 +101,17 @@ func fatal(format string, args ...any) {
|
||||
log.Fatal(format, args...)
|
||||
}
|
||||
|
||||
func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
func runDump(ctx *cli.Context) error {
|
||||
setting.MustInstalled()
|
||||
|
||||
quite := cmd.Bool("quiet")
|
||||
verbose := cmd.Bool("verbose")
|
||||
quite := ctx.Bool("quiet")
|
||||
verbose := ctx.Bool("verbose")
|
||||
if verbose && quite {
|
||||
fatal("Option --quiet and --verbose cannot both be set")
|
||||
}
|
||||
|
||||
// outFileName is either "-" or a file name (will be made absolute)
|
||||
outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
|
||||
outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
|
||||
if outType == "" {
|
||||
fatal("Invalid output type")
|
||||
}
|
||||
@@ -136,7 +136,10 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
err := db.InitEngine(ctx)
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
err := db.InitEngine(stdCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,20 +148,24 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
dumper, err := dump.NewDumper(ctx, outType, outFile)
|
||||
archiverGeneric, err := archiver.ByExtension("." + outType)
|
||||
if err != nil {
|
||||
fatal("Failed to create archive %q: %v", outFile, err)
|
||||
return err
|
||||
fatal("Unable to get archiver for extension: %v", err)
|
||||
}
|
||||
dumper.Verbose = verbose
|
||||
dumper.GlobalExcludeAbsPath(outFileName)
|
||||
defer func() {
|
||||
if err := dumper.Close(); err != nil {
|
||||
fatal("Failed to save archive %q: %v", outFileName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
|
||||
archiverWriter := archiverGeneric.(archiver.Writer)
|
||||
if err := archiverWriter.Create(outFile); err != nil {
|
||||
fatal("Creating archiver.Writer failed: %v", err)
|
||||
}
|
||||
defer archiverWriter.Close()
|
||||
|
||||
dumper := &dump.Dumper{
|
||||
Writer: archiverWriter,
|
||||
Verbose: verbose,
|
||||
}
|
||||
dumper.GlobalExcludeAbsPath(outFileName)
|
||||
|
||||
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
|
||||
log.Info("Skip dumping local repositories")
|
||||
} else {
|
||||
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
|
||||
@@ -166,7 +173,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
fatal("Failed to include repositories: %v", err)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
|
||||
if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
|
||||
log.Info("Skip dumping LFS data")
|
||||
} else if !setting.LFS.StartServer {
|
||||
log.Info("LFS isn't enabled. Skip dumping LFS data")
|
||||
@@ -175,18 +182,18 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddFileByReader(object, info, path.Join("data", "lfs", objPath))
|
||||
return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump LFS objects: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Bool("skip-db") {
|
||||
if ctx.Bool("skip-db") {
|
||||
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
|
||||
dumper.GlobalExcludeAbsPath(setting.Database.Path)
|
||||
log.Info("Skipping database")
|
||||
} else {
|
||||
tmpDir := cmd.String("tempdir")
|
||||
tmpDir := ctx.String("tempdir")
|
||||
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
|
||||
fatal("Path does not exist: %s", tmpDir)
|
||||
}
|
||||
@@ -202,7 +209,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
}()
|
||||
|
||||
targetDBType := cmd.String("database")
|
||||
targetDBType := ctx.String("database")
|
||||
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
|
||||
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
|
||||
} else {
|
||||
@@ -213,17 +220,17 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
fatal("Failed to dump database: %v", err)
|
||||
}
|
||||
|
||||
if err = dumper.AddFileByPath("gitea-db.sql", dbDump.Name()); err != nil {
|
||||
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
|
||||
fatal("Failed to include gitea-db.sql: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err = dumper.AddFileByPath("app.ini", setting.CustomConf); err != nil {
|
||||
if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
|
||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||
log.Info("Skipping custom directory")
|
||||
} else {
|
||||
customDir, err := os.Stat(setting.CustomPath)
|
||||
@@ -256,7 +263,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
excludes = append(excludes, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||
if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
|
||||
excludes = append(excludes, setting.Indexer.RepoPath)
|
||||
excludes = append(excludes, setting.Indexer.IssuePath)
|
||||
}
|
||||
@@ -265,26 +272,25 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
excludes = append(excludes, setting.LFS.Storage.Path)
|
||||
excludes = append(excludes, setting.Attachment.Storage.Path)
|
||||
excludes = append(excludes, setting.Packages.Storage.Path)
|
||||
excludes = append(excludes, setting.RepoArchive.Storage.Path)
|
||||
excludes = append(excludes, setting.Log.RootPath)
|
||||
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
|
||||
fatal("Failed to include data directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
|
||||
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
|
||||
log.Info("Skip dumping attachment data")
|
||||
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
|
||||
info, err := object.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddFileByReader(object, info, path.Join("data", "attachments", objPath))
|
||||
return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump attachments: %v", err)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
|
||||
if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
|
||||
log.Info("Skip dumping package data")
|
||||
} else if !setting.Packages.Enabled {
|
||||
log.Info("Packages isn't enabled. Skip dumping package data")
|
||||
@@ -293,7 +299,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddFileByReader(object, info, path.Join("data", "packages", objPath))
|
||||
return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump packages: %v", err)
|
||||
}
|
||||
@@ -301,7 +307,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
// 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 cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
|
||||
if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
|
||||
log.Info("Skip dumping log files")
|
||||
} else {
|
||||
isExist, err := util.IsExist(setting.Log.RootPath)
|
||||
@@ -318,6 +324,10 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
if outFileName == "-" {
|
||||
log.Info("Finish dumping to stdout")
|
||||
} else {
|
||||
if err = archiverWriter.Close(); err != nil {
|
||||
_ = os.Remove(outFileName)
|
||||
fatal("Failed to save %q: %v", outFileName, err)
|
||||
}
|
||||
if err = os.Chmod(outFileName, 0o600); err != nil {
|
||||
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"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.
|
||||
@@ -79,18 +79,21 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
||||
},
|
||||
}
|
||||
|
||||
func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
func runDumpRepository(ctx *cli.Context) error {
|
||||
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
|
||||
|
||||
setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// migrations.GiteaLocalUploader depends on git module
|
||||
if err := git.InitSimple(); err != nil {
|
||||
if err := git.InitSimple(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,8 +105,8 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
var (
|
||||
serviceType structs.GitServiceType
|
||||
cloneAddr = cmd.String("clone_addr")
|
||||
serviceStr = cmd.String("git_service")
|
||||
cloneAddr = ctx.String("clone_addr")
|
||||
serviceStr = ctx.String("git_service")
|
||||
)
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
|
||||
@@ -121,13 +124,13 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
opts := base.MigrateOptions{
|
||||
GitServiceType: serviceType,
|
||||
CloneAddr: cloneAddr,
|
||||
AuthUsername: cmd.String("auth_username"),
|
||||
AuthPassword: cmd.String("auth_password"),
|
||||
AuthToken: cmd.String("auth_token"),
|
||||
RepoName: cmd.String("repo_name"),
|
||||
AuthUsername: ctx.String("auth_username"),
|
||||
AuthPassword: ctx.String("auth_password"),
|
||||
AuthToken: ctx.String("auth_token"),
|
||||
RepoName: ctx.String("repo_name"),
|
||||
}
|
||||
|
||||
if len(cmd.String("units")) == 0 {
|
||||
if len(ctx.String("units")) == 0 {
|
||||
opts.Wiki = true
|
||||
opts.Issues = true
|
||||
opts.Milestones = true
|
||||
@@ -137,8 +140,8 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
opts.PullRequests = true
|
||||
opts.ReleaseAssets = true
|
||||
} else {
|
||||
units := strings.SplitSeq(cmd.String("units"), ",")
|
||||
for unit := range units {
|
||||
units := strings.Split(ctx.String("units"), ",")
|
||||
for _, unit := range units {
|
||||
switch strings.ToLower(strings.TrimSpace(unit)) {
|
||||
case "":
|
||||
continue
|
||||
@@ -166,7 +169,7 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
// the repo_dir will be removed if error occurs in DumpRepository
|
||||
// make sure the directory doesn't exist or is empty, prevent from deleting user files
|
||||
repoDir := cmd.String("repo_dir")
|
||||
repoDir := ctx.String("repo_dir")
|
||||
if exists, err := util.IsExist(repoDir); err != nil {
|
||||
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
|
||||
} else if exists {
|
||||
@@ -179,9 +182,9 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
if err := migrations.DumpRepository(
|
||||
ctx,
|
||||
context.Background(),
|
||||
repoDir,
|
||||
cmd.String("owner_name"),
|
||||
ctx.String("owner_name"),
|
||||
opts,
|
||||
); err != nil {
|
||||
log.Fatal("Failed to dump repository: %v", err)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
@@ -20,7 +18,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdEmbedded represents the available extract sub-command.
|
||||
@@ -29,7 +28,7 @@ var (
|
||||
Name: "embedded",
|
||||
Usage: "Extract embedded resources",
|
||||
Description: "A command for extracting embedded resources, like templates and images",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdList,
|
||||
subcmdView,
|
||||
subcmdExtract,
|
||||
@@ -101,7 +100,7 @@ type assetFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func initEmbeddedExtractor(c *cli.Command) error {
|
||||
func initEmbeddedExtractor(c *cli.Context) error {
|
||||
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
|
||||
|
||||
patterns, err := compileCollectPatterns(c.Args().Slice())
|
||||
@@ -116,31 +115,31 @@ func initEmbeddedExtractor(c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runList(_ context.Context, c *cli.Command) error {
|
||||
func runList(c *cli.Context) error {
|
||||
if err := runListDo(c); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runView(_ context.Context, c *cli.Command) error {
|
||||
func runView(c *cli.Context) error {
|
||||
if err := runViewDo(c); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExtract(_ context.Context, c *cli.Command) error {
|
||||
func runExtract(c *cli.Context) error {
|
||||
if err := runExtractDo(c); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runListDo(c *cli.Command) error {
|
||||
func runListDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,7 +151,7 @@ func runListDo(c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runViewDo(c *cli.Command) error {
|
||||
func runViewDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -175,7 +174,7 @@ func runViewDo(c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExtractDo(c *cli.Command) error {
|
||||
func runExtractDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -217,7 +216,7 @@ func runExtractDo(c *cli.Command) error {
|
||||
for _, a := range matchedAssetFiles {
|
||||
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
|
||||
// Non-fatal error
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
|
||||
fmt.Fprintf(os.Stderr, "%s: %v", a.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,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 {
|
||||
@@ -295,14 +294,16 @@ func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string,
|
||||
}
|
||||
}
|
||||
|
||||
func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
|
||||
func compileCollectPatterns(args []string) ([]glob.Glob, error) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"**"}
|
||||
}
|
||||
pat := make([]glob.Glob, len(args))
|
||||
for i := range args {
|
||||
if pat[i], err = glob.Compile(args[i], '/'); err != nil {
|
||||
return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
|
||||
if g, err := glob.Compile(args[i], '/'); err != nil {
|
||||
return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
|
||||
} else { //nolint:revive
|
||||
pat[i] = g
|
||||
}
|
||||
}
|
||||
return pat, nil
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,7 +19,7 @@ var (
|
||||
CmdGenerate = &cli.Command{
|
||||
Name: "generate",
|
||||
Usage: "Generate Gitea's secrets/keys/tokens",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdSecret,
|
||||
},
|
||||
}
|
||||
@@ -28,7 +27,7 @@ var (
|
||||
subcmdSecret = &cli.Command{
|
||||
Name: "secret",
|
||||
Usage: "Generate a secret token",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdGenerateInternalToken,
|
||||
microcmdGenerateLfsJwtSecret,
|
||||
microcmdGenerateSecretKey,
|
||||
@@ -55,7 +54,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
func runGenerateInternalToken(c *cli.Context) error {
|
||||
internalToken, err := generate.NewInternalToken()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -70,7 +69,7 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
|
||||
func runGenerateLfsJwtSecret(c *cli.Context) error {
|
||||
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -85,13 +84,12 @@ func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
|
||||
func runGenerateSecretKey(c *cli.Context) error {
|
||||
secretKey, err := generate.NewSecretKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
fmt.Printf("%s", secretKey)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var authCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Manage authentication",
|
||||
Long: `Manage authentication to Gitea servers.`,
|
||||
}
|
||||
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login to a Gitea server",
|
||||
Long: `Login to a Gitea server and save credentials.
|
||||
|
||||
The credentials are stored in ~/.gitea-cli.yaml and used for subsequent commands.`,
|
||||
RunE: runLogin,
|
||||
}
|
||||
|
||||
var logoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Logout from the current server",
|
||||
RunE: runLogout,
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show authentication status",
|
||||
RunE: runStatus,
|
||||
}
|
||||
|
||||
func init() {
|
||||
loginCmd.Flags().String("server", "", "Gitea server URL (required)")
|
||||
loginCmd.Flags().String("token", "", "API token (if not provided, will prompt)")
|
||||
_ = loginCmd.MarkFlagRequired("server")
|
||||
|
||||
authCmd.AddCommand(loginCmd)
|
||||
authCmd.AddCommand(logoutCmd)
|
||||
authCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
func runLogin(cmd *cobra.Command, args []string) error {
|
||||
server, _ := cmd.Flags().GetString("server")
|
||||
tokenFlag, _ := cmd.Flags().GetString("token")
|
||||
|
||||
// Normalize server URL
|
||||
server = strings.TrimSuffix(server, "/")
|
||||
if !strings.HasPrefix(server, "http://") && !strings.HasPrefix(server, "https://") {
|
||||
server = "https://" + server
|
||||
}
|
||||
|
||||
var apiToken string
|
||||
if tokenFlag != "" {
|
||||
apiToken = tokenFlag
|
||||
} else {
|
||||
// Prompt for token
|
||||
fmt.Print("API Token: ")
|
||||
byteToken, err := term.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
// Fallback if terminal not available
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
apiToken, _ = reader.ReadString('\n')
|
||||
apiToken = strings.TrimSpace(apiToken)
|
||||
} else {
|
||||
apiToken = string(byteToken)
|
||||
fmt.Println() // New line after password
|
||||
}
|
||||
}
|
||||
|
||||
if apiToken == "" {
|
||||
return errors.New("token is required")
|
||||
}
|
||||
|
||||
// Verify the token works
|
||||
fmt.Printf("Verifying credentials with %s...\n", server)
|
||||
|
||||
// TODO: Make actual API call to verify token
|
||||
// For now, just save the config
|
||||
|
||||
// Save configuration
|
||||
config := map[string]string{
|
||||
"server": server,
|
||||
"token": apiToken,
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".gitea-cli.yaml")
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0o600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Logged in to %s\n", server)
|
||||
fmt.Printf(" Configuration saved to %s\n", configPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runLogout(cmd *cobra.Command, args []string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".gitea-cli.yaml")
|
||||
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
fmt.Println("Not currently logged in")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
return fmt.Errorf("failed to remove config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Logged out successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
server := viper.GetString("server")
|
||||
token := viper.GetString("token")
|
||||
|
||||
if server == "" || token == "" {
|
||||
fmt.Println("Not logged in")
|
||||
fmt.Println("\nUse 'gitea-cli auth login --server <url>' to authenticate")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mask token for display
|
||||
maskedToken := token
|
||||
if len(token) > 8 {
|
||||
maskedToken = token[:4] + "..." + token[len(token)-4:]
|
||||
}
|
||||
|
||||
fmt.Printf("Server: %s\n", server)
|
||||
fmt.Printf("Token: %s\n", maskedToken)
|
||||
|
||||
// TODO: Verify token is still valid with API call
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
serverURL string
|
||||
token string
|
||||
verbose bool
|
||||
|
||||
version string
|
||||
buildTime string
|
||||
gitCommit string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "gitea-cli",
|
||||
Short: "A CLI tool for Gitea",
|
||||
Long: `gitea-cli is a command-line tool for interacting with Gitea instances.
|
||||
|
||||
It provides efficient chunked uploads for large files, progress tracking,
|
||||
and the ability to resume interrupted uploads.
|
||||
|
||||
Example usage:
|
||||
gitea-cli auth login --server https://gitea.example.com
|
||||
gitea-cli upload release-asset --repo owner/repo --release v1.0.0 --file ./app.tar.gz
|
||||
gitea-cli upload resume --session sess_abc123`,
|
||||
}
|
||||
|
||||
// Execute runs the root command
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
// SetVersion sets version information
|
||||
func SetVersion(v, bt, gc string) {
|
||||
version = v
|
||||
buildTime = bt
|
||||
gitCommit = gc
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.gitea-cli.yaml)")
|
||||
rootCmd.PersistentFlags().StringVarP(&serverURL, "server", "s", "", "Gitea server URL")
|
||||
rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "API token")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
|
||||
_ = viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server"))
|
||||
_ = viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(authCmd)
|
||||
rootCmd.AddCommand(uploadCmd)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
viper.AddConfigPath(home)
|
||||
viper.AddConfigPath(filepath.Join(home, ".config", "gitea-cli"))
|
||||
viper.SetConfigName(".gitea-cli")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("GITEA")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// versionCmd shows version information
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("gitea-cli version %s\n", version)
|
||||
fmt.Printf(" Build time: %s\n", buildTime)
|
||||
fmt.Printf(" Git commit: %s\n", gitCommit)
|
||||
},
|
||||
}
|
||||
|
||||
// getServer returns the configured server URL
|
||||
func getServer() string {
|
||||
if serverURL != "" {
|
||||
return serverURL
|
||||
}
|
||||
return viper.GetString("server")
|
||||
}
|
||||
|
||||
// getToken returns the configured API token
|
||||
func getToken() string {
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
return viper.GetString("token")
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultChunkSize = 10 * 1024 * 1024 // 10MB
|
||||
maxChunkSize = 100 * 1024 * 1024 // 100MB
|
||||
)
|
||||
|
||||
var uploadCmd = &cobra.Command{
|
||||
Use: "upload",
|
||||
Short: "Upload files to Gitea",
|
||||
Long: `Upload files to Gitea with chunked upload support for large files.`,
|
||||
}
|
||||
|
||||
var releaseAssetCmd = &cobra.Command{
|
||||
Use: "release-asset",
|
||||
Short: "Upload a release asset",
|
||||
Long: `Upload a release asset using chunked upload.
|
||||
|
||||
This command supports large files with progress tracking and resume capability.
|
||||
Interrupted uploads can be resumed using the session ID.`,
|
||||
Example: ` # Basic upload
|
||||
gitea-cli upload release-asset --repo owner/repo --release v1.0.0 --file ./app.tar.gz
|
||||
|
||||
# With options
|
||||
gitea-cli upload release-asset \
|
||||
--repo owner/repo \
|
||||
--release v1.0.0 \
|
||||
--file ./app.tar.gz \
|
||||
--chunk-size 50MB \
|
||||
--parallel 4 \
|
||||
--verify-checksum`,
|
||||
RunE: runReleaseAssetUpload,
|
||||
}
|
||||
|
||||
var resumeCmd = &cobra.Command{
|
||||
Use: "resume",
|
||||
Short: "Resume an interrupted upload",
|
||||
Long: `Resume a previously interrupted chunked upload using its session ID.`,
|
||||
Example: ` gitea-cli upload resume --session sess_abc123 --file ./app.tar.gz`,
|
||||
RunE: runResumeUpload,
|
||||
}
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pending uploads",
|
||||
Long: `List all pending upload sessions for a repository.`,
|
||||
RunE: runListUploads,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// release-asset flags
|
||||
releaseAssetCmd.Flags().StringP("repo", "r", "", "Repository (owner/repo)")
|
||||
releaseAssetCmd.Flags().String("release", "", "Release tag or ID")
|
||||
releaseAssetCmd.Flags().StringP("file", "f", "", "File to upload")
|
||||
releaseAssetCmd.Flags().String("name", "", "Asset name (defaults to filename)")
|
||||
releaseAssetCmd.Flags().String("chunk-size", "10MB", "Chunk size (e.g., 10MB, 50MB)")
|
||||
releaseAssetCmd.Flags().IntP("parallel", "p", 4, "Number of parallel uploads")
|
||||
releaseAssetCmd.Flags().Bool("verify-checksum", true, "Verify checksum after upload")
|
||||
releaseAssetCmd.Flags().Bool("progress", true, "Show progress bar")
|
||||
_ = releaseAssetCmd.MarkFlagRequired("repo")
|
||||
_ = releaseAssetCmd.MarkFlagRequired("release")
|
||||
_ = releaseAssetCmd.MarkFlagRequired("file")
|
||||
|
||||
// resume flags
|
||||
resumeCmd.Flags().String("session", "", "Upload session ID")
|
||||
resumeCmd.Flags().StringP("file", "f", "", "File to upload")
|
||||
_ = resumeCmd.MarkFlagRequired("session")
|
||||
_ = resumeCmd.MarkFlagRequired("file")
|
||||
|
||||
// list flags
|
||||
listCmd.Flags().StringP("repo", "r", "", "Repository (owner/repo)")
|
||||
_ = listCmd.MarkFlagRequired("repo")
|
||||
|
||||
uploadCmd.AddCommand(releaseAssetCmd)
|
||||
uploadCmd.AddCommand(resumeCmd)
|
||||
uploadCmd.AddCommand(listCmd)
|
||||
}
|
||||
|
||||
// UploadSession represents a chunked upload session
|
||||
type UploadSession struct {
|
||||
ID string `json:"id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ChunkSize int64 `json:"chunk_size"`
|
||||
TotalChunks int64 `json:"total_chunks"`
|
||||
ChunksReceived int64 `json:"chunks_received"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// ProgressTracker tracks upload progress
|
||||
type ProgressTracker struct {
|
||||
totalBytes int64
|
||||
bytesWritten int64
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (p *ProgressTracker) Add(n int64) {
|
||||
atomic.AddInt64(&p.bytesWritten, n)
|
||||
}
|
||||
|
||||
func (p *ProgressTracker) Progress() (current, total int64, percent, speed float64, eta time.Duration) {
|
||||
current = atomic.LoadInt64(&p.bytesWritten)
|
||||
total = p.totalBytes
|
||||
if total > 0 {
|
||||
percent = float64(current) / float64(total) * 100
|
||||
}
|
||||
elapsed := time.Since(p.startTime).Seconds()
|
||||
if elapsed > 0 {
|
||||
speed = float64(current) / elapsed
|
||||
if speed > 0 {
|
||||
remaining := total - current
|
||||
eta = time.Duration(float64(remaining)/speed) * time.Second
|
||||
}
|
||||
}
|
||||
return current, total, percent, speed, eta
|
||||
}
|
||||
|
||||
func runReleaseAssetUpload(cmd *cobra.Command, args []string) error {
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
release, _ := cmd.Flags().GetString("release")
|
||||
filePath, _ := cmd.Flags().GetString("file")
|
||||
assetName, _ := cmd.Flags().GetString("name")
|
||||
chunkSizeStr, _ := cmd.Flags().GetString("chunk-size")
|
||||
parallel, _ := cmd.Flags().GetInt("parallel")
|
||||
verifyChecksum, _ := cmd.Flags().GetBool("verify-checksum")
|
||||
showProgress, _ := cmd.Flags().GetBool("progress")
|
||||
|
||||
server := getServer()
|
||||
token := getToken()
|
||||
|
||||
if server == "" || token == "" {
|
||||
return errors.New("not logged in. Use 'gitea-cli auth login' first")
|
||||
}
|
||||
|
||||
// Parse repo
|
||||
parts := strings.Split(repo, "/")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("invalid repository format. Use owner/repo")
|
||||
}
|
||||
owner, repoName := parts[0], parts[1]
|
||||
|
||||
// Parse chunk size
|
||||
chunkSize, err := parseSize(chunkSizeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid chunk size: %w", err)
|
||||
}
|
||||
chunkSize = min(chunkSize, maxChunkSize)
|
||||
|
||||
// Open file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
fileSize := stat.Size()
|
||||
|
||||
if assetName == "" {
|
||||
assetName = filepath.Base(filePath)
|
||||
}
|
||||
|
||||
fmt.Printf("Uploading %s (%s)\n", assetName, formatSize(fileSize))
|
||||
|
||||
// Calculate checksum if requested
|
||||
var checksum string
|
||||
if verifyChecksum {
|
||||
fmt.Print("Calculating checksum... ")
|
||||
checksum, err = calculateSHA256(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
fmt.Printf("done (%s)\n", checksum[:16]+"...")
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("failed to seek file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create upload session
|
||||
fmt.Print("Creating upload session... ")
|
||||
session, err := createUploadSession(server, token, owner, repoName, release, assetName, fileSize, chunkSize, checksum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
fmt.Printf("done (%s)\n", session.ID)
|
||||
|
||||
// Upload chunks
|
||||
tracker := &ProgressTracker{
|
||||
totalBytes: fileSize,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = uploadChunks(ctx, server, token, session, file, parallel, tracker, showProgress)
|
||||
if err != nil {
|
||||
fmt.Printf("\n❌ Upload failed: %v\n", err)
|
||||
fmt.Printf(" Resume with: gitea-cli upload resume --session %s --file %s\n", session.ID, filePath)
|
||||
return err
|
||||
}
|
||||
|
||||
// Complete upload
|
||||
fmt.Print("\nFinalizing... ")
|
||||
result, err := completeUpload(server, token, session.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to complete upload: %w", err)
|
||||
}
|
||||
fmt.Println("done")
|
||||
|
||||
if verifyChecksum && result.ChecksumVerified {
|
||||
fmt.Println("Verifying checksum... ✓ SHA256 matches")
|
||||
}
|
||||
|
||||
elapsed := time.Since(tracker.startTime)
|
||||
fmt.Printf("\n✅ Upload complete!\n")
|
||||
fmt.Printf(" Asset ID: %d\n", result.ID)
|
||||
fmt.Printf(" Time: %s\n", elapsed.Round(time.Second))
|
||||
fmt.Printf(" Speed: %s/s (avg)\n", formatSize(int64(float64(fileSize)/elapsed.Seconds())))
|
||||
if result.DownloadURL != "" {
|
||||
fmt.Printf(" Download: %s\n", result.DownloadURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runResumeUpload(cmd *cobra.Command, args []string) error {
|
||||
sessionID, _ := cmd.Flags().GetString("session")
|
||||
filePath, _ := cmd.Flags().GetString("file")
|
||||
|
||||
server := getServer()
|
||||
token := getToken()
|
||||
|
||||
if server == "" || token == "" {
|
||||
return errors.New("not logged in")
|
||||
}
|
||||
|
||||
// Get session status
|
||||
session, err := getUploadSession(server, token, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get session: %w", err)
|
||||
}
|
||||
|
||||
if session.Status == "complete" {
|
||||
fmt.Println("Upload already completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.Status == "expired" {
|
||||
return errors.New("upload session has expired")
|
||||
}
|
||||
|
||||
// Open file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fmt.Printf("Resuming upload: %s\n", session.FileName)
|
||||
fmt.Printf(" Chunks: %d/%d complete\n", session.ChunksReceived, session.TotalChunks)
|
||||
|
||||
tracker := &ProgressTracker{
|
||||
totalBytes: session.FileSize,
|
||||
bytesWritten: session.ChunksReceived * session.ChunkSize,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = uploadChunks(ctx, server, token, session, file, 4, tracker, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Complete
|
||||
fmt.Print("\nFinalizing... ")
|
||||
result, err := completeUpload(server, token, session.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to complete: %w", err)
|
||||
}
|
||||
fmt.Println("done")
|
||||
|
||||
fmt.Printf("\n✅ Upload complete!\n")
|
||||
fmt.Printf(" Asset ID: %d\n", result.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runListUploads(cmd *cobra.Command, args []string) error {
|
||||
repo, _ := cmd.Flags().GetString("repo")
|
||||
|
||||
server := getServer()
|
||||
token := getToken()
|
||||
|
||||
if server == "" || token == "" {
|
||||
return errors.New("not logged in")
|
||||
}
|
||||
|
||||
parts := strings.Split(repo, "/")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("invalid repository format")
|
||||
}
|
||||
|
||||
sessions, err := listUploadSessions(server, token, parts[0], parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sessions) == 0 {
|
||||
fmt.Println("No pending uploads")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Pending uploads for %s:\n\n", repo)
|
||||
for _, s := range sessions {
|
||||
progress := float64(s.ChunksReceived) / float64(s.TotalChunks) * 100
|
||||
fmt.Printf(" %s\n", s.ID)
|
||||
fmt.Printf(" File: %s (%s)\n", s.FileName, formatSize(s.FileSize))
|
||||
fmt.Printf(" Progress: %.1f%% (%d/%d chunks)\n", progress, s.ChunksReceived, s.TotalChunks)
|
||||
fmt.Printf(" Expires: %s\n", s.ExpiresAt.Format(time.RFC3339))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadChunks(ctx context.Context, server, token string, session *UploadSession, file *os.File, parallel int, tracker *ProgressTracker, showProgress bool) error {
|
||||
totalChunks := session.TotalChunks
|
||||
chunkSize := session.ChunkSize
|
||||
|
||||
// Create worker pool
|
||||
type chunkJob struct {
|
||||
number int64
|
||||
data []byte
|
||||
}
|
||||
|
||||
jobs := make(chan chunkJob, parallel)
|
||||
errors := make(chan error, totalChunks)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Start workers
|
||||
for range parallel {
|
||||
wg.Go(func() {
|
||||
for job := range jobs {
|
||||
err := uploadChunk(server, token, session.ID, job.number, job.data)
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("chunk %d: %w", job.number, err)
|
||||
return
|
||||
}
|
||||
tracker.Add(int64(len(job.data)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Progress display
|
||||
done := make(chan struct{})
|
||||
if showProgress {
|
||||
go func() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
current, total, percent, speed, eta := tracker.Progress()
|
||||
fmt.Printf("\r [%-50s] %5.1f%% %s/%s %s/s ETA %s ",
|
||||
progressBar(percent, 50),
|
||||
percent,
|
||||
formatSize(current),
|
||||
formatSize(total),
|
||||
formatSize(int64(speed)),
|
||||
formatDuration(eta))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Read and queue chunks
|
||||
for chunkNum := session.ChunksReceived; chunkNum < totalChunks; chunkNum++ {
|
||||
offset := chunkNum * chunkSize
|
||||
if _, err := file.Seek(offset, 0); err != nil {
|
||||
close(jobs)
|
||||
close(done)
|
||||
return fmt.Errorf("failed to seek: %w", err)
|
||||
}
|
||||
|
||||
size := chunkSize
|
||||
if chunkNum == totalChunks-1 {
|
||||
size = session.FileSize - offset
|
||||
}
|
||||
|
||||
data := make([]byte, size)
|
||||
n, err := io.ReadFull(file, data)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
close(jobs)
|
||||
close(done)
|
||||
return fmt.Errorf("failed to read chunk %d: %w", chunkNum, err)
|
||||
}
|
||||
data = data[:n]
|
||||
|
||||
select {
|
||||
case err := <-errors:
|
||||
close(jobs)
|
||||
close(done)
|
||||
return err
|
||||
case jobs <- chunkJob{number: chunkNum, data: data}:
|
||||
case <-ctx.Done():
|
||||
close(jobs)
|
||||
close(done)
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
close(done)
|
||||
|
||||
// Check for errors
|
||||
select {
|
||||
case err := <-errors:
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func progressBar(percent float64, width int) string {
|
||||
filled := int(percent / 100 * float64(width))
|
||||
filled = min(filled, width)
|
||||
return strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
||||
}
|
||||
|
||||
func formatSize(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Second {
|
||||
return "<1s"
|
||||
}
|
||||
d = d.Round(time.Second)
|
||||
h := d / time.Hour
|
||||
d -= h * time.Hour
|
||||
m := d / time.Minute
|
||||
d -= m * time.Minute
|
||||
s := d / time.Second
|
||||
if h > 0 {
|
||||
return fmt.Sprintf("%dh%dm", h, m)
|
||||
}
|
||||
if m > 0 {
|
||||
return fmt.Sprintf("%dm%ds", m, s)
|
||||
}
|
||||
return fmt.Sprintf("%ds", s)
|
||||
}
|
||||
|
||||
func parseSize(s string) (int64, error) {
|
||||
s = strings.ToUpper(strings.TrimSpace(s))
|
||||
multiplier := int64(1)
|
||||
|
||||
if strings.HasSuffix(s, "GB") {
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
s = strings.TrimSuffix(s, "GB")
|
||||
} else if strings.HasSuffix(s, "MB") {
|
||||
multiplier = 1024 * 1024
|
||||
s = strings.TrimSuffix(s, "MB")
|
||||
} else if strings.HasSuffix(s, "KB") {
|
||||
multiplier = 1024
|
||||
s = strings.TrimSuffix(s, "KB")
|
||||
} else if suffix, found := strings.CutSuffix(s, "B"); found {
|
||||
s = suffix
|
||||
}
|
||||
|
||||
var value int64
|
||||
_, err := fmt.Sscanf(s, "%d", &value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value * multiplier, nil
|
||||
}
|
||||
|
||||
func calculateSHA256(file *os.File) (string, error) {
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// API functions
|
||||
|
||||
func createUploadSession(server, token, owner, repo, release, fileName string, fileSize, chunkSize int64, checksum string) (*UploadSession, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%s/assets/upload-session", server, owner, repo, release)
|
||||
|
||||
body := map[string]any{
|
||||
"name": fileName,
|
||||
"size": fileSize,
|
||||
"chunk_size": chunkSize,
|
||||
}
|
||||
if checksum != "" {
|
||||
body["checksum"] = checksum
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var session UploadSession
|
||||
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func getUploadSession(server, token, sessionID string) (*UploadSession, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/uploads/%s", server, sessionID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
var session UploadSession
|
||||
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func uploadChunk(server, token, sessionID string, chunkNum int64, data []byte) error {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/uploads/%s/chunks/%d", server, sessionID, chunkNum)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed: %s", string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CompleteResult struct {
|
||||
ID int64 `json:"id"`
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
ChecksumVerified bool `json:"checksum_verified"`
|
||||
}
|
||||
|
||||
func completeUpload(server, token, sessionID string) (*CompleteResult, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/uploads/%s/complete", server, sessionID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed: %s", string(body))
|
||||
}
|
||||
|
||||
var result CompleteResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func listUploadSessions(server, token, owner, repo string) ([]*UploadSession, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/uploads", server, owner, repo)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to list sessions")
|
||||
}
|
||||
|
||||
var sessions []*UploadSession
|
||||
if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// gitea-cli is a command-line tool for interacting with Gitea instances.
|
||||
// It provides efficient chunked uploads, progress tracking, and resume capability.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/cmd/gitea-cli/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.SetVersion(Version, BuildTime, GitCommit)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
34
cmd/hook.go
34
cmd/hook.go
@@ -15,13 +15,12 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"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 (
|
||||
@@ -33,10 +32,9 @@ var (
|
||||
CmdHook = &cli.Command{
|
||||
Name: "hook",
|
||||
Usage: "(internal) Should only be called by Git",
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Delegate commands to corresponding Git hooks",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdHookPreReceive,
|
||||
subcmdHookUpdate,
|
||||
subcmdHookPostReceive,
|
||||
@@ -163,10 +161,12 @@ 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()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
@@ -186,7 +186,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||||
deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
|
||||
actionPerm, _ := strconv.Atoi(os.Getenv(repo_module.EnvActionPerm))
|
||||
actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64)
|
||||
|
||||
hookOptions := private.HookOptions{
|
||||
UserID: userID,
|
||||
@@ -196,7 +196,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
GitPushOptions: pushOptions(),
|
||||
PullRequestID: prID,
|
||||
DeployKeyID: deployKeyID,
|
||||
ActionPerm: actionPerm,
|
||||
ActionPerm: int(actionPerm),
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
@@ -292,7 +292,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
|
||||
// runHookUpdate avoid to do heavy operations on update hook because it will be
|
||||
// invoked for every ref update which does not like pre-receive and post-receive
|
||||
func runHookUpdate(_ context.Context, c *cli.Command) error {
|
||||
func runHookUpdate(c *cli.Context) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
@@ -309,12 +309,15 @@ func runHookUpdate(_ context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHookPostReceive(ctx context.Context, c *cli.Command) error {
|
||||
func runHookPostReceive(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
// First of all run update-server-info no matter what
|
||||
if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx); err != nil {
|
||||
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
|
||||
if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil {
|
||||
return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
|
||||
}
|
||||
|
||||
// Now if we're an internal don't do anything else
|
||||
@@ -482,7 +485,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) {
|
||||
func pushOptions() map[string]string {
|
||||
opts := make(map[string]string)
|
||||
if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
|
||||
for idx := range pushCount {
|
||||
for idx := 0; idx < pushCount; idx++ {
|
||||
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
|
||||
kv := strings.SplitN(opt, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
@@ -493,7 +496,10 @@ func pushOptions() map[string]string {
|
||||
return opts
|
||||
}
|
||||
|
||||
func runHookProcReceive(ctx context.Context, c *cli.Command) error {
|
||||
func runHookProcReceive(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
@@ -734,7 +740,7 @@ func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType)
|
||||
|
||||
// read prefix
|
||||
lengthBytes := make([]byte, 4)
|
||||
for i := range 4 {
|
||||
for i := 0; i < 4; i++ {
|
||||
lengthBytes[i], err = in.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
|
||||
|
||||
11
cmd/keys.go
11
cmd/keys.go
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -12,14 +11,13 @@ import (
|
||||
"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
|
||||
var CmdKeys = &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "(internal) Should only be called by SSH server",
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runKeys,
|
||||
@@ -51,7 +49,7 @@ var CmdKeys = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -70,6 +68,9 @@ func runKeys(ctx context.Context, c *cli.Command) error {
|
||||
return errors.New("No key type and content provided")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
|
||||
@@ -77,6 +78,6 @@ func runKeys(ctx context.Context, c *cli.Command) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,18 +4,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
func runSendMail(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setting.MustInstalled()
|
||||
|
||||
if err := argsSet(c, "title"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject := c.String("title")
|
||||
confirmSkiped := c.Bool("force")
|
||||
body := c.String("content")
|
||||
|
||||
186
cmd/main.go
186
cmd/main.go
@@ -4,40 +4,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var cliHelpPrinterOld = cli.HelpPrinter
|
||||
|
||||
func init() {
|
||||
cli.HelpPrinter = cliHelpPrinterNew
|
||||
}
|
||||
|
||||
// cliHelpPrinterNew helps to print "DEFAULT CONFIGURATION" for the following cases ( "-c" can apper in any position):
|
||||
// * ./gitea -c /dev/null -h
|
||||
// * ./gitea -c help /dev/null help
|
||||
// * ./gitea help -c /dev/null
|
||||
// * ./gitea help -c /dev/null web
|
||||
// * ./gitea help web -c /dev/null
|
||||
// * ./gitea web help -c /dev/null
|
||||
// * ./gitea web -h -c /dev/null
|
||||
func cliHelpPrinterNew(out io.Writer, templ string, data any) {
|
||||
cmd, _ := data.(*cli.Command)
|
||||
if cmd != nil {
|
||||
prepareWorkPathAndCustomConf(cmd)
|
||||
}
|
||||
cliHelpPrinterOld(out, templ, data)
|
||||
if setting.CustomConf != "" {
|
||||
_, _ = fmt.Fprintf(out, `
|
||||
// cmdHelp is our own help subcommand with more information
|
||||
// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information
|
||||
func cmdHelp() *cli.Command {
|
||||
c := &cli.Command{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
Usage: "Shows a list of commands or help for one command",
|
||||
ArgsUsage: "[command]",
|
||||
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.Command.Name == "help" {
|
||||
targetCmdIdx = 1
|
||||
}
|
||||
if lineage[targetCmdIdx+1].Command != nil {
|
||||
err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name)
|
||||
} else {
|
||||
err = cli.ShowAppHelp(c)
|
||||
}
|
||||
_, _ = fmt.Fprintf(c.App.Writer, `
|
||||
DEFAULT CONFIGURATION:
|
||||
AppPath: %s
|
||||
WorkPath: %s
|
||||
@@ -45,75 +41,94 @@ DEFAULT CONFIGURATION:
|
||||
ConfigFile: %s
|
||||
|
||||
`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
|
||||
return err
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func appGlobalFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
// make the builtin flags at the top
|
||||
cli.HelpFlag,
|
||||
|
||||
// shared configuration flags, they are for global and for each sub-command at the same time
|
||||
// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
|
||||
// keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore.
|
||||
&cli.StringFlag{
|
||||
Name: "custom-path",
|
||||
Aliases: []string{"C"},
|
||||
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Value: setting.CustomConf,
|
||||
Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "work-path",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||
originBefore := originCmd.Before
|
||||
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
||||
ctx = ctxOrig
|
||||
if originBefore != nil {
|
||||
ctx, err = originBefore(ctx, cmd)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
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.Subcommands = append(command.Subcommands, cmdHelp())
|
||||
}
|
||||
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(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 ctx.Lineage() {
|
||||
if curCtx.IsSet("work-path") && args.WorkPath == "" {
|
||||
args.WorkPath = curCtx.String("work-path")
|
||||
}
|
||||
if curCtx.IsSet("custom-path") && args.CustomPath == "" {
|
||||
args.CustomPath = curCtx.String("custom-path")
|
||||
}
|
||||
if curCtx.IsSet("config") && args.CustomConf == "" {
|
||||
args.CustomConf = curCtx.String("config")
|
||||
}
|
||||
}
|
||||
prepareWorkPathAndCustomConf(cmd)
|
||||
return ctx, nil
|
||||
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
|
||||
if ctx.Bool("help") || action == nil {
|
||||
// the default behavior of "urfave/cli": "nil action" means "show help"
|
||||
return cmdHelp().Action(ctx)
|
||||
}
|
||||
return action(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// prepareWorkPathAndCustomConf tries to prepare the work path, custom path and custom config from various inputs:
|
||||
// command line flags, environment variables, config file
|
||||
func prepareWorkPathAndCustomConf(cmd *cli.Command) {
|
||||
var args setting.ArgWorkPathAndCustomConf
|
||||
if cmd.IsSet("work-path") {
|
||||
args.WorkPath = cmd.String("work-path")
|
||||
}
|
||||
if cmd.IsSet("custom-path") {
|
||||
args.CustomPath = cmd.String("custom-path")
|
||||
}
|
||||
if cmd.IsSet("config") {
|
||||
args.CustomConf = cmd.String("config")
|
||||
}
|
||||
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
|
||||
}
|
||||
|
||||
type AppVersion struct {
|
||||
Version string
|
||||
Extra string
|
||||
}
|
||||
|
||||
func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
app := &cli.Command{}
|
||||
app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
func NewMainApp(appVer AppVersion) *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "Gitea"
|
||||
app.HelpName = "gitea"
|
||||
app.Usage = "A painless self-hosted Git service"
|
||||
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
|
||||
app.Version = appVer.Version + appVer.Extra
|
||||
app.EnableShellCompletion = true
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "work-path",
|
||||
Aliases: []string{"w"},
|
||||
TakesFile: true,
|
||||
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
TakesFile: true,
|
||||
Value: setting.CustomConf,
|
||||
Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "custom-path",
|
||||
Aliases: []string{"C"},
|
||||
TakesFile: true,
|
||||
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
|
||||
},
|
||||
}
|
||||
// these sub-commands need to use a config file
|
||||
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,
|
||||
@@ -132,19 +147,20 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
|
||||
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
|
||||
subCmdStandalone := []*cli.Command{
|
||||
cmdConfig(),
|
||||
cmdCert(),
|
||||
CmdCert,
|
||||
CmdGenerate,
|
||||
CmdDocs,
|
||||
}
|
||||
|
||||
// TODO: we should eventually drop the default command,
|
||||
// but not sure whether it would break Windows users who used to double-click the EXE to run.
|
||||
app.DefaultCommand = CmdWeb.Name
|
||||
|
||||
globalFlags := appGlobalFlags()
|
||||
app.Flags = append(app.Flags, cli.VersionFlag)
|
||||
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 {
|
||||
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
|
||||
prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags)
|
||||
}
|
||||
app.Commands = append(app.Commands, subCmdWithConfig...)
|
||||
app.Commands = append(app.Commands, subCmdStandalone...)
|
||||
@@ -153,10 +169,8 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
return app
|
||||
}
|
||||
|
||||
func RunMainApp(app *cli.Command, args ...string) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
err := app.Run(ctx, args)
|
||||
func RunMainApp(app *cli.App, args ...string) error {
|
||||
err := app.Run(args)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
107
cmd/main_test.go
107
cmd/main_test.go
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,10 +14,9 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -29,11 +27,11 @@ func makePathOutput(workPath, customPath, customConf string) string {
|
||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||
}
|
||||
|
||||
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
|
||||
app := NewMainApp(AppVersion{})
|
||||
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||
prepareSubcommandWithGlobalFlags(&testCmd)
|
||||
app.Commands = append(app.Commands, &testCmd)
|
||||
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
|
||||
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,7 +65,7 @@ func TestCliCmd(t *testing.T) {
|
||||
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
|
||||
|
||||
cli.CommandHelpTemplate = "(command help template)"
|
||||
cli.RootCommandHelpTemplate = "(app help template)"
|
||||
cli.AppHelpTemplate = "(app help template)"
|
||||
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
||||
|
||||
cases := []struct {
|
||||
@@ -75,56 +73,12 @@ func TestCliCmd(t *testing.T) {
|
||||
cmd string
|
||||
exp string
|
||||
}{
|
||||
// help commands
|
||||
{
|
||||
cmd: "./gitea -h",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
// main command help
|
||||
{
|
||||
cmd: "./gitea help",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -h -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
// parse paths
|
||||
{
|
||||
cmd: "./gitea test-cmd",
|
||||
@@ -155,14 +109,12 @@ func TestCliCmd(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
@@ -176,54 +128,31 @@ func TestCliCmd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCliCmdError(t *testing.T) {
|
||||
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||
app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") })
|
||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *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")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 2, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "exit error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
app = newTestApp(func(ctx *cli.Context) error { return nil })
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout)
|
||||
assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr ....
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
app = newTestApp(func(ctx *cli.Context) error { return nil })
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.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)
|
||||
}
|
||||
|
||||
func TestCliCmdBefore(t *testing.T) {
|
||||
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||
configValues := map[string]string{}
|
||||
setting.CustomConf = "/tmp/any.ini"
|
||||
var actionCtx context.Context
|
||||
app := newTestApp(cli.Command{
|
||||
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||
configValues["before"] = setting.CustomConf
|
||||
return ctxNew, nil
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
configValues["action"] = setting.CustomConf
|
||||
actionCtx = ctx
|
||||
return nil
|
||||
},
|
||||
})
|
||||
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ctxNew, actionCtx)
|
||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||
assert.Equal(t, "/dev/null", configValues["action"])
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,7 +18,7 @@ var (
|
||||
Name: "manager",
|
||||
Usage: "Manage the running gitea process",
|
||||
Description: "This is a command for managing the running gitea process",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdShutdown,
|
||||
subcmdRestart,
|
||||
subcmdReloadTemplates,
|
||||
@@ -109,31 +108,46 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runShutdown(ctx context.Context, c *cli.Command) error {
|
||||
func runShutdown(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
extra := private.Shutdown(ctx)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runRestart(ctx context.Context, c *cli.Command) error {
|
||||
func runRestart(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
extra := private.Restart(ctx)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runReloadTemplates(ctx context.Context, c *cli.Command) error {
|
||||
func runReloadTemplates(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
extra := private.ReloadTemplates(ctx)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runFlushQueues(ctx context.Context, c *cli.Command) error {
|
||||
func runFlushQueues(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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 {
|
||||
func runProcesses(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -12,7 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -61,7 +60,7 @@ var (
|
||||
subcmdLogging = &cli.Command{
|
||||
Name: "logging",
|
||||
Usage: "Adjust logging commands",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "pause",
|
||||
Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)",
|
||||
@@ -105,7 +104,7 @@ var (
|
||||
}, {
|
||||
Name: "add",
|
||||
Usage: "Add a logger",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "file",
|
||||
Usage: "Add a file logger",
|
||||
@@ -193,7 +192,10 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
func runRemoveLogger(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
logger := c.String("logger")
|
||||
if len(logger) == 0 {
|
||||
@@ -205,7 +207,10 @@ func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
|
||||
func runAddConnLogger(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "conn"
|
||||
@@ -229,10 +234,13 @@ 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 {
|
||||
func runAddFileLogger(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "file"
|
||||
@@ -259,10 +267,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()
|
||||
}
|
||||
@@ -289,33 +297,46 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
|
||||
if c.IsSet("writer") {
|
||||
writer = c.String("writer")
|
||||
}
|
||||
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 {
|
||||
func runPauseLogging(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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 {
|
||||
func runResumeLogging(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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 {
|
||||
func runReleaseReopenLogging(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
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 {
|
||||
func runSetLogSQL(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
extra := private.SetLogSQL(ctx, !c.Bool("off"))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Gitea MCP Server - Model Context Protocol server for Gitea Actions
|
||||
//
|
||||
// This standalone server implements the MCP protocol over stdio,
|
||||
// proxying requests to a Gitea instance's /api/v2/mcp endpoint.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gitea-mcp-server --url https://git.example.com --token YOUR_API_TOKEN
|
||||
//
|
||||
// Configure in Claude Code's settings.json:
|
||||
//
|
||||
// {
|
||||
// "mcpServers": {
|
||||
// "gitea": {
|
||||
// "command": "gitea-mcp-server",
|
||||
// "args": ["--url", "https://git.example.com", "--token", "YOUR_TOKEN"]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
var (
|
||||
giteaURL string
|
||||
giteaToken string
|
||||
debug bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&giteaURL, "url", "", "Gitea server URL (e.g., https://git.example.com)")
|
||||
flag.StringVar(&giteaToken, "token", "", "Gitea API token")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable debug logging to stderr")
|
||||
flag.Parse()
|
||||
|
||||
// Also check environment variables
|
||||
if giteaURL == "" {
|
||||
giteaURL = os.Getenv("GITEA_URL")
|
||||
}
|
||||
if giteaToken == "" {
|
||||
giteaToken = os.Getenv("GITEA_TOKEN")
|
||||
}
|
||||
|
||||
if giteaURL == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --url or GITEA_URL is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
debugLog("Gitea MCP Server starting")
|
||||
debugLog("Connecting to: %s", giteaURL)
|
||||
|
||||
// Read JSON-RPC messages from stdin, forward to Gitea, write responses to stdout
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
debugLog("EOF received, exiting")
|
||||
break
|
||||
}
|
||||
debugLog("Read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Received: %s", string(line))
|
||||
|
||||
// Forward to Gitea's MCP endpoint
|
||||
response, err := forwardToGitea(line)
|
||||
if err != nil {
|
||||
debugLog("Forward error: %v", err)
|
||||
// Send error response
|
||||
errorResp := map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": nil,
|
||||
"error": map[string]any{
|
||||
"code": -32603,
|
||||
"message": "Internal error",
|
||||
"data": err.Error(),
|
||||
},
|
||||
}
|
||||
writeResponse(errorResp)
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Response: %s", string(response))
|
||||
|
||||
// Write response to stdout
|
||||
fmt.Println(string(response))
|
||||
}
|
||||
}
|
||||
|
||||
func forwardToGitea(request []byte) ([]byte, error) {
|
||||
mcpURL := giteaURL + "/api/v2/mcp"
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, mcpURL, bytes.NewReader(request))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if giteaToken != "" {
|
||||
req.Header.Set("Authorization", "token "+giteaToken)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func writeResponse(resp any) {
|
||||
data, _ := json.Marshal(resp)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
func debugLog(format string, args ...any) {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/versioned_migration"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdMigrate represents the available migrate sub-command.
|
||||
@@ -22,8 +22,11 @@ var CmdMigrate = &cli.Command{
|
||||
Action: runMigrate,
|
||||
}
|
||||
|
||||
func runMigrate(ctx context.Context, c *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
func runMigrate(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/versioned_migration"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdMigrateStorage represents the available migrate storage sub-command.
|
||||
@@ -36,7 +36,7 @@ var CmdMigrateStorage = &cli.Command{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "storage",
|
||||
@@ -213,8 +213,11 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
|
||||
})
|
||||
}
|
||||
|
||||
func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
func runMigrateStorage(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -235,51 +238,51 @@ func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
var dstStorage storage.ObjectStorage
|
||||
var err error
|
||||
switch strings.ToLower(cmd.String("storage")) {
|
||||
switch strings.ToLower(ctx.String("storage")) {
|
||||
case "":
|
||||
fallthrough
|
||||
case string(setting.LocalStorageType):
|
||||
p := cmd.String("path")
|
||||
p := ctx.String("path")
|
||||
if p == "" {
|
||||
log.Fatal("Path must be given when storage is local")
|
||||
return nil
|
||||
}
|
||||
dstStorage, err = storage.NewLocalStorage(
|
||||
ctx,
|
||||
stdCtx,
|
||||
&setting.Storage{
|
||||
Path: p,
|
||||
})
|
||||
case string(setting.MinioStorageType):
|
||||
dstStorage, err = storage.NewMinioStorage(
|
||||
ctx,
|
||||
stdCtx,
|
||||
&setting.Storage{
|
||||
MinioConfig: setting.MinioStorageConfig{
|
||||
Endpoint: cmd.String("minio-endpoint"),
|
||||
AccessKeyID: cmd.String("minio-access-key-id"),
|
||||
SecretAccessKey: cmd.String("minio-secret-access-key"),
|
||||
Bucket: cmd.String("minio-bucket"),
|
||||
Location: cmd.String("minio-location"),
|
||||
BasePath: cmd.String("minio-base-path"),
|
||||
UseSSL: cmd.Bool("minio-use-ssl"),
|
||||
InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"),
|
||||
ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"),
|
||||
BucketLookUpType: cmd.String("minio-bucket-lookup-type"),
|
||||
Endpoint: ctx.String("minio-endpoint"),
|
||||
AccessKeyID: ctx.String("minio-access-key-id"),
|
||||
SecretAccessKey: ctx.String("minio-secret-access-key"),
|
||||
Bucket: ctx.String("minio-bucket"),
|
||||
Location: ctx.String("minio-location"),
|
||||
BasePath: ctx.String("minio-base-path"),
|
||||
UseSSL: ctx.Bool("minio-use-ssl"),
|
||||
InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
|
||||
ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"),
|
||||
BucketLookUpType: ctx.String("minio-bucket-lookup-type"),
|
||||
},
|
||||
})
|
||||
case string(setting.AzureBlobStorageType):
|
||||
dstStorage, err = storage.NewAzureBlobStorage(
|
||||
ctx,
|
||||
stdCtx,
|
||||
&setting.Storage{
|
||||
AzureBlobConfig: setting.AzureBlobStorageConfig{
|
||||
Endpoint: cmd.String("azureblob-endpoint"),
|
||||
AccountName: cmd.String("azureblob-account-name"),
|
||||
AccountKey: cmd.String("azureblob-account-key"),
|
||||
Container: cmd.String("azureblob-container"),
|
||||
BasePath: cmd.String("azureblob-base-path"),
|
||||
Endpoint: ctx.String("azureblob-endpoint"),
|
||||
AccountName: ctx.String("azureblob-account-name"),
|
||||
AccountKey: ctx.String("azureblob-account-key"),
|
||||
Container: ctx.String("azureblob-container"),
|
||||
BasePath: ctx.String("azureblob-base-path"),
|
||||
},
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("unsupported storage type: %s", cmd.String("storage"))
|
||||
return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -296,14 +299,14 @@ func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
|
||||
"actions-artifacts": migrateActionsArtifacts,
|
||||
}
|
||||
|
||||
tp := strings.ToLower(cmd.String("type"))
|
||||
tp := strings.ToLower(ctx.String("type"))
|
||||
if m, ok := migratedMethods[tp]; ok {
|
||||
if err := m(ctx, dstStorage); err != nil {
|
||||
if err := m(stdCtx, dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("%s files have successfully been copied to the new storage.", tp)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported storage: %s", cmd.String("type"))
|
||||
return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
@@ -29,7 +30,7 @@ func TestMigratePackages(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer buf.Close()
|
||||
|
||||
v, f, err := packages_service.CreatePackageAndAddFile(t.Context(), &packages_service.PackageCreationInfo{
|
||||
v, f, err := packages_service.CreatePackageAndAddFile(db.DefaultContext, &packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: creator,
|
||||
PackageType: packages.TypeGeneric,
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
@@ -49,7 +48,10 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
||||
},
|
||||
}
|
||||
|
||||
func runRestoreRepository(ctx context.Context, c *cli.Command) error {
|
||||
func runRestoreRepository(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setting.MustInstalled()
|
||||
var units []string
|
||||
if s := c.String("units"); s != "" {
|
||||
|
||||
86
cmd/serv.go
86
cmd/serv.go
@@ -13,14 +13,14 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/lfstransfer"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -31,8 +31,9 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// CmdServ represents the available serv sub-command.
|
||||
@@ -40,7 +41,6 @@ var CmdServ = &cli.Command{
|
||||
Name: "serv",
|
||||
Usage: "(internal) Should only be called by SSH shell",
|
||||
Description: "Serv provides access auth for repositories",
|
||||
Hidden: true, // Internal commands shouldn't be visible in help
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runServ,
|
||||
Flags: []cli.Flag{
|
||||
@@ -64,7 +64,7 @@ func setup(ctx context.Context, debug bool) {
|
||||
_ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
|
||||
return
|
||||
}
|
||||
if err := git.InitSimple(); err != nil {
|
||||
if err := git.InitSimple(context.Background()); err != nil {
|
||||
_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,31 @@ func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
|
||||
func runServ(ctx context.Context, c *cli.Command) error {
|
||||
func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) {
|
||||
now := time.Now()
|
||||
claims := lfs.Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
RepoID: results.RepoID,
|
||||
Op: lfsVerb,
|
||||
UserID: results.UserID,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
||||
if err != nil {
|
||||
return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
|
||||
}
|
||||
return "Bearer " + tokenString, nil
|
||||
}
|
||||
|
||||
func runServ(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
// FIXME: This needs to internationalised
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
@@ -191,7 +215,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
fmt.Print(`{"type":"agit","version":1}`)
|
||||
fmt.Print(`{"type":"gitea","version":1}`)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -207,7 +231,12 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
username := repoPathFields[0]
|
||||
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
||||
|
||||
if !repo_model.IsValidSSHAccessRepoName(reponame) {
|
||||
// LowerCase and trim the repoPath as that's how they are stored.
|
||||
// This should be done after splitting the repoPath into username and reponame
|
||||
// so that username and reponame are not affected.
|
||||
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
|
||||
|
||||
if !repo.IsValidSSHAccessRepoName(reponame) {
|
||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
||||
}
|
||||
|
||||
@@ -253,16 +282,9 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
|
||||
}
|
||||
|
||||
// because the original repoPath maybe redirected, we need to use the returned actual repository information
|
||||
if results.IsWiki {
|
||||
repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName)
|
||||
} else {
|
||||
repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName)
|
||||
}
|
||||
|
||||
// LFS SSH protocol
|
||||
if verb == git.CmdVerbLfsTransfer {
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -273,7 +295,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
if verb == git.CmdVerbLfsAuthenticate {
|
||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -292,30 +314,30 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var command *exec.Cmd
|
||||
gitBinPath := filepath.Dir(gitcmd.GitExecutable) // e.g. /usr/bin
|
||||
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
|
||||
var gitcmd *exec.Cmd
|
||||
gitBinPath := filepath.Dir(git.GitExecutable) // e.g. /usr/bin
|
||||
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
|
||||
if _, err := os.Stat(gitBinVerb); err != nil {
|
||||
// if the command "git-upload-pack" doesn't exist, try to split "git-upload-pack" to use the sub-command with git
|
||||
// ps: Windows only has "git.exe" in the bin path, so Windows always uses this way
|
||||
verbFields := strings.SplitN(verb, "-", 2)
|
||||
if len(verbFields) == 2 {
|
||||
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
|
||||
command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], repoPath)
|
||||
gitcmd = exec.CommandContext(ctx, git.GitExecutable, verbFields[1], repoPath)
|
||||
}
|
||||
}
|
||||
if command == nil {
|
||||
if gitcmd == nil {
|
||||
// by default, use the verb (it has been checked above by allowedCommands)
|
||||
command = exec.CommandContext(ctx, gitBinVerb, repoPath)
|
||||
gitcmd = exec.CommandContext(ctx, gitBinVerb, repoPath)
|
||||
}
|
||||
|
||||
process.SetSysProcAttribute(command)
|
||||
command.Dir = setting.RepoRootPath
|
||||
command.Stdout = os.Stdout
|
||||
command.Stdin = os.Stdin
|
||||
command.Stderr = os.Stderr
|
||||
command.Env = append(command.Env, os.Environ()...)
|
||||
command.Env = append(command.Env,
|
||||
process.SetSysProcAttribute(gitcmd)
|
||||
gitcmd.Dir = setting.RepoRootPath
|
||||
gitcmd.Stdout = os.Stdout
|
||||
gitcmd.Stdin = os.Stdin
|
||||
gitcmd.Stderr = os.Stderr
|
||||
gitcmd.Env = append(gitcmd.Env, os.Environ()...)
|
||||
gitcmd.Env = append(gitcmd.Env,
|
||||
repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
|
||||
repo_module.EnvRepoName+"="+results.RepoName,
|
||||
repo_module.EnvRepoUsername+"="+results.OwnerName,
|
||||
@@ -330,9 +352,9 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
)
|
||||
// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
|
||||
// it could be re-considered whether to use the same git.CommonGitCmdEnvs() as "git" command later.
|
||||
command.Env = append(command.Env, gitcmd.CommonCmdServEnvs()...)
|
||||
gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...)
|
||||
|
||||
if err = command.Run(); err != nil {
|
||||
if err = gitcmd.Run(); err != nil {
|
||||
return fail(ctx, "Failed to execute git command", "Failed to execute git command: %v", err)
|
||||
}
|
||||
|
||||
|
||||
54
cmd/web.go
54
cmd/web.go
@@ -8,13 +8,14 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
"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
|
||||
@@ -129,19 +130,19 @@ func showWebStartupMessage(msg string) {
|
||||
}
|
||||
}
|
||||
|
||||
func serveInstall(cmd *cli.Command) error {
|
||||
func serveInstall(ctx *cli.Context) error {
|
||||
showWebStartupMessage("Prepare to run install page")
|
||||
|
||||
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
|
||||
|
||||
// Flag for port number in case first time run conflict
|
||||
if cmd.IsSet("port") {
|
||||
if err := setPort(cmd.String("port")); err != nil {
|
||||
if ctx.IsSet("port") {
|
||||
if err := setPort(ctx.String("port")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cmd.IsSet("install-port") {
|
||||
if err := setPort(cmd.String("install-port")); err != nil {
|
||||
if ctx.IsSet("install-port") {
|
||||
if err := setPort(ctx.String("install-port")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -155,13 +156,14 @@ func serveInstall(cmd *cli.Command) error {
|
||||
case <-graceful.GetManager().IsShutdown():
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveInstalled(c *cli.Command) error {
|
||||
func serveInstalled(ctx *cli.Context) error {
|
||||
setting.InitCfgProvider(setting.CustomConf)
|
||||
setting.LoadCommonSettings()
|
||||
setting.MustInstalled()
|
||||
@@ -216,8 +218,8 @@ func serveInstalled(c *cli.Command) error {
|
||||
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
|
||||
|
||||
// Override the provided port number within the configuration
|
||||
if c.IsSet("port") {
|
||||
if err := setPort(c.String("port")); err != nil {
|
||||
if ctx.IsSet("port") {
|
||||
if err := setPort(ctx.String("port")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -229,37 +231,27 @@ func serveInstalled(c *cli.Command) error {
|
||||
err := listen(webRoutes, true)
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func servePprof() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
mux.Handle("/debug/fgprof", fgprof.Handler())
|
||||
// FIXME: it should use a proper context
|
||||
_, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true)
|
||||
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it.
|
||||
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
|
||||
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
|
||||
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
|
||||
log.Info("Starting pprof server on localhost:6060")
|
||||
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", mux))
|
||||
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
|
||||
finished()
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, cmd *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))
|
||||
}
|
||||
}()
|
||||
|
||||
if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid {
|
||||
return fmt.Errorf("unknown command: %s", subCmdName)
|
||||
}
|
||||
|
||||
managerCtx, cancel := context.WithCancel(ctx)
|
||||
managerCtx, cancel := context.WithCancel(context.Background())
|
||||
graceful.InitManager(managerCtx)
|
||||
defer cancel()
|
||||
|
||||
@@ -270,12 +262,12 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
// Set pid file setting
|
||||
if cmd.IsSet("pid") {
|
||||
createPIDFile(cmd.String("pid"))
|
||||
if ctx.IsSet("pid") {
|
||||
createPIDFile(ctx.String("pid"))
|
||||
}
|
||||
|
||||
if !setting.InstallLock {
|
||||
if err := serveInstall(cmd); err != nil {
|
||||
if err := serveInstall(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -286,7 +278,7 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
|
||||
go servePprof()
|
||||
}
|
||||
|
||||
return serveInstalled(cmd)
|
||||
return serveInstalled(ctx)
|
||||
}
|
||||
|
||||
func setPort(port string) error {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user