Compare commits

...

14 Commits

Author SHA1 Message Date
Joe Chen
d958a47a0e ci: fix up docker workflow issue 2026-01-23 12:38:11 -05:00
Joe Chen
89ad16f9b3 Suppress golangci-lint errors 2026-01-23 12:28:00 -05:00
Joe Chen
9ea429faaf Fix CI setup and errors 2026-01-23 12:18:28 -05:00
Joe Chen
c7d2d8b525 release: update version to 0.13.4 2026-01-23 10:21:06 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
4dc0a99919 repo: validate Git server hook name for editing (#8103) 2026-01-23 09:40:55 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
9e70cdf437 api: verify write access to update repo content (#8102) 2026-01-23 09:19:26 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
961a79e8f9 api: verify owner access to delete repos (#8101) 2026-01-22 22:53:17 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
d568e04831 two_factor: verify recovery code ownership upon using (#8100) 2026-01-22 22:35:52 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
af825ff56f wiki: sanitize old wiki page name when editing (#8099) 2026-01-22 11:06:41 -05:00
Jakub Domeracki
71a72a72ad security: patch mermaid package version
https://github.com/gogs/gogs/security/advisories/GHSA-26gq-grmh-6xm6

Co-authored-by: Jakub Domeracki <jdomeracki.itsec@gmail.com>
Co-authored-by: ᴊᴏᴇ ᴄʜᴇɴ <jc@unknwon.io>
2026-01-22 09:48:30 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
4167a4d568 wiki: auto-detect default branch (#8094) 2026-01-20 23:39:05 -05:00
Mukaiu
5b5793bb4a api: fix nil pointer dereference when listing user repos (#8069)
Co-authored-by: Joe Chen <jc@unknwon.io>
2026-01-20 23:33:43 -05:00
ᴊᴏᴇ ᴄʜᴇɴ
c3eca1fca3 repository: reject any updates that has symlink in path hierarchy (#8082)
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 23:31:41 -05:00
Neptunium93
33990972fa repo: fix potential null pointer dereference in mirror sync (#8065) 2026-01-20 23:25:23 -05:00
28 changed files with 3413 additions and 262 deletions

View File

@@ -1,35 +0,0 @@
# Docs: https://github.com/sturdy-dev/codeball-action
name: Codeball
on: [ pull_request ]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
codeball:
runs-on: ubuntu-latest
name: Codeball
steps:
# Start a new Codeball review job
# This step is asynchronous and will return a job id
- name: Trigger Codeball
id: codeball_baller
uses: sturdy-dev/codeball-action/baller@v2
# Wait for Codeball to return the status
- name: Get Status
id: codeball_status
uses: sturdy-dev/codeball-action/status@v2
with:
codeball-job-id: ${{ steps.codeball_baller.outputs.codeball-job-id }}
# If Codeball approved the contribution, approve the PR
- name: Approve PR
uses: sturdy-dev/codeball-action/approver@v2
if: ${{ steps.codeball_status.outputs.approved == 'true' }}
with:
message: "Codeball: LGTM! :+1:"

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.28.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.28.3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.28.3

View File

@@ -5,6 +5,7 @@ on:
- main
pull_request:
paths:
- '.trivy.yaml'
- 'Dockerfile'
- 'docker/**'
- '.github/workflows/docker.yml'
@@ -13,29 +14,25 @@ on:
jobs:
buildx:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'gogs/gogs' }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
packages: write
steps:
- name: Canel previous runs
uses: styfle/cancel-workflow-action@0.9.1
with:
all_but_latest: true
access_token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
config-inline: |
[worker.oci]
max-parallelism = 2
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
@@ -44,18 +41,18 @@ jobs:
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container registry
uses: docker/login-action@v1
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v2
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
@@ -63,14 +60,13 @@ jobs:
tags: |
gogs/gogs:latest
ghcr.io/gogs/gogs:latest
registry.digitalocean.com/gogs/gogs:latest
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: gogs/gogs:latest
exit-code: '1'
- name: Send email on failure
uses: dawidd6/action-send-mail@v3
uses: dawidd6/action-send-mail@2cea9617b09d79a095af21254fbcb7ae95903dde # v3.12.0
if: ${{ failure() }}
with:
server_address: smtp.mailgun.org
@@ -93,10 +89,10 @@ jobs:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
with:
config-inline: |
[worker.oci]
@@ -110,19 +106,19 @@ jobs:
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
- name: Compute short commit SHA
id: short-sha
uses: benjlevesque/short-sha@v2.1
uses: benjlevesque/short-sha@599815c8ee942a9616c92bcfb4f947a3b670ab0b # v3.0
- name: Build and push images
uses: docker/build-push-action@v2
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64
push: true
tags: |
ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:1d
ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:7d
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:1d
image-ref: ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:7d
exit-code: '1'
# Updates to the following section needs to be synced to all release branches within their lifecycles.
@@ -137,16 +133,14 @@ jobs:
- name: Compute image tag name
run: echo "IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -c 2-)" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
config-inline: |
[worker.oci]
max-parallelism = 2
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
@@ -155,18 +149,18 @@ jobs:
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container registry
uses: docker/login-action@v1
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v2
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
@@ -175,7 +169,7 @@ jobs:
gogs/gogs:${{ env.IMAGE_TAG }}
ghcr.io/gogs/gogs:${{ env.IMAGE_TAG }}
- name: Send email on failure
uses: dawidd6/action-send-mail@v3
uses: dawidd6/action-send-mail@2cea9617b09d79a095af21254fbcb7ae95903dde # v3.12.0
if: ${{ failure() }}
with:
server_address: smtp.mailgun.org

View File

@@ -30,13 +30,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.23.x
go-version: 1.25.x
- name: Install Task
uses: arduino/setup-task@v2
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check Go module tidiness and generated files
@@ -52,7 +52,7 @@ jobs:
exit 1
fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@9fae48acfc02a90574d7c304a1758ef9895495fa # v7.0.1
with:
version: latest
args: --timeout=30m
@@ -61,25 +61,25 @@ jobs:
name: Test
strategy:
matrix:
go-version: [ 1.23.x ]
go-version: [ 1.25.x ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v1.5.0
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
file: ./coverage
flags: unittests
- name: Send email on failure
uses: dawidd6/action-send-mail@v3
uses: dawidd6/action-send-mail@2cea9617b09d79a095af21254fbcb7ae95903dde # v3.12.0
if: ${{ failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
server_address: smtp.mailgun.org
@@ -98,28 +98,28 @@ jobs:
# Running tests with race detection consumes too much memory on Windows,
# see https://github.com/golang/go/issues/46099 for details.
test-windows:
name: Test
name: Test Windows
strategy:
matrix:
go-version: [ 1.23.x ]
go-version: [ 1.25.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -shuffle=on -v -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v1.5.0
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
file: ./coverage
flags: unittests
- name: Send email on failure
uses: dawidd6/action-send-mail@v3
uses: dawidd6/action-send-mail@2cea9617b09d79a095af21254fbcb7ae95903dde # v3.12.0
if: ${{ failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
server_address: smtp.mailgun.org
@@ -139,7 +139,7 @@ jobs:
name: Postgres
strategy:
matrix:
go-version: [ 1.23.x ]
go-version: [ 1.25.x ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
services:
@@ -155,12 +155,12 @@ jobs:
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./internal/db/...
env:
@@ -175,18 +175,18 @@ jobs:
name: MySQL
strategy:
matrix:
go-version: [ 1.23.x ]
platform: [ ubuntu-22.04 ]
go-version: [ 1.25.x ]
platform: [ ubuntu-22.04 ] # Use the lowest version possible for backwards compatibility
runs-on: ${{ matrix.platform }}
steps:
- name: Start MySQL server
run: sudo systemctl start mysql
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./internal/db/...
env:
@@ -200,16 +200,16 @@ jobs:
name: SQLite - Go
strategy:
matrix:
go-version: [ 1.23.x ]
go-version: [ 1.25.x ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: go test -shuffle=on -v -race -parallel=1 -coverprofile=coverage -covermode=atomic ./internal/db/...
env:

View File

@@ -16,7 +16,7 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
issue-inactive-days: '90'

View File

@@ -12,6 +12,6 @@ jobs:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0

View File

@@ -1,29 +1,153 @@
linters-settings:
staticcheck:
checks: [
"all",
"-SA1019" # There are valid use cases of strings.Title
]
nakedret:
max-func-lines: 0 # Disallow any unnamed return statement
govet:
disable:
# printf: non-constant format string in call to fmt.Errorf (govet)
# showing up since golangci-lint version 1.60.1
- printf
version: "2"
linters:
enable:
- unused
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- nakedret
- gofmt
- rowserrcheck
- unconvert
- goimports
- unparam
settings:
govet:
disable:
# printf: non-constant format string in call to fmt.Errorf (govet)
# showing up since golangci-lint version 1.60.1
- printf
staticcheck:
checks:
- all
- "-SA1019" # This project is under active refactoring and not all code is up to date.
- "-QF1001" # I'm a math noob
- "-ST1016" # Some legit code uses this pattern
nakedret:
max-func-lines: 0 # Disallow any unnamed return statement
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
rules:
- path: internal/auth/ldap/config.go
linters: [staticcheck]
- path: internal/cmd/import.go
linters: [staticcheck]
- path: internal/context/context.go
linters: [staticcheck]
- path: internal/db/attachment.go
linters: [staticcheck]
- path: internal/db/errors/errors.go
linters: [staticcheck]
- path: internal/db/issue.go
linters: [staticcheck]
- path: internal/db/issue_mail.go
linters: [staticcheck]
- path: internal/db/org.go
linters: [staticcheck]
- path: internal/db/repo.go
linters: [staticcheck]
- path: internal/db/schemadoc/main.go
linters: [staticcheck]
- path: internal/db/users.go
linters: [staticcheck]
- path: internal/db/webhook.go
linters: [staticcheck]
- path: internal/db/webhook_dingtalk.go
linters: [staticcheck]
- path: internal/email/email.go
linters: [staticcheck]
- path: internal/email/message.go
linters: [staticcheck]
- path: internal/form/repo.go
linters: [staticcheck]
- path: internal/form/user.go
linters: [staticcheck]
- path: internal/httplib/httplib.go
linters: [staticcheck]
- path: internal/markup/markdown.go
linters: [staticcheck]
- path: internal/route/api/v1/repo/commits.go
linters: [staticcheck]
- path: internal/route/api/v1/user/follower.go
linters: [staticcheck]
- path: internal/route/repo/branch.go
linters: [staticcheck]
- path: internal/route/repo/commit.go
linters: [staticcheck]
- path: internal/route/repo/issue.go
linters: [staticcheck]
- path: internal/route/user/profile.go
linters: [staticcheck]
- path: internal/template/template.go
linters: [staticcheck]
- path: internal/tool/tool.go
linters: [staticcheck]
- path: internal/cmd/serv.go
linters: [staticcheck]
- path: internal/db/actions_test.go
linters: [staticcheck]
- path: internal/db/milestone.go
linters: [staticcheck]
- path: internal/db/pull.go
linters: [staticcheck]
- path: internal/route/home.go
linters: [staticcheck]
- path: internal/db/release.go
linters: [staticcheck]
- path: internal/route/org/members.go
linters: [staticcheck]
- path: internal/route/org/setting.go
linters: [staticcheck]
- path: internal/db/repo_branch.go
linters: [staticcheck]
- path: internal/db/user_mail.go
linters: [staticcheck]
- path: internal/route/user/auth.go
linters: [staticcheck]
- path: internal/db/webhook_slack.go
linters: [staticcheck]
- path: internal/form/form.go
linters: [staticcheck]
- path: internal/route/org/teams.go
linters: [staticcheck]
- path: internal/route/admin/auths.go
linters: [staticcheck]
- path: internal/route/admin/users.go
linters: [staticcheck]
- path: internal/db/admin.go
linters: [staticcheck]
- path: internal/db/comment.go
linters: [staticcheck]
- path: internal/route/user/home.go
linters: [staticcheck]
- path: internal/route/user/setting.go
linters: [staticcheck]
- path: internal/db/org_team.go
linters: [staticcheck]
- path: internal/db/repo_editor.go
linters: [staticcheck]
- path: internal/process/manager.go
linters: [staticcheck]
- path: internal/db/ssh_key.go
linters: [staticcheck]
- path: internal/route/repo/pull.go
linters: [staticcheck]
- path: internal/route/repo/release.go
linters: [staticcheck]
- path: internal/route/repo/setting.go
linters: [staticcheck]
- path: internal/route/repo/wiki.go
linters: [staticcheck]
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

4
gen.go
View File

@@ -4,5 +4,5 @@
package main
//go:generate go install golang.org/x/tools/cmd/goimports@v0.17.0
//go:generate go run github.com/derision-test/go-mockgen/v2/cmd/go-mockgen@v2.0.1
//go:generate go install golang.org/x/tools/cmd/goimports@v0.33.0
//go:generate go run github.com/unknwon/go-mockgen/cmd/go-mockgen@v0.0.0-20251002032800-a9a94b119e3b

View File

@@ -18,7 +18,7 @@ import (
)
func init() {
conf.App.Version = "0.13.3"
conf.App.Version = "0.13.4"
}
func main() {

View File

@@ -324,7 +324,7 @@ func SyncMirrors() {
m, err := GetMirrorByRepoID(com.StrTo(repoID).MustInt64())
if err != nil {
log.Error("GetMirrorByRepoID [%d]: %v", m.RepoID, err)
log.Error("GetMirrorByRepoID [%v]: %v", repoID, err)
continue
}

View File

@@ -1,4 +1,4 @@
// Code generated by go-mockgen 1.3.7; DO NOT EDIT.
// Code generated by go-mockgen 2.1.1; DO NOT EDIT.
//
// This file was generated by running `go-mockgen` at the root of this repository.
// To add additional mocks to this or another package, add a new entry to the

View File

@@ -107,6 +107,19 @@ func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error {
return nil
}
// hasSymlinkInPath returns true if there is any symlink in path hierarchy using
// the given base and relative path.
func hasSymlinkInPath(base, relPath string) bool {
parts := strings.Split(filepath.ToSlash(relPath), "/")
for i := range parts {
filePath := path.Join(append([]string{base}, parts[:i+1]...)...)
if osutil.IsSymlink(filePath) {
return true
}
}
return false
}
type UpdateRepoFileOptions struct {
OldBranch string
NewBranch string
@@ -118,7 +131,7 @@ type UpdateRepoFileOptions struct {
}
// UpdateRepoFile adds or updates a file in repository.
func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) {
func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) error {
// 🚨 SECURITY: Prevent uploading files into the ".git" directory.
if isRepositoryGitPath(opts.NewTreeName) {
return errors.Errorf("bad tree path %q", opts.NewTreeName)
@@ -127,15 +140,21 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
if err := repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
return fmt.Errorf("discard local repo branch[%s] changes: %v", opts.OldBranch, err)
} else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
}
repoPath := repo.RepoPath()
localPath := repo.LocalCopyPath()
// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
// that involve symlinks.
if hasSymlinkInPath(localPath, opts.OldTreeName) || hasSymlinkInPath(localPath, opts.NewTreeName) {
return errors.New("cannot update file with symbolic link in path")
}
repoPath := repo.RepoPath()
if opts.OldBranch != opts.NewBranch {
// Directly return error if new branch already exists in the server
if git.RepoHasBranch(repoPath, opts.NewBranch) {
@@ -144,7 +163,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
// Otherwise, delete branch from local copy in case out of sync
if git.RepoHasBranch(localPath, opts.NewBranch) {
if err = git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
if err := git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
Force: true,
}); err != nil {
return fmt.Errorf("delete branch %q: %v", opts.NewBranch, err)
@@ -157,46 +176,31 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
}
oldFilePath := path.Join(localPath, opts.OldTreeName)
filePath := path.Join(localPath, opts.NewTreeName)
if err = os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil {
return err
newFilePath := path.Join(localPath, opts.NewTreeName)
// Prompt the user if the meant-to-be new file already exists.
if osutil.IsExist(newFilePath) && opts.IsNewFile {
return ErrRepoFileAlreadyExist{newFilePath}
}
// If it's meant to be a new file, make sure it doesn't exist.
if opts.IsNewFile {
// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
// a symlink.
if osutil.IsSymlink(filePath) {
return fmt.Errorf("cannot update symbolic link: %s", opts.NewTreeName)
}
if osutil.IsExist(filePath) {
return ErrRepoFileAlreadyExist{filePath}
}
if err := os.MkdirAll(path.Dir(newFilePath), os.ModePerm); err != nil {
return errors.Wrapf(err, "create parent directories of %q", newFilePath)
}
// Ignore move step if it's a new file under a directory.
// Otherwise, move the file when name changed.
if osutil.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
// a symlink.
if osutil.IsSymlink(oldFilePath) {
return fmt.Errorf("cannot move symbolic link: %s", opts.OldTreeName)
}
if err = git.Move(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
return fmt.Errorf("git mv %q %q: %v", opts.OldTreeName, opts.NewTreeName, err)
if err := git.Move(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
return errors.Wrapf(err, "git mv %q %q", opts.OldTreeName, opts.NewTreeName)
}
}
if err = os.WriteFile(filePath, []byte(opts.Content), 0600); err != nil {
if err := os.WriteFile(newFilePath, []byte(opts.Content), 0o600); err != nil {
return fmt.Errorf("write file: %v", err)
}
if err = git.Add(localPath, git.AddOptions{All: true}); err != nil {
if err := git.Add(localPath, git.AddOptions{All: true}); err != nil {
return fmt.Errorf("git add --all: %v", err)
}
err = git.CreateCommit(
err := git.CreateCommit(
localPath,
&git.Signature{
Name: doer.DisplayName(),
@@ -230,7 +234,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
}
// GetDiffPreview produces and returns diff result of a file which is not yet committed.
func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *gitutil.Diff, err error) {
func (repo *Repository) GetDiffPreview(branch, treePath, content string) (*gitutil.Diff, error) {
// 🚨 SECURITY: Prevent uploading files into the ".git" directory.
if isRepositoryGitPath(treePath) {
return nil, errors.Errorf("bad tree path %q", treePath)
@@ -239,7 +243,7 @@ func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
if err = repo.DiscardLocalRepoBranchChanges(branch); err != nil {
if err := repo.DiscardLocalRepoBranchChanges(branch); err != nil {
return nil, fmt.Errorf("discard local repo branch[%s] changes: %v", branch, err)
} else if err = repo.UpdateLocalCopyBranch(branch); err != nil {
return nil, fmt.Errorf("update local copy branch[%s]: %v", branch, err)
@@ -248,14 +252,15 @@ func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *
localPath := repo.LocalCopyPath()
filePath := path.Join(localPath, treePath)
// 🚨 SECURITY: Prevent updating files in surprising place, check if the target is
// a symlink.
if osutil.IsSymlink(filePath) {
return nil, fmt.Errorf("cannot get diff preview for symbolic link: %s", treePath)
// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
// that involve symlinks.
if hasSymlinkInPath(localPath, treePath) {
return nil, errors.New("cannot update file with symbolic link in path")
}
if err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return nil, err
} else if err = os.WriteFile(filePath, []byte(content), 0600); err != nil {
if err := os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil {
return nil, errors.Wrap(err, "create parent directories")
} else if err = os.WriteFile(filePath, []byte(content), 0o600); err != nil {
return nil, fmt.Errorf("write file: %v", err)
}
@@ -276,15 +281,13 @@ func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *
pid := process.Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", repo.RepoPath()), cmd)
defer process.Remove(pid)
diff, err = gitutil.ParseDiff(stdout, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars)
diff, err := gitutil.ParseDiff(stdout, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars)
if err != nil {
return nil, fmt.Errorf("parse diff: %v", err)
}
if err = cmd.Wait(); err != nil {
return nil, fmt.Errorf("wait: %v", err)
}
return diff, nil
}
@@ -319,21 +322,21 @@ func (repo *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) (
return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
}
localPath := repo.LocalCopyPath()
// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
// that involve symlinks.
if hasSymlinkInPath(localPath, opts.TreePath) {
return errors.New("cannot update file with symbolic link in path")
}
if opts.OldBranch != opts.NewBranch {
if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
return fmt.Errorf("checkout new branch[%s] from old branch[%s]: %v", opts.NewBranch, opts.OldBranch, err)
}
}
localPath := repo.LocalCopyPath()
filePath := path.Join(localPath, opts.TreePath)
// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
// a symlink.
if osutil.IsSymlink(filePath) {
return fmt.Errorf("cannot delete symbolic link: %s", opts.TreePath)
}
if err = os.Remove(filePath); err != nil {
return fmt.Errorf("remove file %q: %v", opts.TreePath, err)
}

View File

@@ -21,12 +21,6 @@ func Test_SSHParsePublicKey(t *testing.T) {
expType string
expLength int
}{
{
name: "dsa-1024",
content: "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment",
expType: "dsa",
expLength: 1024,
},
{
name: "rsa-1024",
content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment",

View File

@@ -109,21 +109,3 @@ func IsTwoFactorRecoveryCodeNotFound(err error) bool {
func (err ErrTwoFactorRecoveryCodeNotFound) Error() string {
return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
}
// UseRecoveryCode validates recovery code of given user and marks it is used if valid.
func UseRecoveryCode(_ int64, code string) error {
recoveryCode := new(TwoFactorRecoveryCode)
has, err := x.Where("code = ?", code).And("is_used = ?", false).Get(recoveryCode)
if err != nil {
return fmt.Errorf("get unused code: %v", err)
} else if !has {
return ErrTwoFactorRecoveryCodeNotFound{Code: code}
}
recoveryCode.IsUsed = true
if _, err = x.Id(recoveryCode.ID).Cols("is_used").Update(recoveryCode); err != nil {
return fmt.Errorf("mark code as used: %v", err)
}
return nil
}

View File

@@ -32,6 +32,7 @@ type TwoFactorsStore interface {
GetByUserID(ctx context.Context, userID int64) (*TwoFactor, error)
// IsEnabled returns true if the user has enabled 2FA.
IsEnabled(ctx context.Context, userID int64) bool
UseRecoveryCode(ctx context.Context, userID int64, code string) error
}
var TwoFactors TwoFactorsStore
@@ -121,6 +122,28 @@ func (db *twoFactors) IsEnabled(ctx context.Context, userID int64) bool {
return count > 0
}
// UseRecoveryCode validates a recovery code of given user and marks it as used
// if valid. It returns ErrTwoFactorRecoveryCodeNotFound if the code is invalid
// or already used.
func (db *twoFactors) UseRecoveryCode(ctx context.Context, userID int64, code string) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var recoveryCode TwoFactorRecoveryCode
err := tx.Where("user_id = ? AND code = ? AND is_used = ?", userID, code, false).First(&recoveryCode).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return ErrTwoFactorRecoveryCodeNotFound{Code: code}
}
return errors.Wrap(err, "get unused recovery code")
}
err = tx.Model(&recoveryCode).Update("is_used", true).Error
if err != nil {
return errors.Wrap(err, "mark recovery code as used")
}
return nil
})
}
// generateRecoveryCodes generates N number of recovery codes for 2FA.
func generateRecoveryCodes(userID int64, n int) ([]*TwoFactorRecoveryCode, error) {
recoveryCodes := make([]*TwoFactorRecoveryCode, n)

View File

@@ -79,6 +79,7 @@ func TestTwoFactors(t *testing.T) {
{"Create", twoFactorsCreate},
{"GetByUserID", twoFactorsGetByUserID},
{"IsEnabled", twoFactorsIsEnabled},
{"UseRecoveryCode", twoFactorsUseRecoveryCode},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
@@ -139,3 +140,57 @@ func twoFactorsIsEnabled(t *testing.T, db *twoFactors) {
assert.True(t, db.IsEnabled(ctx, 1))
assert.False(t, db.IsEnabled(ctx, 2))
}
func twoFactorsUseRecoveryCode(t *testing.T, db *twoFactors) {
ctx := context.Background()
// Create 2FA tokens for two users
err := db.Create(ctx, 1, "secure-key", "secure-secret")
require.NoError(t, err)
err = db.Create(ctx, 2, "secure-key", "secure-secret")
require.NoError(t, err)
// Get recovery codes for both users
var user1Codes []TwoFactorRecoveryCode
err = db.DB.Where("user_id = ?", 1).Find(&user1Codes).Error
require.NoError(t, err)
require.NotEmpty(t, user1Codes)
var user2Codes []TwoFactorRecoveryCode
err = db.DB.Where("user_id = ?", 2).Find(&user2Codes).Error
require.NoError(t, err)
require.NotEmpty(t, user2Codes)
// User 1 should be able to use their own recovery code
err = db.UseRecoveryCode(ctx, 1, user1Codes[0].Code)
require.NoError(t, err)
// Verify the code is now marked as used
var usedCode TwoFactorRecoveryCode
err = db.DB.Where("id = ?", user1Codes[0].ID).First(&usedCode).Error
require.NoError(t, err)
assert.True(t, usedCode.IsUsed)
// User 1 should NOT be able to use user 2's recovery code
// This is the key security test - recovery codes must be scoped by user
err = db.UseRecoveryCode(ctx, 1, user2Codes[0].Code)
assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected recovery code not found error when using another user's code")
// User 2's code should still be unused
var user2Code TwoFactorRecoveryCode
err = db.DB.Where("id = ?", user2Codes[0].ID).First(&user2Code).Error
require.NoError(t, err)
assert.False(t, user2Code.IsUsed, "user 2's recovery code should not be marked as used")
// User 2 should be able to use their own code
err = db.UseRecoveryCode(ctx, 2, user2Codes[0].Code)
require.NoError(t, err)
// Using an already-used code should fail
err = db.UseRecoveryCode(ctx, 1, user1Codes[0].Code)
assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected error when reusing a recovery code")
// Using a non-existent code should fail
err = db.UseRecoveryCode(ctx, 1, "invalid-code")
assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected error for invalid recovery code")
}

View File

@@ -18,23 +18,33 @@ import (
"github.com/gogs/git-module"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/pathutil"
"gogs.io/gogs/internal/repoutil"
"gogs.io/gogs/internal/sync"
)
var wikiWorkingPool = sync.NewExclusivePool()
// WikiBranch returns the branch name used by the wiki repository. It checks if
// "main" branch exists, otherwise falls back to "master".
func WikiBranch(repoPath string) string {
if git.RepoHasBranch(repoPath, "main") {
return "main"
}
return "master"
}
// ToWikiPageURL formats a string to corresponding wiki URL name.
func ToWikiPageURL(name string) string {
return url.QueryEscape(name)
}
// ToWikiPageName formats a URL back to corresponding wiki page name,
// and removes leading characters './' to prevent changing files
// that are not belong to wiki repository.
// ToWikiPageName formats a URL back to corresponding wiki page name. It enforces
// single-level hierarchy by replacing all "/" with spaces.
func ToWikiPageName(urlString string) string {
name, _ := url.QueryUnescape(urlString)
return strings.ReplaceAll(strings.TrimLeft(path.Clean("/"+name), "/"), "/", " ")
name = pathutil.Clean(name)
return strings.ReplaceAll(name, "/", " ")
}
// WikiCloneLink returns clone URLs of repository wiki.
@@ -79,24 +89,25 @@ func (repo *Repository) LocalWikiPath() string {
// UpdateLocalWiki makes sure the local copy of repository wiki is up-to-date.
func (repo *Repository) UpdateLocalWiki() error {
return UpdateLocalCopyBranch(repo.WikiPath(), repo.LocalWikiPath(), "master", true)
wikiPath := repo.WikiPath()
return UpdateLocalCopyBranch(wikiPath, repo.LocalWikiPath(), WikiBranch(wikiPath), true)
}
func discardLocalWikiChanges(localPath string) error {
return discardLocalRepoBranchChanges(localPath, "master")
return discardLocalRepoBranchChanges(localPath, WikiBranch(localPath))
}
// updateWikiPage adds new page to repository wiki.
func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, message string, isNew bool) (err error) {
func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, message string, isNew bool) error {
wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
if err = repo.InitWiki(); err != nil {
if err := repo.InitWiki(); err != nil {
return fmt.Errorf("InitWiki: %v", err)
}
localPath := repo.LocalWikiPath()
if err = discardLocalWikiChanges(localPath); err != nil {
if err := discardLocalWikiChanges(localPath); err != nil {
return fmt.Errorf("discardLocalWikiChanges: %v", err)
} else if err = repo.UpdateLocalWiki(); err != nil {
return fmt.Errorf("UpdateLocalWiki: %v", err)
@@ -111,7 +122,8 @@ func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, mes
return ErrWikiAlreadyExist{filename}
}
} else {
os.Remove(path.Join(localPath, oldTitle+".md"))
oldTitle = ToWikiPageName(oldTitle)
_ = os.Remove(path.Join(localPath, oldTitle+".md"))
}
// SECURITY: if new file is a symlink to non-exist critical file,
@@ -119,20 +131,20 @@ func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, mes
// as a new page operation.
// So we want to make sure the symlink is removed before write anything.
// The new file we created will be in normal text format.
os.Remove(filename)
_ = os.Remove(filename)
if err = os.WriteFile(filename, []byte(content), 0666); err != nil {
if err := os.WriteFile(filename, []byte(content), 0o666); err != nil {
return fmt.Errorf("WriteFile: %v", err)
}
if message == "" {
message = "Update page '" + title + "'"
}
if err = git.Add(localPath, git.AddOptions{All: true}); err != nil {
if err := git.Add(localPath, git.AddOptions{All: true}); err != nil {
return fmt.Errorf("add all changes: %v", err)
}
err = git.CreateCommit(
err := git.CreateCommit(
localPath,
&git.Signature{
Name: doer.DisplayName(),
@@ -143,7 +155,7 @@ func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, mes
)
if err != nil {
return fmt.Errorf("commit changes: %v", err)
} else if err = git.Push(localPath, "origin", "master"); err != nil {
} else if err = git.Push(localPath, "origin", WikiBranch(localPath)); err != nil {
return fmt.Errorf("push: %v", err)
}
@@ -170,8 +182,7 @@ func (repo *Repository) DeleteWikiPage(doer *User, title string) (err error) {
}
title = ToWikiPageName(title)
filename := path.Join(localPath, title+".md")
os.Remove(filename)
_ = os.Remove(path.Join(localPath, title+".md"))
message := "Delete page '" + title + "'"
@@ -190,7 +201,7 @@ func (repo *Repository) DeleteWikiPage(doer *User, title string) (err error) {
)
if err != nil {
return fmt.Errorf("commit changes: %v", err)
} else if err = git.Push(localPath, "origin", "master"); err != nil {
} else if err = git.Push(localPath, "origin", WikiBranch(localPath)); err != nil {
return fmt.Errorf("push: %v", err)
}

27
internal/db/wiki_test.go Normal file
View File

@@ -0,0 +1,27 @@
package db
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestToWikiPageName(t *testing.T) {
tests := []struct {
input string
want string
}{
{input: "Home", want: "Home"},
{input: "../../../../tmp/target_file", want: "tmp target_file"},
{input: "..\\..\\..\\..\\tmp\\target_file", want: "tmp target_file"},
{input: "A/B", want: "A B"},
{input: "../pwn", want: "pwn"},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
got := ToWikiPageName(test.input)
require.Equal(t, test.want, got)
})
}
}

View File

@@ -9,7 +9,8 @@ import (
"os/user"
)
// IsFile returns true if given path exists as a file (i.e. not a directory).
// IsFile returns true if given path exists as a file (i.e. not a directory)
// following any symlinks.
func IsFile(path string) bool {
f, e := os.Stat(path)
if e != nil {
@@ -18,8 +19,8 @@ func IsFile(path string) bool {
return !f.IsDir()
}
// IsDir returns true if given path is a directory, and returns false when it's
// a file or does not exist.
// IsDir returns true if given path is a directory following any symlinks, and
// returns false when it's a file or does not exist.
func IsDir(dir string) bool {
f, e := os.Stat(dir)
if e != nil {
@@ -28,7 +29,7 @@ func IsDir(dir string) bool {
return f.IsDir()
}
// IsExist returns true if a file or directory exists.
// IsExist returns true if a file or directory exists following any symlinks.
func IsExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
@@ -36,15 +37,10 @@ func IsExist(path string) bool {
// IsSymlink returns true if given path is a symbolic link.
func IsSymlink(path string) bool {
if !IsExist(path) {
return false
}
fileInfo, err := os.Lstat(path)
if err != nil {
return false
}
return fileInfo.Mode()&os.ModeSymlink != 0
}

View File

@@ -148,7 +148,7 @@ func reqRepoWriter() macaron.Handler {
}
}
// reqRepoWriter makes sure the context user has at least admin access to the repository.
// reqRepoAdmin makes sure the context user has at least admin access to the repository.
func reqRepoAdmin() macaron.Handler {
return func(c *context.Context) {
if !c.Repo.IsAdmin() {
@@ -158,6 +158,16 @@ func reqRepoAdmin() macaron.Handler {
}
}
// reqRepoOwner makes sure the context user has owner access to the repository.
func reqRepoOwner() macaron.Handler {
return func(c *context.Context) {
if !c.Repo.IsOwner() {
c.Status(http.StatusForbidden)
return
}
}
}
func mustEnableIssues(c *context.APIContext) {
if !c.Repo.Repository.EnableIssues || c.Repo.Repository.EnableExternalTracker {
c.NotFound()
@@ -250,7 +260,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/repos", func() {
m.Post("/migrate", bind(form.MigrateRepo{}), repo.Migrate)
m.Delete("/:username/:reponame", repoAssignment(), repo.Delete)
m.Delete("/:username/:reponame", repoAssignment(), reqRepoOwner(), repo.Delete)
m.Group("/:username/:reponame", func() {
m.Group("/hooks", func() {
@@ -275,7 +285,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", repo.GetContents)
m.Combo("/*").
Get(repo.GetContents).
Put(bind(repo.PutContentsRequest{}), repo.PutContents)
Put(reqRepoWriter(), bind(repo.PutContentsRequest{}), repo.PutContents)
})
m.Get("/archive/*", repo.GetArchive)
m.Group("/git", func() {

View File

@@ -116,19 +116,28 @@ func listUserRepositories(c *context.APIContext, username string) {
return
}
accessibleRepos, err := db.Repos.GetByCollaboratorIDWithAccessMode(c.Req.Context(), user.ID)
accessibleReposWithAccessMode, err := db.Repos.GetByCollaboratorIDWithAccessMode(c.Req.Context(), user.ID)
if err != nil {
c.Error(err, "get repositories accesses by collaborator")
return
}
accessibleRepos := make([]*db.Repository, 0, len(accessibleReposWithAccessMode))
for repo := range accessibleReposWithAccessMode {
accessibleRepos = append(accessibleRepos, repo)
}
if err = db.RepositoryList(accessibleRepos).LoadAttributes(); err != nil {
c.Error(err, "load attributes for accessible repositories")
return
}
numOwnRepos := len(ownRepos)
repos := make([]*api.Repository, 0, numOwnRepos+len(accessibleRepos))
repos := make([]*api.Repository, 0, numOwnRepos+len(accessibleReposWithAccessMode))
for _, r := range ownRepos {
repos = append(repos, r.APIFormatLegacy(&api.Permission{Admin: true, Push: true, Pull: true}))
}
for repo, access := range accessibleRepos {
for repo, access := range accessibleReposWithAccessMode {
repos = append(repos,
repo.APIFormatLegacy(&api.Permission{
Admin: access >= db.AccessModeAdmin,

View File

@@ -1,4 +1,4 @@
// Code generated by go-mockgen 1.3.7; DO NOT EDIT.
// Code generated by go-mockgen 2.1.1; DO NOT EDIT.
//
// This file was generated by running `go-mockgen` at the root of this repository.
// To add additional mocks to this or another package, add a new entry to the
@@ -2780,6 +2780,9 @@ type MockTwoFactorsStore struct {
// IsEnabledFunc is an instance of a mock function object controlling
// the behavior of the method IsEnabled.
IsEnabledFunc *TwoFactorsStoreIsEnabledFunc
// UseRecoveryCodeFunc is an instance of a mock function object
// controlling the behavior of the method UseRecoveryCode.
UseRecoveryCodeFunc *TwoFactorsStoreUseRecoveryCodeFunc
}
// NewMockTwoFactorsStore creates a new mock of the TwoFactorsStore
@@ -2802,6 +2805,11 @@ func NewMockTwoFactorsStore() *MockTwoFactorsStore {
return
},
},
UseRecoveryCodeFunc: &TwoFactorsStoreUseRecoveryCodeFunc{
defaultHook: func(context.Context, int64, string) (r0 error) {
return
},
},
}
}
@@ -2824,6 +2832,11 @@ func NewStrictMockTwoFactorsStore() *MockTwoFactorsStore {
panic("unexpected invocation of MockTwoFactorsStore.IsEnabled")
},
},
UseRecoveryCodeFunc: &TwoFactorsStoreUseRecoveryCodeFunc{
defaultHook: func(context.Context, int64, string) error {
panic("unexpected invocation of MockTwoFactorsStore.UseRecoveryCode")
},
},
}
}
@@ -2841,6 +2854,9 @@ func NewMockTwoFactorsStoreFrom(i db.TwoFactorsStore) *MockTwoFactorsStore {
IsEnabledFunc: &TwoFactorsStoreIsEnabledFunc{
defaultHook: i.IsEnabled,
},
UseRecoveryCodeFunc: &TwoFactorsStoreUseRecoveryCodeFunc{
defaultHook: i.UseRecoveryCode,
},
}
}
@@ -3168,6 +3184,116 @@ func (c TwoFactorsStoreIsEnabledFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// TwoFactorsStoreUseRecoveryCodeFunc describes the behavior when the
// UseRecoveryCode method of the parent MockTwoFactorsStore instance is
// invoked.
type TwoFactorsStoreUseRecoveryCodeFunc struct {
defaultHook func(context.Context, int64, string) error
hooks []func(context.Context, int64, string) error
history []TwoFactorsStoreUseRecoveryCodeFuncCall
mutex sync.Mutex
}
// UseRecoveryCode delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockTwoFactorsStore) UseRecoveryCode(v0 context.Context, v1 int64, v2 string) error {
r0 := m.UseRecoveryCodeFunc.nextHook()(v0, v1, v2)
m.UseRecoveryCodeFunc.appendCall(TwoFactorsStoreUseRecoveryCodeFuncCall{v0, v1, v2, r0})
return r0
}
// SetDefaultHook sets function that is called when the UseRecoveryCode
// method of the parent MockTwoFactorsStore instance is invoked and the hook
// queue is empty.
func (f *TwoFactorsStoreUseRecoveryCodeFunc) SetDefaultHook(hook func(context.Context, int64, string) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// UseRecoveryCode method of the parent MockTwoFactorsStore instance invokes
// the hook at the front of the queue and discards it. After the queue is
// empty, the default hook function is invoked for any future action.
func (f *TwoFactorsStoreUseRecoveryCodeFunc) PushHook(hook func(context.Context, int64, string) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *TwoFactorsStoreUseRecoveryCodeFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, int64, string) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *TwoFactorsStoreUseRecoveryCodeFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, int64, string) error {
return r0
})
}
func (f *TwoFactorsStoreUseRecoveryCodeFunc) nextHook() func(context.Context, int64, string) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *TwoFactorsStoreUseRecoveryCodeFunc) appendCall(r0 TwoFactorsStoreUseRecoveryCodeFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of TwoFactorsStoreUseRecoveryCodeFuncCall
// objects describing the invocations of this function.
func (f *TwoFactorsStoreUseRecoveryCodeFunc) History() []TwoFactorsStoreUseRecoveryCodeFuncCall {
f.mutex.Lock()
history := make([]TwoFactorsStoreUseRecoveryCodeFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// TwoFactorsStoreUseRecoveryCodeFuncCall is an object that describes an
// invocation of method UseRecoveryCode on an instance of
// MockTwoFactorsStore.
type TwoFactorsStoreUseRecoveryCodeFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c TwoFactorsStoreUseRecoveryCodeFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c TwoFactorsStoreUseRecoveryCodeFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// MockUsersStore is a mock implementation of the UsersStore interface (from
// the package gogs.io/gogs/internal/db) used for unit testing.
type MockUsersStore struct {

View File

@@ -582,13 +582,27 @@ func SettingsGitHooks(c *context.Context) {
c.Success(SETTINGS_GITHOOKS)
}
func isValidHookName(name git.HookName) bool {
for _, h := range git.ServerSideHooks {
if h == name {
return true
}
}
return false
}
func SettingsGitHooksEdit(c *context.Context) {
c.Data["Title"] = c.Tr("repo.settings.githooks")
c.Data["PageIsSettingsGitHooks"] = true
c.Data["RequireSimpleMDE"] = true
name := c.Params(":name")
hook, err := c.Repo.GitRepo.Hook("custom_hooks", git.HookName(name))
name := git.HookName(c.Params(":name"))
if !isValidHookName(name) {
c.NotFound()
return
}
hook, err := c.Repo.GitRepo.Hook("custom_hooks", name)
if err != nil {
c.NotFoundOrError(osutil.NewError(err), "get hook")
return
@@ -598,8 +612,13 @@ func SettingsGitHooksEdit(c *context.Context) {
}
func SettingsGitHooksEditPost(c *context.Context) {
name := c.Params(":name")
hook, err := c.Repo.GitRepo.Hook("custom_hooks", git.HookName(name))
name := git.HookName(c.Params(":name"))
if !isValidHookName(name) {
c.NotFound()
return
}
hook, err := c.Repo.GitRepo.Hook("custom_hooks", name)
if err != nil {
c.NotFoundOrError(osutil.NewError(err), "get hook")
return

View File

@@ -43,12 +43,13 @@ type PageMeta struct {
}
func renderWikiPage(c *context.Context, isViewPage bool) (*git.Repository, string) {
wikiRepo, err := git.Open(c.Repo.Repository.WikiPath())
wikiPath := c.Repo.Repository.WikiPath()
wikiRepo, err := git.Open(wikiPath)
if err != nil {
c.Error(err, "open repository")
return nil, ""
}
commit, err := wikiRepo.BranchCommit("master")
commit, err := wikiRepo.BranchCommit(db.WikiBranch(wikiPath))
if err != nil {
c.Error(err, "get branch commit")
return nil, ""
@@ -124,7 +125,8 @@ func Wiki(c *context.Context) {
}
// Get last change information.
commits, err := wikiRepo.Log(git.RefsHeads+"master", git.LogOptions{Path: pageName + ".md"})
branch := db.WikiBranch(c.Repo.Repository.WikiPath())
commits, err := wikiRepo.Log(git.RefsHeads+branch, git.LogOptions{Path: pageName + ".md"})
if err != nil {
c.Error(err, "get commits by path")
return
@@ -143,12 +145,15 @@ func WikiPages(c *context.Context) {
return
}
wikiRepo, err := git.Open(c.Repo.Repository.WikiPath())
wikiPath := c.Repo.Repository.WikiPath()
wikiRepo, err := git.Open(wikiPath)
if err != nil {
c.Error(err, "open repository")
return
}
commit, err := wikiRepo.BranchCommit("master")
branch := db.WikiBranch(wikiPath)
commit, err := wikiRepo.BranchCommit(branch)
if err != nil {
c.Error(err, "get branch commit")
return
@@ -162,7 +167,7 @@ func WikiPages(c *context.Context) {
pages := make([]PageMeta, 0, len(entries))
for i := range entries {
if entries[i].Type() == git.ObjectBlob && strings.HasSuffix(entries[i].Name(), ".md") {
commits, err := wikiRepo.Log(git.RefsHeads+"master", git.LogOptions{Path: entries[i].Name()})
commits, err := wikiRepo.Log(git.RefsHeads+branch, git.LogOptions{Path: entries[i].Name()})
if err != nil {
c.Error(err, "get commits by path")
return

View File

@@ -267,7 +267,7 @@ func LoginTwoFactorRecoveryCodePost(c *context.Context) {
return
}
if err := db.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil {
if err := db.TwoFactors.UseRecoveryCode(c.Req.Context(), userID, c.Query("recovery_code")); err != nil {
if db.IsTwoFactorRecoveryCodeNotFound(err) {
c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code"))
c.RedirectSubpath("/user/login/two_factor_recovery_code")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -59,7 +59,7 @@
<script src="{{AppSubURL}}/plugins/autosize-4.0.2/autosize.min.js"></script>
{{end}}
{{if .IsMarkdown}}
<script src="{{AppSubURL}}/plugins/mermaid-8.14.0/mermaid.min.js"></script>
<script src="{{AppSubURL}}/plugins/mermaid-11.12.1/mermaid.min.js"></script>
<script>
$(document).ready(function () {
mermaid.init({startOnLoad: true, noteMargin: 10}, ".language-mermaid");