mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			47 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cccd54999a | ||
| 
						 | 
					3e7cb5b639 | ||
| 
						 | 
					ec146b4200 | ||
| 
						 | 
					51fa86f0fa | ||
| 
						 | 
					e67b004535 | ||
| 
						 | 
					a4cc867401 | ||
| 
						 | 
					8e5aa8fb1e | ||
| 
						 | 
					046fc8684c | ||
| 
						 | 
					e3e705200a | ||
| 
						 | 
					a9d5ab8f88 | ||
| 
						 | 
					5546b4279c | ||
| 
						 | 
					4f6d09fb68 | ||
| 
						 | 
					4e5aca62ee | ||
| 
						 | 
					030ed9462d | ||
| 
						 | 
					60f175f7ff | ||
| 
						 | 
					260816d64a | ||
| 
						 | 
					a01f56a61a | ||
| 
						 | 
					c01459a504 | ||
| 
						 | 
					6e4e3ca012 | ||
| 
						 | 
					ed0e8865f3 | ||
| 
						 | 
					dc5adce70d | ||
| 
						 | 
					328ce0485f | ||
| 
						 | 
					39b6abf955 | ||
| 
						 | 
					3eda79647b | ||
| 
						 | 
					97171be1b4 | ||
| 
						 | 
					9ef2a338d8 | ||
| 
						 | 
					66ccae8b90 | ||
| 
						 | 
					0e6489317e | ||
| 
						 | 
					67dc1ff926 | ||
| 
						 | 
					4ee4c06b07 | ||
| 
						 | 
					3063e37802 | ||
| 
						 | 
					a40e15a116 | ||
| 
						 | 
					8f75f61b64 | ||
| 
						 | 
					25e409e025 | ||
| 
						 | 
					f8f24d83cf | ||
| 
						 | 
					15e93a751c | ||
| 
						 | 
					5a9b3bfa50 | ||
| 
						 | 
					dd901983c0 | ||
| 
						 | 
					7f962a16c9 | ||
| 
						 | 
					5d81f6d73f | ||
| 
						 | 
					eee4a752a5 | ||
| 
						 | 
					b3516767fb | ||
| 
						 | 
					8d1be2a9c5 | ||
| 
						 | 
					e46f9ff534 | ||
| 
						 | 
					690e810bcc | ||
| 
						 | 
					35983ac0a8 | ||
| 
						 | 
					4b3400bd9c | 
							
								
								
									
										26
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							@@ -59,6 +59,8 @@ jobs:
 | 
			
		||||
          aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
 | 
			
		||||
  nightly-docker-rootful:
 | 
			
		||||
    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
 | 
			
		||||
@@ -85,17 +87,27 @@ jobs:
 | 
			
		||||
        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 rootful docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
 | 
			
		||||
          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
 | 
			
		||||
@@ -122,6 +134,12 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
@@ -131,4 +149,6 @@ jobs:
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
 | 
			
		||||
          tags: |-
 | 
			
		||||
            gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
 | 
			
		||||
            ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							@@ -69,6 +69,8 @@ jobs:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
 | 
			
		||||
  docker-rootful:
 | 
			
		||||
    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
 | 
			
		||||
@@ -79,7 +81,9 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=false
 | 
			
		||||
          # 1.2.3-rc0
 | 
			
		||||
@@ -90,16 +94,24 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
          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
 | 
			
		||||
    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
 | 
			
		||||
@@ -110,7 +122,9 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          # each tag below will have the suffix of -rootless
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=false
 | 
			
		||||
@@ -123,11 +137,17 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,6 +14,8 @@ concurrency:
 | 
			
		||||
jobs:
 | 
			
		||||
  binary:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-binary
 | 
			
		||||
    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
 | 
			
		||||
@@ -71,6 +73,8 @@ jobs:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
 | 
			
		||||
  docker-rootful:
 | 
			
		||||
    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
 | 
			
		||||
@@ -81,7 +85,9 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          # this will generate tags in the following format:
 | 
			
		||||
          # latest
 | 
			
		||||
          # 1
 | 
			
		||||
@@ -96,11 +102,17 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
@@ -116,7 +128,9 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          # each tag below will have the suffix of -rootless
 | 
			
		||||
          flavor: |
 | 
			
		||||
            suffix=-rootless,onlatest=true
 | 
			
		||||
@@ -134,11 +148,17 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,59 @@ 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.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
 | 
			
		||||
  * Update net package (#34228) (#34232)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix releases sidebar navigation link (#34436) #34439
 | 
			
		||||
  * Fix bug webhook milestone is not right. (#34419) #34429
 | 
			
		||||
  * Fix two missed null value checks on the wiki page. (#34205) (#34215)
 | 
			
		||||
  * Swift files can be passed either as file or as form value (#34068) (#34236)
 | 
			
		||||
  * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
 | 
			
		||||
  * Upgrade github v61 -> v71 to fix migrating bug (#34389)
 | 
			
		||||
  * Fix bug when visiting comparation page (#34334) (#34364)
 | 
			
		||||
  * Fix wrong review requests when updating the pull request (#34286) (#34304)
 | 
			
		||||
  * Fix github migration error when using multiple tokens (#34144) (#34302)
 | 
			
		||||
  * Explicitly not update indexes when sync database schemas (#34281) (#34295)
 | 
			
		||||
  * Fix panic when comment is nil (#34257) (#34277)
 | 
			
		||||
  * Fix project board links to related Pull Requests (#34213) (#34222)
 | 
			
		||||
  * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
 | 
			
		||||
* DOCUMENTATION
 | 
			
		||||
  * Update token creation API swagger documentation (#34288) (#34296)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Fix CI Build (#34315)
 | 
			
		||||
  * Add riscv64 support (#34199) (#34204)
 | 
			
		||||
  * 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
 | 
			
		||||
 | 
			
		||||
* Enhancements
 | 
			
		||||
  * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
 | 
			
		||||
  * Also check default ssh-cert location for host (#34099) (#34100) (#34116)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
 | 
			
		||||
  * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
 | 
			
		||||
  * Fix invalid version in RPM package path (#34112) (#34115)
 | 
			
		||||
  * Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
 | 
			
		||||
  * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
 | 
			
		||||
  * Try to fix check-attr bug (#34029) (#34033)
 | 
			
		||||
  * Git client will follow 301 but 307 (#34005) (#34010)
 | 
			
		||||
  * Fix block expensive for 1.23 (#34127)
 | 
			
		||||
  * Fix markdown frontmatter rendering (#34102) (#34107)
 | 
			
		||||
  * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
 | 
			
		||||
  * Do not show 500 error when default branch doesn't exist (#34096) (#34097)
 | 
			
		||||
  * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
 | 
			
		||||
  * Simplify emoji rendering (#34048) (#34049)
 | 
			
		||||
  * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
 | 
			
		||||
  * Pull request updates will also trigger code owners review requests (#33744) (#34045)
 | 
			
		||||
  * Fix org repo creation being limited by user limits (#34030) (#34044)
 | 
			
		||||
  * Fix git client accessing renamed repo (#34034) (#34043)
 | 
			
		||||
  * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
 | 
			
		||||
  * Polyfill WeakRef (#34025) (#34028)
 | 
			
		||||
 | 
			
		||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@@ -109,7 +109,7 @@ endif
 | 
			
		||||
 | 
			
		||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
 | 
			
		||||
 | 
			
		||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
 | 
			
		||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
 | 
			
		||||
 | 
			
		||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
 | 
			
		||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
 | 
			
		||||
			Name:  "access-token",
 | 
			
		||||
			Usage: "Generate access token for the user",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "access-token-name",
 | 
			
		||||
			Usage: `Name of the generated access token`,
 | 
			
		||||
			Value: "gitea-admin",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "access-token-scopes",
 | 
			
		||||
			Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
 | 
			
		||||
			Value: "all",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "restricted",
 | 
			
		||||
			Usage: "Make a restricted user account",
 | 
			
		||||
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
 | 
			
		||||
		IsRestricted: restricted,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var accessTokenName string
 | 
			
		||||
	var accessTokenScope auth_model.AccessTokenScope
 | 
			
		||||
	if c.IsSet("access-token") {
 | 
			
		||||
		accessTokenName = strings.TrimSpace(c.String("access-token-name"))
 | 
			
		||||
		if accessTokenName == "" {
 | 
			
		||||
			return errors.New("access-token-name cannot be empty")
 | 
			
		||||
		}
 | 
			
		||||
		var err error
 | 
			
		||||
		accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("invalid access token scope provided: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		if !accessTokenScope.HasPermissionScope() {
 | 
			
		||||
			return errors.New("access token does not have any permission")
 | 
			
		||||
		}
 | 
			
		||||
	} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
 | 
			
		||||
		return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// arguments should be prepared before creating the user & access token, in case there is anything wrong
 | 
			
		||||
 | 
			
		||||
	// create the user
 | 
			
		||||
	if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
 | 
			
		||||
		return fmt.Errorf("CreateUser: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Bool("access-token") {
 | 
			
		||||
		t := &auth_model.AccessToken{
 | 
			
		||||
			Name: "gitea-admin",
 | 
			
		||||
			UID:  u.ID,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	// create the access token
 | 
			
		||||
	if accessTokenScope != "" {
 | 
			
		||||
		t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
 | 
			
		||||
		if err := auth_model.NewAccessToken(ctx, t); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Printf("Access token was successfully created... %s\n", t.Token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Printf("New user '%s' has been successfully created!\n", username)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,37 +8,97 @@ import (
 | 
			
		||||
	"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/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAdminUserCreate(t *testing.T) {
 | 
			
		||||
	app := NewMainApp(AppVersion{})
 | 
			
		||||
 | 
			
		||||
	reset := func() {
 | 
			
		||||
		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
 | 
			
		||||
		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
 | 
			
		||||
		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) {
 | 
			
		||||
		type check struct{ IsAdmin, MustChangePassword bool }
 | 
			
		||||
		createCheck := func(name, args string) check {
 | 
			
		||||
			assert.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{u.IsAdmin, u.MustChangePassword}
 | 
			
		||||
		}
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
 | 
			
		||||
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
 | 
			
		||||
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	createUser := func(name, args string) error {
 | 
			
		||||
		return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type createCheck struct{ IsAdmin, MustChangePassword bool }
 | 
			
		||||
	createUser := func(name, args string) createCheck {
 | 
			
		||||
		assert.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 createCheck{u.IsAdmin, u.MustChangePassword}
 | 
			
		||||
	}
 | 
			
		||||
	reset()
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
 | 
			
		||||
	t.Run("AccessToken", func(t *testing.T) {
 | 
			
		||||
		// no generated access token
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
 | 
			
		||||
	reset()
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
 | 
			
		||||
		// using "--access-token" only means "all" access
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password --access-token"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
 | 
			
		||||
		hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.True(t, hasScopes)
 | 
			
		||||
 | 
			
		||||
	reset()
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
 | 
			
		||||
		// using "--access-token" with name & scopes
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
 | 
			
		||||
		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.True(t, hasScopes)
 | 
			
		||||
		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.False(t, hasScopes)
 | 
			
		||||
 | 
			
		||||
		// using "--access-token-name" without "--access-token"
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password --access-token-name new-token-name")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
 | 
			
		||||
		// using "--access-token-scopes" without "--access-token"
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password --access-token-scopes read:issue")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
 | 
			
		||||
		// empty permission
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access token does not have any permission")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "scopes",
 | 
			
		||||
			Value: "",
 | 
			
		||||
			Usage: "Comma separated list of scopes to apply to access token",
 | 
			
		||||
			Value: "all",
 | 
			
		||||
			Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	Action: runGenerateAccessToken,
 | 
			
		||||
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
 | 
			
		||||
 | 
			
		||||
func runGenerateAccessToken(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("username") {
 | 
			
		||||
		return errors.New("You must provide a username to generate a token for")
 | 
			
		||||
		return errors.New("you must provide a username to generate a token for")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("invalid access token scope provided: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if !accessTokenScope.HasPermissionScope() {
 | 
			
		||||
		return errors.New("access token does not have any permission")
 | 
			
		||||
	}
 | 
			
		||||
	t.Scope = accessTokenScope
 | 
			
		||||
 | 
			
		||||
	// create the token
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-github/v61/github"
 | 
			
		||||
	"github.com/google/go-github/v71/github"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -774,6 +774,9 @@ LEVEL = Info
 | 
			
		||||
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
 | 
			
		||||
;;
 | 
			
		||||
;; User must sign in to view anything.
 | 
			
		||||
;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
 | 
			
		||||
;; for example: block anonymous AI crawlers from accessing repo code pages.
 | 
			
		||||
;; The "expensive" mode is experimental and subject to change.
 | 
			
		||||
;REQUIRE_SIGNIN_VIEW = false
 | 
			
		||||
;;
 | 
			
		||||
;; Mail notification
 | 
			
		||||
 
 | 
			
		||||
@@ -22,3 +22,8 @@ manifests:
 | 
			
		||||
      architecture: arm64
 | 
			
		||||
      os: linux
 | 
			
		||||
      variant: v8
 | 
			
		||||
  -
 | 
			
		||||
    image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64-rootless
 | 
			
		||||
    platform:
 | 
			
		||||
      architecture: riscv64
 | 
			
		||||
      os: linux
 | 
			
		||||
 
 | 
			
		||||
@@ -22,3 +22,8 @@ manifests:
 | 
			
		||||
      architecture: arm64
 | 
			
		||||
      os: linux
 | 
			
		||||
      variant: v8
 | 
			
		||||
  -
 | 
			
		||||
    image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64
 | 
			
		||||
    platform:
 | 
			
		||||
      architecture: riscv64
 | 
			
		||||
      os: linux
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,21 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
 | 
			
		||||
  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
 | 
			
		||||
# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
 | 
			
		||||
# the generated key file name is `{keyname}-cert.pub`
 | 
			
		||||
if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
 | 
			
		||||
  SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
 | 
			
		||||
  SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
 | 
			
		||||
  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -d /etc/ssh ]; then
 | 
			
		||||
    SSH_PORT=${SSH_PORT:-"22"} \
 | 
			
		||||
    SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
module code.gitea.io/gitea
 | 
			
		||||
 | 
			
		||||
go 1.23.6
 | 
			
		||||
go 1.23.8
 | 
			
		||||
 | 
			
		||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
 | 
			
		||||
// But some CAs use negative serial number, just relax the check. related:
 | 
			
		||||
@@ -65,7 +65,7 @@ require (
 | 
			
		||||
	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
 | 
			
		||||
	github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.2.2
 | 
			
		||||
	github.com/google/go-github/v61 v61.0.0
 | 
			
		||||
	github.com/google/go-github/v71 v71.0.0
 | 
			
		||||
	github.com/google/licenseclassifier/v2 v2.0.0
 | 
			
		||||
	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
 | 
			
		||||
	github.com/google/uuid v1.6.0
 | 
			
		||||
@@ -120,7 +120,7 @@ require (
 | 
			
		||||
	github.com/yuin/goldmark-meta v1.1.0
 | 
			
		||||
	golang.org/x/crypto v0.36.0
 | 
			
		||||
	golang.org/x/image v0.21.0
 | 
			
		||||
	golang.org/x/net v0.37.0
 | 
			
		||||
	golang.org/x/net v0.38.0
 | 
			
		||||
	golang.org/x/oauth2 v0.27.0
 | 
			
		||||
	golang.org/x/sync v0.12.0
 | 
			
		||||
	golang.org/x/sys v0.31.0
 | 
			
		||||
@@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra
 | 
			
		||||
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
 | 
			
		||||
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
 | 
			
		||||
 | 
			
		||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
 | 
			
		||||
 | 
			
		||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
 | 
			
		||||
 | 
			
		||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								go.sum
									
									
									
									
									
								
							@@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 | 
			
		||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 | 
			
		||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 | 
			
		||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 | 
			
		||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
 | 
			
		||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
 | 
			
		||||
gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4=
 | 
			
		||||
gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
 | 
			
		||||
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
 | 
			
		||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
 | 
			
		||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
 | 
			
		||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
 | 
			
		||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
 | 
			
		||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
 | 
			
		||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
 | 
			
		||||
@@ -410,10 +410,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 | 
			
		||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
 | 
			
		||||
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
 | 
			
		||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 | 
			
		||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 | 
			
		||||
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
 | 
			
		||||
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
 | 
			
		||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 | 
			
		||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 | 
			
		||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
 | 
			
		||||
@@ -870,8 +871,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 | 
			
		||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 | 
			
		||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 | 
			
		||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 | 
			
		||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
 | 
			
		||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 | 
			
		||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
 | 
			
		||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 | 
			
		||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
 | 
			
		||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 | 
			
		||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
 
 | 
			
		||||
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
 | 
			
		||||
	return bitmap.toScope(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AccessTokenScope) HasPermissionScope() bool {
 | 
			
		||||
	return s != "" && s != AccessTokenScopePublicOnly
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PublicOnly checks if this token scope is limited to public resources
 | 
			
		||||
func (s AccessTokenScope) PublicOnly() (bool, error) {
 | 
			
		||||
	bitmap, err := s.parse()
 | 
			
		||||
 
 | 
			
		||||
@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 | 
			
		||||
	return &branch, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsBranchExist returns true if the branch exists in the repository.
 | 
			
		||||
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
 | 
			
		||||
	var branch Branch
 | 
			
		||||
	has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	} else if !has {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
	return !branch.IsDeleted, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
 | 
			
		||||
	branches := make([]*Branch, 0, len(branchNames))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -663,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if review != nil {
 | 
			
		||||
		// skip it when reviewer hase been request to review
 | 
			
		||||
		// skip it when reviewer has been request to review
 | 
			
		||||
		if review.Type == ReviewTypeRequest {
 | 
			
		||||
			return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
 | 
			
		||||
		ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.Sync(new(Comment), new(Issue))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(Comment), new(Issue))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error {
 | 
			
		||||
		ForcePushAllowlistTeamIDs    []int64 `xorm:"JSON TEXT"`
 | 
			
		||||
		ForcePushAllowlistDeployKeys bool    `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	}
 | 
			
		||||
	return x.Sync(new(ProtectedBranch))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(ProtectedBranch))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
 | 
			
		||||
	type oauth2Application struct {
 | 
			
		||||
		SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
 | 
			
		||||
	}
 | 
			
		||||
	return x.Sync(new(oauth2Application))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(oauth2Application))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error {
 | 
			
		||||
		CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.Sync(new(Comment))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(Comment))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error {
 | 
			
		||||
	type ProtectedBranch struct {
 | 
			
		||||
		BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	}
 | 
			
		||||
	return x.Sync(new(ProtectedBranch))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(ProtectedBranch))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error {
 | 
			
		||||
		Priority int64 `xorm:"NOT NULL DEFAULT 0"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.Sync(new(ProtectedBranch))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(ProtectedBranch))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
 | 
			
		||||
	type Issue struct {
 | 
			
		||||
		TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.Sync(new(Issue))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreConstrains: true,
 | 
			
		||||
		IgnoreIndices:    true,
 | 
			
		||||
	}, new(Issue))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
 | 
			
		||||
	default:
 | 
			
		||||
		e.Desc("package_version.created_unix")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sort by id for stable order with duplicates in the other field
 | 
			
		||||
	e.Asc("package_version.id")
 | 
			
		||||
	e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchVersions gets all versions of packages matching the search options
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
 | 
			
		||||
 | 
			
		||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
 | 
			
		||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
 | 
			
		||||
	if u.IsGhost() || u.IsGiteaActions() {
 | 
			
		||||
	// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
 | 
			
		||||
	// which comes from git configure information
 | 
			
		||||
	if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
 | 
			
		||||
		return avatars.DefaultAvatarLink()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 | 
			
		||||
				matchTimes++
 | 
			
		||||
			}
 | 
			
		||||
		case "paths":
 | 
			
		||||
			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
 | 
			
		||||
			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		case "paths-ignore":
 | 
			
		||||
			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
 | 
			
		||||
			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
 | 
			
		||||
			} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -350,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		||||
	// We need to check if the context is canceled by the program on Windows.
 | 
			
		||||
	// This is because Windows does not have signal checking when terminating the process.
 | 
			
		||||
	// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
 | 
			
		||||
	// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
 | 
			
		||||
	if runtime.GOOS == "windows" &&
 | 
			
		||||
		err != nil &&
 | 
			
		||||
		err.Error() == "" &&
 | 
			
		||||
		(err.Error() == "" || err.Error() == "exit status 1") &&
 | 
			
		||||
		cmd.ProcessState.ExitCode() == 1 &&
 | 
			
		||||
		ctx.Err() == context.Canceled {
 | 
			
		||||
		return ctx.Err()
 | 
			
		||||
 
 | 
			
		||||
@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	wr.tmp = append(wr.tmp, p...)
 | 
			
		||||
	return len(p), nil
 | 
			
		||||
	return l, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
package markdown
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/yuin/goldmark/ast"
 | 
			
		||||
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
 | 
			
		||||
 | 
			
		||||
// NewDetails returns a new Paragraph node.
 | 
			
		||||
func NewDetails() *Details {
 | 
			
		||||
	return &Details{
 | 
			
		||||
		BaseBlock: ast.BaseBlock{},
 | 
			
		||||
	}
 | 
			
		||||
	return &Details{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Summary is a block that contains the summary of details block
 | 
			
		||||
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
 | 
			
		||||
 | 
			
		||||
// NewSummary returns a new Summary node.
 | 
			
		||||
func NewSummary() *Summary {
 | 
			
		||||
	return &Summary{
 | 
			
		||||
		BaseBlock: ast.BaseBlock{},
 | 
			
		||||
	}
 | 
			
		||||
	return &Summary{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
 | 
			
		||||
@@ -95,29 +92,6 @@ type Icon struct {
 | 
			
		||||
	Name []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dump implements Node.Dump .
 | 
			
		||||
func (n *Icon) Dump(source []byte, level int) {
 | 
			
		||||
	m := map[string]string{}
 | 
			
		||||
	m["Name"] = string(n.Name)
 | 
			
		||||
	ast.DumpHelper(n, source, level, m, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// KindIcon is the NodeKind for Icon
 | 
			
		||||
var KindIcon = ast.NewNodeKind("Icon")
 | 
			
		||||
 | 
			
		||||
// Kind implements Node.Kind.
 | 
			
		||||
func (n *Icon) Kind() ast.NodeKind {
 | 
			
		||||
	return KindIcon
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewIcon returns a new Paragraph node.
 | 
			
		||||
func NewIcon(name string) *Icon {
 | 
			
		||||
	return &Icon{
 | 
			
		||||
		BaseInline: ast.BaseInline{},
 | 
			
		||||
		Name:       []byte(name),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ColorPreview is an inline for a color preview
 | 
			
		||||
type ColorPreview struct {
 | 
			
		||||
	ast.BaseInline
 | 
			
		||||
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
 | 
			
		||||
		AttentionType: attentionType,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var KindRawHTML = ast.NewNodeKind("RawHTML")
 | 
			
		||||
 | 
			
		||||
type RawHTML struct {
 | 
			
		||||
	ast.BaseBlock
 | 
			
		||||
	rawHTML template.HTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *RawHTML) Dump(source []byte, level int) {
 | 
			
		||||
	m := map[string]string{}
 | 
			
		||||
	m["RawHTML"] = string(n.rawHTML)
 | 
			
		||||
	ast.DumpHelper(n, source, level, m, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *RawHTML) Kind() ast.NodeKind {
 | 
			
		||||
	return KindRawHTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRawHTML(rawHTML template.HTML) *RawHTML {
 | 
			
		||||
	return &RawHTML{rawHTML: rawHTML}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,23 +4,22 @@
 | 
			
		||||
package markdown
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/htmlutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
 | 
			
		||||
	"github.com/yuin/goldmark/ast"
 | 
			
		||||
	east "github.com/yuin/goldmark/extension/ast"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func nodeToTable(meta *yaml.Node) ast.Node {
 | 
			
		||||
	for {
 | 
			
		||||
		if meta == nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		switch meta.Kind {
 | 
			
		||||
		case yaml.DocumentNode:
 | 
			
		||||
			meta = meta.Content[0]
 | 
			
		||||
			continue
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	for meta != nil && meta.Kind == yaml.DocumentNode {
 | 
			
		||||
		meta = meta.Content[0]
 | 
			
		||||
	}
 | 
			
		||||
	if meta == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	switch meta.Kind {
 | 
			
		||||
	case yaml.MappingNode:
 | 
			
		||||
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
 | 
			
		||||
	return table
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
 | 
			
		||||
func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
 | 
			
		||||
	for meta != nil && meta.Kind == yaml.DocumentNode {
 | 
			
		||||
		meta = meta.Content[0]
 | 
			
		||||
	}
 | 
			
		||||
	if meta == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if meta.Kind != yaml.MappingNode {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	var keys []string
 | 
			
		||||
	for i := 0; i < len(meta.Content); i += 2 {
 | 
			
		||||
		if meta.Content[i].Kind == yaml.ScalarNode {
 | 
			
		||||
			keys = append(keys, meta.Content[i].Value)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	details := NewDetails()
 | 
			
		||||
	details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
 | 
			
		||||
	summary := NewSummary()
 | 
			
		||||
	summary.AppendChild(summary, NewIcon(icon))
 | 
			
		||||
	summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
 | 
			
		||||
	summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
 | 
			
		||||
	details.AppendChild(details, summary)
 | 
			
		||||
	details.AppendChild(details, nodeToTable(meta))
 | 
			
		||||
 | 
			
		||||
	return details
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,6 @@ package markdown
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
			
		||||
 | 
			
		||||
	tocList := make([]Header, 0, 20)
 | 
			
		||||
	if rc.yamlNode != nil {
 | 
			
		||||
		metaNode := rc.toMetaNode()
 | 
			
		||||
		metaNode := rc.toMetaNode(g)
 | 
			
		||||
		if metaNode != nil {
 | 
			
		||||
			node.InsertBefore(node, firstChild, metaNode)
 | 
			
		||||
		}
 | 
			
		||||
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// it is copied from old code, which is quite doubtful whether it is correct
 | 
			
		||||
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
 | 
			
		||||
	return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
 | 
			
		||||
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
 | 
			
		||||
	r := &HTMLRenderer{
 | 
			
		||||
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 | 
			
		||||
	reg.Register(ast.KindDocument, r.renderDocument)
 | 
			
		||||
	reg.Register(KindDetails, r.renderDetails)
 | 
			
		||||
	reg.Register(KindSummary, r.renderSummary)
 | 
			
		||||
	reg.Register(KindIcon, r.renderIcon)
 | 
			
		||||
	reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
 | 
			
		||||
	reg.Register(KindAttention, r.renderAttention)
 | 
			
		||||
	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
 | 
			
		||||
	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 | 
			
		||||
	reg.Register(KindRawHTML, r.renderRawHTML)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
			
		||||
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
 | 
			
		||||
	return ast.WalkContinue, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
			
		||||
func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
			
		||||
	if !entering {
 | 
			
		||||
		return ast.WalkContinue, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := node.(*Icon)
 | 
			
		||||
 | 
			
		||||
	name := strings.TrimSpace(strings.ToLower(string(n.Name)))
 | 
			
		||||
 | 
			
		||||
	if len(name) == 0 {
 | 
			
		||||
		// skip this
 | 
			
		||||
		return ast.WalkContinue, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reValidIconName().MatchString(name) {
 | 
			
		||||
		// skip this
 | 
			
		||||
		return ast.WalkContinue, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
 | 
			
		||||
	err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
 | 
			
		||||
	n := node.(*RawHTML)
 | 
			
		||||
	_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ast.WalkStop, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ast.WalkContinue, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 | 
			
		||||
	// Preserve original length.
 | 
			
		||||
	bufWithMetadataLength := len(buf)
 | 
			
		||||
 | 
			
		||||
	rc := &RenderConfig{
 | 
			
		||||
		Meta: markup.RenderMetaAsDetails,
 | 
			
		||||
		Icon: "table",
 | 
			
		||||
		Lang: "",
 | 
			
		||||
	}
 | 
			
		||||
	rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
 | 
			
		||||
	buf, _ = ExtractMetadataBytes(buf, rc)
 | 
			
		||||
 | 
			
		||||
	metaLength := bufWithMetadataLength - len(buf)
 | 
			
		||||
 
 | 
			
		||||
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTaskList(t *testing.T) {
 | 
			
		||||
func TestMarkdownFrontmatter(t *testing.T) {
 | 
			
		||||
	testcases := []struct {
 | 
			
		||||
		testcase string
 | 
			
		||||
		name     string
 | 
			
		||||
		input    string
 | 
			
		||||
		expected string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"MapInFrontmatter",
 | 
			
		||||
			`---
 | 
			
		||||
key1: val1
 | 
			
		||||
key2: val2
 | 
			
		||||
---
 | 
			
		||||
test
 | 
			
		||||
`,
 | 
			
		||||
			`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
 | 
			
		||||
<thead>
 | 
			
		||||
<tr>
 | 
			
		||||
<th>key1</th>
 | 
			
		||||
<th>key2</th>
 | 
			
		||||
</tr>
 | 
			
		||||
</thead>
 | 
			
		||||
<tbody>
 | 
			
		||||
<tr>
 | 
			
		||||
<td>val1</td>
 | 
			
		||||
<td>val2</td>
 | 
			
		||||
</tr>
 | 
			
		||||
</tbody>
 | 
			
		||||
</table>
 | 
			
		||||
</details><p>test</p>
 | 
			
		||||
`,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			"ListInFrontmatter",
 | 
			
		||||
			`---
 | 
			
		||||
- item1
 | 
			
		||||
- item2
 | 
			
		||||
---
 | 
			
		||||
test
 | 
			
		||||
`,
 | 
			
		||||
			`- item1
 | 
			
		||||
- item2
 | 
			
		||||
 | 
			
		||||
<p>test</p>
 | 
			
		||||
`,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			"StringInFrontmatter",
 | 
			
		||||
			`---
 | 
			
		||||
anything
 | 
			
		||||
---
 | 
			
		||||
test
 | 
			
		||||
`,
 | 
			
		||||
			`anything
 | 
			
		||||
 | 
			
		||||
<p>test</p>
 | 
			
		||||
`,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			// data-source-position should take into account YAML frontmatter.
 | 
			
		||||
			"ListAfterFrontmatter",
 | 
			
		||||
			`---
 | 
			
		||||
foo: bar
 | 
			
		||||
---
 | 
			
		||||
- [ ] task 1`,
 | 
			
		||||
			`<details><summary><i class="icon table"></i></summary><table>
 | 
			
		||||
			`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
 | 
			
		||||
<thead>
 | 
			
		||||
<tr>
 | 
			
		||||
<th>foo</th>
 | 
			
		||||
@@ -414,9 +470,9 @@ foo: bar
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range testcases {
 | 
			
		||||
		res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
 | 
			
		||||
		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 | 
			
		||||
		res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
 | 
			
		||||
		assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
 | 
			
		||||
		assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ import (
 | 
			
		||||
// RenderConfig represents rendering configuration for this file
 | 
			
		||||
type RenderConfig struct {
 | 
			
		||||
	Meta     markup.RenderMetaMode
 | 
			
		||||
	Icon     string
 | 
			
		||||
	TOC      string // "false": hide,  "side"/empty: in sidebar,  "main"/"true": in main view
 | 
			
		||||
	Lang     string
 | 
			
		||||
	yamlNode *yaml.Node
 | 
			
		||||
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 | 
			
		||||
 | 
			
		||||
	type yamlRenderConfig struct {
 | 
			
		||||
		Meta *string `yaml:"meta"`
 | 
			
		||||
		Icon *string `yaml:"details_icon"`
 | 
			
		||||
		Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
 | 
			
		||||
		TOC  *string `yaml:"include_toc"`
 | 
			
		||||
		Lang *string `yaml:"lang"`
 | 
			
		||||
	}
 | 
			
		||||
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 | 
			
		||||
		rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cfg.Gitea.Icon != nil {
 | 
			
		||||
		rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
 | 
			
		||||
		rc.Lang = *cfg.Gitea.Lang
 | 
			
		||||
	}
 | 
			
		||||
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rc *RenderConfig) toMetaNode() ast.Node {
 | 
			
		||||
func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
 | 
			
		||||
	if rc.yamlNode == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
 | 
			
		||||
	case markup.RenderMetaAsTable:
 | 
			
		||||
		return nodeToTable(rc.yamlNode)
 | 
			
		||||
	case markup.RenderMetaAsDetails:
 | 
			
		||||
		return nodeToDetails(rc.yamlNode, rc.Icon)
 | 
			
		||||
		return nodeToDetails(g, rc.yamlNode)
 | 
			
		||||
	default:
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
		{
 | 
			
		||||
			"empty", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"lang", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "test",
 | 
			
		||||
			}, "lang: test",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"metatable", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "gitea: table",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"metanone", &RenderConfig{
 | 
			
		||||
				Meta: "none",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "gitea: none",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"metadetails", &RenderConfig{
 | 
			
		||||
				Meta: "details",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "gitea: details",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"metawrong", &RenderConfig{
 | 
			
		||||
				Meta: "details",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "gitea: wrong",
 | 
			
		||||
		},
 | 
			
		||||
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
			"toc", &RenderConfig{
 | 
			
		||||
				TOC:  "true",
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "include_toc: true",
 | 
			
		||||
		},
 | 
			
		||||
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
			"tocfalse", &RenderConfig{
 | 
			
		||||
				TOC:  "false",
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}, "include_toc: false",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"toclang", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				TOC:  "true",
 | 
			
		||||
				Lang: "testlang",
 | 
			
		||||
			}, `
 | 
			
		||||
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
		{
 | 
			
		||||
			"complexlang", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "testlang",
 | 
			
		||||
			}, `
 | 
			
		||||
				gitea:
 | 
			
		||||
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
		{
 | 
			
		||||
			"complexlang2", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "testlang",
 | 
			
		||||
			}, `
 | 
			
		||||
	lang: notright
 | 
			
		||||
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
		{
 | 
			
		||||
			"complexlang", &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "testlang",
 | 
			
		||||
			}, `
 | 
			
		||||
	gitea:
 | 
			
		||||
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
				Lang: "two",
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				TOC:  "true",
 | 
			
		||||
				Icon: "smiley",
 | 
			
		||||
			}, `
 | 
			
		||||
	lang: one
 | 
			
		||||
	include_toc: true
 | 
			
		||||
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got := &RenderConfig{
 | 
			
		||||
				Meta: "table",
 | 
			
		||||
				Icon: "table",
 | 
			
		||||
				Lang: "",
 | 
			
		||||
			}
 | 
			
		||||
			if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", "    ")), got); err != nil {
 | 
			
		||||
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if got.Meta != tt.expected.Meta {
 | 
			
		||||
				t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta)
 | 
			
		||||
			}
 | 
			
		||||
			if got.Icon != tt.expected.Icon {
 | 
			
		||||
				t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
 | 
			
		||||
			}
 | 
			
		||||
			if got.Lang != tt.expected.Lang {
 | 
			
		||||
				t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
 | 
			
		||||
			}
 | 
			
		||||
			if got.TOC != tt.expected.TOC {
 | 
			
		||||
				t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
 | 
			
		||||
			}
 | 
			
		||||
			assert.Equal(t, tt.expected.Meta, got.Meta)
 | 
			
		||||
			assert.Equal(t, tt.expected.Lang, got.Lang)
 | 
			
		||||
			assert.Equal(t, tt.expected.TOC, got.TOC)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ type ConfigKey interface {
 | 
			
		||||
	In(defaultVal string, candidates []string) string
 | 
			
		||||
	String() string
 | 
			
		||||
	Strings(delim string) []string
 | 
			
		||||
	Bool() (bool, error)
 | 
			
		||||
 | 
			
		||||
	MustString(defaultVal string) string
 | 
			
		||||
	MustBool(defaultVal ...bool) bool
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,8 @@ var Service = struct {
 | 
			
		||||
	ShowRegistrationButton                  bool
 | 
			
		||||
	EnablePasswordSignInForm                bool
 | 
			
		||||
	ShowMilestonesDashboardPage             bool
 | 
			
		||||
	RequireSignInView                       bool
 | 
			
		||||
	RequireSignInViewStrict                 bool
 | 
			
		||||
	BlockAnonymousAccessExpensive           bool
 | 
			
		||||
	EnableNotifyMail                        bool
 | 
			
		||||
	EnableBasicAuth                         bool
 | 
			
		||||
	EnablePasskeyAuth                       bool
 | 
			
		||||
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
 | 
			
		||||
	Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
 | 
			
		||||
	Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
 | 
			
		||||
	Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
 | 
			
		||||
	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
 | 
			
		||||
 | 
			
		||||
	// boolean values are considered as "strict"
 | 
			
		||||
	var err error
 | 
			
		||||
	Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
 | 
			
		||||
	if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
 | 
			
		||||
		// non-boolean value only supports "expensive" at the moment
 | 
			
		||||
		Service.BlockAnonymousAccessExpensive = s == "expensive"
 | 
			
		||||
		if !Service.BlockAnonymousAccessExpensive {
 | 
			
		||||
			log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
 | 
			
		||||
	Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
 | 
			
		||||
	Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,14 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
 | 
			
		||||
	"github.com/gobwas/glob"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestLoadServices(t *testing.T) {
 | 
			
		||||
	oldService := Service
 | 
			
		||||
	defer func() {
 | 
			
		||||
		Service = oldService
 | 
			
		||||
	}()
 | 
			
		||||
	defer test.MockVariableValue(&Service)()
 | 
			
		||||
 | 
			
		||||
	cfg, err := NewConfigProviderFromData(`
 | 
			
		||||
[service]
 | 
			
		||||
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestLoadServiceVisibilityModes(t *testing.T) {
 | 
			
		||||
	oldService := Service
 | 
			
		||||
	defer func() {
 | 
			
		||||
		Service = oldService
 | 
			
		||||
	}()
 | 
			
		||||
	defer test.MockVariableValue(&Service)()
 | 
			
		||||
 | 
			
		||||
	kases := map[string]func(){
 | 
			
		||||
		`
 | 
			
		||||
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestLoadServiceRequireSignInView(t *testing.T) {
 | 
			
		||||
	defer test.MockVariableValue(&Service)()
 | 
			
		||||
 | 
			
		||||
	cfg, err := NewConfigProviderFromData(`
 | 
			
		||||
[service]
 | 
			
		||||
`)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	loadServiceFrom(cfg)
 | 
			
		||||
	assert.False(t, Service.RequireSignInViewStrict)
 | 
			
		||||
	assert.False(t, Service.BlockAnonymousAccessExpensive)
 | 
			
		||||
 | 
			
		||||
	cfg, err = NewConfigProviderFromData(`
 | 
			
		||||
[service]
 | 
			
		||||
REQUIRE_SIGNIN_VIEW = true
 | 
			
		||||
`)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	loadServiceFrom(cfg)
 | 
			
		||||
	assert.True(t, Service.RequireSignInViewStrict)
 | 
			
		||||
	assert.False(t, Service.BlockAnonymousAccessExpensive)
 | 
			
		||||
 | 
			
		||||
	cfg, err = NewConfigProviderFromData(`
 | 
			
		||||
[service]
 | 
			
		||||
REQUIRE_SIGNIN_VIEW = expensive
 | 
			
		||||
`)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	loadServiceFrom(cfg)
 | 
			
		||||
	assert.False(t, Service.RequireSignInViewStrict)
 | 
			
		||||
	assert.True(t, Service.BlockAnonymousAccessExpensive)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,11 @@ type AccessToken struct {
 | 
			
		||||
type AccessTokenList []*AccessToken
 | 
			
		||||
 | 
			
		||||
// CreateAccessTokenOption options when create access token
 | 
			
		||||
// swagger:model CreateAccessTokenOption
 | 
			
		||||
type CreateAccessTokenOption struct {
 | 
			
		||||
	// required: true
 | 
			
		||||
	Name   string   `json:"name" binding:"Required"`
 | 
			
		||||
	Name string `json:"name" binding:"Required"`
 | 
			
		||||
	// example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"]
 | 
			
		||||
	Scopes []string `json:"scopes"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
applet
 | 
			
		||||
application.linux-arm64
 | 
			
		||||
application.linux-armv6hf
 | 
			
		||||
application.linux-riscv64
 | 
			
		||||
application.linux32
 | 
			
		||||
application.linux64
 | 
			
		||||
application.windows32
 | 
			
		||||
 
 | 
			
		||||
@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
 | 
			
		||||
branch.restore_failed = Failed to restore branch "%s".
 | 
			
		||||
branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted.
 | 
			
		||||
branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted.
 | 
			
		||||
branch.default_branch_not_exist = Default branch "%s" does not exist.
 | 
			
		||||
branch.restore = Restore Branch "%s"
 | 
			
		||||
branch.download = Download Branch "%s"
 | 
			
		||||
branch.rename = Rename Branch "%s"
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
 | 
			
		||||
 | 
			
		||||
// https://rust-lang.github.io/rfcs/2789-sparse-index.html
 | 
			
		||||
func RepositoryConfig(ctx *context.Context) {
 | 
			
		||||
	ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
 | 
			
		||||
	ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EnumeratePackageVersions(ctx *context.Context) {
 | 
			
		||||
 
 | 
			
		||||
@@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
 | 
			
		||||
func ReqContainerAccess(ctx *context.Context) {
 | 
			
		||||
	if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
 | 
			
		||||
	if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
 | 
			
		||||
		apiUnauthorizedError(ctx)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
 | 
			
		||||
	u := ctx.Doer
 | 
			
		||||
	packageScope := auth_service.GetAccessScope(ctx.Data)
 | 
			
		||||
	if u == nil {
 | 
			
		||||
		if setting.Service.RequireSignInView {
 | 
			
		||||
		if setting.Service.RequireSignInViewStrict {
 | 
			
		||||
			apiUnauthorizedError(ctx)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
 | 
			
		||||
// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
 | 
			
		||||
func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
 | 
			
		||||
	multipartFile, _, err := ctx.Req.FormFile(formKey)
 | 
			
		||||
	if err != nil && !errors.Is(err, http.ErrMissingFile) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if multipartFile != nil {
 | 
			
		||||
		return multipartFile, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := ctx.Req.FormValue(formKey)
 | 
			
		||||
	if content == "" {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	return io.NopCloser(strings.NewReader(ctx.Req.FormValue(formKey))), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
 | 
			
		||||
func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
	packageScope := ctx.PathParam("scope")
 | 
			
		||||
	packageName := ctx.PathParam("name")
 | 
			
		||||
@@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	packageVersion := v.Core().String()
 | 
			
		||||
 | 
			
		||||
	file, _, err := ctx.Req.FormFile("source-archive")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, err)
 | 
			
		||||
	file, err := formFileOptionalReadCloser(ctx, "source-archive")
 | 
			
		||||
	if file == nil || err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
@@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer buf.Close()
 | 
			
		||||
 | 
			
		||||
	var mr io.Reader
 | 
			
		||||
	metadata := ctx.Req.FormValue("metadata")
 | 
			
		||||
	if metadata != "" {
 | 
			
		||||
		mr = strings.NewReader(metadata)
 | 
			
		||||
	mr, err := formFileOptionalReadCloser(ctx, "metadata")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if mr != nil {
 | 
			
		||||
		defer mr.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
 | 
			
		||||
 
 | 
			
		||||
@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
 | 
			
		||||
 | 
			
		||||
func reqExploreSignIn() func(ctx *context.APIContext) {
 | 
			
		||||
	return func(ctx *context.APIContext) {
 | 
			
		||||
		if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
 | 
			
		||||
		if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -874,7 +874,7 @@ func Routes() *web.Router {
 | 
			
		||||
	m.Use(apiAuth(buildAuthGroup()))
 | 
			
		||||
 | 
			
		||||
	m.Use(verifyAuthWithOptions(&common.VerifyOptions{
 | 
			
		||||
		SignInRequired: setting.Service.RequireSignInView,
 | 
			
		||||
		SignInRequired: setting.Service.RequireSignInViewStrict,
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	addActionsRoutes := func(
 | 
			
		||||
 
 | 
			
		||||
@@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
 | 
			
		||||
		issue.MilestoneID != *form.Milestone {
 | 
			
		||||
		oldMilestoneID := issue.MilestoneID
 | 
			
		||||
		issue.MilestoneID = *form.Milestone
 | 
			
		||||
		if issue.MilestoneID > 0 {
 | 
			
		||||
			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			issue.Milestone = nil
 | 
			
		||||
		}
 | 
			
		||||
		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
 | 
			
		||||
			return
 | 
			
		||||
 
 | 
			
		||||
@@ -694,6 +694,11 @@ func EditPullRequest(ctx *context.APIContext) {
 | 
			
		||||
		issue.MilestoneID != form.Milestone {
 | 
			
		||||
		oldMilestoneID := issue.MilestoneID
 | 
			
		||||
		issue.MilestoneID = form.Milestone
 | 
			
		||||
		issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
 | 
			
		||||
			return
 | 
			
		||||
@@ -1638,7 +1643,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 | 
			
		||||
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, limit)
 | 
			
		||||
	for i := start; i < start+limit; i++ {
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
 | 
			
		||||
		// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
 | 
			
		||||
		// The head repository might have been deleted, so we should not rely on it here.
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
 | 
			
		||||
 
 | 
			
		||||
@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get commit count - wiki revisions
 | 
			
		||||
	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
 | 
			
		||||
	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 | 
			
		||||
 | 
			
		||||
	// Get last change information.
 | 
			
		||||
	lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
 | 
			
		||||
@@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get commit count - wiki revisions
 | 
			
		||||
	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
 | 
			
		||||
	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 | 
			
		||||
 | 
			
		||||
	page := ctx.FormInt("page")
 | 
			
		||||
	if page <= 1 {
 | 
			
		||||
@@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
 | 
			
		||||
	// get Commit Count
 | 
			
		||||
	commitsHistory, err := wikiRepo.CommitsByFileAndRange(
 | 
			
		||||
		git.CommitsByFileAndRangeOptions{
 | 
			
		||||
			Revision: "master",
 | 
			
		||||
			Revision: ctx.Repo.Repository.DefaultWikiBranch,
 | 
			
		||||
			File:     pageFilename,
 | 
			
		||||
			Page:     page,
 | 
			
		||||
		})
 | 
			
		||||
@@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := wikiRepo.GetBranchCommit("master")
 | 
			
		||||
	commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if git.IsErrNotExist(err) {
 | 
			
		||||
			ctx.NotFound(err)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								routers/common/blockexpensive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								routers/common/blockexpensive.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi/v5"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func BlockExpensive() func(next http.Handler) http.Handler {
 | 
			
		||||
	if !setting.Service.BlockAnonymousAccessExpensive {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
			ret := determineRequestPriority(req.Context())
 | 
			
		||||
			if !ret.SignedIn {
 | 
			
		||||
				if ret.Expensive || ret.LongPolling {
 | 
			
		||||
					http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			next.ServeHTTP(w, req)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isRoutePathExpensive(routePattern string) bool {
 | 
			
		||||
	if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expensivePaths := []string{
 | 
			
		||||
		// code related
 | 
			
		||||
		"/{username}/{reponame}/archive/",
 | 
			
		||||
		"/{username}/{reponame}/blame/",
 | 
			
		||||
		"/{username}/{reponame}/commit/",
 | 
			
		||||
		"/{username}/{reponame}/commits/",
 | 
			
		||||
		"/{username}/{reponame}/graph",
 | 
			
		||||
		"/{username}/{reponame}/media/",
 | 
			
		||||
		"/{username}/{reponame}/raw/",
 | 
			
		||||
		"/{username}/{reponame}/src/",
 | 
			
		||||
 | 
			
		||||
		// issue & PR related (no trailing slash)
 | 
			
		||||
		"/{username}/{reponame}/issues",
 | 
			
		||||
		"/{username}/{reponame}/{type:issues}",
 | 
			
		||||
		"/{username}/{reponame}/pulls",
 | 
			
		||||
		"/{username}/{reponame}/{type:pulls}",
 | 
			
		||||
		"/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only
 | 
			
		||||
 | 
			
		||||
		// wiki
 | 
			
		||||
		"/{username}/{reponame}/wiki/",
 | 
			
		||||
 | 
			
		||||
		// activity
 | 
			
		||||
		"/{username}/{reponame}/activity/",
 | 
			
		||||
	}
 | 
			
		||||
	for _, path := range expensivePaths {
 | 
			
		||||
		if strings.HasPrefix(routePattern, path) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isRoutePathForLongPolling(routePattern string) bool {
 | 
			
		||||
	return routePattern == "/user/events"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func determineRequestPriority(ctx context.Context) (ret struct {
 | 
			
		||||
	SignedIn    bool
 | 
			
		||||
	Expensive   bool
 | 
			
		||||
	LongPolling bool
 | 
			
		||||
},
 | 
			
		||||
) {
 | 
			
		||||
	dataStore := middleware.GetContextData(ctx)
 | 
			
		||||
	chiRoutePath := chi.RouteContext(ctx).RoutePattern()
 | 
			
		||||
	if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
 | 
			
		||||
		ret.SignedIn = true
 | 
			
		||||
	} else {
 | 
			
		||||
		ret.Expensive = isRoutePathExpensive(chiRoutePath)
 | 
			
		||||
		ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
 | 
			
		||||
	}
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								routers/common/blockexpensive_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								routers/common/blockexpensive_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestBlockExpensive(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		expensive bool
 | 
			
		||||
		routePath string
 | 
			
		||||
	}{
 | 
			
		||||
		{false, "/user/xxx"},
 | 
			
		||||
		{false, "/login/xxx"},
 | 
			
		||||
		{true, "/{username}/{reponame}/archive/xxx"},
 | 
			
		||||
		{true, "/{username}/{reponame}/graph"},
 | 
			
		||||
		{true, "/{username}/{reponame}/src/xxx"},
 | 
			
		||||
		{true, "/{username}/{reponame}/wiki/xxx"},
 | 
			
		||||
		{true, "/{username}/{reponame}/activity/xxx"},
 | 
			
		||||
	}
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	assert.True(t, isRoutePathForLongPolling("/user/events"))
 | 
			
		||||
}
 | 
			
		||||
@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
 | 
			
		||||
	form.DisableRegistration = setting.Service.DisableRegistration
 | 
			
		||||
	form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
 | 
			
		||||
	form.EnableCaptcha = setting.Service.EnableCaptcha
 | 
			
		||||
	form.RequireSignInView = setting.Service.RequireSignInView
 | 
			
		||||
	form.RequireSignInView = setting.Service.RequireSignInViewStrict
 | 
			
		||||
	form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
 | 
			
		||||
	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
 | 
			
		||||
	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) {
 | 
			
		||||
	ownerName := ctx.PathParam(":owner")
 | 
			
		||||
	repoName := ctx.PathParam(":repo")
 | 
			
		||||
	mode := perm.AccessMode(ctx.FormInt("mode"))
 | 
			
		||||
	verb := ctx.FormString("verb")
 | 
			
		||||
 | 
			
		||||
	// Set the basic parts of the results to return
 | 
			
		||||
	results := private.ServCommandResults{
 | 
			
		||||
@@ -286,7 +287,7 @@ func ServCommand(ctx *context.PrivateContext) {
 | 
			
		||||
			repo.IsPrivate ||
 | 
			
		||||
			owner.Visibility.IsPrivate() ||
 | 
			
		||||
			(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
 | 
			
		||||
			setting.Service.RequireSignInView) {
 | 
			
		||||
			setting.Service.RequireSignInViewStrict) {
 | 
			
		||||
		if key.Type == asymkey_model.KeyTypeDeploy {
 | 
			
		||||
			if deployKey.Mode < mode {
 | 
			
		||||
				ctx.JSON(http.StatusUnauthorized, private.Response{
 | 
			
		||||
@@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// Because of the special ref "refs/for" we will need to delay write permission check
 | 
			
		||||
			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode {
 | 
			
		||||
			// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
 | 
			
		||||
			// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
 | 
			
		||||
			// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
 | 
			
		||||
			// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
 | 
			
		||||
			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == "git-receive-pack" {
 | 
			
		||||
				mode = perm.AccessModeRead
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,26 +4,12 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/web/repo"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func addOwnerRepoGitHTTPRouters(m *web.Router) {
 | 
			
		||||
	reqGitSignIn := func(ctx *context.Context) {
 | 
			
		||||
		if !setting.Service.RequireSignInView {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// rely on the results of Contexter
 | 
			
		||||
		if !ctx.IsSigned {
 | 
			
		||||
			// TODO: support digit auth - which would be Authorization header with digit
 | 
			
		||||
			ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	m.Group("/{username}/{reponame}", func() {
 | 
			
		||||
		m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
 | 
			
		||||
		m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
 | 
			
		||||
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
 | 
			
		||||
		m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
 | 
			
		||||
		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
 | 
			
		||||
		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
 | 
			
		||||
	}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
 | 
			
		||||
	}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package actions
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	stdCtx "context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"slices"
 | 
			
		||||
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	} else if !empty {
 | 
			
		||||
		commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
			ctx.NotFound("GetBranchCommit", err)
 | 
			
		||||
			return
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			ctx.ServerError("GetBranchCommit", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	activities_model "code.gitea.io/gitea/models/activities"
 | 
			
		||||
	"code.gitea.io/gitea/models/git"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["DateUntil"] = timeUntil
 | 
			
		||||
	ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
 | 
			
		||||
 | 
			
		||||
	canReadCode := ctx.Repo.CanRead(unit.TypeCode)
 | 
			
		||||
	if canReadCode {
 | 
			
		||||
		// GetActivityStats needs to read the default branch to get some information
 | 
			
		||||
		branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		if !branchExist {
 | 
			
		||||
			ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
			ctx.NotFound("", nil)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
 | 
			
		||||
	// TODO: refactor these arguments to a struct
 | 
			
		||||
	ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
 | 
			
		||||
		ctx.Repo.CanRead(unit.TypeReleases),
 | 
			
		||||
		ctx.Repo.CanRead(unit.TypeIssues),
 | 
			
		||||
		ctx.Repo.CanRead(unit.TypePullRequests),
 | 
			
		||||
		ctx.Repo.CanRead(unit.TypeCode)); err != nil {
 | 
			
		||||
		canReadCode,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetActivityStats", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
 | 
			
		||||
			ctx.Status(http.StatusAccepted)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.ServerError("GetCodeFrequencyData", err)
 | 
			
		||||
		ctx.ServerError("GetContributorStats", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -405,7 +405,6 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
 | 
			
		||||
			ctx.ServerError("OpenRepository", err)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		defer ci.HeadGitRepo.Close()
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.NotFound("ParseCompareInfo", nil)
 | 
			
		||||
		return nil
 | 
			
		||||
@@ -708,7 +707,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 | 
			
		||||
func CompareDiff(ctx *context.Context) {
 | 
			
		||||
	ci := ParseCompareInfo(ctx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if ci != nil && ci.HeadGitRepo != nil {
 | 
			
		||||
		if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
 | 
			
		||||
			ci.HeadGitRepo.Close()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
 | 
			
		||||
	// Only public pull don't need auth.
 | 
			
		||||
	isPublicPull := repoExist && !repo.IsPrivate && isPull
 | 
			
		||||
	var (
 | 
			
		||||
		askAuth = !isPublicPull || setting.Service.RequireSignInView
 | 
			
		||||
		askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
 | 
			
		||||
		environ []string
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		issue.MilestoneID = milestoneID
 | 
			
		||||
		if milestoneID > 0 {
 | 
			
		||||
			var err error
 | 
			
		||||
			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.ServerError("GetMilestoneByRepoID", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			issue.Milestone = nil
 | 
			
		||||
		}
 | 
			
		||||
		if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
			
		||||
			ctx.ServerError("ChangeMilestoneAssign", err)
 | 
			
		||||
			return
 | 
			
		||||
 
 | 
			
		||||
@@ -1263,7 +1263,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ci := ParseCompareInfo(ctx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if ci != nil && ci.HeadGitRepo != nil {
 | 
			
		||||
		if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
 | 
			
		||||
			ci.HeadGitRepo.Close()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,10 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	contributors_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplRecentCommits)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RecentCommitsData returns JSON of recent commits data
 | 
			
		||||
func RecentCommitsData(ctx *context.Context) {
 | 
			
		||||
	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
 | 
			
		||||
		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
 | 
			
		||||
			ctx.Status(http.StatusAccepted)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.ServerError("RecentCommitsData", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import (
 | 
			
		||||
	"image"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
 | 
			
		||||
		if workFlowErr != nil {
 | 
			
		||||
			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
 | 
			
		||||
		}
 | 
			
		||||
	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
 | 
			
		||||
	} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
 | 
			
		||||
		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
 | 
			
		||||
			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
 | 
			
		||||
			if len(warnings) > 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -285,23 +285,23 @@ func Routes() *web.Router {
 | 
			
		||||
	mid = append(mid, repo.GetActiveStopwatch)
 | 
			
		||||
	mid = append(mid, goGet)
 | 
			
		||||
 | 
			
		||||
	others := web.NewRouter()
 | 
			
		||||
	others.Use(mid...)
 | 
			
		||||
	registerRoutes(others)
 | 
			
		||||
	routes.Mount("", others)
 | 
			
		||||
	webRoutes := web.NewRouter()
 | 
			
		||||
	webRoutes.Use(mid...)
 | 
			
		||||
	webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
 | 
			
		||||
	routes.Mount("", webRoutes)
 | 
			
		||||
	return routes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
 | 
			
		||||
 | 
			
		||||
// registerRoutes register routes
 | 
			
		||||
func registerRoutes(m *web.Router) {
 | 
			
		||||
// registerWebRoutes register routes
 | 
			
		||||
func registerWebRoutes(m *web.Router) {
 | 
			
		||||
	// required to be signed in or signed out
 | 
			
		||||
	reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
 | 
			
		||||
	reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
 | 
			
		||||
	// optional sign in (if signed in, use the user as doer, if not, no doer)
 | 
			
		||||
	optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
 | 
			
		||||
	optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
 | 
			
		||||
	optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
 | 
			
		||||
	optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
 | 
			
		||||
 | 
			
		||||
	validation.AddBindingRules()
 | 
			
		||||
 | 
			
		||||
@@ -1454,20 +1454,23 @@ func registerRoutes(m *web.Router) {
 | 
			
		||||
	m.Group("/{username}/{reponame}/activity", func() {
 | 
			
		||||
		m.Get("", repo.Activity)
 | 
			
		||||
		m.Get("/{period}", repo.Activity)
 | 
			
		||||
		m.Group("/contributors", func() {
 | 
			
		||||
			m.Get("", repo.Contributors)
 | 
			
		||||
			m.Get("/data", repo.ContributorsData)
 | 
			
		||||
		})
 | 
			
		||||
		m.Group("/code-frequency", func() {
 | 
			
		||||
			m.Get("", repo.CodeFrequency)
 | 
			
		||||
			m.Get("/data", repo.CodeFrequencyData)
 | 
			
		||||
		})
 | 
			
		||||
		m.Group("/recent-commits", func() {
 | 
			
		||||
			m.Get("", repo.RecentCommits)
 | 
			
		||||
			m.Get("/data", repo.RecentCommitsData)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		m.Group("", func() {
 | 
			
		||||
			m.Group("/contributors", func() {
 | 
			
		||||
				m.Get("", repo.Contributors)
 | 
			
		||||
				m.Get("/data", repo.ContributorsData)
 | 
			
		||||
			})
 | 
			
		||||
			m.Group("/code-frequency", func() {
 | 
			
		||||
				m.Get("", repo.CodeFrequency)
 | 
			
		||||
				m.Get("/data", repo.CodeFrequencyData)
 | 
			
		||||
			})
 | 
			
		||||
			m.Group("/recent-commits", func() {
 | 
			
		||||
				m.Get("", repo.RecentCommits)
 | 
			
		||||
				m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
 | 
			
		||||
			})
 | 
			
		||||
		}, reqRepoCodeReader)
 | 
			
		||||
	},
 | 
			
		||||
		optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
 | 
			
		||||
		optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
 | 
			
		||||
		context.RepoRef(), repo.MustBeNotEmpty,
 | 
			
		||||
	)
 | 
			
		||||
	// end "/{username}/{reponame}/activity"
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
 | 
			
		||||
 | 
			
		||||
	ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
 | 
			
		||||
	ctx.Data["Title"] = "Page Not Found"
 | 
			
		||||
	ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
 | 
			
		||||
	ctx.HTML(http.StatusNotFound, "status/404")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
 | 
			
		||||
	if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
 | 
			
		||||
	if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
 | 
			
		||||
		return perm.AccessModeNone, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
 | 
			
		||||
	if ctx.Req.URL.RawQuery != "" {
 | 
			
		||||
		redirectPath += "?" + ctx.Req.URL.RawQuery
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
 | 
			
		||||
	// Git client needs a 301 redirect by default to follow the new location
 | 
			
		||||
	// It's not documentated in git documentation, but it's the behavior of git client
 | 
			
		||||
	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
 | 
			
		||||
					storer: storage.LFS,
 | 
			
		||||
					isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
 | 
			
		||||
						// The oid of an LFS stored object is the name but with all the path.Separators removed
 | 
			
		||||
						oid := strings.ReplaceAll(path, "/", "")
 | 
			
		||||
						oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
 | 
			
		||||
						exists, err := git.ExistsLFSObject(ctx, oid)
 | 
			
		||||
						return !exists, err
 | 
			
		||||
					},
 | 
			
		||||
 
 | 
			
		||||
@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 | 
			
		||||
 | 
			
		||||
	var reviewNotifiers []*ReviewRequestNotifier
 | 
			
		||||
	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
 | 
			
		||||
		if err := issue.LoadPullRequest(ctx); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
 | 
			
		||||
		reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("PullRequestCodeOwnersReview: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package issue
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
@@ -40,20 +41,27 @@ type ReviewRequestNotifier struct {
 | 
			
		||||
	ReviewTeam *org_model.Team
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
 | 
			
		||||
	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
 | 
			
		||||
var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
 | 
			
		||||
 | 
			
		||||
func IsCodeOwnerFile(f string) bool {
 | 
			
		||||
	return slices.Contains(codeOwnerFiles, f)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
 | 
			
		||||
	if err := pr.LoadIssue(ctx); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	issue := pr.Issue
 | 
			
		||||
	if pr.IsWorkInProgress(ctx) {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pr.LoadHeadRepo(ctx); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pr.LoadBaseRepo(ctx); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	pr.Issue.Repo = pr.BaseRepo
 | 
			
		||||
 | 
			
		||||
	if pr.BaseRepo.IsFork {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
@@ -71,7 +79,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data string
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
	for _, file := range codeOwnerFiles {
 | 
			
		||||
		if blob, err := commit.GetBlobByPath(file); err == nil {
 | 
			
		||||
			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
@@ -79,8 +87,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if data == "" {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
 | 
			
		||||
	if len(rules) == 0 {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get the mergebase
 | 
			
		||||
	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
 | 
			
		||||
@@ -116,13 +130,31 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// load all reviews from database
 | 
			
		||||
	latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contain := func(list issues_model.ReviewList, u *user_model.User) bool {
 | 
			
		||||
		for _, review := range list {
 | 
			
		||||
			if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range uniqUsers {
 | 
			
		||||
		if u.ID != issue.Poster.ID {
 | 
			
		||||
		if u.ID != issue.Poster.ID && !contain(latestReivews, u) {
 | 
			
		||||
			comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			notifiers = append(notifiers, &ReviewRequestNotifier{
 | 
			
		||||
				Comment:  comment,
 | 
			
		||||
				IsAdd:    true,
 | 
			
		||||
@@ -130,12 +162,16 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, t := range uniqTeams {
 | 
			
		||||
		comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		notifiers = append(notifiers, &ReviewRequestNotifier{
 | 
			
		||||
			Comment:    comment,
 | 
			
		||||
			IsAdd:      true,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ package migrations
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-github/v61/github"
 | 
			
		||||
	"github.com/google/go-github/v71/github"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ErrRepoNotCreated returns the error that repository not created
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/proxy"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-github/v61/github"
 | 
			
		||||
	"github.com/google/go-github/v71/github"
 | 
			
		||||
	"golang.org/x/oauth2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -135,7 +135,7 @@ func (g *GithubDownloaderV3) LogString() string {
 | 
			
		||||
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
 | 
			
		||||
	githubClient := github.NewClient(client)
 | 
			
		||||
	if baseURL != "https://github.com" {
 | 
			
		||||
		githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
 | 
			
		||||
		githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
 | 
			
		||||
	}
 | 
			
		||||
	g.clients = append(g.clients, githubClient)
 | 
			
		||||
	g.rates = append(g.rates, nil)
 | 
			
		||||
@@ -448,9 +448,11 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
 | 
			
		||||
		if !g.SkipReactions {
 | 
			
		||||
			for i := 1; ; i++ {
 | 
			
		||||
				g.waitAndPickClient()
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
 | 
			
		||||
					Page:    i,
 | 
			
		||||
					PerPage: perPage,
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListReactionOptions{
 | 
			
		||||
					ListOptions: github.ListOptions{
 | 
			
		||||
						Page:    i,
 | 
			
		||||
						PerPage: perPage,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, false, err
 | 
			
		||||
@@ -534,9 +536,11 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
 | 
			
		||||
			if !g.SkipReactions {
 | 
			
		||||
				for i := 1; ; i++ {
 | 
			
		||||
					g.waitAndPickClient()
 | 
			
		||||
					res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
 | 
			
		||||
						Page:    i,
 | 
			
		||||
						PerPage: g.maxPerPage,
 | 
			
		||||
					res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
 | 
			
		||||
						ListOptions: github.ListOptions{
 | 
			
		||||
							Page:    i,
 | 
			
		||||
							PerPage: g.maxPerPage,
 | 
			
		||||
						},
 | 
			
		||||
					})
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return nil, err
 | 
			
		||||
@@ -609,9 +613,11 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
 | 
			
		||||
		if !g.SkipReactions {
 | 
			
		||||
			for i := 1; ; i++ {
 | 
			
		||||
				g.waitAndPickClient()
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
 | 
			
		||||
					Page:    i,
 | 
			
		||||
					PerPage: g.maxPerPage,
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
 | 
			
		||||
					ListOptions: github.ListOptions{
 | 
			
		||||
						Page:    i,
 | 
			
		||||
						PerPage: g.maxPerPage,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, false, err
 | 
			
		||||
@@ -680,9 +686,11 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
 | 
			
		||||
		if !g.SkipReactions {
 | 
			
		||||
			for i := 1; ; i++ {
 | 
			
		||||
				g.waitAndPickClient()
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
 | 
			
		||||
					Page:    i,
 | 
			
		||||
					PerPage: perPage,
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListReactionOptions{
 | 
			
		||||
					ListOptions: github.ListOptions{
 | 
			
		||||
						Page:    i,
 | 
			
		||||
						PerPage: perPage,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, false, err
 | 
			
		||||
@@ -767,9 +775,11 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
 | 
			
		||||
		if !g.SkipReactions {
 | 
			
		||||
			for i := 1; ; i++ {
 | 
			
		||||
				g.waitAndPickClient()
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
 | 
			
		||||
					Page:    i,
 | 
			
		||||
					PerPage: g.maxPerPage,
 | 
			
		||||
				res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListReactionOptions{
 | 
			
		||||
					ListOptions: github.ListOptions{
 | 
			
		||||
						Page:    i,
 | 
			
		||||
						PerPage: g.maxPerPage,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
@@ -879,3 +889,18 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
 | 
			
		||||
	}
 | 
			
		||||
	return allReviews, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatCloneURL add authentication into remote URLs
 | 
			
		||||
func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
 | 
			
		||||
	u, err := url.Parse(remoteAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if len(opts.AuthToken) > 0 {
 | 
			
		||||
		// "multiple tokens" are used to benefit more "API rate limit quota"
 | 
			
		||||
		// git clone doesn't count for rate limits, so only use the first token.
 | 
			
		||||
		// source: https://github.com/orgs/community/discussions/44515
 | 
			
		||||
		u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
 | 
			
		||||
	}
 | 
			
		||||
	return u.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import (
 | 
			
		||||
	base "code.gitea.io/gitea/modules/migration"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGitHubDownloadRepo(t *testing.T) {
 | 
			
		||||
@@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) {
 | 
			
		||||
		},
 | 
			
		||||
	}, reviews)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGithubMultiToken(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		desc             string
 | 
			
		||||
		token            string
 | 
			
		||||
		expectedCloneURL string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			desc:             "Single Token",
 | 
			
		||||
			token:            "single_token",
 | 
			
		||||
			expectedCloneURL: "https://oauth2:single_token@github.com",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			desc:             "Multi Token",
 | 
			
		||||
			token:            "token1,token2",
 | 
			
		||||
			expectedCloneURL: "https://oauth2:token1@github.com",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	factory := GithubDownloaderV3Factory{}
 | 
			
		||||
 | 
			
		||||
	for _, tC := range testCases {
 | 
			
		||||
		t.Run(tC.desc, func(t *testing.T) {
 | 
			
		||||
			opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
 | 
			
		||||
			client, err := factory.New(context.Background(), opts)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			assert.Equal(t, tC.expectedCloneURL, cloneURL)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
 | 
			
		||||
		"Initialize Cargo Config",
 | 
			
		||||
		func(t *files_service.TemporaryUploadRepository) error {
 | 
			
		||||
			var b bytes.Buffer
 | 
			
		||||
			err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
 | 
			
		||||
			err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
 | 
			
		||||
				files = append(files, f)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
 | 
			
		||||
		packages = append(packages, &Package{
 | 
			
		||||
			Type:         "rpm",
 | 
			
		||||
			Name:         pd.Package.Name,
 | 
			
		||||
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
 | 
			
		||||
				Archive:   pd.FileMetadata.ArchiveSize,
 | 
			
		||||
			},
 | 
			
		||||
			Location: Location{
 | 
			
		||||
				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
 | 
			
		||||
				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
 | 
			
		||||
			},
 | 
			
		||||
			Format: Format{
 | 
			
		||||
				License:   pd.VersionMetadata.License,
 | 
			
		||||
 
 | 
			
		||||
@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
 | 
			
		||||
	}
 | 
			
		||||
	defer releaser()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
 | 
			
		||||
		go AddTestPullRequestTask(TestPullRequestOptions{
 | 
			
		||||
			RepoID:      pr.BaseRepo.ID,
 | 
			
		||||
			Doer:        doer,
 | 
			
		||||
			Branch:      pr.BaseBranch,
 | 
			
		||||
			IsSync:      false,
 | 
			
		||||
			IsForcePush: false,
 | 
			
		||||
			OldCommitID: "",
 | 
			
		||||
			NewCommitID: "",
 | 
			
		||||
		})
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
 | 
			
		||||
 
 | 
			
		||||
@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !pr.IsWorkInProgress(ctx) {
 | 
			
		||||
			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
 | 
			
		||||
			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TestPullRequestOptions struct {
 | 
			
		||||
	RepoID      int64
 | 
			
		||||
	Doer        *user_model.User
 | 
			
		||||
	Branch      string
 | 
			
		||||
	IsSync      bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
 | 
			
		||||
	IsForcePush bool
 | 
			
		||||
	OldCommitID string
 | 
			
		||||
	NewCommitID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
 | 
			
		||||
// and generate new patch for testing as needed.
 | 
			
		||||
func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
 | 
			
		||||
	log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
 | 
			
		||||
func AddTestPullRequestTask(opts TestPullRequestOptions) {
 | 
			
		||||
	log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
 | 
			
		||||
	graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
 | 
			
		||||
		// There is no sensible way to shut this down ":-("
 | 
			
		||||
		// If you don't let it run all the way then you will lose data
 | 
			
		||||
		// TODO: graceful: AddTestPullRequestTask needs to become a queue!
 | 
			
		||||
 | 
			
		||||
		// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
 | 
			
		||||
		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
 | 
			
		||||
		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
 | 
			
		||||
			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			AddToTaskQueue(ctx, pr)
 | 
			
		||||
			comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
 | 
			
		||||
			comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
 | 
			
		||||
			if err == nil && comment != nil {
 | 
			
		||||
				notify_service.PullRequestPushCommits(ctx, doer, pr, comment)
 | 
			
		||||
				notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if isSync {
 | 
			
		||||
			requests := issues_model.PullRequestList(prs)
 | 
			
		||||
			if err = requests.LoadAttributes(ctx); err != nil {
 | 
			
		||||
		if opts.IsSync {
 | 
			
		||||
			if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
 | 
			
		||||
				log.Error("PullRequestList.LoadAttributes: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
			if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
 | 
			
		||||
			if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
 | 
			
		||||
				log.Error("checkForInvalidation: %v", invalidationErr)
 | 
			
		||||
			}
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				for _, pr := range prs {
 | 
			
		||||
					objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 | 
			
		||||
					if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
 | 
			
		||||
						changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
 | 
			
		||||
					if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
 | 
			
		||||
						changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							log.Error("checkIfPRContentChanged: %v", err)
 | 
			
		||||
						}
 | 
			
		||||
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 | 
			
		||||
								log.Error("GetFirstMatchProtectedBranchRule: %v", err)
 | 
			
		||||
							}
 | 
			
		||||
							if pb != nil && pb.DismissStaleApprovals {
 | 
			
		||||
								if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
 | 
			
		||||
								if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
 | 
			
		||||
									log.Error("DismissApprovalReviews: %v", err)
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
 | 
			
		||||
						if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
 | 
			
		||||
							log.Error("MarkReviewsAsNotStale: %v", err)
 | 
			
		||||
						}
 | 
			
		||||
						divergence, err := GetDiverging(ctx, pr)
 | 
			
		||||
@@ -430,15 +439,25 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					notify_service.PullRequestSynchronized(ctx, doer, pr)
 | 
			
		||||
					if !pr.IsWorkInProgress(ctx) {
 | 
			
		||||
						reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							log.Error("PullRequestCodeOwnersReview: %v", err)
 | 
			
		||||
						}
 | 
			
		||||
						if len(reviewNotifiers) > 0 {
 | 
			
		||||
							issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
 | 
			
		||||
		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
 | 
			
		||||
		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
 | 
			
		||||
		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
 | 
			
		||||
			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		for _, pr := range prs {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
 | 
			
		||||
 | 
			
		||||
	if rebase {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
 | 
			
		||||
			go AddTestPullRequestTask(TestPullRequestOptions{
 | 
			
		||||
				RepoID:      pr.BaseRepo.ID,
 | 
			
		||||
				Doer:        doer,
 | 
			
		||||
				Branch:      pr.BaseBranch,
 | 
			
		||||
				IsSync:      false,
 | 
			
		||||
				IsForcePush: false,
 | 
			
		||||
				OldCommitID: "",
 | 
			
		||||
				NewCommitID: "",
 | 
			
		||||
			})
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		return updateHeadByRebaseOnToBase(ctx, pr, doer)
 | 
			
		||||
@@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
 | 
			
		||||
	_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "")
 | 
			
		||||
		go AddTestPullRequestTask(TestPullRequestOptions{
 | 
			
		||||
			RepoID:      reversePR.HeadRepo.ID,
 | 
			
		||||
			Doer:        doer,
 | 
			
		||||
			Branch:      reversePR.HeadBranch,
 | 
			
		||||
			IsSync:      false,
 | 
			
		||||
			IsForcePush: false,
 | 
			
		||||
			OldCommitID: "",
 | 
			
		||||
			NewCommitID: "",
 | 
			
		||||
		})
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
 
 | 
			
		||||
@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 | 
			
		||||
			branch := opts.RefFullName.BranchName()
 | 
			
		||||
			if !opts.IsDelRef() {
 | 
			
		||||
				log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
 | 
			
		||||
				go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
 | 
			
		||||
 | 
			
		||||
				newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
@@ -212,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 | 
			
		||||
						log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
 | 
			
		||||
					go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
 | 
			
		||||
						RepoID:      repo.ID,
 | 
			
		||||
						Doer:        pusher,
 | 
			
		||||
						Branch:      branch,
 | 
			
		||||
						IsSync:      true,
 | 
			
		||||
						IsForcePush: isForcePush,
 | 
			
		||||
						OldCommitID: opts.OldCommitID,
 | 
			
		||||
						NewCommitID: opts.NewCommitID,
 | 
			
		||||
					})
 | 
			
		||||
 | 
			
		||||
					if isForcePush {
 | 
			
		||||
						log.Trace("Push %s is a force push", opts.NewCommitID)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	webhook_model "code.gitea.io/gitea/models/webhook"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -101,6 +102,13 @@ var (
 | 
			
		||||
	redColor         = color("ff3232")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
 | 
			
		||||
// Discord has some limits in place for the embeds.
 | 
			
		||||
// According to some tests, there is no consistent limit for different character sets.
 | 
			
		||||
// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
 | 
			
		||||
// To keep it simple, we currently truncate at 2000.
 | 
			
		||||
const discordDescriptionCharactersLimit = 2000
 | 
			
		||||
 | 
			
		||||
type discordConvertor struct {
 | 
			
		||||
	Username  string
 | 
			
		||||
	AvatarURL string
 | 
			
		||||
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
 | 
			
		||||
		Embeds: []DiscordEmbed{
 | 
			
		||||
			{
 | 
			
		||||
				Title:       title,
 | 
			
		||||
				Description: text,
 | 
			
		||||
				Description: base.TruncateString(text, discordDescriptionCharactersLimit),
 | 
			
		||||
				URL:         url,
 | 
			
		||||
				Color:       color,
 | 
			
		||||
				Author: DiscordEmbedAuthor{
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,7 @@
 | 
			
		||||
				<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
 | 
			
		||||
				<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
 | 
			
		||||
				<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
 | 
			
		||||
				<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
 | 
			
		||||
				<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
 | 
			
		||||
				<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
 | 
			
		||||
				<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
 | 
			
		||||
				<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 | 
			
		||||
 | 
			
		||||
<div class="ui container tw-max-w-full">
 | 
			
		||||
	<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
 | 
			
		||||
		<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
 | 
			
		||||
			<div class="project-toolbar-right">
 | 
			
		||||
				<div class="ui secondary filter menu labels">
 | 
			
		||||
	<div class="flex-text-block tw-flex-wrap tw-mb-4">
 | 
			
		||||
		<h2 class="tw-mb-0">{{.Project.Title}}</h2>
 | 
			
		||||
		<div class="tw-flex-1"></div>
 | 
			
		||||
				<div class="ui secondary menu tw-m-0">
 | 
			
		||||
					{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
 | 
			
		||||
 | 
			
		||||
					{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
 | 
			
		||||
@@ -19,7 +19,6 @@
 | 
			
		||||
						"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
 | 
			
		||||
					}}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		{{if $canWriteProject}}
 | 
			
		||||
			<div class="ui compact mini menu">
 | 
			
		||||
				<a class="item" href="{{.Link}}/edit?redirect=project">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<button class="ui primary button js-btn-clone-panel">
 | 
			
		||||
	{{svg "octicon-code" 16}}
 | 
			
		||||
	<span>Code</span>
 | 
			
		||||
	<span>{{ctx.Locale.Tr "repo.code"}}</span>
 | 
			
		||||
	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
			
		||||
</button>
 | 
			
		||||
<div class="clone-panel-popup tippy-target">
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,7 @@
 | 
			
		||||
				<div class="ui attached segment">
 | 
			
		||||
					{{template "base/alert" .}}
 | 
			
		||||
					{{template "repo/create_helper" .}}
 | 
			
		||||
 | 
			
		||||
					{{if not .CanCreateRepo}}
 | 
			
		||||
						<div class="ui negative message">
 | 
			
		||||
							<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
 | 
			
		||||
						</div>
 | 
			
		||||
					{{end}}
 | 
			
		||||
					<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
 | 
			
		||||
					<div class="inline required field {{if .Err_Owner}}error{{end}}">
 | 
			
		||||
						<label>{{ctx.Locale.Tr "repo.owner"}}</label>
 | 
			
		||||
						<div class="ui selection owner dropdown">
 | 
			
		||||
@@ -26,7 +21,11 @@
 | 
			
		||||
							</span>
 | 
			
		||||
							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
			
		||||
							<div class="menu">
 | 
			
		||||
								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
 | 
			
		||||
								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
 | 
			
		||||
								{{if not .CanCreateRepo}}
 | 
			
		||||
									data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}"
 | 
			
		||||
								{{end}}
 | 
			
		||||
								>
 | 
			
		||||
									{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
 | 
			
		||||
									<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
 | 
			
		||||
								</div>
 | 
			
		||||
@@ -209,7 +208,7 @@
 | 
			
		||||
					<br>
 | 
			
		||||
					<div class="inline field">
 | 
			
		||||
						<label></label>
 | 
			
		||||
						<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
 | 
			
		||||
						<button class="ui primary button">
 | 
			
		||||
							{{ctx.Locale.Tr "repo.create_repo"}}
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
		<div class="flex-item">
 | 
			
		||||
			<div class="flex-item-main">
 | 
			
		||||
				<div class="flex-item-title">
 | 
			
		||||
					<a class="item muted" href="{{.Link}}/releases">
 | 
			
		||||
					<a class="item muted" href="{{.RepoLink}}/releases">
 | 
			
		||||
						{{ctx.Locale.Tr "repo.releases"}}
 | 
			
		||||
						<span class="ui small label">{{.NumReleases}}</span>
 | 
			
		||||
					</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
		{{if $.Page.LinkedPRs}}
 | 
			
		||||
		{{range index $.Page.LinkedPRs .ID}}
 | 
			
		||||
		<div class="meta tw-my-1">
 | 
			
		||||
			<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
 | 
			
		||||
			<a href="{{.Repo.Link}}/pulls/{{.Index}}">
 | 
			
		||||
				<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
 | 
			
		||||
				<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
 | 
			
		||||
			</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,10 @@
 | 
			
		||||
		</div>
 | 
			
		||||
	{{end}}
 | 
			
		||||
 | 
			
		||||
		<div class="list-header">
 | 
			
		||||
			{{template "repo/issue/navbar" .}}
 | 
			
		||||
		<div class="list-header flex-text-block">
 | 
			
		||||
			{{template "repo/issue/search" .}}
 | 
			
		||||
			<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
 | 
			
		||||
			<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 | 
			
		||||
			{{if not .Repository.IsArchived}}
 | 
			
		||||
				{{if .PageIsIssueList}}
 | 
			
		||||
					<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,18 @@
 | 
			
		||||
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
 | 
			
		||||
 | 
			
		||||
<div class="ui fluid vertical menu">
 | 
			
		||||
	<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
 | 
			
		||||
	</a>
 | 
			
		||||
	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
 | 
			
		||||
	</a>
 | 
			
		||||
	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
 | 
			
		||||
	</a>
 | 
			
		||||
	<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
 | 
			
		||||
	</a>
 | 
			
		||||
	{{if $canReadCode}}
 | 
			
		||||
		<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
 | 
			
		||||
			{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
 | 
			
		||||
		</a>
 | 
			
		||||
		<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
 | 
			
		||||
			{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
 | 
			
		||||
		</a>
 | 
			
		||||
		<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
 | 
			
		||||
			{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
 | 
			
		||||
		</a>
 | 
			
		||||
	{{end}}
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,9 @@
 | 
			
		||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 | 
			
		||||
	{{template "repo/header" .}}
 | 
			
		||||
	<div class="ui container padded">
 | 
			
		||||
		<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
 | 
			
		||||
			{{template "repo/issue/navbar" .}}
 | 
			
		||||
		<div class="flex-text-block tw-justify-end tw-mb-4">
 | 
			
		||||
			<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
 | 
			
		||||
			<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 | 
			
		||||
			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
		<div class="ui stackable grid">
 | 
			
		||||
			<div class="ui eight wide column">
 | 
			
		||||
				<div class="ui header">
 | 
			
		||||
					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}"><span>{{.revision}}</span> {{svg "octicon-home"}}</a>
 | 
			
		||||
					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{if .revision}}<span>{{.revision}}</span> {{end}}{{svg "octicon-home"}}</a>
 | 
			
		||||
					{{$title}}
 | 
			
		||||
					<div class="ui sub header tw-break-anywhere">
 | 
			
		||||
						{{$timeSince := DateUtils.TimeSince .Author.When}}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
		<div class="ui dividing header">
 | 
			
		||||
			<div class="flex-text-block tw-flex-wrap tw-justify-end">
 | 
			
		||||
				<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
 | 
			
		||||
					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" ><span>{{.CommitCount}}</span> {{svg "octicon-history"}}</a>
 | 
			
		||||
					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
 | 
			
		||||
					<div class="tw-flex-1 gt-ellipsis">
 | 
			
		||||
						{{$title}}
 | 
			
		||||
						<div class="ui sub header gt-ellipsis">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -19616,7 +19616,18 @@
 | 
			
		||||
          "items": {
 | 
			
		||||
            "type": "string"
 | 
			
		||||
          },
 | 
			
		||||
          "x-go-name": "Scopes"
 | 
			
		||||
          "x-go-name": "Scopes",
 | 
			
		||||
          "example": [
 | 
			
		||||
            "all",
 | 
			
		||||
            "read:activitypub",
 | 
			
		||||
            "read:issue",
 | 
			
		||||
            "write:misc",
 | 
			
		||||
            "read:notification",
 | 
			
		||||
            "read:organization",
 | 
			
		||||
            "read:package",
 | 
			
		||||
            "read:repository",
 | 
			
		||||
            "read:user"
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@
 | 
			
		||||
package integration
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
@@ -24,7 +26,9 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
	release_service "code.gitea.io/gitea/services/release"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
 | 
			
		||||
		assert.NotNil(t, run)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestClosePullRequestWithPath(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		// user2 is the owner of the base repo
 | 
			
		||||
		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
		user2Token := getTokenForLoggedInUser(t, loginUser(t, user2.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 | 
			
		||||
		// user4 is the owner of the fork repo
 | 
			
		||||
		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
			
		||||
		user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 | 
			
		||||
 | 
			
		||||
		// create the base repo
 | 
			
		||||
		req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
 | 
			
		||||
			Name:          "close-pull-request-with-path",
 | 
			
		||||
			Private:       false,
 | 
			
		||||
			Readme:        "Default",
 | 
			
		||||
			AutoInit:      true,
 | 
			
		||||
			DefaultBranch: "main",
 | 
			
		||||
		}).AddTokenAuth(user2Token)
 | 
			
		||||
		resp := MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
		var apiBaseRepo api.Repository
 | 
			
		||||
		DecodeJSON(t, resp, &apiBaseRepo)
 | 
			
		||||
		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
 | 
			
		||||
		user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
 | 
			
		||||
		// init the workflow
 | 
			
		||||
		wfTreePath := ".gitea/workflows/pull.yml"
 | 
			
		||||
		wfFileContent := `name: Pull Request
 | 
			
		||||
on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    types:
 | 
			
		||||
      - closed
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'app/**'
 | 
			
		||||
jobs:
 | 
			
		||||
  echo:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo 'Hello World'
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", baseRepo.OwnerName, baseRepo.Name, wfTreePath), &api.CreateFileOptions{
 | 
			
		||||
			FileOptions: api.FileOptions{
 | 
			
		||||
				BranchName: baseRepo.DefaultBranch,
 | 
			
		||||
				Message:    "create " + wfTreePath,
 | 
			
		||||
				Author: api.Identity{
 | 
			
		||||
					Name:  user2.Name,
 | 
			
		||||
					Email: user2.Email,
 | 
			
		||||
				},
 | 
			
		||||
				Committer: api.Identity{
 | 
			
		||||
					Name:  user2.Name,
 | 
			
		||||
					Email: user2.Email,
 | 
			
		||||
				},
 | 
			
		||||
				Dates: api.CommitDateOptions{
 | 
			
		||||
					Author:    time.Now(),
 | 
			
		||||
					Committer: time.Now(),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			ContentBase64: base64.StdEncoding.EncodeToString([]byte(wfFileContent)),
 | 
			
		||||
		}).AddTokenAuth(user2Token)
 | 
			
		||||
		MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		// user4 forks the repo
 | 
			
		||||
		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
 | 
			
		||||
			&api.CreateForkOption{
 | 
			
		||||
				Name: util.ToPointer("close-pull-request-with-path-fork"),
 | 
			
		||||
			}).AddTokenAuth(user4Token)
 | 
			
		||||
		resp = MakeRequest(t, req, http.StatusAccepted)
 | 
			
		||||
		var apiForkRepo api.Repository
 | 
			
		||||
		DecodeJSON(t, resp, &apiForkRepo)
 | 
			
		||||
		forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
 | 
			
		||||
		user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
 | 
			
		||||
		// user4 creates a pull request to add file "app/main.go"
 | 
			
		||||
		doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{
 | 
			
		||||
			FileOptions: api.FileOptions{
 | 
			
		||||
				NewBranchName: "user4/add-main",
 | 
			
		||||
				Message:       "create main.go",
 | 
			
		||||
				Author: api.Identity{
 | 
			
		||||
					Name:  user4.Name,
 | 
			
		||||
					Email: user4.Email,
 | 
			
		||||
				},
 | 
			
		||||
				Committer: api.Identity{
 | 
			
		||||
					Name:  user4.Name,
 | 
			
		||||
					Email: user4.Email,
 | 
			
		||||
				},
 | 
			
		||||
				Dates: api.CommitDateOptions{
 | 
			
		||||
					Author:    time.Now(),
 | 
			
		||||
					Committer: time.Now(),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			ContentBase64: base64.StdEncoding.EncodeToString([]byte("// main.go")),
 | 
			
		||||
		})(t)
 | 
			
		||||
		apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/add-main")(t)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, apiPull.Index)(t)
 | 
			
		||||
 | 
			
		||||
		pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
 | 
			
		||||
 | 
			
		||||
		// load and compare ActionRun
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
 | 
			
		||||
		actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
 | 
			
		||||
		assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
 | 
			
		||||
		assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestAPIOrgDeny(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, func(*testing.T, *url.URL) {
 | 
			
		||||
		setting.Service.RequireSignInView = true
 | 
			
		||||
		setting.Service.RequireSignInViewStrict = true
 | 
			
		||||
		defer func() {
 | 
			
		||||
			setting.Service.RequireSignInView = false
 | 
			
		||||
			setting.Service.RequireSignInViewStrict = false
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		orgName := "user1_org"
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
 | 
			
		||||
				AddTokenAuth(anonymousToken)
 | 
			
		||||
			MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
			defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
 | 
			
		||||
			defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
 | 
			
		||||
 | 
			
		||||
			req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
 | 
			
		||||
			MakeRequest(t, req, http.StatusUnauthorized)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
		t.Run("RequireSignInView", func(t *testing.T) {
 | 
			
		||||
			defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
			setting.Service.RequireSignInView = true
 | 
			
		||||
			defer func() {
 | 
			
		||||
				setting.Service.RequireSignInView = false
 | 
			
		||||
			}()
 | 
			
		||||
			defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
 | 
			
		||||
 | 
			
		||||
			req = NewRequest(t, "GET", url+"/dummy.bin")
 | 
			
		||||
			MakeRequest(t, req, http.StatusUnauthorized)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPackageSwift(t *testing.T) {
 | 
			
		||||
@@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
 | 
			
		||||
	packageName := "test_package"
 | 
			
		||||
	packageID := packageScope + "." + packageName
 | 
			
		||||
	packageVersion := "1.0.3"
 | 
			
		||||
	packageVersion2 := "1.0.4"
 | 
			
		||||
	packageAuthor := "KN4CK3R"
 | 
			
		||||
	packageDescription := "Gitea Test Package"
 | 
			
		||||
	packageRepositoryURL := "https://gitea.io/gitea/gitea"
 | 
			
		||||
@@ -183,6 +185,94 @@ func TestPackageSwift(t *testing.T) {
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UploadMultipart", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
 | 
			
		||||
			var body bytes.Buffer
 | 
			
		||||
			mpw := multipart.NewWriter(&body)
 | 
			
		||||
 | 
			
		||||
			// Read the source archive content
 | 
			
		||||
			sourceContent, err := io.ReadAll(sr)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			mpw.WriteField("source-archive", string(sourceContent))
 | 
			
		||||
 | 
			
		||||
			if metadata != "" {
 | 
			
		||||
				mpw.WriteField("metadata", metadata)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mpw.Close()
 | 
			
		||||
 | 
			
		||||
			req := NewRequestWithBody(t, "PUT", url, &body).
 | 
			
		||||
				SetHeader("Content-Type", mpw.FormDataContentType()).
 | 
			
		||||
				SetHeader("Accept", swift_router.AcceptJSON).
 | 
			
		||||
				AddBasicAuth(user.Name)
 | 
			
		||||
			MakeRequest(t, req, expectedStatus)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createArchive := func(files map[string]string) *bytes.Buffer {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			zw := zip.NewWriter(&buf)
 | 
			
		||||
			for filename, content := range files {
 | 
			
		||||
				w, _ := zw.Create(filename)
 | 
			
		||||
				w.Write([]byte(content))
 | 
			
		||||
			}
 | 
			
		||||
			zw.Close()
 | 
			
		||||
			return &buf
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion2)
 | 
			
		||||
 | 
			
		||||
		req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
 | 
			
		||||
		MakeRequest(t, req, http.StatusUnauthorized)
 | 
			
		||||
 | 
			
		||||
		// Test with metadata as form field
 | 
			
		||||
		uploadPackage(
 | 
			
		||||
			t,
 | 
			
		||||
			uploadURL,
 | 
			
		||||
			http.StatusCreated,
 | 
			
		||||
			createArchive(map[string]string{
 | 
			
		||||
				"Package.swift":           contentManifest1,
 | 
			
		||||
				"Package@swift-5.6.swift": contentManifest2,
 | 
			
		||||
			}),
 | 
			
		||||
			`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		require.Len(t, pvs, 2) // ATTENTION: many subtests are unable to run separately, they depend on the results of previous tests
 | 
			
		||||
		thisPackageVersion := pvs[0]
 | 
			
		||||
		pd, err := packages.GetPackageDescriptor(db.DefaultContext, thisPackageVersion)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, pd.SemVer)
 | 
			
		||||
		assert.Equal(t, packageID, pd.Package.Name)
 | 
			
		||||
		assert.Equal(t, packageVersion2, pd.Version.Version)
 | 
			
		||||
		assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
 | 
			
		||||
		metadata := pd.Metadata.(*swift_module.Metadata)
 | 
			
		||||
		assert.Equal(t, packageDescription, metadata.Description)
 | 
			
		||||
		assert.Len(t, metadata.Manifests, 2)
 | 
			
		||||
		assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
 | 
			
		||||
		assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
 | 
			
		||||
		assert.Len(t, pd.VersionProperties, 1)
 | 
			
		||||
		assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
 | 
			
		||||
 | 
			
		||||
		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, thisPackageVersion.ID)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, pfs, 1)
 | 
			
		||||
		assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion2), pfs[0].Name)
 | 
			
		||||
		assert.True(t, pfs[0].IsLead)
 | 
			
		||||
 | 
			
		||||
		uploadPackage(
 | 
			
		||||
			t,
 | 
			
		||||
			uploadURL,
 | 
			
		||||
			http.StatusConflict,
 | 
			
		||||
			createArchive(map[string]string{
 | 
			
		||||
				"Package.swift": contentManifest1,
 | 
			
		||||
			}),
 | 
			
		||||
			"",
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Download", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
@@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
 | 
			
		||||
			SetHeader("Accept", swift_router.AcceptJSON)
 | 
			
		||||
		resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion)
 | 
			
		||||
		versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion2)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
 | 
			
		||||
		assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
 | 
			
		||||
@@ -221,9 +311,9 @@ func TestPackageSwift(t *testing.T) {
 | 
			
		||||
		var result *swift_router.EnumeratePackageVersionsResponse
 | 
			
		||||
		DecodeJSON(t, resp, &result)
 | 
			
		||||
 | 
			
		||||
		assert.Len(t, result.Releases, 1)
 | 
			
		||||
		assert.Contains(t, result.Releases, packageVersion)
 | 
			
		||||
		assert.Equal(t, versionURL, result.Releases[packageVersion].URL)
 | 
			
		||||
		assert.Len(t, result.Releases, 2)
 | 
			
		||||
		assert.Contains(t, result.Releases, packageVersion2)
 | 
			
		||||
		assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
 | 
			
		||||
 | 
			
		||||
		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
 | 
			
		||||
			AddBasicAuth(user.Name)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,10 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -18,11 +21,15 @@ import (
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/services/convert"
 | 
			
		||||
	"code.gitea.io/gitea/services/forms"
 | 
			
		||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
			
		||||
	issue_service "code.gitea.io/gitea/services/issue"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
	files_service "code.gitea.io/gitea/services/repository/files"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
@@ -425,3 +432,94 @@ func TestAPICommitPullRequest(t *testing.T) {
 | 
			
		||||
	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
 | 
			
		||||
	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAPIViewPullFilesWithHeadRepoDeleted(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
			
		||||
		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
			
		||||
 | 
			
		||||
		ctx := NewAPITestContext(t, "user1", baseRepo.Name, auth_model.AccessTokenScopeAll)
 | 
			
		||||
 | 
			
		||||
		doAPIForkRepository(ctx, "user2")(t)
 | 
			
		||||
 | 
			
		||||
		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ForkID: baseRepo.ID, OwnerName: "user1"})
 | 
			
		||||
 | 
			
		||||
		// add a new file to the forked repo
 | 
			
		||||
		addFileToForkedResp, err := files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, user1, &files_service.ChangeRepoFilesOptions{
 | 
			
		||||
			Files: []*files_service.ChangeRepoFile{
 | 
			
		||||
				{
 | 
			
		||||
					Operation:     "create",
 | 
			
		||||
					TreePath:      "file_1.txt",
 | 
			
		||||
					ContentReader: strings.NewReader("file1"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Message:   "add file1",
 | 
			
		||||
			OldBranch: "master",
 | 
			
		||||
			NewBranch: "fork-branch-1",
 | 
			
		||||
			Author: &files_service.IdentityOptions{
 | 
			
		||||
				Name:  user1.Name,
 | 
			
		||||
				Email: user1.Email,
 | 
			
		||||
			},
 | 
			
		||||
			Committer: &files_service.IdentityOptions{
 | 
			
		||||
				Name:  user1.Name,
 | 
			
		||||
				Email: user1.Email,
 | 
			
		||||
			},
 | 
			
		||||
			Dates: &files_service.CommitDateOptions{
 | 
			
		||||
				Author:    time.Now(),
 | 
			
		||||
				Committer: time.Now(),
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotEmpty(t, addFileToForkedResp)
 | 
			
		||||
 | 
			
		||||
		// create Pull
 | 
			
		||||
		pullIssue := &issues_model.Issue{
 | 
			
		||||
			RepoID:   baseRepo.ID,
 | 
			
		||||
			Title:    "Test pull-request-target-event",
 | 
			
		||||
			PosterID: user1.ID,
 | 
			
		||||
			Poster:   user1,
 | 
			
		||||
			IsPull:   true,
 | 
			
		||||
		}
 | 
			
		||||
		pullRequest := &issues_model.PullRequest{
 | 
			
		||||
			HeadRepoID: forkedRepo.ID,
 | 
			
		||||
			BaseRepoID: baseRepo.ID,
 | 
			
		||||
			HeadBranch: "fork-branch-1",
 | 
			
		||||
			BaseBranch: "master",
 | 
			
		||||
			HeadRepo:   forkedRepo,
 | 
			
		||||
			BaseRepo:   baseRepo,
 | 
			
		||||
			Type:       issues_model.PullRequestGitea,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
 | 
			
		||||
		err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		pr := convert.ToAPIPullRequest(context.Background(), pullRequest, user1)
 | 
			
		||||
 | 
			
		||||
		ctx = NewAPITestContext(t, "user2", baseRepo.Name, auth_model.AccessTokenScopeAll)
 | 
			
		||||
		doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
 | 
			
		||||
			if assert.Len(t, files, 1) {
 | 
			
		||||
				assert.Equal(t, "file_1.txt", files[0].Filename)
 | 
			
		||||
				assert.Empty(t, files[0].PreviousFilename)
 | 
			
		||||
				assert.Equal(t, 1, files[0].Additions)
 | 
			
		||||
				assert.Equal(t, 1, files[0].Changes)
 | 
			
		||||
				assert.Equal(t, 0, files[0].Deletions)
 | 
			
		||||
				assert.Equal(t, "added", files[0].Status)
 | 
			
		||||
			}
 | 
			
		||||
		})(t)
 | 
			
		||||
 | 
			
		||||
		// delete the head repository of the pull request
 | 
			
		||||
		forkCtx := NewAPITestContext(t, "user1", forkedRepo.Name, auth_model.AccessTokenScopeAll)
 | 
			
		||||
		doAPIDeleteRepository(forkCtx)(t)
 | 
			
		||||
 | 
			
		||||
		doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
 | 
			
		||||
			if assert.Len(t, files, 1) {
 | 
			
		||||
				assert.Equal(t, "file_1.txt", files[0].Filename)
 | 
			
		||||
				assert.Empty(t, files[0].PreviousFilename)
 | 
			
		||||
				assert.Equal(t, 1, files[0].Additions)
 | 
			
		||||
				assert.Equal(t, 1, files[0].Changes)
 | 
			
		||||
				assert.Equal(t, 0, files[0].Deletions)
 | 
			
		||||
				assert.Equal(t, "added", files[0].Status)
 | 
			
		||||
			}
 | 
			
		||||
		})(t)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user