mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			153 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					08c6ea6728 | ||
| 
						 | 
					67977f0b1c | ||
| 
						 | 
					78fbcf35ad | ||
| 
						 | 
					8f5b1d27d4 | ||
| 
						 | 
					89c99a4dcb | ||
| 
						 | 
					3c7e7a19dd | ||
| 
						 | 
					8313b5d998 | ||
| 
						 | 
					6ca73bf662 | ||
| 
						 | 
					5e10def7f7 | ||
| 
						 | 
					1b8efb6fc7 | ||
| 
						 | 
					8f89e1e174 | ||
| 
						 | 
					cbc595b9d9 | ||
| 
						 | 
					cc5ccf44dc | ||
| 
						 | 
					f91e35b8b7 | ||
| 
						 | 
					f52ed422dc | ||
| 
						 | 
					0266ee5de7 | ||
| 
						 | 
					ac03e65cf4 | ||
| 
						 | 
					f3e6672c09 | ||
| 
						 | 
					136ec9ef81 | ||
| 
						 | 
					79018ae726 | ||
| 
						 | 
					e11176192a | ||
| 
						 | 
					4e0269e890 | ||
| 
						 | 
					04114c637a | ||
| 
						 | 
					e5540bfa81 | ||
| 
						 | 
					d22d6ca0d8 | ||
| 
						 | 
					d49feab428 | ||
| 
						 | 
					9162f4403a | ||
| 
						 | 
					d05cf08fad | ||
| 
						 | 
					f4b4b0bf98 | ||
| 
						 | 
					99596044d7 | ||
| 
						 | 
					693d26914f | ||
| 
						 | 
					315f197790 | ||
| 
						 | 
					76b8f0c3a7 | ||
| 
						 | 
					f99bbd7f3f | ||
| 
						 | 
					f7ef657b5a | ||
| 
						 | 
					486d274be6 | ||
| 
						 | 
					ab3d2a944c | ||
| 
						 | 
					12bfa9e83d | ||
| 
						 | 
					dd661e92df | ||
| 
						 | 
					0b31272c7e | ||
| 
						 | 
					ec0c418719 | ||
| 
						 | 
					6dc19fc29a | ||
| 
						 | 
					9f1baa7d18 | ||
| 
						 | 
					e13deb7a16 | ||
| 
						 | 
					e5c1b8b632 | ||
| 
						 | 
					e931b62f33 | ||
| 
						 | 
					81ee93e5bc | ||
| 
						 | 
					053f9186bc | ||
| 
						 | 
					68fcdb6122 | ||
| 
						 | 
					14ca309c39 | ||
| 
						 | 
					4aba42519d | ||
| 
						 | 
					9adf175df0 | ||
| 
						 | 
					c3fa2a8729 | ||
| 
						 | 
					89dfed32e0 | ||
| 
						 | 
					d5062d0c27 | ||
| 
						 | 
					90e9e79232 | ||
| 
						 | 
					c6467edcb1 | ||
| 
						 | 
					5d5b695527 | ||
| 
						 | 
					0af7a7b79f | ||
| 
						 | 
					9339661078 | ||
| 
						 | 
					1e69f085d6 | ||
| 
						 | 
					0bfccd8ecf | ||
| 
						 | 
					534b9b35dd | ||
| 
						 | 
					dbadc59b56 | ||
| 
						 | 
					a57e2c4bc3 | ||
| 
						 | 
					acd4e10990 | ||
| 
						 | 
					0a1df294c8 | ||
| 
						 | 
					52a964d1fc | ||
| 
						 | 
					d3dbe0d9ce | ||
| 
						 | 
					cdbbdbef06 | ||
| 
						 | 
					79f555d465 | ||
| 
						 | 
					ae2b795693 | ||
| 
						 | 
					d1fdbf46bd | ||
| 
						 | 
					f27a75564a | ||
| 
						 | 
					958d0db4f4 | ||
| 
						 | 
					4c2441ba5d | ||
| 
						 | 
					6f5f0be9e3 | ||
| 
						 | 
					23d2d224c2 | ||
| 
						 | 
					a43d829de8 | ||
| 
						 | 
					8ab1363fef | ||
| 
						 | 
					178fd90852 | ||
| 
						 | 
					b39f7a37d1 | ||
| 
						 | 
					b9ed8fceff | ||
| 
						 | 
					e6ce72b14a | ||
| 
						 | 
					2eecd58bbe | ||
| 
						 | 
					64b9b21790 | ||
| 
						 | 
					3290aff964 | ||
| 
						 | 
					7ed1e8987e | ||
| 
						 | 
					f10e909fce | ||
| 
						 | 
					a3b25436f2 | ||
| 
						 | 
					b947bc4363 | ||
| 
						 | 
					18dc41d6f8 | ||
| 
						 | 
					bf5d00074d | ||
| 
						 | 
					fb4e9f92f9 | ||
| 
						 | 
					468d1919b5 | ||
| 
						 | 
					1b788946a7 | ||
| 
						 | 
					e8646ad1d8 | ||
| 
						 | 
					29dc9c784e | ||
| 
						 | 
					b1cc4bf77f | ||
| 
						 | 
					d35161ceb8 | ||
| 
						 | 
					8defca6d39 | ||
| 
						 | 
					fac434da0a | ||
| 
						 | 
					e18eae7129 | ||
| 
						 | 
					c60bc26fd3 | ||
| 
						 | 
					bacc69db83 | ||
| 
						 | 
					c5da032193 | ||
| 
						 | 
					3ace45c118 | ||
| 
						 | 
					5d6c5ce71a | ||
| 
						 | 
					7baa6fa47c | ||
| 
						 | 
					f9a0b077a7 | ||
| 
						 | 
					d3317ebabe | ||
| 
						 | 
					e9481e1da3 | ||
| 
						 | 
					8965c068e9 | ||
| 
						 | 
					eaaa158df3 | ||
| 
						 | 
					f5498421c4 | ||
| 
						 | 
					a6a14c9a92 | ||
| 
						 | 
					d0ec1788b8 | ||
| 
						 | 
					c1202f1b57 | ||
| 
						 | 
					1162cbccc0 | ||
| 
						 | 
					038990e0ff | ||
| 
						 | 
					03ff09870d | ||
| 
						 | 
					8bf4f2cc8f | ||
| 
						 | 
					21731c1370 | ||
| 
						 | 
					a0e272d95a | ||
| 
						 | 
					47537a8361 | ||
| 
						 | 
					d018c1b4b1 | ||
| 
						 | 
					d2cbe2fba0 | ||
| 
						 | 
					d6233c25b5 | ||
| 
						 | 
					2bf2d00c8a | ||
| 
						 | 
					9bd56a8ba0 | ||
| 
						 | 
					a1dc3c9bd1 | ||
| 
						 | 
					47ee84d1f3 | ||
| 
						 | 
					89f1df033a | ||
| 
						 | 
					94b67f1967 | ||
| 
						 | 
					0a9a84df11 | ||
| 
						 | 
					cdac263bb8 | ||
| 
						 | 
					a5c7df7a4c | ||
| 
						 | 
					6d738fecc4 | ||
| 
						 | 
					38cc7453e2 | ||
| 
						 | 
					b44175c071 | ||
| 
						 | 
					947358dffe | ||
| 
						 | 
					be1090cb2d | ||
| 
						 | 
					c8f3402841 | ||
| 
						 | 
					a3a95a0b67 | ||
| 
						 | 
					ed527b664d | ||
| 
						 | 
					e4717d426e | ||
| 
						 | 
					16f15d2f7b | ||
| 
						 | 
					b3f5196241 | ||
| 
						 | 
					6c5f0af45d | ||
| 
						 | 
					c95cb7c7e2 | ||
| 
						 | 
					6747e3e0eb | ||
| 
						 | 
					a12b5b3640 | ||
| 
						 | 
					834dad8cef | 
@@ -7,7 +7,7 @@
 | 
			
		||||
      "version": "20"
 | 
			
		||||
    },
 | 
			
		||||
    "ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
 | 
			
		||||
    "ghcr.io/devcontainers-contrib/features/poetry:2": {},
 | 
			
		||||
    "ghcr.io/devcontainers-extra/features/poetry:2": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/python:1": {
 | 
			
		||||
      "version": "3.12"
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    services:
 | 
			
		||||
      pgsql:
 | 
			
		||||
        image: postgres:12
 | 
			
		||||
        image: postgres:14
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_DB: test
 | 
			
		||||
          POSTGRES_PASSWORD: postgres
 | 
			
		||||
@@ -31,7 +31,7 @@ jobs:
 | 
			
		||||
      minio:
 | 
			
		||||
        # as github actions doesn't support "entrypoint", we need to use a non-official image
 | 
			
		||||
        # that has a custom entrypoint set to "minio server /data"
 | 
			
		||||
        image: bitnami/minio:2023.8.31
 | 
			
		||||
        image: bitnamilegacy/minio:2023.8.31
 | 
			
		||||
        env:
 | 
			
		||||
          MINIO_ROOT_USER: 123456
 | 
			
		||||
          MINIO_ROOT_PASSWORD: 12345678
 | 
			
		||||
@@ -113,7 +113,7 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - 6379:6379
 | 
			
		||||
      minio:
 | 
			
		||||
        image: bitnami/minio:2021.3.17
 | 
			
		||||
        image: bitnamilegacy/minio:2021.3.17
 | 
			
		||||
        env:
 | 
			
		||||
          MINIO_ACCESS_KEY: 123456
 | 
			
		||||
          MINIO_SECRET_KEY: 12345678
 | 
			
		||||
@@ -155,7 +155,7 @@ jobs:
 | 
			
		||||
    services:
 | 
			
		||||
      mysql:
 | 
			
		||||
        # the bitnami mysql image has more options than the official one, it's easier to customize
 | 
			
		||||
        image: bitnami/mysql:8.0
 | 
			
		||||
        image: bitnamilegacy/mysql:8.0
 | 
			
		||||
        env:
 | 
			
		||||
          ALLOW_EMPTY_PASSWORD: true
 | 
			
		||||
          MYSQL_DATABASE: testgitea
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -39,14 +39,10 @@ _testmain.go
 | 
			
		||||
coverage.all
 | 
			
		||||
cpu.out
 | 
			
		||||
 | 
			
		||||
/modules/migration/bindata.go
 | 
			
		||||
/modules/migration/bindata.go.hash
 | 
			
		||||
/modules/options/bindata.go
 | 
			
		||||
/modules/options/bindata.go.hash
 | 
			
		||||
/modules/public/bindata.go
 | 
			
		||||
/modules/public/bindata.go.hash
 | 
			
		||||
/modules/templates/bindata.go
 | 
			
		||||
/modules/templates/bindata.go.hash
 | 
			
		||||
/modules/migration/bindata.*
 | 
			
		||||
/modules/options/bindata.*
 | 
			
		||||
/modules/public/bindata.*
 | 
			
		||||
/modules/templates/bindata.*
 | 
			
		||||
 | 
			
		||||
*.db
 | 
			
		||||
*.log
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										533
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										533
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,529 @@ 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.24.6](https://github.com/go-gitea/gitea/releases/tag/1.24.6) - 2025-09-10
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Upgrade xz to v0.5.15 (#35385)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix a compare page 404 bug when the pull request disabled (#35441) (#35453)
 | 
			
		||||
  * Fix bug when issue disabled, pull request number in the commit message cannot be redirected (#35420) (#35442)
 | 
			
		||||
  * Add author.name field to Swift Package Registry API response (#35410) (#35431)
 | 
			
		||||
  * Remove usernames when empty in discord webhook (#35412) (#35417)
 | 
			
		||||
  * Allow foreachref parser to grow its buffer (#35365) (#35376)
 | 
			
		||||
  * Allow deleting comment with content via API like web did (#35346) (#35354)
 | 
			
		||||
  * Fix atom/rss mixed error (#35345) (#35347)
 | 
			
		||||
  * Fix review request webhook bug (#35339)
 | 
			
		||||
  * Remove duplicate html IDs (#35210) (#35325)
 | 
			
		||||
  * Fix LFS range size header response (#35277) (#35293)
 | 
			
		||||
  * Fix GitHub release assets URL validation (#35287) (#35290)
 | 
			
		||||
  * Fix token lifetime, closes #35230 (#35271) (#35281)
 | 
			
		||||
  * Fix push commits comments when changing the pull request target branch (#35386) (#35443)
 | 
			
		||||
 | 
			
		||||
## [1.24.5](https://github.com/go-gitea/gitea/releases/tag/v1.24.5) - 2025-08-12
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix a bug where lfs gc never worked. (#35198) (#35255)
 | 
			
		||||
  * Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
 | 
			
		||||
  * Fix bug when review pull request commits (#35192) (#35246)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Vertically center "Show Resolved" (#35211) (#35218)
 | 
			
		||||
 | 
			
		||||
## [1.24.4](https://github.com/go-gitea/gitea/releases/tag/v1.24.4) - 2025-08-03
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix various bugs (1.24) (#35186)
 | 
			
		||||
  * Fix migrate input box bug (#35166) (#35171)
 | 
			
		||||
  * Only hide dropzone when no files have been uploaded (#35156) (#35167)
 | 
			
		||||
  * Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099)
 | 
			
		||||
  * Fix submodule nil check (#35096) (#35098)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
 | 
			
		||||
  * Increase gap on latest commit (#35104) (#35113)
 | 
			
		||||
 | 
			
		||||
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/v1.24.3) - 2025-07-15
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix form property assignment edge case (#35073) (#35078)
 | 
			
		||||
  * Improve submodule relative path handling (#35056) (#35075)
 | 
			
		||||
  * Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
 | 
			
		||||
  * Fix updating user visibility (#35036) (#35044)
 | 
			
		||||
  * Support base64-encoded agit push options (#35037) (#35041)
 | 
			
		||||
  * Make submodule link work with relative path (#35034) (#35038)
 | 
			
		||||
  * Fix bug when displaying git user avatar in commits list (#35006)
 | 
			
		||||
  * Fix API response for swagger spec (#35029)
 | 
			
		||||
  * Start automerge check again after the conflict check and the schedule (#34988) (#35002)
 | 
			
		||||
  * Fix the response format for actions/workflows (#35009) (#35016)
 | 
			
		||||
  * Fix repo settings and protocol log problems (#35012) (#35013)
 | 
			
		||||
  * Fix project images scroll (#34971) (#34972)
 | 
			
		||||
  * Mark old reviews as stale on agit pr updates (#34933) (#34965)
 | 
			
		||||
  * Fix git graph page (#34948) (#34949)
 | 
			
		||||
  * Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
 | 
			
		||||
  * Fix some log and UI problems (#34863) (#34868)
 | 
			
		||||
  * Fix archive API (#34853) (#34857)
 | 
			
		||||
  * Ignore force pushes for changed files in a PR review (#34837) (#34843)
 | 
			
		||||
  * Fix SSH LFS timeout (#34838) (#34842)
 | 
			
		||||
  * Fix team permissions (#34827) (#34836)
 | 
			
		||||
  * Fix job status aggregation logic (#34823) (#34835)
 | 
			
		||||
  * Fix issue filter (#34914) (#34915)
 | 
			
		||||
  * Fix typo in pull request merge warning message text (#34899) (#34903)
 | 
			
		||||
  * Support the open-icon of folder (#34168) (#34896)
 | 
			
		||||
  * Optimize flex layout of release attachment area (#34885) (#34886)
 | 
			
		||||
  * Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
 | 
			
		||||
  * Skip updating timestamp when sync branch (#34875)
 | 
			
		||||
  * Fix required contexts and commit status matching bug (#34815) (#34829)
 | 
			
		||||
 | 
			
		||||
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/v1.24.2) - 2025-06-20
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix container range bug (#34795) (#34796)
 | 
			
		||||
  * Upgrade chi to v5.2.2 (#34798) (#34799)
 | 
			
		||||
* BUILD
 | 
			
		||||
  * Bump poetry feature to new url for dev container (#34787) (#34790)
 | 
			
		||||
 | 
			
		||||
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/v1.24.1) - 2025-06-18
 | 
			
		||||
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Improve alignment of commit status icon on commit page (#34750) (#34757)
 | 
			
		||||
  * Support title and body query parameters for new PRs (#34537) (#34752)
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * When using rules to delete packages, remove unclean bugs (#34632) (#34761)
 | 
			
		||||
  * Fix ghost user in feeds when pushing in an actions, it should be gitea-actions (#34703) (#34756)
 | 
			
		||||
  * Prevent double markdown link brackets when pasting URL (#34745) (#34748)
 | 
			
		||||
  * Prevent duplicate form submissions when creating forks (#34714) (#34735)
 | 
			
		||||
  * Fix markdown wrap (#34697) (#34702)
 | 
			
		||||
  * Fix pull requests API convert panic when head repository is deleted. (#34685) (#34687)
 | 
			
		||||
  * Fix commit message rendering and some UI problems (#34680) (#34683)
 | 
			
		||||
  * Fix container range bug (#34725) (#34732)
 | 
			
		||||
  * Fix incorrect cli default values (#34765) (#34766)
 | 
			
		||||
  * Fix dropdown filter (#34708) (#34711)
 | 
			
		||||
  * Hide href attribute of a tag if there is no target_url (#34556) (#34684)
 | 
			
		||||
  * Fix tag target (#34781) #34783
 | 
			
		||||
 | 
			
		||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
 | 
			
		||||
  * Improve log format (#33814)
 | 
			
		||||
  * Fix markdown render behaviors (#34122)
 | 
			
		||||
  * Add package version api endpoints (#34173)
 | 
			
		||||
 | 
			
		||||
* FEATURES
 | 
			
		||||
  * Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187)
 | 
			
		||||
  * Add fullscreen mode as a more efficient operation way to view projects (#34081)
 | 
			
		||||
  * Add anonymous access support for private/unlisted repositories (#34051)
 | 
			
		||||
  * Support public code/issue access for private repositories (#33127)
 | 
			
		||||
  * Add middleware for request prioritization (#33951)
 | 
			
		||||
  * Add cli flags LDAP group configuration (#33933)
 | 
			
		||||
  * Add file tree to file view page (#32721)
 | 
			
		||||
  * Add material icons for file list (#33837)
 | 
			
		||||
  * Artifacts download api for artifact actions v4 (#33510)
 | 
			
		||||
  * Support choose email when creating a commit via web UI (#33432)
 | 
			
		||||
  * Add basic auth support to rss/atom feeds (#33371)
 | 
			
		||||
  * Add sorting by exclusive labels (issue priority) (#33206)
 | 
			
		||||
  * Add sub issue list support (#32940)
 | 
			
		||||
  * Private README.md for organization (#32872)
 | 
			
		||||
  * Email option to embed images as base64 instead of link (#32061)
 | 
			
		||||
  * Option to delay conflict checking of old pull requests until page view (#27779)
 | 
			
		||||
  * Worktime tracking for the organization level (#19808)
 | 
			
		||||
 | 
			
		||||
* PERFORMANCE
 | 
			
		||||
  * Add cache for common package queries (#22491)
 | 
			
		||||
  * Move issue pin to an standalone table for querying performance (#33452)
 | 
			
		||||
  * Improve commits list performance to reduce unnecessary database queries (#33528)
 | 
			
		||||
  * Optimize total count of feed when loading activities in user dashboard. (#33841)
 | 
			
		||||
  * Optimize heatmap query (#33853)
 | 
			
		||||
  * Only use prev and next buttons for pagination on user dashboard (#33981)
 | 
			
		||||
  * Improve pull request list API performance (#34052)
 | 
			
		||||
  * Cache GPG keys, emails and users when list commits (#34086)
 | 
			
		||||
  * Refactor Git Attribute & performance optimization (#34154)
 | 
			
		||||
  * Performance optimization for tags synchronization (#34355) #34522
 | 
			
		||||
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Code
 | 
			
		||||
    * Display when a release attachment was uploaded (#34261)
 | 
			
		||||
    * Support creating relative link to raw path in markdown (#34105)
 | 
			
		||||
    * Improve code block readability and isolate copy button (#34009)
 | 
			
		||||
    * Improve repository commit view (#33877)
 | 
			
		||||
    * Full-file syntax highlighting for diff pages (#33766)
 | 
			
		||||
    * Clone repository with Tea CLI (#33725)
 | 
			
		||||
    * Improve sync fork behavior (#33319)
 | 
			
		||||
    * Make git clone URL could use current signed-in user (#33091)
 | 
			
		||||
    * Add submodule diff links (#33097)
 | 
			
		||||
    * Link to tree views of submodules if possible (#33424)
 | 
			
		||||
    * Only keep popular licenses (#33832)
 | 
			
		||||
    * De-emphasize signed commits (#31160)
 | 
			
		||||
 | 
			
		||||
  * Actions
 | 
			
		||||
    * Add flat-square action badge style (#34062)
 | 
			
		||||
    * Update action status badge layout (#34018)
 | 
			
		||||
    * Download actions job logs from API (#33858)
 | 
			
		||||
    * Always show the "rerun" button for action jobs (#33692)
 | 
			
		||||
    * Add auto-expanding running actions step (#30058)
 | 
			
		||||
    * Update status check for all supported on.pull_request.types in Gitea (#33117)
 | 
			
		||||
    * Workflow_dispatch use workflow from trigger branch (#33098)
 | 
			
		||||
    * Add action auto-scroll (#30057)
 | 
			
		||||
    * Add workflow_job webhook (#33694)
 | 
			
		||||
    * Add a button editing action secret (#34462)
 | 
			
		||||
 | 
			
		||||
  * Pull Request
 | 
			
		||||
    * Auto expand "New PR" form (#33971)
 | 
			
		||||
    * Mark parent directory as viewed when all files are viewed (#33958)
 | 
			
		||||
    * Show info about maintainers are allowed to edit a PR (#33738)
 | 
			
		||||
    * Automerge supports deleting branch automatically after merging (#32343)
 | 
			
		||||
    * Add additional command hints for PowerShell & CMD (#33548)
 | 
			
		||||
 | 
			
		||||
  * Issues
 | 
			
		||||
    * Allow filtering issues by any assignee (#33343)
 | 
			
		||||
    * Show warning on navigation if currently editing comment or title (#32920)
 | 
			
		||||
    * Make tracked time representation display as hours (#33315)
 | 
			
		||||
    * Add No Results Prompt Message on Issue List Page (#33699)
 | 
			
		||||
    * Add sort option recentclose for issues and pulls (#34525) #34539
 | 
			
		||||
 | 
			
		||||
  * Packages
 | 
			
		||||
    * Link to nuget dependencies (#26554)
 | 
			
		||||
    * Add composor source field (#33502)
 | 
			
		||||
 | 
			
		||||
  * Administration
 | 
			
		||||
    * Improve navbar: add "admin" tip, add "active" style (#32927)
 | 
			
		||||
    * Add a option "--user-type bot" to admin user create, improve role display (#27885)
 | 
			
		||||
    * Improve admin user view page (#33735)
 | 
			
		||||
    * Support performance trace (#32973)
 | 
			
		||||
    * Change pprof labels to be prometheus compatible (#32865)
 | 
			
		||||
    * Allow admins and org owners to change org member public status (#28294)
 | 
			
		||||
    * Optimize the installation page (#32994)
 | 
			
		||||
    * Make public URL generation configurable (#34250)
 | 
			
		||||
    * Add a --fullname arg to gitea admin user create. (#34241)
 | 
			
		||||
 | 
			
		||||
  * Others
 | 
			
		||||
    * Improve oauth2 error handling (#33969)
 | 
			
		||||
    * Fail mirroring more gracefully (#34002)
 | 
			
		||||
    * Align User Details Page Header Layout with Design Specifications (#34192)
 | 
			
		||||
    * Webhook add X-Gitea-Hook-Installation-Target-Type Header (#33752)
 | 
			
		||||
    * Optimize the dashboard (#32990)
 | 
			
		||||
    * Improve button layout on small screens (#33633)
 | 
			
		||||
    * Add cropping support when modifying the user/org/repo avatar (#33498)
 | 
			
		||||
    * Make ROOT_URL support using request Host header (#32564)
 | 
			
		||||
    * Add `show more` organizations icon in user's profile (#32986)
 | 
			
		||||
    * Introduce `--page-space-bottom` at 64px (#30692)
 | 
			
		||||
    * Improve theme display (#30671)
 | 
			
		||||
    * Add alphabetical project sorting (#33504)
 | 
			
		||||
    * Add global lock for migrations to make upgrade more safe with multiple replications (#33706)
 | 
			
		||||
    * Add descriptions for private repo public access settings and improve the UI (#34057)
 | 
			
		||||
 | 
			
		||||
* API
 | 
			
		||||
  * Actions Runner rest api (#33873)
 | 
			
		||||
  * Inclusion of rename organization api (#33303)
 | 
			
		||||
  * Add API to support link package to repository and unlink it (#33481)
 | 
			
		||||
  * Add API endpoint to request contents of multiple files simultaniously (#34139)
 | 
			
		||||
  * Actions artifacts API list/download check status upload confirmed (#34273)
 | 
			
		||||
  * Add API routes to lock and unlock issues (#34165)
 | 
			
		||||
  * Fix some user name usages (#33689)
 | 
			
		||||
  * Allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684)
 | 
			
		||||
  * Improve swagger generation (#33664)
 | 
			
		||||
  * Support Ephemeral action runners (#33570)
 | 
			
		||||
  * Support workflow event dispatch via API (#33545)
 | 
			
		||||
  * Support workflow event dispatch via API (#32059)
 | 
			
		||||
  * Added Description Field for Secrets and Variables  (#33526)
 | 
			
		||||
  * Reject star-related requests if stars are disabled (#33208)
 | 
			
		||||
  * Let API create and edit system webhooks, attempt 2 (#33180)
 | 
			
		||||
  * Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089)
 | 
			
		||||
  * Add `last_committer_date` and `last_author_date` for file contents API (#32921)
 | 
			
		||||
 | 
			
		||||
* REFACTORS
 | 
			
		||||
  * Remove context from git struct (#33793)
 | 
			
		||||
  * Refactor admin/common.ts (#33788)
 | 
			
		||||
  * Refactor repo-settings.ts (#33785)
 | 
			
		||||
  * Refactor repo-issue.ts (#33784)
 | 
			
		||||
  * Small refactor to reduce unnecessary database queries and remove duplicated functions (#33779)
 | 
			
		||||
  * Refactor initRepoBranchTagSelector to use new init framework (#33776)
 | 
			
		||||
  * Refactor buttons to use new init framework (#33774)
 | 
			
		||||
  * Refactor markup and pdf-viewer to use new init framework (#33772)
 | 
			
		||||
  * Refactor error system (#33771)
 | 
			
		||||
  * Refactor mail code (#33768)
 | 
			
		||||
  * Update TypeScript types (#33799)
 | 
			
		||||
  * Refactor older tests to use testify (#33140)
 | 
			
		||||
  * Move notifywatch to service layer (#33825)
 | 
			
		||||
  * Decouple context from repository related structs (#33823)
 | 
			
		||||
  * Remove context from mail struct (#33811)
 | 
			
		||||
  * Refactor dropdown ellipsis (#34123)
 | 
			
		||||
  * Refactor functions to reduce repopath expose (#33892)
 | 
			
		||||
  * Refactor repo-diff.ts (#33746)
 | 
			
		||||
  * Refactor web route handler (#33488)
 | 
			
		||||
  * Refactor user & avatar (#33433)
 | 
			
		||||
  * Refactor user package (#33423)
 | 
			
		||||
  * Refactor decouple context from migration structs (#33399)
 | 
			
		||||
  * Refactor context flash msg and global variables (#33375)
 | 
			
		||||
  * Refactor response writer & access logger (#33323)
 | 
			
		||||
  * Refactor ref type (#33242)
 | 
			
		||||
  * Refactor context repository (#33202)
 | 
			
		||||
  * Refactor legacy JS (#33115)
 | 
			
		||||
  * Refactor legacy line-number and scroll code (#33094)
 | 
			
		||||
  * Refactor env var related code (#33075)
 | 
			
		||||
  * Move SetMerged to service layer (#33045)
 | 
			
		||||
  * Merge updatecommentattachment functions (#33044)
 | 
			
		||||
  * Refactor pull-request compare&create page (#33071)
 | 
			
		||||
  * Refactor repo-new.ts (#33070)
 | 
			
		||||
  * Refactor pagination (#33037)
 | 
			
		||||
  * Refactor tests (#33021)
 | 
			
		||||
  * Refactor markup render to fix various path problems (#34114)
 | 
			
		||||
  * Refactor Branch struct in package modules/git (#33980)
 | 
			
		||||
  * Don't create duplicated functions for code repositories and wiki repositories (#33924)
 | 
			
		||||
  * Move git references checking to gitrepo packages to reduce expose of repository path (#33891)
 | 
			
		||||
  * Refactor cache-control (#33861)
 | 
			
		||||
  * Decouple diff stats query from actual diffing (#33810)
 | 
			
		||||
  * Move part of updating protected branch logic to service layer (#33742)
 | 
			
		||||
  * Decouple Batch from git.Repository to simplify usage without requiring the creation of a Repository struct. (#34001)
 | 
			
		||||
  * Refactor tmpl and blob_excerpt (#32967)
 | 
			
		||||
  * Refactor template & test related code (#32938)
 | 
			
		||||
  * Refactor db package and remove unnecessary `DumpTables` (#32930)
 | 
			
		||||
  * Refactor pprof labels and process desc (#32909)
 | 
			
		||||
  * Refactor repo-projects.ts (#32892)
 | 
			
		||||
  * Refactor getpatch/getdiff functions and remove unnecessary fallback (#32817)
 | 
			
		||||
  * Uniform all temporary directories and allow customizing temp path (#32352)
 | 
			
		||||
  * Remove context from retry downloader (#33871)
 | 
			
		||||
  * Refactor global init code and add more comments (#33755)
 | 
			
		||||
  * Remove some unnecessary template helpers (#33069)
 | 
			
		||||
  * Move and rename UpdateRepository (#34136)
 | 
			
		||||
  * Move hooks function to gitrepo and reduce expose repopath (#33890)
 | 
			
		||||
  * Add abstraction layer to delete repository from disk (#33879)
 | 
			
		||||
  * Add abstraction layer to check if the repository exists on disk (#33874)
 | 
			
		||||
  * Move ParseCommitWithSSHSignature to service layer (#34087)
 | 
			
		||||
  * Move duplicated functions (#33977)
 | 
			
		||||
  * Extract code to their own functions for push update (#33944)
 | 
			
		||||
  * Move gitgraph from modules to services layer (#33527)
 | 
			
		||||
  * Move commits signature and verify functions to service layers (#33605)
 | 
			
		||||
  * Use `CloseIssue` and `ReopenIssue` instead of `ChangeStatus` (#32467)
 | 
			
		||||
  * Refactor arch route handlers (#32993)
 | 
			
		||||
  * Refactor "string truncate" (#32984)
 | 
			
		||||
  * Refactor arch route handlers (#32972)
 | 
			
		||||
  * Clarify path param naming (#32969)
 | 
			
		||||
  * Refactor request context (#32956)
 | 
			
		||||
  * Move some errors to their own sub packages (#32880)
 | 
			
		||||
  * Move RepoTransfer from models to models/repo sub package (#32506)
 | 
			
		||||
  * Move delete deploy keys into service layer (#32201)
 | 
			
		||||
  * Refactor webhook events (#33337)
 | 
			
		||||
  * Move some Actions related functions from `routers` to `services` (#33280)
 | 
			
		||||
  * Refactor RefName (#33234)
 | 
			
		||||
  * Refactor context RefName and RepoAssignment (#33226)
 | 
			
		||||
  * Refactor repository transfer (#33211)
 | 
			
		||||
  * Refactor error system (#33626)
 | 
			
		||||
  * Refactor error system (#33610)
 | 
			
		||||
  * Refactor package (routes and error handling, npm peer dependency) (#33111)
 | 
			
		||||
  * Use test context in tests and new loop system in benchmarks (#33648)
 | 
			
		||||
  * Some small refactors (#33144)
 | 
			
		||||
  * Simplify context ref name (#33267)
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix some dropdown problems on the issue sidebar (#34308) #34327
 | 
			
		||||
  * Do not return archive download URLs in API if downloads are disabled (#34324) #34338
 | 
			
		||||
  * Fix LFS files being editable in web UI (#34356) #34362
 | 
			
		||||
  * Fix only text/* being viewable in web UI (#34374) #34378
 | 
			
		||||
  * Fix LFS file not stored in LFS when uploaded/edited via API or web UI (#34367)
 | 
			
		||||
  * Grey out expired artifact on Artifacts list (#34314) #34404
 | 
			
		||||
  * Fix incorrect divergence cache after switching default branch (#34370) #34406
 | 
			
		||||
  * Refactor commit message rendering and fix bugs (#34412) #34414
 | 
			
		||||
  * Merge and tweak markup editor expander CSS (#34409) #34415
 | 
			
		||||
  * Fix GetUsersByEmails (#34423) #34425
 | 
			
		||||
  * Only git operations should update last changed of a repository (#34388) #34427
 | 
			
		||||
  * Fix comment textarea scroll issue in Firefox (#34438) #34446
 | 
			
		||||
  * Fix repo broken check (#34444) #34452
 | 
			
		||||
  * Fix remove org user failure on mssql (#34449) #34453
 | 
			
		||||
  * Fix Workflow run Not Found page (#34459) #34466
 | 
			
		||||
  * When updating comment, if the content is the same, just return and not update the database (#34422) #34464
 | 
			
		||||
  * Fix project board view (#34470) #34475
 | 
			
		||||
  * Fix get / delete runner to use consistent http 404 and 500 status (#34480) #34488
 | 
			
		||||
  * Fix url validation in webhook add/edit API (#34492) #34496
 | 
			
		||||
  * Fix edithook api can not update package, status and workflow_job events (#34495) #34499
 | 
			
		||||
  * Fix ephemeral runner deletion (#34447) #34513
 | 
			
		||||
  * Don't display error log when .git-blame-ignore-revs doesn't exist (#34457)
 | 
			
		||||
  * Only allow admins to rename default/protected branches (#33276)
 | 
			
		||||
  * Improve "lock conversation" UI (#34207)
 | 
			
		||||
  * Fix incorrect file links (#34189)
 | 
			
		||||
  * Optimize Overflow Menu (#34183)
 | 
			
		||||
  * Check user/org repo limit instead of doer (#34147)
 | 
			
		||||
  * Make markdown render match GitHub's behavior (#34129)
 | 
			
		||||
  * Fix team permission (#34128)
 | 
			
		||||
  * Correctly handle submodule view and avoid throwing 500 error (#34121)
 | 
			
		||||
  * Fix users being able bypass limits with repo transfers (#34031)
 | 
			
		||||
  * Avoid creating unnecessary temporary cat file sub process (#33942)
 | 
			
		||||
  * Refactor organization menu (#33928)
 | 
			
		||||
  * Fix various Fomantic UI and htmx problems (#33851)
 | 
			
		||||
  * Fix 500 error when error occurred in migration page (#33256)
 | 
			
		||||
  * Validate that the tag doesn't exist when creating a tag via the web (#33241)
 | 
			
		||||
  * Add missed transaction on setmerged (#33079)
 | 
			
		||||
  * Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035)
 | 
			
		||||
  * Valid email address should only start with alphanumeric (#28174)
 | 
			
		||||
  * Fix webhook url (#34186)
 | 
			
		||||
  * Fix "toAbsoluteLocaleDate" test when system locale is not en-US (#33939)
 | 
			
		||||
  * Fix file name could not be searched if the file was not a text file when using the Bleve indexer (#33959)
 | 
			
		||||
  * Fix cannot delete runners via the modal dialog (#33895)
 | 
			
		||||
  * Fix unpin hint on the pinned pull requests (#33207)
 | 
			
		||||
  * Fix parentCommit invalid memory address or nil pointer dereference. (#33204)
 | 
			
		||||
  * Fix comment header padding (#33377)
 | 
			
		||||
  * Fix some migration and repo name problems (#33986)
 | 
			
		||||
  * Fix various trivial frontend problems (#34263)
 | 
			
		||||
  * Fix Set Email Preference dropdown and button placement (#34255)
 | 
			
		||||
  * Fix quoted replies incorrectly render user input as part of the quote (#34216)
 | 
			
		||||
  * Fix button alignments and remove unnecessary styles (#34206)
 | 
			
		||||
  * Restore form inputs on organization create error (#34201)
 | 
			
		||||
  * Try to fix ACME (3rd) (#33807)
 | 
			
		||||
  * Fix incorrect ref "blob" (#33240)
 | 
			
		||||
  * Fix dynamic content loading init problem (#33748)
 | 
			
		||||
  * Fix git empty check and HEAD request (#33690)
 | 
			
		||||
  * Fix Untranslated Text on Actions Page (#33635)
 | 
			
		||||
  * Fix issue label delete incorrect labels webhook payload (#34575)
 | 
			
		||||
  * Fix incorrect page navigation with up and down arrow on last item of dashboard repos (#34570)
 | 
			
		||||
  * Fix/improve avatar sync from LDAP (#34573)
 | 
			
		||||
  * Fix some trivial problems (#34579)
 | 
			
		||||
  * Retain issue sort type when a keyword search is introduced (#34559)
 | 
			
		||||
  * Always use an empty line to separate the commit message and trailer (#34512)
 | 
			
		||||
  * Fix line-button issue after file selection in file tree (#34574)
 | 
			
		||||
  * Fix doctor deleting orphaned issues attachments (#34142)
 | 
			
		||||
  * Add webhook assigning test and fix possible bug (#34420)
 | 
			
		||||
  * Fix possible nil description of pull request when migrating from CodeCommit (#34541)
 | 
			
		||||
  * Refactor commit reader (#34542)
 | 
			
		||||
  * Fix possible pull request broken when leave the page immediately after clicking the update button #34509
 | 
			
		||||
  * Ignore "Close" error when uploading container blob (#34620)
 | 
			
		||||
  * Fix missed merge commit sha and time when migrating from codecommit (#34645)
 | 
			
		||||
  * Fix GetUsersByEmails (#34643)
 | 
			
		||||
  * Misc CSS fixes (#34638)
 | 
			
		||||
  * Add codecommit to supported services in api docs (#34626)
 | 
			
		||||
  * Validate hex colors when creating/editing labels (#34623)
 | 
			
		||||
  * Fix possible pull request broken when leave the page immediately after clicking the update button (#34509)
 | 
			
		||||
  * Fix margin issue in markup paragraph rendering (#34599)
 | 
			
		||||
  * Fix migration pull request title too long (#34577)
 | 
			
		||||
  * Fix footnote jump behavior on the issue page. (#34621)
 | 
			
		||||
  * Fix "oras" OCI client compatibility (#34666)
 | 
			
		||||
  * Fix last admin check when syncing users (#34649)
 | 
			
		||||
  * Fix skip paths check on tag push events in workflows (#34602) #34670
 | 
			
		||||
 | 
			
		||||
* MISC
 | 
			
		||||
 | 
			
		||||
  * Bump to alpine 3.22 (#34613)
 | 
			
		||||
  * Make pull request and issue history more compact (#34588)
 | 
			
		||||
  * Run integration tests against postgres 14 (#34514) #34536
 | 
			
		||||
  * Enable addtional linters (#34085)
 | 
			
		||||
  * Enable testifylint rules (#34075)
 | 
			
		||||
  * Enable staticcheck QFxxxx rules (#34064)
 | 
			
		||||
  * Improve Actions test (#32883)
 | 
			
		||||
  * Drop fomantic build (#33845)
 | 
			
		||||
  * Go1.24 (#33562)
 | 
			
		||||
  * Run yamllint with strict mode, fix issue (#33551)
 | 
			
		||||
  * Disable cron task to update license (#33486)
 | 
			
		||||
  * Optimize makefile help information generation (#33390)
 | 
			
		||||
  * Convert github.com/xanzy/go-gitlab into gitlab.com/gitlab-org/api/client-go (#33126)
 | 
			
		||||
  * Add missed changelogs (#33649)
 | 
			
		||||
  * Update .changelog file to add performance label group (#33472)
 | 
			
		||||
  * Add missing POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES in app.example.ini (#33363)
 | 
			
		||||
  * Update README screenshots (#33347)
 | 
			
		||||
  * Update unrs-resolver (#34279)
 | 
			
		||||
  * Update go&js dependencies (#34262)
 | 
			
		||||
  * Optimize the calling code of queryElems (#34235)
 | 
			
		||||
  * Update protected_branch.tmpl (#34193)
 | 
			
		||||
  * Feat/optimize span svg layout (#34185)
 | 
			
		||||
  * Set MERMAID_MAX_SOURCE_CHARACTERS to 50000 (#34152)
 | 
			
		||||
  * Update JS and PY deps (#34143)
 | 
			
		||||
  * Add Chinese translations for README files (#34132)
 | 
			
		||||
  * Use `overflow-wrap: anywhere` to replace `word-break: break-all` (#34126)
 | 
			
		||||
  * Clarify ownership in password change error messages (#34092)
 | 
			
		||||
  * Add toggleClass function in dom.ts (#34063)
 | 
			
		||||
  * Update to golangci-lint v2 (#34054)
 | 
			
		||||
  * Update Makefile test comments (#34013)
 | 
			
		||||
  * Update go mod dependencies (#33988)
 | 
			
		||||
  * Use filepath.Join instead of path.Join for file system file operations (#33978)
 | 
			
		||||
  * Prepare common tmpl functions in a middleware (#33957)
 | 
			
		||||
  * Remove unused or abused styles (#33918)
 | 
			
		||||
  * Update JS and PY deps, misc tweaks (#33903)
 | 
			
		||||
  * Try to figure out attribute checker problem (#33901)
 | 
			
		||||
  * Add lock for a repository pull mirror (#33876)
 | 
			
		||||
  * Fine tune push mirror UI (#33866)
 | 
			
		||||
  * Improve issue & code search (#33860)
 | 
			
		||||
  * Use pullrequestlist instead of []*pullrequest (#33765)
 | 
			
		||||
  * Upgrade act to 0.261.4 and actions-proto-go to v0.4.1 (#33760)
 | 
			
		||||
  * Align sidebar gears to the right (#33721)
 | 
			
		||||
  * Update Go dependencies (skip blevesearch, meilisearch) (#33655)
 | 
			
		||||
  * Add migrations and doctor fixes (#33556)
 | 
			
		||||
  * Remove "class-name" from svg icon (#33540)
 | 
			
		||||
  * Update MAINTAINERS (#33529)
 | 
			
		||||
  * Add "No data available" display when list is empty (#33517)
 | 
			
		||||
  * Use `git diff-tree` for `DiffFileTree` on diff pages (#33514)
 | 
			
		||||
  * Give organisation members access to organisation feeds (#33508)
 | 
			
		||||
  * Update feishu icon (#33470)
 | 
			
		||||
  * Hide/disable unusable UI elements when a repository is archived (#33459)
 | 
			
		||||
  * Update `@github/text-expander-element` to 2.9.0 (#33435)
 | 
			
		||||
  * Do not access GitRepo when a repo is being created (#33380)
 | 
			
		||||
  * Fix incorrect ref usages (#33301)
 | 
			
		||||
  * Prepare for support performance trace (#33286)
 | 
			
		||||
  * Enable Typescript `noImplicitThis` (#33250)
 | 
			
		||||
  * Remove unused CSS styles and move some styles to proper files (#33217)
 | 
			
		||||
  * Add .run to gitignore (#33175)
 | 
			
		||||
  * Fix typo in gitea downloader test and add missing codebase in `ToGitServiceType` (#33146)
 | 
			
		||||
  * Remove extended glob pattern from branch protection UI (#33125)
 | 
			
		||||
  * Clean up legacy form CSS styles (#33081)
 | 
			
		||||
  * Unset XDG_HOME_CONFIG as gitea manages configuration locations (#33067)
 | 
			
		||||
  * Add IntelliJ Gateway's .uuid to gitignore (#33052)
 | 
			
		||||
  * User facing messages for AGit errors (#33012)
 | 
			
		||||
  * Always show assignees on right (#33006)
 | 
			
		||||
  * Fix eslint (#33002)
 | 
			
		||||
  * Update JS dependencies (#32914)
 | 
			
		||||
  * Bump x/net (#32896) (#32900)
 | 
			
		||||
  * Only activity tab needs heatmap data loading (#34652)
 | 
			
		||||
 | 
			
		||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
 | 
			
		||||
  * 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/v1.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
 | 
			
		||||
@@ -76,7 +599,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix a bug caused by status webhook template #33512
 | 
			
		||||
 | 
			
		||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
 | 
			
		||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Add tests for webhook and fix some webhook bugs (#33396) (#33442)
 | 
			
		||||
@@ -2606,7 +3129,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Improve decryption failure message (#24573) (#24575)
 | 
			
		||||
  * Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
 | 
			
		||||
 | 
			
		||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
 | 
			
		||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
 | 
			
		||||
@@ -2619,7 +3142,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Fix incorrect CurrentUser check for docker rootless (#24435)
 | 
			
		||||
  * Getting the tag list does not require being signed in (#24413) (#24416)
 | 
			
		||||
 | 
			
		||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
 | 
			
		||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
 | 
			
		||||
@@ -3118,7 +3641,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Display attachments of review comment when comment content is blank (#23035) (#23046)
 | 
			
		||||
  * Return empty url for submodule tree entries (#23043) (#23048)
 | 
			
		||||
 | 
			
		||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
 | 
			
		||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Provide the ability to set password hash algorithm parameters (#22942) (#22943)
 | 
			
		||||
@@ -3545,7 +4068,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
 | 
			
		||||
  * Fix UI mis-align for PR commit history (#20845) (#20859)
 | 
			
		||||
 | 
			
		||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
 | 
			
		||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Correctly escape within tribute.js (#20831) (#20832)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# Build stage
 | 
			
		||||
FROM docker.io/library/golang:1.24-alpine3.21 AS build-env
 | 
			
		||||
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
 | 
			
		||||
 | 
			
		||||
ARG GOPROXY
 | 
			
		||||
ENV GOPROXY=${GOPROXY:-direct}
 | 
			
		||||
@@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/environment-to-ini
 | 
			
		||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 | 
			
		||||
 | 
			
		||||
FROM docker.io/library/alpine:3.21
 | 
			
		||||
FROM docker.io/library/alpine:3.22
 | 
			
		||||
LABEL maintainer="maintainers@gitea.io"
 | 
			
		||||
 | 
			
		||||
EXPOSE 22 3000
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# Build stage
 | 
			
		||||
FROM docker.io/library/golang:1.24-alpine3.21 AS build-env
 | 
			
		||||
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
 | 
			
		||||
 | 
			
		||||
ARG GOPROXY
 | 
			
		||||
ENV GOPROXY=${GOPROXY:-direct}
 | 
			
		||||
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/environment-to-ini
 | 
			
		||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 | 
			
		||||
 | 
			
		||||
FROM docker.io/library/alpine:3.21
 | 
			
		||||
FROM docker.io/library/alpine:3.22
 | 
			
		||||
LABEL maintainer="maintainers@gitea.io"
 | 
			
		||||
 | 
			
		||||
EXPOSE 2222 3000
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Makefile
									
									
									
									
									
								
							@@ -47,6 +47,17 @@ ifeq ($(HAS_GO), yes)
 | 
			
		||||
	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
CGO_ENABLED ?= 0
 | 
			
		||||
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
 | 
			
		||||
	CGO_ENABLED = 1
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
STATIC ?=
 | 
			
		||||
EXTLDFLAGS ?=
 | 
			
		||||
ifneq ($(STATIC),)
 | 
			
		||||
	EXTLDFLAGS = -extldflags "-static"
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(GOOS),windows)
 | 
			
		||||
	IS_WINDOWS := yes
 | 
			
		||||
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
 | 
			
		||||
@@ -740,7 +751,10 @@ security-check:
 | 
			
		||||
	go run $(GOVULNCHECK_PACKAGE) -show color ./...
 | 
			
		||||
 | 
			
		||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 | 
			
		||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
 | 
			
		||||
  $(error pam support set via TAGS doesn't support static builds)
 | 
			
		||||
endif
 | 
			
		||||
	CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
 | 
			
		||||
 | 
			
		||||
.PHONY: release
 | 
			
		||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -38,12 +38,10 @@ var (
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "force-smtps",
 | 
			
		||||
			Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "skip-verify",
 | 
			
		||||
			Usage: "Skip TLS verify.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "helo-hostname",
 | 
			
		||||
@@ -53,7 +51,6 @@ var (
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "disable-helo",
 | 
			
		||||
			Usage: "Disable SMTP helo.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "allowed-domains",
 | 
			
		||||
@@ -63,7 +60,6 @@ var (
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "skip-local-2fa",
 | 
			
		||||
			Usage: "Skip 2FA to log on.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "active",
 | 
			
		||||
 
 | 
			
		||||
@@ -80,6 +80,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDumpRepository(ctx *cli.Context) error {
 | 
			
		||||
	setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
 | 
			
		||||
 | 
			
		||||
	setting.DisableLoggerInit()
 | 
			
		||||
	setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
 | 
			
		||||
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	hookBatchSize = 30
 | 
			
		||||
	hookBatchSize = 500
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
 
 | 
			
		||||
@@ -118,7 +118,6 @@ var (
 | 
			
		||||
								Name:    "rotate",
 | 
			
		||||
								Aliases: []string{"r"},
 | 
			
		||||
								Usage:   "Rotate logs",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.Int64Flag{
 | 
			
		||||
								Name:    "max-size",
 | 
			
		||||
@@ -129,7 +128,6 @@ var (
 | 
			
		||||
								Name:    "daily",
 | 
			
		||||
								Aliases: []string{"d"},
 | 
			
		||||
								Usage:   "Rotate logs daily",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.IntFlag{
 | 
			
		||||
								Name:    "max-days",
 | 
			
		||||
@@ -140,7 +138,6 @@ var (
 | 
			
		||||
								Name:    "compress",
 | 
			
		||||
								Aliases: []string{"z"},
 | 
			
		||||
								Usage:   "Compress rotated logs",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.IntFlag{
 | 
			
		||||
								Name:    "compression-level",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								cmd/serv.go
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								cmd/serv.go
									
									
									
									
									
								
							@@ -11,7 +11,6 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -20,7 +19,7 @@ import (
 | 
			
		||||
	asymkey_model "code.gitea.io/gitea/models/asymkey"
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/lfstransfer"
 | 
			
		||||
@@ -37,14 +36,6 @@ import (
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	verbUploadPack      = "git-upload-pack"
 | 
			
		||||
	verbUploadArchive   = "git-upload-archive"
 | 
			
		||||
	verbReceivePack     = "git-receive-pack"
 | 
			
		||||
	verbLfsAuthenticate = "git-lfs-authenticate"
 | 
			
		||||
	verbLfsTransfer     = "git-lfs-transfer"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdServ represents the available serv sub-command.
 | 
			
		||||
var CmdServ = &cli.Command{
 | 
			
		||||
	Name:        "serv",
 | 
			
		||||
@@ -78,22 +69,6 @@ func setup(ctx context.Context, debug bool) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// keep getAccessMode() in sync
 | 
			
		||||
	allowedCommands = container.SetOf(
 | 
			
		||||
		verbUploadPack,
 | 
			
		||||
		verbUploadArchive,
 | 
			
		||||
		verbReceivePack,
 | 
			
		||||
		verbLfsAuthenticate,
 | 
			
		||||
		verbLfsTransfer,
 | 
			
		||||
	)
 | 
			
		||||
	allowedCommandsLfs = container.SetOf(
 | 
			
		||||
		verbLfsAuthenticate,
 | 
			
		||||
		verbLfsTransfer,
 | 
			
		||||
	)
 | 
			
		||||
	alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// fail prints message to stdout, it's mainly used for git serv and git hook commands.
 | 
			
		||||
// The output will be passed to git client and shown to user.
 | 
			
		||||
func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error {
 | 
			
		||||
@@ -139,19 +114,20 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
 | 
			
		||||
 | 
			
		||||
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
 | 
			
		||||
	switch verb {
 | 
			
		||||
	case verbUploadPack, verbUploadArchive:
 | 
			
		||||
	case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
 | 
			
		||||
		return perm.AccessModeRead
 | 
			
		||||
	case verbReceivePack:
 | 
			
		||||
	case git.CmdVerbReceivePack:
 | 
			
		||||
		return perm.AccessModeWrite
 | 
			
		||||
	case verbLfsAuthenticate, verbLfsTransfer:
 | 
			
		||||
	case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
 | 
			
		||||
		switch lfsVerb {
 | 
			
		||||
		case "upload":
 | 
			
		||||
		case git.CmdSubVerbLfsUpload:
 | 
			
		||||
			return perm.AccessModeWrite
 | 
			
		||||
		case "download":
 | 
			
		||||
		case git.CmdSubVerbLfsDownload:
 | 
			
		||||
			return perm.AccessModeRead
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// should be unreachable
 | 
			
		||||
	setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
 | 
			
		||||
	return perm.AccessModeNone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -230,12 +206,12 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
		log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	words, err := shellquote.Split(cmd)
 | 
			
		||||
	sshCmdArgs, err := shellquote.Split(cmd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(words) < 2 {
 | 
			
		||||
	if len(sshCmdArgs) < 2 {
 | 
			
		||||
		if git.DefaultFeatures().SupportProcReceive {
 | 
			
		||||
			// for AGit Flow
 | 
			
		||||
			if cmd == "ssh_info" {
 | 
			
		||||
@@ -246,25 +222,21 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
		return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	verb := words[0]
 | 
			
		||||
	repoPath := strings.TrimPrefix(words[1], "/")
 | 
			
		||||
 | 
			
		||||
	var lfsVerb string
 | 
			
		||||
 | 
			
		||||
	rr := strings.SplitN(repoPath, "/", 2)
 | 
			
		||||
	if len(rr) != 2 {
 | 
			
		||||
	repoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
 | 
			
		||||
	repoPathFields := strings.SplitN(repoPath, "/", 2)
 | 
			
		||||
	if len(repoPathFields) != 2 {
 | 
			
		||||
		return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	username := rr[0]
 | 
			
		||||
	reponame := strings.TrimSuffix(rr[1], ".git")
 | 
			
		||||
	username := repoPathFields[0]
 | 
			
		||||
	reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
 | 
			
		||||
 | 
			
		||||
	// LowerCase and trim the repoPath as that's how they are stored.
 | 
			
		||||
	// This should be done after splitting the repoPath into username and reponame
 | 
			
		||||
	// so that username and reponame are not affected.
 | 
			
		||||
	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
 | 
			
		||||
 | 
			
		||||
	if alphaDashDotPattern.MatchString(reponame) {
 | 
			
		||||
	if !repo.IsValidSSHAccessRepoName(reponame) {
 | 
			
		||||
		return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -286,22 +258,23 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if allowedCommands.Contains(verb) {
 | 
			
		||||
		if allowedCommandsLfs.Contains(verb) {
 | 
			
		||||
			if !setting.LFS.StartServer {
 | 
			
		||||
				return fail(ctx, "LFS Server is not enabled", "")
 | 
			
		||||
			}
 | 
			
		||||
			if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
				return fail(ctx, "LFS SSH transfer is not enabled", "")
 | 
			
		||||
			}
 | 
			
		||||
			if len(words) > 2 {
 | 
			
		||||
				lfsVerb = words[2]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
	verb, lfsVerb := sshCmdArgs[0], ""
 | 
			
		||||
	if !git.IsAllowedVerbForServe(verb) {
 | 
			
		||||
		return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if git.IsAllowedVerbForServeLfs(verb) {
 | 
			
		||||
		if !setting.LFS.StartServer {
 | 
			
		||||
			return fail(ctx, "LFS Server is not enabled", "")
 | 
			
		||||
		}
 | 
			
		||||
		if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
			return fail(ctx, "LFS SSH transfer is not enabled", "")
 | 
			
		||||
		}
 | 
			
		||||
		if len(sshCmdArgs) > 2 {
 | 
			
		||||
			lfsVerb = sshCmdArgs[2]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestedMode := getAccessMode(verb, lfsVerb)
 | 
			
		||||
 | 
			
		||||
	results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
 | 
			
		||||
@@ -310,7 +283,7 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// LFS SSH protocol
 | 
			
		||||
	if verb == verbLfsTransfer {
 | 
			
		||||
	if verb == git.CmdVerbLfsTransfer {
 | 
			
		||||
		token, err := getLFSAuthToken(ctx, lfsVerb, results)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@@ -319,7 +292,7 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// LFS token authentication
 | 
			
		||||
	if verb == verbLfsAuthenticate {
 | 
			
		||||
	if verb == git.CmdVerbLfsAuthenticate {
 | 
			
		||||
		url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
 | 
			
		||||
 | 
			
		||||
		token, err := getLFSAuthToken(ctx, lfsVerb, results)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,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"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@@ -20,11 +20,11 @@
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1739214665,
 | 
			
		||||
        "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
 | 
			
		||||
        "lastModified": 1752480373,
 | 
			
		||||
        "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
 | 
			
		||||
        "owner": "nixos",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
 | 
			
		||||
        "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								flake.nix
									
									
									
									
									
								
							@@ -11,33 +11,45 @@
 | 
			
		||||
        pkgs = nixpkgs.legacyPackages.${system};
 | 
			
		||||
      in
 | 
			
		||||
      {
 | 
			
		||||
        devShells.default = pkgs.mkShell {
 | 
			
		||||
          buildInputs = with pkgs; [
 | 
			
		||||
            # generic
 | 
			
		||||
            git
 | 
			
		||||
            git-lfs
 | 
			
		||||
            gnumake
 | 
			
		||||
            gnused
 | 
			
		||||
            gnutar
 | 
			
		||||
            gzip
 | 
			
		||||
        devShells.default =
 | 
			
		||||
          with pkgs;
 | 
			
		||||
          let
 | 
			
		||||
            # only bump toolchain versions here
 | 
			
		||||
            go = go_1_24;
 | 
			
		||||
            nodejs = nodejs_24;
 | 
			
		||||
            python3 = python312;
 | 
			
		||||
          in
 | 
			
		||||
          pkgs.mkShell {
 | 
			
		||||
            buildInputs = [
 | 
			
		||||
              # generic
 | 
			
		||||
              git
 | 
			
		||||
              git-lfs
 | 
			
		||||
              gnumake
 | 
			
		||||
              gnused
 | 
			
		||||
              gnutar
 | 
			
		||||
              gzip
 | 
			
		||||
 | 
			
		||||
            # frontend
 | 
			
		||||
            nodejs_22
 | 
			
		||||
              # frontend
 | 
			
		||||
              nodejs
 | 
			
		||||
 | 
			
		||||
            # linting
 | 
			
		||||
            python312
 | 
			
		||||
            poetry
 | 
			
		||||
              # linting
 | 
			
		||||
              python3
 | 
			
		||||
              poetry
 | 
			
		||||
 | 
			
		||||
            # backend
 | 
			
		||||
            go_1_24
 | 
			
		||||
            gofumpt
 | 
			
		||||
            sqlite
 | 
			
		||||
          ];
 | 
			
		||||
          shellHook = ''
 | 
			
		||||
            export GO="${pkgs.go_1_24}/bin/go"
 | 
			
		||||
            export GOROOT="${pkgs.go_1_24}/share/go"
 | 
			
		||||
          '';
 | 
			
		||||
        };
 | 
			
		||||
              # backend
 | 
			
		||||
              go
 | 
			
		||||
              glibc.static
 | 
			
		||||
              gofumpt
 | 
			
		||||
              sqlite
 | 
			
		||||
            ];
 | 
			
		||||
            CFLAGS = "-I${glibc.static.dev}/include";
 | 
			
		||||
            LDFLAGS = "-L ${glibc.static}/lib";
 | 
			
		||||
            GO = "${go}/bin/go";
 | 
			
		||||
            GOROOT = "${go}/share/go";
 | 
			
		||||
 | 
			
		||||
            TAGS = "sqlite sqlite_unlock_notify";
 | 
			
		||||
            STATIC = "true";
 | 
			
		||||
          };
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@@ -51,7 +51,7 @@ require (
 | 
			
		||||
	github.com/gliderlabs/ssh v0.3.8
 | 
			
		||||
	github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
 | 
			
		||||
	github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
 | 
			
		||||
	github.com/go-chi/chi/v5 v5.2.1
 | 
			
		||||
	github.com/go-chi/chi/v5 v5.2.2
 | 
			
		||||
	github.com/go-chi/cors v1.2.1
 | 
			
		||||
	github.com/go-co-op/gocron v1.37.0
 | 
			
		||||
	github.com/go-enry/go-enry/v2 v2.9.2
 | 
			
		||||
@@ -66,7 +66,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-20250422154841-e1f9c1950416
 | 
			
		||||
	github.com/google/uuid v1.6.0
 | 
			
		||||
@@ -109,7 +109,7 @@ require (
 | 
			
		||||
	github.com/stretchr/testify v1.10.0
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	github.com/tstranex/u2f v1.0.0
 | 
			
		||||
	github.com/ulikunitz/xz v0.5.12
 | 
			
		||||
	github.com/ulikunitz/xz v0.5.15
 | 
			
		||||
	github.com/urfave/cli/v2 v2.27.6
 | 
			
		||||
	github.com/wneessen/go-mail v0.6.2
 | 
			
		||||
	github.com/xeipuuv/gojsonschema v1.2.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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
 | 
			
		||||
gitea.com/gitea/act v0.261.4/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=
 | 
			
		||||
@@ -301,8 +301,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 | 
			
		||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 | 
			
		||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 | 
			
		||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
 | 
			
		||||
@@ -420,8 +420,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
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/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-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.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
 | 
			
		||||
@@ -757,8 +757,8 @@ github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGB
 | 
			
		||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
			
		||||
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
 | 
			
		||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
 | 
			
		||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
 | 
			
		||||
 
 | 
			
		||||
@@ -171,6 +171,7 @@ func (run *ActionRun) IsSchedule() bool {
 | 
			
		||||
 | 
			
		||||
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
 | 
			
		||||
	_, err := db.GetEngine(ctx).ID(repo.ID).
 | 
			
		||||
		NoAutoTime().
 | 
			
		||||
		SetExpr("num_action_runs",
 | 
			
		||||
			builder.Select("count(*)").From("action_run").
 | 
			
		||||
				Where(builder.Eq{"repo_id": repo.ID}),
 | 
			
		||||
 
 | 
			
		||||
@@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
 | 
			
		||||
		return StatusSuccess
 | 
			
		||||
	case hasCancelled:
 | 
			
		||||
		return StatusCancelled
 | 
			
		||||
	case hasFailure:
 | 
			
		||||
		return StatusFailure
 | 
			
		||||
	case hasRunning:
 | 
			
		||||
		return StatusRunning
 | 
			
		||||
	case hasFailure:
 | 
			
		||||
		return StatusFailure
 | 
			
		||||
	case hasWaiting:
 | 
			
		||||
		return StatusWaiting
 | 
			
		||||
	case hasBlocked:
 | 
			
		||||
 
 | 
			
		||||
@@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
 | 
			
		||||
		{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
 | 
			
		||||
		{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
 | 
			
		||||
 | 
			
		||||
		// failure with other status, fail fast
 | 
			
		||||
		// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
 | 
			
		||||
		// failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
 | 
			
		||||
		// another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
 | 
			
		||||
		{[]Status{StatusFailure}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
 | 
			
		||||
		{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusRunning}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusRunning}, StatusRunning},
 | 
			
		||||
		{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
 | 
			
		||||
 | 
			
		||||
		// skipped with other status
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ package actions
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteEphemeralRunner deletes a ephemeral runner by given ID.
 | 
			
		||||
func DeleteEphemeralRunner(ctx context.Context, id int64) error {
 | 
			
		||||
	runner, err := GetRunnerByID(ctx, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !runner.Ephemeral {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = db.DeleteByID[ActionRunner](ctx, id)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateRunner creates new runner.
 | 
			
		||||
func CreateRunner(ctx context.Context, t *ActionRunner) error {
 | 
			
		||||
	if t.OwnerID != 0 && t.RepoID != 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
 | 
			
		||||
		sess.Cols(cols...)
 | 
			
		||||
	}
 | 
			
		||||
	_, err := sess.Update(task)
 | 
			
		||||
 | 
			
		||||
	// Automatically delete the ephemeral runner if the task is done
 | 
			
		||||
	if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") {
 | 
			
		||||
		return DeleteEphemeralRunner(ctx, task.RunnerID)
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -191,7 +191,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
	a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID)
 | 
			
		||||
	a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return
 | 
			
		||||
	} else if user_model.IsErrUserNotExist(err) {
 | 
			
		||||
@@ -530,7 +530,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
 | 
			
		||||
 | 
			
		||||
	if opts.RequestedTeam != nil {
 | 
			
		||||
		env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
 | 
			
		||||
		teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos)
 | 
			
		||||
		teamRepoIDs, err := env.RepoIDs(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("GetTeamRepositories: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
 | 
			
		||||
		Select(groupBy+" AS timestamp, count(user_id) as contributions").
 | 
			
		||||
		Table("action").
 | 
			
		||||
		Where(cond).
 | 
			
		||||
		And("created_unix > ?", timeutil.TimeStampNow()-31536000).
 | 
			
		||||
		And("created_unix > ?", timeutil.TimeStampNow()-(366+7)*86400). // (366+7) days to include the first week for the heatmap
 | 
			
		||||
		GroupBy(groupByName).
 | 
			
		||||
		OrderBy("timestamp").
 | 
			
		||||
		Find(&hdata)
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
 | 
			
		||||
			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
			log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
 | 
			
		||||
			return nil, ErrGPGInvalidTokenSignature{
 | 
			
		||||
				ID:      ekeys[0].PrimaryKey.KeyIdString(),
 | 
			
		||||
				Wrapped: err,
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if signer == nil {
 | 
			
		||||
		log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
		log.Debug("VerifyGPGKey failed: no signer")
 | 
			
		||||
		return "", ErrGPGInvalidTokenSignature{
 | 
			
		||||
			ID: key.KeyID,
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
 | 
			
		||||
		// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
 | 
			
		||||
		// see https://github.com/PowerShell/PowerShell/issues/5974
 | 
			
		||||
		if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
 | 
			
		||||
			log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
			log.Debug("VerifySSHKey sshsig.Verify failed: %v", err)
 | 
			
		||||
			return "", ErrSSHInvalidTokenSignature{
 | 
			
		||||
				Fingerprint: key.Fingerprint,
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,3 +38,14 @@
 | 
			
		||||
  repo_id: 0
 | 
			
		||||
  description: "This runner is going to be deleted"
 | 
			
		||||
  agent_labels: '["runner_to_be_deleted","linux"]'
 | 
			
		||||
-
 | 
			
		||||
  id: 34350
 | 
			
		||||
  name: runner_to_be_deleted-org-ephemeral
 | 
			
		||||
  uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
 | 
			
		||||
  token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
 | 
			
		||||
  ephemeral: true
 | 
			
		||||
  version: "1.0.0"
 | 
			
		||||
  owner_id: 3
 | 
			
		||||
  repo_id: 0
 | 
			
		||||
  description: "This runner is going to be deleted"
 | 
			
		||||
  agent_labels: '["runner_to_be_deleted","linux"]'
 | 
			
		||||
 
 | 
			
		||||
@@ -117,3 +117,23 @@
 | 
			
		||||
  log_length: 707
 | 
			
		||||
  log_size: 90179
 | 
			
		||||
  log_expired: 0
 | 
			
		||||
-
 | 
			
		||||
  id: 52
 | 
			
		||||
  job_id: 196
 | 
			
		||||
  attempt: 1
 | 
			
		||||
  runner_id: 34350
 | 
			
		||||
  status: 6 # running
 | 
			
		||||
  started: 1683636528
 | 
			
		||||
  stopped: 1683636626
 | 
			
		||||
  repo_id: 4
 | 
			
		||||
  owner_id: 1
 | 
			
		||||
  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
			
		||||
  is_fork_pull_request: 0
 | 
			
		||||
  token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
 | 
			
		||||
  token_salt: ffffffffff
 | 
			
		||||
  token_last_eight: ffffffff
 | 
			
		||||
  log_filename: artifact-test2/2f/47.log
 | 
			
		||||
  log_in_storage: 1
 | 
			
		||||
  log_length: 707
 | 
			
		||||
  log_size: 90179
 | 
			
		||||
  log_expired: 0
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@
 | 
			
		||||
-
 | 
			
		||||
  id: 11
 | 
			
		||||
  uid: 4
 | 
			
		||||
  email: user4@example.com
 | 
			
		||||
  email: User4@Example.Com
 | 
			
		||||
  lower_email: user4@example.com
 | 
			
		||||
  is_activated: true
 | 
			
		||||
  is_primary: true
 | 
			
		||||
 
 | 
			
		||||
@@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
 | 
			
		||||
		return currentWhitelist, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -719,7 +719,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Comment) loadReview(ctx context.Context) (err error) {
 | 
			
		||||
// LoadReview loads the associated review
 | 
			
		||||
func (c *Comment) LoadReview(ctx context.Context) (err error) {
 | 
			
		||||
	if c.ReviewID == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -736,11 +737,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadReview loads the associated review
 | 
			
		||||
func (c *Comment) LoadReview(ctx context.Context) error {
 | 
			
		||||
	return c.loadReview(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
 | 
			
		||||
func (c *Comment) DiffSide() string {
 | 
			
		||||
	if c.Line < 0 {
 | 
			
		||||
@@ -860,7 +856,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 | 
			
		||||
		}
 | 
			
		||||
		if comment.ReviewID != 0 {
 | 
			
		||||
			if comment.Review == nil {
 | 
			
		||||
				if err := comment.loadReview(ctx); err != nil {
 | 
			
		||||
				if err := comment.LoadReview(ctx); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ package issues
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/renderhelper"
 | 
			
		||||
@@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
 | 
			
		||||
		rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
 | 
			
		||||
			FootnoteContextID: strconv.FormatInt(comment.ID, 10),
 | 
			
		||||
		})
 | 
			
		||||
		if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -206,6 +206,7 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	issue.Labels = nil
 | 
			
		||||
	issue.isLabelsLoaded = false
 | 
			
		||||
	return issue.LoadLabels(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
 | 
			
		||||
		sess.Asc("issue.created_unix").Asc("issue.id")
 | 
			
		||||
	case "recentupdate":
 | 
			
		||||
		sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id")
 | 
			
		||||
	case "recentclose":
 | 
			
		||||
		sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id")
 | 
			
		||||
	case "leastupdate":
 | 
			
		||||
		sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id")
 | 
			
		||||
	case "mostcomment":
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/organization"
 | 
			
		||||
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
			
		||||
	project_model "code.gitea.io/gitea/models/project"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	system_model "code.gitea.io/gitea/models/system"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
@@ -715,138 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteIssuesByRepoID deletes issues by repositories id
 | 
			
		||||
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
 | 
			
		||||
	// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
 | 
			
		||||
	// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
 | 
			
		||||
	sess := db.GetEngine(ctx)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		issueIDs := make([]int64, 0, db.DefaultMaxInSize)
 | 
			
		||||
 | 
			
		||||
		err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(issueIDs) == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Delete content histories
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Delete comments and attachments
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Dependencies for issues in this repository
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Delete dependencies for issues in other repositories
 | 
			
		||||
		_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var attachments []*repo_model.Attachment
 | 
			
		||||
		err = sess.In("issue_id", issueIDs).Find(&attachments)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for j := range attachments {
 | 
			
		||||
			attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = sess.In("id", issueIDs).Delete(&Issue{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
 | 
			
		||||
	var repoIDs []int64
 | 
			
		||||
	if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
 | 
			
		||||
		Join("LEFT", "repository", "issue.repo_id=repository.id").
 | 
			
		||||
		Where(builder.IsNull{"repository.id"}).
 | 
			
		||||
		Find(&repoIDs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return attachmentPaths, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteOrphanedIssues delete issues without a repo
 | 
			
		||||
func DeleteOrphanedIssues(ctx context.Context) error {
 | 
			
		||||
	var attachmentPaths []string
 | 
			
		||||
	err := db.WithTx(ctx, func(ctx context.Context) error {
 | 
			
		||||
		var ids []int64
 | 
			
		||||
 | 
			
		||||
		if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
 | 
			
		||||
			Join("LEFT", "repository", "issue.repo_id=repository.id").
 | 
			
		||||
			Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
 | 
			
		||||
			Find(&ids); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for i := range ids {
 | 
			
		||||
			paths, err := DeleteIssuesByRepoID(ctx, ids[i])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			attachmentPaths = append(attachmentPaths, paths...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Remove issue attachment files.
 | 
			
		||||
	for i := range attachmentPaths {
 | 
			
		||||
		// FIXME: it's not right, because the attachment might not be on local filesystem
 | 
			
		||||
		system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	return repoIDs, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -235,7 +235,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 | 
			
		||||
 | 
			
		||||
// AddCrossReferences add cross references
 | 
			
		||||
func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
 | 
			
		||||
	if c.Type != CommentTypeCode && c.Type != CommentTypeComment {
 | 
			
		||||
	if !c.Type.HasContentSupport() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.LoadIssue(stdCtx); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
 | 
			
		||||
	applySorts(findSession, opts.SortType, 0)
 | 
			
		||||
	findSession = db.SetSessionPagination(findSession, opts)
 | 
			
		||||
	prs := make([]*PullRequest, 0, opts.PageSize)
 | 
			
		||||
	return prs, maxResults, findSession.Find(&prs)
 | 
			
		||||
	found := findSession.Find(&prs)
 | 
			
		||||
	return prs, maxResults, found
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PullRequestList defines a list of pull requests
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPullRequest_LoadAttributes(t *testing.T) {
 | 
			
		||||
@@ -76,6 +77,47 @@ func TestPullRequestsNewest(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
 | 
			
		||||
	// Issue ID | Closed At.  | Updated At
 | 
			
		||||
	//    2     | 1707270001  | 1707270001
 | 
			
		||||
	//    3     | 1707271000  | 1707279999
 | 
			
		||||
	//    11    | 1707279999  | 1707275555
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		sortType             string
 | 
			
		||||
		expectedIssueIDOrder []int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"recentupdate", []int64{3, 11, 2}},
 | 
			
		||||
		{"recentclose", []int64{11, 3, 2}},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	_, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.sortType, func(t *testing.T) {
 | 
			
		||||
			prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{
 | 
			
		||||
				ListOptions: db.ListOptions{
 | 
			
		||||
					Page: 1,
 | 
			
		||||
				},
 | 
			
		||||
				State:    "closed",
 | 
			
		||||
				SortType: test.sortType,
 | 
			
		||||
			})
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			if assert.Len(t, prs, len(test.expectedIssueIDOrder)) {
 | 
			
		||||
				for i := range test.expectedIssueIDOrder {
 | 
			
		||||
					assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestLoadRequestedReviewers(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
 | 
			
		||||
		Stopped    timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
 | 
			
		||||
		LogExpired bool               `xorm:"index(stopped_log_expired)"`
 | 
			
		||||
	}
 | 
			
		||||
	return x.Sync(new(ActionTask))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreDropIndices: true,
 | 
			
		||||
	}, new(ActionTask))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								models/migrations/v1_23/v302_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								models/migrations/v1_23/v302_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package v1_23 //nolint
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/migrations/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
 | 
			
		||||
	type ActionTask struct {
 | 
			
		||||
		ID       int64
 | 
			
		||||
		JobID    int64
 | 
			
		||||
		Attempt  int64
 | 
			
		||||
		RunnerID int64              `xorm:"index"`
 | 
			
		||||
		Status   int                `xorm:"index"`
 | 
			
		||||
		Started  timeutil.TimeStamp `xorm:"index"`
 | 
			
		||||
		Stopped  timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
 | 
			
		||||
 | 
			
		||||
		RepoID            int64  `xorm:"index"`
 | 
			
		||||
		OwnerID           int64  `xorm:"index"`
 | 
			
		||||
		CommitSHA         string `xorm:"index"`
 | 
			
		||||
		IsForkPullRequest bool
 | 
			
		||||
 | 
			
		||||
		Token          string `xorm:"-"`
 | 
			
		||||
		TokenHash      string `xorm:"UNIQUE"` // sha256 of token
 | 
			
		||||
		TokenSalt      string
 | 
			
		||||
		TokenLastEight string `xorm:"index token_last_eight"`
 | 
			
		||||
 | 
			
		||||
		LogFilename  string  // file name of log
 | 
			
		||||
		LogInStorage bool    // read log from database or from storage
 | 
			
		||||
		LogLength    int64   // lines count
 | 
			
		||||
		LogSize      int64   // blob size
 | 
			
		||||
		LogIndexes   []int64 `xorm:"LONGBLOB"`                   // line number to offset
 | 
			
		||||
		LogExpired   bool    `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
 | 
			
		||||
 | 
			
		||||
		Created timeutil.TimeStamp `xorm:"created"`
 | 
			
		||||
		Updated timeutil.TimeStamp `xorm:"updated index"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prepare and load the testing database
 | 
			
		||||
	x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
 | 
			
		||||
	defer deferable()
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
 | 
			
		||||
}
 | 
			
		||||
@@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
 | 
			
		||||
	type Release struct {
 | 
			
		||||
		Sha1 string `xorm:"INDEX VARCHAR(64)"`
 | 
			
		||||
	}
 | 
			
		||||
	return x.Sync(new(Release))
 | 
			
		||||
	_, err := x.SyncWithOptions(xorm.SyncOptions{
 | 
			
		||||
		IgnoreDropIndices: true,
 | 
			
		||||
	}, new(Release))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								models/migrations/v1_23/v304_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								models/migrations/v1_23/v304_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package v1_23 //nolint
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/migrations/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_AddIndexForReleaseSha1(t *testing.T) {
 | 
			
		||||
	type Release struct {
 | 
			
		||||
		ID               int64  `xorm:"pk autoincr"`
 | 
			
		||||
		RepoID           int64  `xorm:"INDEX UNIQUE(n)"`
 | 
			
		||||
		PublisherID      int64  `xorm:"INDEX"`
 | 
			
		||||
		TagName          string `xorm:"INDEX UNIQUE(n)"`
 | 
			
		||||
		OriginalAuthor   string
 | 
			
		||||
		OriginalAuthorID int64 `xorm:"index"`
 | 
			
		||||
		LowerTagName     string
 | 
			
		||||
		Target           string
 | 
			
		||||
		Title            string
 | 
			
		||||
		Sha1             string `xorm:"VARCHAR(64)"`
 | 
			
		||||
		NumCommits       int64
 | 
			
		||||
		Note             string             `xorm:"TEXT"`
 | 
			
		||||
		IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
		IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
		IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
 | 
			
		||||
		CreatedUnix      timeutil.TimeStamp `xorm:"INDEX"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prepare and load the testing database
 | 
			
		||||
	x, deferable := base.PrepareTestEnv(t, 0, new(Release))
 | 
			
		||||
	defer deferable()
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, AddIndexForReleaseSha1(x))
 | 
			
		||||
}
 | 
			
		||||
@@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
 | 
			
		||||
			"team_user.uid":    userID,
 | 
			
		||||
		})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
 | 
			
		||||
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
 | 
			
		||||
	return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
 | 
			
		||||
	testSuccess := func(userID int64, expectedRepoIDs []int64) {
 | 
			
		||||
		env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100)
 | 
			
		||||
		repoIDs, err := env.RepoIDs(db.DefaultContext)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, expectedRepoIDs, repoIDs)
 | 
			
		||||
	}
 | 
			
		||||
@@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
 | 
			
		||||
	testSuccess(4, []int64{3, 32})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccessibleReposEnv_Repos(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
			
		||||
	testSuccess := func(userID int64, expectedRepoIDs []int64) {
 | 
			
		||||
		env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		repos, err := env.Repos(db.DefaultContext, 1, 100)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
 | 
			
		||||
		for i, repoID := range expectedRepoIDs {
 | 
			
		||||
			expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
 | 
			
		||||
				&repo_model.Repository{ID: repoID})
 | 
			
		||||
		}
 | 
			
		||||
		assert.Equal(t, expectedRepos, repos)
 | 
			
		||||
	}
 | 
			
		||||
	testSuccess(2, []int64{3, 5, 32})
 | 
			
		||||
	testSuccess(4, []int64{3, 32})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TeamRepo represents an team-repository relation.
 | 
			
		||||
@@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
 | 
			
		||||
func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) {
 | 
			
		||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
 | 
			
		||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
 | 
			
		||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
 | 
			
		||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
 | 
			
		||||
	teams := make([]*Team, 0, 5)
 | 
			
		||||
	return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode).
 | 
			
		||||
		Join("INNER", "team_repo", "team_repo.team_id = team.id").
 | 
			
		||||
		And("team_repo.org_id = ?", orgID).
 | 
			
		||||
		And("team_repo.repo_id = ?", repoID).
 | 
			
		||||
		OrderBy("name").
 | 
			
		||||
		Find(&teams)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
 | 
			
		||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
 | 
			
		||||
	teams := make([]*Team, 0, 5)
 | 
			
		||||
	return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
 | 
			
		||||
	sub := builder.Select("team_id").From("team_unit").
 | 
			
		||||
		Where(builder.Expr("team_unit.team_id = team.id")).
 | 
			
		||||
		And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
 | 
			
		||||
		And(builder.Expr("team_unit.access_mode >= ?", mode))
 | 
			
		||||
 | 
			
		||||
	err := db.GetEngine(ctx).
 | 
			
		||||
		Join("INNER", "team_repo", "team_repo.team_id = team.id").
 | 
			
		||||
		Join("INNER", "team_unit", "team_unit.team_id = team.id").
 | 
			
		||||
		And("team_repo.org_id = ?", orgID).
 | 
			
		||||
		And("team_repo.repo_id = ?", repoID).
 | 
			
		||||
		And("team_unit.type = ?", unitType).
 | 
			
		||||
		And(builder.Or(
 | 
			
		||||
			builder.Expr("team.authorize >= ?", mode),
 | 
			
		||||
			builder.In("team.id", sub),
 | 
			
		||||
		)).
 | 
			
		||||
		OrderBy("name").
 | 
			
		||||
		Find(&teams)
 | 
			
		||||
 | 
			
		||||
	return teams, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
 | 
			
		||||
	org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
 | 
			
		||||
	repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
 | 
			
		||||
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	if assert.Len(t, teams, 2) {
 | 
			
		||||
		assert.EqualValues(t, 21, teams[0].ID)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptio
 | 
			
		||||
		Where(cond).
 | 
			
		||||
		OrderBy("package.name ASC")
 | 
			
		||||
	if opts.Paginator != nil {
 | 
			
		||||
		skip, take := opts.GetSkipTake()
 | 
			
		||||
		skip, take := opts.Paginator.GetSkipTake()
 | 
			
		||||
		inner = inner.Limit(take, skip)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ErrDuplicatePackageVersion indicates a duplicated package version error
 | 
			
		||||
@@ -187,7 +188,7 @@ type PackageSearchOptions struct {
 | 
			
		||||
	HasFileWithName string                // only results are found which are associated with a file with the specific name
 | 
			
		||||
	HasFiles        optional.Option[bool] // only results are found which have associated files
 | 
			
		||||
	Sort            VersionSort
 | 
			
		||||
	db.Paginator
 | 
			
		||||
	Paginator       db.Paginator
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts *PackageSearchOptions) ToConds() builder.Cond {
 | 
			
		||||
@@ -282,6 +283,18 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
 | 
			
		||||
	e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func searchVersionsBySession(sess *xorm.Session, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
 | 
			
		||||
	opts.configureOrderBy(sess)
 | 
			
		||||
	pvs := make([]*PackageVersion, 0, 10)
 | 
			
		||||
	if opts.Paginator != nil {
 | 
			
		||||
		sess = db.SetSessionPagination(sess, opts.Paginator)
 | 
			
		||||
		count, err := sess.FindAndCount(&pvs)
 | 
			
		||||
		return pvs, count, err
 | 
			
		||||
	}
 | 
			
		||||
	err := sess.Find(&pvs)
 | 
			
		||||
	return pvs, int64(len(pvs)), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchVersions gets all versions of packages matching the search options
 | 
			
		||||
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
 | 
			
		||||
	sess := db.GetEngine(ctx).
 | 
			
		||||
@@ -289,16 +302,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package
 | 
			
		||||
		Table("package_version").
 | 
			
		||||
		Join("INNER", "package", "package.id = package_version.package_id").
 | 
			
		||||
		Where(opts.ToConds())
 | 
			
		||||
 | 
			
		||||
	opts.configureOrderBy(sess)
 | 
			
		||||
 | 
			
		||||
	if opts.Paginator != nil {
 | 
			
		||||
		sess = db.SetSessionPagination(sess, opts)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pvs := make([]*PackageVersion, 0, 10)
 | 
			
		||||
	count, err := sess.FindAndCount(&pvs)
 | 
			
		||||
	return pvs, count, err
 | 
			
		||||
	return searchVersionsBySession(sess, opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchLatestVersions gets the latest version of every package matching the search options
 | 
			
		||||
@@ -316,15 +320,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
 | 
			
		||||
		Join("INNER", "package", "package.id = package_version.package_id").
 | 
			
		||||
		Where(builder.In("package_version.id", in))
 | 
			
		||||
 | 
			
		||||
	opts.configureOrderBy(sess)
 | 
			
		||||
 | 
			
		||||
	if opts.Paginator != nil {
 | 
			
		||||
		sess = db.SetSessionPagination(sess, opts)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pvs := make([]*PackageVersion, 0, 10)
 | 
			
		||||
	count, err := sess.FindAndCount(&pvs)
 | 
			
		||||
	return pvs, count, err
 | 
			
		||||
	return searchVersionsBySession(sess, opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExistVersion checks if a version matching the search options exist
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool {
 | 
			
		||||
 | 
			
		||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
 | 
			
		||||
// It doesn't count the "public(anonymous/everyone) access mode".
 | 
			
		||||
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
 | 
			
		||||
func (p *Permission) HasAnyUnitAccess() bool {
 | 
			
		||||
	for _, v := range p.unitsMode {
 | 
			
		||||
		if v >= perm_model.AccessModeRead {
 | 
			
		||||
@@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	perm.units = repo.Units
 | 
			
		||||
 | 
			
		||||
	// anonymous user visit private repo.
 | 
			
		||||
	// TODO: anonymous user visit public unit of private repo???
 | 
			
		||||
	if user == nil && repo.IsPrivate {
 | 
			
		||||
		perm.AccessMode = perm_model.AccessModeNone
 | 
			
		||||
		return perm, nil
 | 
			
		||||
@@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prevent strangers from checking out public repo of private organization/users
 | 
			
		||||
	// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
 | 
			
		||||
	// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
 | 
			
		||||
	// TODO: rename it to "IsOwnerVisibleToDoer"
 | 
			
		||||
	if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
 | 
			
		||||
		perm.AccessMode = perm_model.AccessModeNone
 | 
			
		||||
		return perm, nil
 | 
			
		||||
@@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// plain user
 | 
			
		||||
	// plain user TODO: this check should be replaced, only need to check collaborator access mode
 | 
			
		||||
	perm.AccessMode, err = accessLevel(ctx, user, repo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
@@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// now: the owner is visible to doer, if the repo is public, then the min access mode is read
 | 
			
		||||
	minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
 | 
			
		||||
	perm.AccessMode = max(perm.AccessMode, minAccessMode)
 | 
			
		||||
 | 
			
		||||
	// get units mode from teams
 | 
			
		||||
	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(teams) == 0 {
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
 | 
			
		||||
 | 
			
		||||
	// Collaborators on organization
 | 
			
		||||
@@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get units mode from teams
 | 
			
		||||
	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if user in an owner team
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		if team.HasAdminAccess() {
 | 
			
		||||
@@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range repo.Units {
 | 
			
		||||
		var found bool
 | 
			
		||||
		for _, team := range teams {
 | 
			
		||||
			unitAccessMode := minAccessMode
 | 
			
		||||
			if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
 | 
			
		||||
				perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
 | 
			
		||||
				found = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
 | 
			
		||||
		if !found && !repo.IsPrivate && !user.IsRestricted {
 | 
			
		||||
			if _, ok := perm.unitsMode[u.Type]; !ok {
 | 
			
		||||
				perm.unitsMode[u.Type] = perm_model.AccessModeRead
 | 
			
		||||
				unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
 | 
			
		||||
			}
 | 
			
		||||
			perm.unitsMode[u.Type] = unitAccessMode
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,16 @@ package access
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/organization"
 | 
			
		||||
	perm_model "code.gitea.io/gitea/models/perm"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"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 TestHasAnyUnitAccess(t *testing.T) {
 | 
			
		||||
@@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
	assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserRepoPermission(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
 | 
			
		||||
	require.NoError(t, repo32.LoadOwner(ctx))
 | 
			
		||||
	require.True(t, repo32.Owner.IsOrganization())
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
 | 
			
		||||
	org := repo32.Owner
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
			
		||||
	team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, team))
 | 
			
		||||
 | 
			
		||||
	t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
 | 
			
		||||
		require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
 | 
			
		||||
	t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
 | 
			
		||||
	t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,14 @@ package pull
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
 | 
			
		||||
@@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
 | 
			
		||||
		return false, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
 | 
			
		||||
	doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
 | 
			
		||||
	if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
		doer, err = user_model.NewGhostUser(), nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
 | 
			
		||||
		c.gitRepo, c.gitRepoCloser = r, closer
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
 | 
			
		||||
	exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition.
 | 
			
		||||
	c.commitCache[commitID] = exist
 | 
			
		||||
	return exist
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -44,30 +44,31 @@ type RepoCommentOptions struct {
 | 
			
		||||
	DeprecatedRepoName  string // it is only a patch for the non-standard "markup" api
 | 
			
		||||
	DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
 | 
			
		||||
	CurrentRefPath      string // eg: "branch/main" or "commit/11223344"
 | 
			
		||||
	FootnoteContextID   string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
 | 
			
		||||
	helper := &RepoComment{
 | 
			
		||||
		repoLink: repo.Link(),
 | 
			
		||||
		opts:     util.OptionalArg(opts),
 | 
			
		||||
	}
 | 
			
		||||
	helper := &RepoComment{opts: util.OptionalArg(opts)}
 | 
			
		||||
	rctx := markup.NewRenderContext(ctx)
 | 
			
		||||
	helper.ctx = rctx
 | 
			
		||||
	var metas map[string]string
 | 
			
		||||
	if repo != nil {
 | 
			
		||||
		helper.repoLink = repo.Link()
 | 
			
		||||
		helper.commitChecker = newCommitChecker(ctx, repo)
 | 
			
		||||
		rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
 | 
			
		||||
		metas = repo.ComposeCommentMetas(ctx)
 | 
			
		||||
	} else {
 | 
			
		||||
		// this is almost dead code, only to pass the incorrect tests
 | 
			
		||||
		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
 | 
			
		||||
		rctx = rctx.WithMetas(map[string]string{
 | 
			
		||||
			"user": helper.opts.DeprecatedOwnerName,
 | 
			
		||||
			"repo": helper.opts.DeprecatedRepoName,
 | 
			
		||||
 | 
			
		||||
			"markdownNewLineHardBreak":     "true",
 | 
			
		||||
			"markupAllowShortIssuePattern": "true",
 | 
			
		||||
		})
 | 
			
		||||
		// repo can be nil when rendering a commit message in user's dashboard feedback whose repository has been deleted
 | 
			
		||||
		metas = map[string]string{}
 | 
			
		||||
		if helper.opts.DeprecatedOwnerName != "" {
 | 
			
		||||
			// this is almost dead code, only to pass the incorrect tests
 | 
			
		||||
			helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
 | 
			
		||||
			metas["user"] = helper.opts.DeprecatedOwnerName
 | 
			
		||||
			metas["repo"] = helper.opts.DeprecatedRepoName
 | 
			
		||||
		}
 | 
			
		||||
		metas["markdownNewLineHardBreak"] = "true"
 | 
			
		||||
		metas["markupAllowShortIssuePattern"] = "true"
 | 
			
		||||
	}
 | 
			
		||||
	rctx = rctx.WithHelper(helper)
 | 
			
		||||
	metas["footnoteContextId"] = helper.opts.FootnoteContextID
 | 
			
		||||
	rctx = rctx.WithMetas(metas).WithHelper(helper)
 | 
			
		||||
	return rctx
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -72,4 +72,11 @@ func TestRepoComment(t *testing.T) {
 | 
			
		||||
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
 | 
			
		||||
`, rendered)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoRepo", func(t *testing.T) {
 | 
			
		||||
		rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName)
 | 
			
		||||
		rendered, err := markup.RenderString(rctx, "any")
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "<p>any</p>\n", rendered)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -85,8 +85,8 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, unittest.NonexistentID, perm.AccessModeAdmin))
 | 
			
		||||
 | 
			
		||||
	// Disvard invalid input.
 | 
			
		||||
	assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, 4, perm.AccessMode(unittest.NonexistentID)))
 | 
			
		||||
	// Discard invalid input.
 | 
			
		||||
	assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, 4, perm.AccessMode(-1)))
 | 
			
		||||
 | 
			
		||||
	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
 | 
			
		||||
// accessible to a particular user
 | 
			
		||||
type AccessibleReposEnvironment interface {
 | 
			
		||||
	CountRepos(ctx context.Context) (int64, error)
 | 
			
		||||
	RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error)
 | 
			
		||||
	Repos(ctx context.Context, page, pageSize int) (RepositoryList, error)
 | 
			
		||||
	RepoIDs(ctx context.Context) ([]int64, error)
 | 
			
		||||
	MirrorRepos(ctx context.Context) (RepositoryList, error)
 | 
			
		||||
	AddKeyword(keyword string)
 | 
			
		||||
	SetSort(db.SearchOrderBy)
 | 
			
		||||
@@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
 | 
			
		||||
	return repoCount, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) {
 | 
			
		||||
	if page <= 0 {
 | 
			
		||||
		page = 1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoIDs := make([]int64, 0, pageSize)
 | 
			
		||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
 | 
			
		||||
	var repoIDs []int64
 | 
			
		||||
	return repoIDs, db.GetEngine(ctx).
 | 
			
		||||
		Table("repository").
 | 
			
		||||
		Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
 | 
			
		||||
		Where(env.cond()).
 | 
			
		||||
		GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]).
 | 
			
		||||
		GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]).
 | 
			
		||||
		OrderBy(string(env.orderBy)).
 | 
			
		||||
		Limit(pageSize, (page-1)*pageSize).
 | 
			
		||||
		Cols("`repository`.id").
 | 
			
		||||
		Find(&repoIDs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) {
 | 
			
		||||
	repoIDs, err := env.RepoIDs(ctx, page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repos := make([]*Repository, 0, len(repoIDs))
 | 
			
		||||
	if len(repoIDs) == 0 {
 | 
			
		||||
		return repos, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repos, db.GetEngine(ctx).
 | 
			
		||||
		In("`repository`.id", repoIDs).
 | 
			
		||||
		OrderBy(string(env.orderBy)).
 | 
			
		||||
		Find(&repos)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
 | 
			
		||||
	repoIDs := make([]int64, 0, 10)
 | 
			
		||||
	return repoIDs, db.GetEngine(ctx).
 | 
			
		||||
 
 | 
			
		||||
@@ -161,6 +161,11 @@ func UpdateRelease(ctx context.Context, rel *Release) error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
 | 
			
		||||
	_, err := db.GetEngine(ctx).ID(rel.ID).Cols("num_commits").Update(rel)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddReleaseAttachments adds a release attachments
 | 
			
		||||
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
 | 
			
		||||
	// Check attachments
 | 
			
		||||
@@ -418,8 +423,8 @@ func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushUpdateDeleteTagsContext updates a number of delete tags with context
 | 
			
		||||
func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error {
 | 
			
		||||
// PushUpdateDeleteTags updates a number of delete tags with context
 | 
			
		||||
func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) error {
 | 
			
		||||
	if len(tags) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -448,58 +453,6 @@ func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []s
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushUpdateDeleteTag must be called for any push actions to delete tag
 | 
			
		||||
func PushUpdateDeleteTag(ctx context.Context, repo *Repository, tagName string) error {
 | 
			
		||||
	rel, err := GetRelease(ctx, repo.ID, tagName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if IsErrReleaseNotExist(err) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return fmt.Errorf("GetRelease: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if rel.IsTag {
 | 
			
		||||
		if _, err = db.DeleteByID[Release](ctx, rel.ID); err != nil {
 | 
			
		||||
			return fmt.Errorf("Delete: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		rel.IsDraft = true
 | 
			
		||||
		rel.NumCommits = 0
 | 
			
		||||
		rel.Sha1 = ""
 | 
			
		||||
		if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
 | 
			
		||||
			return fmt.Errorf("Update: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveOrUpdateTag must be called for any push actions to add tag
 | 
			
		||||
func SaveOrUpdateTag(ctx context.Context, repo *Repository, newRel *Release) error {
 | 
			
		||||
	rel, err := GetRelease(ctx, repo.ID, newRel.TagName)
 | 
			
		||||
	if err != nil && !IsErrReleaseNotExist(err) {
 | 
			
		||||
		return fmt.Errorf("GetRelease: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rel == nil {
 | 
			
		||||
		rel = newRel
 | 
			
		||||
		if _, err = db.GetEngine(ctx).Insert(rel); err != nil {
 | 
			
		||||
			return fmt.Errorf("InsertOne: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		rel.Sha1 = newRel.Sha1
 | 
			
		||||
		rel.CreatedUnix = newRel.CreatedUnix
 | 
			
		||||
		rel.NumCommits = newRel.NumCommits
 | 
			
		||||
		rel.IsDraft = false
 | 
			
		||||
		if rel.IsTag && newRel.PublisherID > 0 {
 | 
			
		||||
			rel.PublisherID = newRel.PublisherID
 | 
			
		||||
		}
 | 
			
		||||
		if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
 | 
			
		||||
			return fmt.Errorf("Update: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemapExternalUser ExternalUserRemappable interface
 | 
			
		||||
func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
 | 
			
		||||
	r.OriginalAuthor = externalName
 | 
			
		||||
 
 | 
			
		||||
@@ -64,18 +64,18 @@ func (err ErrRepoIsArchived) Error() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type globalVarsStruct struct {
 | 
			
		||||
	validRepoNamePattern   *regexp.Regexp
 | 
			
		||||
	invalidRepoNamePattern *regexp.Regexp
 | 
			
		||||
	reservedRepoNames      []string
 | 
			
		||||
	reservedRepoPatterns   []string
 | 
			
		||||
	validRepoNamePattern     *regexp.Regexp
 | 
			
		||||
	invalidRepoNamePattern   *regexp.Regexp
 | 
			
		||||
	reservedRepoNames        []string
 | 
			
		||||
	reservedRepoNamePatterns []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
 | 
			
		||||
	return &globalVarsStruct{
 | 
			
		||||
		validRepoNamePattern:   regexp.MustCompile(`[-.\w]+`),
 | 
			
		||||
		invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`),
 | 
			
		||||
		reservedRepoNames:      []string{".", "..", "-"},
 | 
			
		||||
		reservedRepoPatterns:   []string{"*.git", "*.wiki", "*.rss", "*.atom"},
 | 
			
		||||
		validRepoNamePattern:     regexp.MustCompile(`^[-.\w]+$`),
 | 
			
		||||
		invalidRepoNamePattern:   regexp.MustCompile(`[.]{2,}`),
 | 
			
		||||
		reservedRepoNames:        []string{".", "..", "-"},
 | 
			
		||||
		reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"},
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@@ -86,7 +86,16 @@ func IsUsableRepoName(name string) error {
 | 
			
		||||
		// Note: usually this error is normally caught up earlier in the UI
 | 
			
		||||
		return db.ErrNameCharsNotAllowed{Name: name}
 | 
			
		||||
	}
 | 
			
		||||
	return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name)
 | 
			
		||||
	return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code
 | 
			
		||||
func IsValidSSHAccessRepoName(name string) bool {
 | 
			
		||||
	vars := globalVars()
 | 
			
		||||
	if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TrustModelType defines the types of trust model for this repository
 | 
			
		||||
 
 | 
			
		||||
@@ -216,8 +216,23 @@ func TestIsUsableRepoName(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("-"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("🌞"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("the/repo"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("the..repo"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("foo.wiki"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("foo.git"))
 | 
			
		||||
	assert.Error(t, IsUsableRepoName("foo.RSS"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIsValidSSHAccessRepoName(t *testing.T) {
 | 
			
		||||
	assert.True(t, IsValidSSHAccessRepoName("a"))
 | 
			
		||||
	assert.True(t, IsValidSSHAccessRepoName("-1_."))
 | 
			
		||||
	assert.True(t, IsValidSSHAccessRepoName(".profile"))
 | 
			
		||||
	assert.True(t, IsValidSSHAccessRepoName("foo.wiki"))
 | 
			
		||||
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("-"))
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("🌞"))
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("the/repo"))
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("the..repo"))
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("foo.git"))
 | 
			
		||||
	assert.False(t, IsValidSSHAccessRepoName("foo.RSS"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -249,7 +249,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repo.Status = RepositoryPendingTransfer
 | 
			
		||||
		if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
 | 
			
		||||
		if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName st
 | 
			
		||||
	}
 | 
			
		||||
	defer committer.Close()
 | 
			
		||||
 | 
			
		||||
	if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{
 | 
			
		||||
	if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").NoAutoTime().Update(&Repository{
 | 
			
		||||
		OwnerName: ownerName,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -40,8 +40,8 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepositoryCols updates repository's columns
 | 
			
		||||
func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) error {
 | 
			
		||||
// UpdateRepositoryColsWithAutoTime updates repository's columns
 | 
			
		||||
func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
 | 
			
		||||
	_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ package user
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"image/png"
 | 
			
		||||
	"io"
 | 
			
		||||
@@ -106,7 +105,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool {
 | 
			
		||||
	if !u.UseCustomAvatar || len(u.Avatar) == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
 | 
			
		||||
	avatarID := avatar.HashAvatar(u.ID, data)
 | 
			
		||||
	return u.Avatar != avatarID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1146,8 +1146,8 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range oldCommits {
 | 
			
		||||
		user, ok := emailUserMap[c.Author.Email]
 | 
			
		||||
		if !ok {
 | 
			
		||||
		user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
 | 
			
		||||
		if user == nil {
 | 
			
		||||
			user = &User{
 | 
			
		||||
				Name:  c.Author.Name,
 | 
			
		||||
				Email: c.Author.Email,
 | 
			
		||||
@@ -1161,19 +1161,29 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
 | 
			
		||||
	return newCommits, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, error) {
 | 
			
		||||
type EmailUserMap struct {
 | 
			
		||||
	m map[string]*User
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (eum *EmailUserMap) GetByEmail(email string) *User {
 | 
			
		||||
	return eum.m[strings.ToLower(email)]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
 | 
			
		||||
	if len(emails) == 0 {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	needCheckEmails := make(container.Set[string])
 | 
			
		||||
	needCheckUserNames := make(container.Set[string])
 | 
			
		||||
	noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
 | 
			
		||||
	for _, email := range emails {
 | 
			
		||||
		if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
 | 
			
		||||
			username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
 | 
			
		||||
			needCheckUserNames.Add(username)
 | 
			
		||||
		emailLower := strings.ToLower(email)
 | 
			
		||||
		if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
 | 
			
		||||
			needCheckUserNames.Add(noReplyUserNameLower)
 | 
			
		||||
			needCheckEmails.Add(emailLower)
 | 
			
		||||
		} else {
 | 
			
		||||
			needCheckEmails.Add(strings.ToLower(email))
 | 
			
		||||
			needCheckEmails.Add(emailLower)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -1198,7 +1208,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
 | 
			
		||||
		for _, email := range emailAddresses {
 | 
			
		||||
			user := users[email.UID]
 | 
			
		||||
			if user != nil {
 | 
			
		||||
				results[user.GetEmail()] = user
 | 
			
		||||
				results[email.LowerEmail] = user
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -1208,9 +1218,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, user := range users {
 | 
			
		||||
		results[user.GetPlaceholderEmail()] = user
 | 
			
		||||
		results[strings.ToLower(user.GetPlaceholderEmail())] = user
 | 
			
		||||
	}
 | 
			
		||||
	return results, nil
 | 
			
		||||
	return &EmailUserMap{results}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByEmail returns the user object by given e-mail if exists.
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIsUsableUsername(t *testing.T) {
 | 
			
		||||
@@ -48,14 +49,47 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
 | 
			
		||||
	assert.NotNil(t, user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserEmailsByNames(t *testing.T) {
 | 
			
		||||
func TestUserEmails(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	// ignore none active user email
 | 
			
		||||
	assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
 | 
			
		||||
	assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
 | 
			
		||||
 | 
			
		||||
	assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
 | 
			
		||||
	t.Run("GetUserEmailsByNames", func(t *testing.T) {
 | 
			
		||||
		// ignore none active user email
 | 
			
		||||
		assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
 | 
			
		||||
		assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
 | 
			
		||||
		assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("GetUsersByEmails", func(t *testing.T) {
 | 
			
		||||
		defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
 | 
			
		||||
		testGetUserByEmail := func(t *testing.T, email string, uid int64) {
 | 
			
		||||
			m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{email})
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			user := m.GetByEmail(email)
 | 
			
		||||
			if uid == 0 {
 | 
			
		||||
				require.Nil(t, user)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			require.NotNil(t, user)
 | 
			
		||||
			assert.Equal(t, uid, user.ID)
 | 
			
		||||
		}
 | 
			
		||||
		cases := []struct {
 | 
			
		||||
			Email string
 | 
			
		||||
			UID   int64
 | 
			
		||||
		}{
 | 
			
		||||
			{"UseR1@example.com", 1},
 | 
			
		||||
			{"user1-2@example.COM", 1},
 | 
			
		||||
			{"USER2@" + setting.Service.NoReplyAddress, 2},
 | 
			
		||||
			{"user4@example.com", 4},
 | 
			
		||||
			{"no-such", 0},
 | 
			
		||||
		}
 | 
			
		||||
		for _, c := range cases {
 | 
			
		||||
			t.Run(c.Email, func(t *testing.T) {
 | 
			
		||||
				testGetUserByEmail(t, c.Email, c.UID)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		t.Run("NoReplyConflict", func(t *testing.T) {
 | 
			
		||||
			setting.Service.NoReplyAddress = "example.com"
 | 
			
		||||
			testGetUserByEmail(t, "user1-2@example.COM", 1)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCanCreateOrganization(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -311,6 +311,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
 | 
			
		||||
				matchTimes++
 | 
			
		||||
			}
 | 
			
		||||
		case "paths":
 | 
			
		||||
			if refName.IsTag() {
 | 
			
		||||
				matchTimes++
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
 | 
			
		||||
@@ -324,6 +328,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		case "paths-ignore":
 | 
			
		||||
			if refName.IsTag() {
 | 
			
		||||
				matchTimes++
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
 | 
			
		||||
 
 | 
			
		||||
@@ -125,6 +125,24 @@ func TestDetectMatched(t *testing.T) {
 | 
			
		||||
			yamlOn:       "on: schedule",
 | 
			
		||||
			expected:     true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			desc:         "push to tag matches workflow with paths condition (should skip paths check)",
 | 
			
		||||
			triggedEvent: webhook_module.HookEventPush,
 | 
			
		||||
			payload: &api.PushPayload{
 | 
			
		||||
				Ref:    "refs/tags/v1.0.0",
 | 
			
		||||
				Before: "0000000",
 | 
			
		||||
				Commits: []*api.PayloadCommit{
 | 
			
		||||
					{
 | 
			
		||||
						ID:      "abcdef123456",
 | 
			
		||||
						Added:   []string{"src/main.go"},
 | 
			
		||||
						Message: "Release v1.0.0",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			commit:   nil,
 | 
			
		||||
			yamlOn:   "on:\n  push:\n    paths:\n      - src/**",
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,22 +6,26 @@ package fileicon
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func BasicEntryIconName(entry *EntryInfo) string {
 | 
			
		||||
	svgName := "octicon-file"
 | 
			
		||||
	switch {
 | 
			
		||||
	case entry.IsLink():
 | 
			
		||||
	case entry.EntryMode.IsLink():
 | 
			
		||||
		svgName = "octicon-file-symlink-file"
 | 
			
		||||
		if te, err := entry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
		if entry.SymlinkToMode.IsDir() {
 | 
			
		||||
			svgName = "octicon-file-directory-symlink"
 | 
			
		||||
		}
 | 
			
		||||
	case entry.IsDir():
 | 
			
		||||
		svgName = "octicon-file-directory-fill"
 | 
			
		||||
	case entry.IsSubModule():
 | 
			
		||||
	case entry.EntryMode.IsDir():
 | 
			
		||||
		svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
 | 
			
		||||
	case entry.EntryMode.IsSubModule():
 | 
			
		||||
		svgName = "octicon-file-submodule"
 | 
			
		||||
	}
 | 
			
		||||
	return svg.RenderHTML(svgName)
 | 
			
		||||
	return svgName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
 | 
			
		||||
	return svg.RenderHTML(BasicEntryIconName(entry))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package fileicon
 | 
			
		||||
 | 
			
		||||
import "code.gitea.io/gitea/modules/git"
 | 
			
		||||
 | 
			
		||||
type EntryInfo struct {
 | 
			
		||||
	FullName      string
 | 
			
		||||
	EntryMode     git.EntryMode
 | 
			
		||||
	SymlinkToMode git.EntryMode
 | 
			
		||||
	IsOpen        bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
 | 
			
		||||
	ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
 | 
			
		||||
	if gitEntry.IsLink() {
 | 
			
		||||
		if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
			ret.SymlinkToMode = te.Mode()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFolder() *EntryInfo {
 | 
			
		||||
	return &EntryInfo{EntryMode: git.EntryModeTree}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFolderOpen() *EntryInfo {
 | 
			
		||||
	return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
 | 
			
		||||
}
 | 
			
		||||
@@ -9,11 +9,12 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/options"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type materialIconRulesData struct {
 | 
			
		||||
@@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
 | 
			
		||||
	}
 | 
			
		||||
	svgID := "svg-mfi-" + name
 | 
			
		||||
	svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
 | 
			
		||||
	svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
 | 
			
		||||
	if p == nil {
 | 
			
		||||
		return svgHTML
 | 
			
		||||
	}
 | 
			
		||||
	if p.IconSVGs[svgID] == "" {
 | 
			
		||||
		p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
 | 
			
		||||
		p.IconSVGs[svgID] = svgHTML
 | 
			
		||||
	}
 | 
			
		||||
	return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
 | 
			
		||||
	if m.rules == nil {
 | 
			
		||||
		return BasicThemeIcon(entry)
 | 
			
		||||
		return BasicEntryIconHTML(entry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if entry.IsLink() {
 | 
			
		||||
		if te, err := entry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
	if entry.EntryMode.IsLink() {
 | 
			
		||||
		if entry.SymlinkToMode.IsDir() {
 | 
			
		||||
			// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
			return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
 | 
			
		||||
		}
 | 
			
		||||
		return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	name := m.findIconNameByGit(entry)
 | 
			
		||||
	// the material icon pack's "folder" icon doesn't look good, so use our built-in one
 | 
			
		||||
	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
	if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" {
 | 
			
		||||
		// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
		extraClass := "octicon-file"
 | 
			
		||||
		switch {
 | 
			
		||||
		case entry.IsDir():
 | 
			
		||||
			extraClass = "octicon-file-directory-fill"
 | 
			
		||||
		case entry.IsSubModule():
 | 
			
		||||
			extraClass = "octicon-file-submodule"
 | 
			
		||||
	name := m.FindIconName(entry)
 | 
			
		||||
	iconSVG := m.svgs[name]
 | 
			
		||||
	if iconSVG == "" {
 | 
			
		||||
		name = "file"
 | 
			
		||||
		if entry.EntryMode.IsDir() {
 | 
			
		||||
			name = util.Iif(entry.IsOpen, "folder-open", "folder")
 | 
			
		||||
		}
 | 
			
		||||
		iconSVG = m.svgs[name]
 | 
			
		||||
		if iconSVG == "" {
 | 
			
		||||
			setting.PanicInDevOrTesting("missing file icon for %s", name)
 | 
			
		||||
		}
 | 
			
		||||
		return m.renderFileIconSVG(p, name, iconSVG, extraClass)
 | 
			
		||||
	}
 | 
			
		||||
	// TODO: use an interface or wrapper for git.Entry to make the code testable.
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
 | 
			
		||||
	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
	extraClass := "octicon-file"
 | 
			
		||||
	switch {
 | 
			
		||||
	case entry.EntryMode.IsDir():
 | 
			
		||||
		extraClass = BasicEntryIconName(entry)
 | 
			
		||||
	case entry.EntryMode.IsSubModule():
 | 
			
		||||
		extraClass = "octicon-file-submodule"
 | 
			
		||||
	}
 | 
			
		||||
	return m.renderFileIconSVG(p, name, iconSVG, extraClass)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
 | 
			
		||||
@@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
 | 
			
		||||
	fileNameLower := strings.ToLower(path.Base(name))
 | 
			
		||||
	if isDir {
 | 
			
		||||
func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
 | 
			
		||||
	if entry.EntryMode.IsSubModule() {
 | 
			
		||||
		return "folder-git"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileNameLower := strings.ToLower(path.Base(entry.FullName))
 | 
			
		||||
	if entry.EntryMode.IsDir() {
 | 
			
		||||
		if s, ok := m.rules.FolderNames[fileNameLower]; ok {
 | 
			
		||||
			return s
 | 
			
		||||
		}
 | 
			
		||||
		return "folder"
 | 
			
		||||
		return util.Iif(entry.IsOpen, "folder-open", "folder")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s, ok := m.rules.FileNames[fileNameLower]; ok {
 | 
			
		||||
@@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
 | 
			
		||||
 | 
			
		||||
	return "file"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
 | 
			
		||||
	if entry.IsSubModule() {
 | 
			
		||||
		return "folder-git"
 | 
			
		||||
	}
 | 
			
		||||
	return m.FindIconName(entry.Name(), entry.IsDir())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
@@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
 | 
			
		||||
func TestFindIconName(t *testing.T) {
 | 
			
		||||
	unittest.PrepareTestEnv(t)
 | 
			
		||||
	p := fileicon.DefaultMaterialIconProvider()
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName("foo.php", false))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
 | 
			
		||||
	assert.Equal(t, "javascript", p.FindIconName("foo.js", false))
 | 
			
		||||
	assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
 | 
			
		||||
	return template.HTML(sb.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
 | 
			
		||||
 | 
			
		||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
 | 
			
		||||
	if setting.UI.FileIconTheme == "material" {
 | 
			
		||||
		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
 | 
			
		||||
		return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
 | 
			
		||||
	}
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
	// TODO: add "open icon" support
 | 
			
		||||
	if setting.UI.FileIconTheme == "material" {
 | 
			
		||||
		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
 | 
			
		||||
	}
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
	return BasicEntryIconHTML(entry)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ const (
 | 
			
		||||
	GitlabLanguage        = "gitlab-language"
 | 
			
		||||
	Lockable              = "lockable"
 | 
			
		||||
	Filter                = "filter"
 | 
			
		||||
	Diff                  = "diff"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var LinguistAttributes = []string{
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,12 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib
 | 
			
		||||
			)
 | 
			
		||||
			cancel = deleteTemporaryFile
 | 
			
		||||
		}
 | 
			
		||||
	} // else: no treeish, assume it is a not a bare repo, read from working directory
 | 
			
		||||
	} else {
 | 
			
		||||
		// Read from existing index, in cases where the repo is bare and has an index,
 | 
			
		||||
		// or the work tree contains unstaged changes that shouldn't affect the attribute check.
 | 
			
		||||
		// It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes.
 | 
			
		||||
		cmd.AddArguments("--cached")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmd.AddDynamicArguments(attributes...)
 | 
			
		||||
	if len(filenames) > 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -57,8 +57,18 @@ func Test_Checker(t *testing.T) {
 | 
			
		||||
		assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Run git check-attr in bare repository using index", func(t *testing.T) {
 | 
			
		||||
		attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{
 | 
			
		||||
			Filenames:  []string{"i-am-a-python.p"},
 | 
			
		||||
			Attributes: LinguistAttributes,
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, attrs, 1)
 | 
			
		||||
		assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if !git.DefaultFeatures().SupportCheckAttrOnBare {
 | 
			
		||||
		t.Skip("git version 2.40 is required to support run check-attr on bare repo")
 | 
			
		||||
		t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -132,18 +132,22 @@ func (r *BlameReader) Close() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateBlameReader creates reader for given repository, commit and file
 | 
			
		||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
 | 
			
		||||
	reader, stdout, err := os.Pipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) {
 | 
			
		||||
	var ignoreRevsFileName string
 | 
			
		||||
	var ignoreRevsFileCleanup func()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err != nil && ignoreRevsFileCleanup != nil {
 | 
			
		||||
			ignoreRevsFileCleanup()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	cmd := NewCommandNoGlobals("blame", "--porcelain")
 | 
			
		||||
 | 
			
		||||
	var ignoreRevsFileName string
 | 
			
		||||
	var ignoreRevsFileCleanup func() // TODO: maybe it should check the returned err in a defer func to make sure the cleanup could always be executed correctly
 | 
			
		||||
	if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
 | 
			
		||||
		ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit)
 | 
			
		||||
		ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit)
 | 
			
		||||
		if err != nil && !IsErrNotExist(err) {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if ignoreRevsFileName != "" {
 | 
			
		||||
			// Possible improvement: use --ignore-revs-file /dev/stdin on unix
 | 
			
		||||
			// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
 | 
			
		||||
@@ -154,6 +158,10 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
 | 
			
		||||
	cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
 | 
			
		||||
 | 
			
		||||
	done := make(chan error, 1)
 | 
			
		||||
	reader, stdout, err := os.Pipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
		stderr := bytes.Buffer{}
 | 
			
		||||
		// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
 | 
			
		||||
@@ -182,33 +190,29 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) {
 | 
			
		||||
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) {
 | 
			
		||||
	entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return "", nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := entry.Blob().DataAsync()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return "", nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer r.Close()
 | 
			
		||||
 | 
			
		||||
	f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return "", nil, err
 | 
			
		||||
	}
 | 
			
		||||
	filename := f.Name()
 | 
			
		||||
	_, err = io.Copy(f, r)
 | 
			
		||||
	_ = f.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		cleanup()
 | 
			
		||||
		log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err)
 | 
			
		||||
		return "", nil
 | 
			
		||||
		return "", nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filename, cleanup
 | 
			
		||||
	return filename, cleanup, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								modules/git/cmdverb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								modules/git/cmdverb.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package git
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	CmdVerbUploadPack      = "git-upload-pack"
 | 
			
		||||
	CmdVerbUploadArchive   = "git-upload-archive"
 | 
			
		||||
	CmdVerbReceivePack     = "git-receive-pack"
 | 
			
		||||
	CmdVerbLfsAuthenticate = "git-lfs-authenticate"
 | 
			
		||||
	CmdVerbLfsTransfer     = "git-lfs-transfer"
 | 
			
		||||
 | 
			
		||||
	CmdSubVerbLfsUpload   = "upload"
 | 
			
		||||
	CmdSubVerbLfsDownload = "download"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func IsAllowedVerbForServe(verb string) bool {
 | 
			
		||||
	switch verb {
 | 
			
		||||
	case CmdVerbUploadPack,
 | 
			
		||||
		CmdVerbUploadArchive,
 | 
			
		||||
		CmdVerbReceivePack,
 | 
			
		||||
		CmdVerbLfsAuthenticate,
 | 
			
		||||
		CmdVerbLfsTransfer:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsAllowedVerbForServeLfs(verb string) bool {
 | 
			
		||||
	switch verb {
 | 
			
		||||
	case CmdVerbLfsAuthenticate,
 | 
			
		||||
		CmdVerbLfsTransfer:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
@@ -34,7 +34,7 @@ type Commit struct {
 | 
			
		||||
// CommitSignature represents a git commit signature part.
 | 
			
		||||
type CommitSignature struct {
 | 
			
		||||
	Signature string
 | 
			
		||||
	Payload   string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
 | 
			
		||||
	Payload   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
 | 
			
		||||
 
 | 
			
		||||
@@ -9,3 +9,15 @@ type CommitInfo struct {
 | 
			
		||||
	Commit        *Commit
 | 
			
		||||
	SubmoduleFile *CommitSubmoduleFile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
 | 
			
		||||
	submodule, err := commit.GetSubModule(fullPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if submodule == nil {
 | 
			
		||||
		// unable to find submodule from ".gitmodules" file
 | 
			
		||||
		return NewCommitSubmoduleFile(repoLink, fullPath, "", refCommitID.String()), nil
 | 
			
		||||
	}
 | 
			
		||||
	return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, refCommitID.String()), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
	entryPaths := make([]string, len(tes)+1)
 | 
			
		||||
	// Get the commit for the treePath itself
 | 
			
		||||
	entryPaths[0] = ""
 | 
			
		||||
@@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
 | 
			
		||||
			commitsInfo[i].Commit = entryCommit
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the entry is a submodule add a submodule file for this
 | 
			
		||||
		// If the entry is a submodule, add a submodule file for this
 | 
			
		||||
		if entry.IsSubModule() {
 | 
			
		||||
			subModuleURL := ""
 | 
			
		||||
			var fullPath string
 | 
			
		||||
			if len(treePath) > 0 {
 | 
			
		||||
				fullPath = treePath + "/" + entry.Name()
 | 
			
		||||
			} else {
 | 
			
		||||
				fullPath = entry.Name()
 | 
			
		||||
			}
 | 
			
		||||
			if subModule, err := commit.GetSubModule(fullPath); err != nil {
 | 
			
		||||
			commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			} else if subModule != nil {
 | 
			
		||||
				subModuleURL = subModule.URL
 | 
			
		||||
			}
 | 
			
		||||
			subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
 | 
			
		||||
			commitsInfo[i].SubmoduleFile = subModuleFile
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
	entryPaths := make([]string, len(tes)+1)
 | 
			
		||||
	// Get the commit for the treePath itself
 | 
			
		||||
	entryPaths[0] = ""
 | 
			
		||||
@@ -65,22 +65,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
 | 
			
		||||
			log.Debug("missing commit for %s", entry.Name())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the entry is a submodule add a submodule file for this
 | 
			
		||||
		// If the entry is a submodule, add a submodule file for this
 | 
			
		||||
		if entry.IsSubModule() {
 | 
			
		||||
			subModuleURL := ""
 | 
			
		||||
			var fullPath string
 | 
			
		||||
			if len(treePath) > 0 {
 | 
			
		||||
				fullPath = treePath + "/" + entry.Name()
 | 
			
		||||
			} else {
 | 
			
		||||
				fullPath = entry.Name()
 | 
			
		||||
			}
 | 
			
		||||
			if subModule, err := commit.GetSubModule(fullPath); err != nil {
 | 
			
		||||
			commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			} else if subModule != nil {
 | 
			
		||||
				subModuleURL = subModule.URL
 | 
			
		||||
			}
 | 
			
		||||
			subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
 | 
			
		||||
			commitsInfo[i].SubmoduleFile = subModuleFile
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -82,7 +83,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
 | 
			
		||||
		commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path)
 | 
			
		||||
		commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
 | 
			
		||||
		assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.FailNow()
 | 
			
		||||
@@ -120,6 +121,23 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
 | 
			
		||||
	defer clonedRepo1.Close()
 | 
			
		||||
 | 
			
		||||
	testGetCommitsInfo(t, clonedRepo1)
 | 
			
		||||
 | 
			
		||||
	t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
 | 
			
		||||
		commit, err := bareRepo1.GetCommit("HEAD")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, &CommitSubmoduleFile{
 | 
			
		||||
			repoLink: "/any/repo-link",
 | 
			
		||||
			fullPath: "file1.txt",
 | 
			
		||||
			refURL:   "",
 | 
			
		||||
			refID:    "e2129701f1a4d54dc44f03c93bca0a2aec7c5449",
 | 
			
		||||
		}, cisf)
 | 
			
		||||
		// since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link
 | 
			
		||||
		assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
 | 
			
		||||
@@ -159,7 +177,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
 | 
			
		||||
		b.ResetTimer()
 | 
			
		||||
		b.Run(benchmark.name, func(b *testing.B) {
 | 
			
		||||
			for b.Loop() {
 | 
			
		||||
				_, _, err := entries.GetCommitsInfo(b.Context(), commit, "")
 | 
			
		||||
				_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					b.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,44 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	commitHeaderGpgsig       = "gpgsig"
 | 
			
		||||
	commitHeaderGpgsigSha256 = "gpgsig-sha256"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error {
 | 
			
		||||
	if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
 | 
			
		||||
		headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
 | 
			
		||||
	}
 | 
			
		||||
	switch headerKey {
 | 
			
		||||
	case "tree":
 | 
			
		||||
		objID, err := NewIDFromString(string(headerValue))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
 | 
			
		||||
		}
 | 
			
		||||
		commit.Tree = *NewTree(gitRepo, objID)
 | 
			
		||||
	case "parent":
 | 
			
		||||
		objID, err := NewIDFromString(string(headerValue))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("invalid parent ID %q: %w", string(headerValue), err)
 | 
			
		||||
		}
 | 
			
		||||
		commit.Parents = append(commit.Parents, objID)
 | 
			
		||||
	case "author":
 | 
			
		||||
		commit.Author.Decode(headerValue)
 | 
			
		||||
	case "committer":
 | 
			
		||||
		commit.Committer.Decode(headerValue)
 | 
			
		||||
	case commitHeaderGpgsig, commitHeaderGpgsigSha256:
 | 
			
		||||
		// if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid
 | 
			
		||||
		// so we don't need to handle duplicate headers here
 | 
			
		||||
		commit.Signature = &CommitSignature{Signature: string(headerValue)}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CommitFromReader will generate a Commit from a provided reader
 | 
			
		||||
// We need this to interpret commits from cat-file or cat-file --batch
 | 
			
		||||
//
 | 
			
		||||
@@ -21,90 +55,46 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader)
 | 
			
		||||
		Committer: &Signature{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	payloadSB := new(strings.Builder)
 | 
			
		||||
	signatureSB := new(strings.Builder)
 | 
			
		||||
	messageSB := new(strings.Builder)
 | 
			
		||||
	message := false
 | 
			
		||||
	pgpsig := false
 | 
			
		||||
 | 
			
		||||
	bufReader, ok := reader.(*bufio.Reader)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		bufReader = bufio.NewReader(reader)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
readLoop:
 | 
			
		||||
	bufReader := bufio.NewReader(reader)
 | 
			
		||||
	inHeader := true
 | 
			
		||||
	var payloadSB, messageSB bytes.Buffer
 | 
			
		||||
	var headerKey string
 | 
			
		||||
	var headerValue []byte
 | 
			
		||||
	for {
 | 
			
		||||
		line, err := bufReader.ReadBytes('\n')
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if err == io.EOF {
 | 
			
		||||
				if message {
 | 
			
		||||
					_, _ = messageSB.Write(line)
 | 
			
		||||
		if err != nil && err != io.EOF {
 | 
			
		||||
			return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(line) == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if inHeader {
 | 
			
		||||
			inHeader = !(len(line) == 1 && line[0] == '\n') // still in header if line is not just a newline
 | 
			
		||||
			k, v, _ := bytes.Cut(line, []byte{' '})
 | 
			
		||||
			if len(k) != 0 || !inHeader {
 | 
			
		||||
				if headerKey != "" {
 | 
			
		||||
					if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil {
 | 
			
		||||
						return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
				break readLoop
 | 
			
		||||
				headerKey = string(k) // it also resets the headerValue to empty string if not inHeader
 | 
			
		||||
				headerValue = v
 | 
			
		||||
			} else {
 | 
			
		||||
				headerValue = append(headerValue, v...)
 | 
			
		||||
			}
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if pgpsig {
 | 
			
		||||
			if len(line) > 0 && line[0] == ' ' {
 | 
			
		||||
				_, _ = signatureSB.Write(line[1:])
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			pgpsig = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !message {
 | 
			
		||||
			// This is probably not correct but is copied from go-gits interpretation...
 | 
			
		||||
			trimmed := bytes.TrimSpace(line)
 | 
			
		||||
			if len(trimmed) == 0 {
 | 
			
		||||
				message = true
 | 
			
		||||
			if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 {
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			split := bytes.SplitN(trimmed, []byte{' '}, 2)
 | 
			
		||||
			var data []byte
 | 
			
		||||
			if len(split) > 1 {
 | 
			
		||||
				data = split[1]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			switch string(split[0]) {
 | 
			
		||||
			case "tree":
 | 
			
		||||
				commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
			case "parent":
 | 
			
		||||
				commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
			case "author":
 | 
			
		||||
				commit.Author = &Signature{}
 | 
			
		||||
				commit.Author.Decode(data)
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
			case "committer":
 | 
			
		||||
				commit.Committer = &Signature{}
 | 
			
		||||
				commit.Committer.Decode(data)
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
			case "encoding":
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
			case "gpgsig":
 | 
			
		||||
				fallthrough
 | 
			
		||||
			case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present.
 | 
			
		||||
				_, _ = signatureSB.Write(data)
 | 
			
		||||
				_ = signatureSB.WriteByte('\n')
 | 
			
		||||
				pgpsig = true
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			_, _ = messageSB.Write(line)
 | 
			
		||||
			_, _ = payloadSB.Write(line)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	commit.CommitMessage = messageSB.String()
 | 
			
		||||
	commit.Signature = &CommitSignature{
 | 
			
		||||
		Signature: signatureSB.String(),
 | 
			
		||||
		Payload:   payloadSB.String(),
 | 
			
		||||
	}
 | 
			
		||||
	if len(commit.Signature.Signature) == 0 {
 | 
			
		||||
		commit.Signature = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit.CommitMessage = messageSB.String()
 | 
			
		||||
	if commit.Signature != nil {
 | 
			
		||||
		commit.Signature.Payload = payloadSB.String()
 | 
			
		||||
	}
 | 
			
		||||
	return commit, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -60,8 +60,7 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCommitFromReaderSha256(t *testing.T) {
 | 
			
		||||
	commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114
 | 
			
		||||
tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
 | 
			
		||||
	commitString := `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
 | 
			
		||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
 | 
			
		||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
 | 
			
		||||
committer Adam Majer <amajer@suse.de> 1698676906 +0100
 | 
			
		||||
@@ -112,8 +111,7 @@ VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X
 | 
			
		||||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR
 | 
			
		||||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6
 | 
			
		||||
=xybZ
 | 
			
		||||
-----END PGP SIGNATURE-----
 | 
			
		||||
`, commitFromReader.Signature.Signature)
 | 
			
		||||
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
 | 
			
		||||
	assert.Equal(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
 | 
			
		||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
 | 
			
		||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
 | 
			
		||||
	return c.submoduleCache, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSubModule get the submodule according entry name
 | 
			
		||||
// GetSubModule gets the submodule by the entry name.
 | 
			
		||||
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
 | 
			
		||||
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
 | 
			
		||||
	modules, err := c.GetSubModules()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,49 +6,64 @@ package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	giturl "code.gitea.io/gitea/modules/git/url"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CommitSubmoduleFile represents a file with submodule type.
 | 
			
		||||
type CommitSubmoduleFile struct {
 | 
			
		||||
	refURL    string
 | 
			
		||||
	parsedURL *giturl.RepositoryURL
 | 
			
		||||
	parsed    bool
 | 
			
		||||
	refID     string
 | 
			
		||||
	repoLink  string
 | 
			
		||||
	repoLink string
 | 
			
		||||
	fullPath string
 | 
			
		||||
	refURL   string
 | 
			
		||||
	refID    string
 | 
			
		||||
 | 
			
		||||
	parsed           bool
 | 
			
		||||
	parsedTargetLink string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCommitSubmoduleFile create a new submodule file
 | 
			
		||||
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile {
 | 
			
		||||
	return &CommitSubmoduleFile{refURL: refURL, refID: refID}
 | 
			
		||||
func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile {
 | 
			
		||||
	return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RefID returns the commit ID of the submodule, it returns empty string for nil receiver
 | 
			
		||||
func (sf *CommitSubmoduleFile) RefID() string {
 | 
			
		||||
	return sf.refID // this function is only used in templates
 | 
			
		||||
	if sf == nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return sf.refID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
 | 
			
		||||
	if sf == nil {
 | 
			
		||||
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
 | 
			
		||||
	if sf == nil || sf.refURL == "" {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(sf.refURL, "../") {
 | 
			
		||||
		targetLink := path.Join(sf.repoLink, sf.refURL)
 | 
			
		||||
		return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
 | 
			
		||||
	}
 | 
			
		||||
	if !sf.parsed {
 | 
			
		||||
		sf.parsed = true
 | 
			
		||||
		parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		sf.parsedURL = parsedURL
 | 
			
		||||
		sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
 | 
			
		||||
		sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
 | 
			
		||||
	}
 | 
			
		||||
	var commitLink string
 | 
			
		||||
	if len(optCommitID) == 2 {
 | 
			
		||||
		commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
 | 
			
		||||
	} else if len(optCommitID) == 1 {
 | 
			
		||||
		commitLink = sf.repoLink + "/tree/" + optCommitID[0]
 | 
			
		||||
	} else {
 | 
			
		||||
		commitLink = sf.repoLink + "/tree/" + sf.refID
 | 
			
		||||
	}
 | 
			
		||||
	return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
 | 
			
		||||
	return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
 | 
			
		||||
// It returns nil if the submodule does not have a valid URL or is nil
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
 | 
			
		||||
	return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
 | 
			
		||||
// It returns nil if the submodule does not have a valid URL or is nil
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
 | 
			
		||||
	return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,20 +10,31 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCommitSubmoduleLink(t *testing.T) {
 | 
			
		||||
	sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa")
 | 
			
		||||
	assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
 | 
			
		||||
	assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", ""))
 | 
			
		||||
 | 
			
		||||
	wl := sf.SubmoduleWebLink(t.Context())
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
	t.Run("GitHubRepo", func(t *testing.T) {
 | 
			
		||||
		sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
 | 
			
		||||
		wl := sf.SubmoduleWebLinkTree(t.Context())
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
 | 
			
		||||
	wl = sf.SubmoduleWebLink(t.Context(), "1111")
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
 | 
			
		||||
		wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222")
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	t.Run("RelativePath", func(t *testing.T) {
 | 
			
		||||
		sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa")
 | 
			
		||||
		wl := sf.SubmoduleWebLinkTree(t.Context())
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
 | 
			
		||||
	wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context())
 | 
			
		||||
	assert.Nil(t, wl)
 | 
			
		||||
		sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../user/repo", "aaaa")
 | 
			
		||||
		wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,8 +59,7 @@ func TestGetFullCommitIDError(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCommitFromReader(t *testing.T) {
 | 
			
		||||
	commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
 | 
			
		||||
tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
 | 
			
		||||
	commitString := `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
 | 
			
		||||
parent 37991dec2c8e592043f47155ce4808d4580f9123
 | 
			
		||||
author silverwind <me@silverwind.io> 1563741793 +0200
 | 
			
		||||
committer silverwind <me@silverwind.io> 1563741793 +0200
 | 
			
		||||
@@ -108,8 +107,7 @@ sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm
 | 
			
		||||
mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i
 | 
			
		||||
1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs=
 | 
			
		||||
=FRsO
 | 
			
		||||
-----END PGP SIGNATURE-----
 | 
			
		||||
`, commitFromReader.Signature.Signature)
 | 
			
		||||
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
 | 
			
		||||
	assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
 | 
			
		||||
parent 37991dec2c8e592043f47155ce4808d4580f9123
 | 
			
		||||
author silverwind <me@silverwind.io> 1563741793 +0200
 | 
			
		||||
@@ -126,8 +124,7 @@ empty commit`, commitFromReader.Signature.Payload)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCommitWithEncodingFromReader(t *testing.T) {
 | 
			
		||||
	commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
 | 
			
		||||
tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
 | 
			
		||||
	commitString := `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
 | 
			
		||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
 | 
			
		||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
 | 
			
		||||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
 | 
			
		||||
@@ -172,8 +169,7 @@ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
 | 
			
		||||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
 | 
			
		||||
jw4YcO5u
 | 
			
		||||
=r3UU
 | 
			
		||||
-----END PGP SIGNATURE-----
 | 
			
		||||
`, commitFromReader.Signature.Signature)
 | 
			
		||||
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
 | 
			
		||||
	assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
 | 
			
		||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
 | 
			
		||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
 | 
			
		||||
 
 | 
			
		||||
@@ -99,9 +99,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseDiffHunkString parse the diffhunk content and return
 | 
			
		||||
func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
 | 
			
		||||
	ss := strings.Split(diffhunk, "@@")
 | 
			
		||||
// ParseDiffHunkString parse the diff hunk content and return
 | 
			
		||||
func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) {
 | 
			
		||||
	ss := strings.Split(diffHunk, "@@")
 | 
			
		||||
	ranges := strings.Split(ss[1][1:], " ")
 | 
			
		||||
	leftRange := strings.Split(ranges[0], ",")
 | 
			
		||||
	leftLine, _ = strconv.Atoi(leftRange[0][1:])
 | 
			
		||||
@@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
 | 
			
		||||
		rightRange := strings.Split(ranges[1], ",")
 | 
			
		||||
		rightLine, _ = strconv.Atoi(rightRange[0])
 | 
			
		||||
		if len(rightRange) > 1 {
 | 
			
		||||
			righHunk, _ = strconv.Atoi(rightRange[1])
 | 
			
		||||
			rightHunk, _ = strconv.Atoi(rightRange[1])
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Debug("Parse line number failed: %v", diffhunk)
 | 
			
		||||
		log.Debug("Parse line number failed: %v", diffHunk)
 | 
			
		||||
		rightLine = leftLine
 | 
			
		||||
		righHunk = leftHunk
 | 
			
		||||
		rightHunk = leftHunk
 | 
			
		||||
	}
 | 
			
		||||
	return leftLine, leftHunk, rightLine, righHunk
 | 
			
		||||
	if rightLine == 0 {
 | 
			
		||||
		// FIXME: GIT-DIFF-CUT-BUG search this tag to see details
 | 
			
		||||
		// this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases.
 | 
			
		||||
		rightLine++
 | 
			
		||||
	}
 | 
			
		||||
	return leftLine, leftHunk, rightLine, rightHunk
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
 | 
			
		||||
@@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
 | 
			
		||||
			oldNumOfLines++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
 | 
			
		||||
	// FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@"
 | 
			
		||||
	// It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check.
 | 
			
		||||
	// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
 | 
			
		||||
 | 
			
		||||
	// construct the new hunk header
 | 
			
		||||
	newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
 | 
			
		||||
		oldBegin, oldNumOfLines, newBegin, newNumOfLines)
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,10 @@ type Parser struct {
 | 
			
		||||
func NewParser(r io.Reader, format Format) *Parser {
 | 
			
		||||
	scanner := bufio.NewScanner(r)
 | 
			
		||||
 | 
			
		||||
	// default MaxScanTokenSize = 64 kiB may be too small for some references,
 | 
			
		||||
	// so allow the buffer to grow up to 4x if needed
 | 
			
		||||
	scanner.Buffer(nil, 4*bufio.MaxScanTokenSize)
 | 
			
		||||
 | 
			
		||||
	// in addition to the reference delimiter we specified in the --format,
 | 
			
		||||
	// `git for-each-ref` will always add a newline after every reference.
 | 
			
		||||
	refDelim := make([]byte, 0, len(format.refDelim)+1)
 | 
			
		||||
@@ -70,6 +74,9 @@ func NewParser(r io.Reader, format Format) *Parser {
 | 
			
		||||
//	{ "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" }
 | 
			
		||||
func (p *Parser) Next() map[string]string {
 | 
			
		||||
	if !p.scanner.Scan() {
 | 
			
		||||
		if err := p.scanner.Err(); err != nil {
 | 
			
		||||
			p.err = err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	fields, err := p.parseRef(p.scanner.Text())
 | 
			
		||||
 
 | 
			
		||||
@@ -97,17 +97,17 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		isVendored := optional.None[bool]()
 | 
			
		||||
		isGenerated := optional.None[bool]()
 | 
			
		||||
		isDocumentation := optional.None[bool]()
 | 
			
		||||
		isDetectable := optional.None[bool]()
 | 
			
		||||
 | 
			
		||||
		attrs, err := checker.CheckPath(f.Name())
 | 
			
		||||
		attrLinguistGenerated := optional.None[bool]()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if isGenerated = attrs.GetGenerated(); isGenerated.ValueOrDefault(false) {
 | 
			
		||||
			if attrLinguistGenerated = attrs.GetGenerated(); attrLinguistGenerated.ValueOrDefault(false) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -169,7 +169,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) {
 | 
			
		||||
 | 
			
		||||
		// if "generated" attribute is set, use it, otherwise use enry.IsGenerated to guess
 | 
			
		||||
		var isGenerated bool
 | 
			
		||||
		if attrLinguistGenerated.Has() {
 | 
			
		||||
			isGenerated = attrLinguistGenerated.Value()
 | 
			
		||||
		} else {
 | 
			
		||||
			isGenerated = enry.IsGenerated(f.Name(), content)
 | 
			
		||||
		}
 | 
			
		||||
		if isGenerated {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,31 @@ func (e EntryMode) String() string {
 | 
			
		||||
	return strconv.FormatInt(int64(e), 8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsSubModule if the entry is a sub module
 | 
			
		||||
func (e EntryMode) IsSubModule() bool {
 | 
			
		||||
	return e == EntryModeCommit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDir if the entry is a sub dir
 | 
			
		||||
func (e EntryMode) IsDir() bool {
 | 
			
		||||
	return e == EntryModeTree
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsLink if the entry is a symlink
 | 
			
		||||
func (e EntryMode) IsLink() bool {
 | 
			
		||||
	return e == EntryModeSymlink
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsRegular if the entry is a regular file
 | 
			
		||||
func (e EntryMode) IsRegular() bool {
 | 
			
		||||
	return e == EntryModeBlob
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsExecutable if the entry is an executable file (not necessarily binary)
 | 
			
		||||
func (e EntryMode) IsExecutable() bool {
 | 
			
		||||
	return e == EntryModeExec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseEntryMode(mode string) (EntryMode, error) {
 | 
			
		||||
	switch mode {
 | 
			
		||||
	case "000000":
 | 
			
		||||
 
 | 
			
		||||
@@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
 | 
			
		||||
 | 
			
		||||
// IsSubModule if the entry is a sub module
 | 
			
		||||
func (te *TreeEntry) IsSubModule() bool {
 | 
			
		||||
	return te.entryMode == EntryModeCommit
 | 
			
		||||
	return te.entryMode.IsSubModule()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDir if the entry is a sub dir
 | 
			
		||||
func (te *TreeEntry) IsDir() bool {
 | 
			
		||||
	return te.entryMode == EntryModeTree
 | 
			
		||||
	return te.entryMode.IsDir()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsLink if the entry is a symlink
 | 
			
		||||
func (te *TreeEntry) IsLink() bool {
 | 
			
		||||
	return te.entryMode == EntryModeSymlink
 | 
			
		||||
	return te.entryMode.IsLink()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsRegular if the entry is a regular file
 | 
			
		||||
func (te *TreeEntry) IsRegular() bool {
 | 
			
		||||
	return te.entryMode == EntryModeBlob
 | 
			
		||||
	return te.entryMode.IsRegular()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsExecutable if the entry is an executable file (not necessarily binary)
 | 
			
		||||
func (te *TreeEntry) IsExecutable() bool {
 | 
			
		||||
	return te.entryMode == EntryModeExec
 | 
			
		||||
	return te.entryMode.IsExecutable()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blob returns the blob object the entry
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ObjectCache provides thread-safe cache operations.
 | 
			
		||||
@@ -106,3 +108,16 @@ func HashFilePathForWebUI(s string) string {
 | 
			
		||||
	_, _ = h.Write([]byte(s))
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SplitCommitTitleBody(commitMessage string, titleRuneLimit int) (title, body string) {
 | 
			
		||||
	title, body, _ = strings.Cut(commitMessage, "\n")
 | 
			
		||||
	title, title2 := util.EllipsisTruncateRunes(title, titleRuneLimit)
 | 
			
		||||
	if title2 != "" {
 | 
			
		||||
		if body == "" {
 | 
			
		||||
			body = title2
 | 
			
		||||
		} else {
 | 
			
		||||
			body = title2 + "\n" + body
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return title, body
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,3 +15,17 @@ func TestHashFilePathForWebUI(t *testing.T) {
 | 
			
		||||
		HashFilePathForWebUI("foobar"),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSplitCommitTitleBody(t *testing.T) {
 | 
			
		||||
	title, body := SplitCommitTitleBody("啊bcdefg", 4)
 | 
			
		||||
	assert.Equal(t, "啊…", title)
 | 
			
		||||
	assert.Equal(t, "…bcdefg", body)
 | 
			
		||||
 | 
			
		||||
	title, body = SplitCommitTitleBody("abcdefg\n1234567", 4)
 | 
			
		||||
	assert.Equal(t, "a…", title)
 | 
			
		||||
	assert.Equal(t, "…bcdefg\n1234567", body)
 | 
			
		||||
 | 
			
		||||
	title, body = SplitCommitTitleBody("abcdefg\n1234567", 100)
 | 
			
		||||
	assert.Equal(t, "abcdefg", title)
 | 
			
		||||
	assert.Equal(t, "1234567", body)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	req := private.NewInternalRequest(ctx, internalURL, method)
 | 
			
		||||
	req.SetReadWriteTimeout(0)
 | 
			
		||||
	for k, v := range headers {
 | 
			
		||||
		req.Header(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
 | 
			
		||||
		_, _ = w.Write(n.Name)
 | 
			
		||||
		_, _ = w.WriteString(`"><a href="#fn:`)
 | 
			
		||||
		_, _ = w.Write(n.Name)
 | 
			
		||||
		_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
 | 
			
		||||
		_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes
 | 
			
		||||
		_, _ = w.WriteString(is)
 | 
			
		||||
		_, _ = w.WriteString(`</a></sup>`)
 | 
			
		||||
		_, _ = w.WriteString(` </a></sup>`) // the style doesn't work at the moment, so add a space to separate the names
 | 
			
		||||
	}
 | 
			
		||||
	return ast.WalkContinue, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -86,8 +86,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
 | 
			
		||||
	// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
 | 
			
		||||
	v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
 | 
			
		||||
 | 
			
		||||
	// cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style")
 | 
			
		||||
	v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style\b))`)
 | 
			
		||||
	// cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style", "<?", "<%")
 | 
			
		||||
	v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style|%|\?)\b)`)
 | 
			
		||||
	v.nulCleaner = strings.NewReplacer("\000", "")
 | 
			
		||||
	return v
 | 
			
		||||
})
 | 
			
		||||
@@ -253,7 +253,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
 | 
			
		||||
	node, err := html.Parse(io.MultiReader(
 | 
			
		||||
		// prepend "<html><body>"
 | 
			
		||||
		strings.NewReader("<html><body>"),
 | 
			
		||||
		// Strip out nuls - they're always invalid
 | 
			
		||||
		// strip out NULLs (they're always invalid), and escape known tags
 | 
			
		||||
		bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
 | 
			
		||||
		// close the tags
 | 
			
		||||
		strings.NewReader("</body></html>"),
 | 
			
		||||
@@ -320,6 +320,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	processNodeAttrID(node)
 | 
			
		||||
	processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly
 | 
			
		||||
 | 
			
		||||
	if isEmojiNode(node) {
 | 
			
		||||
		// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ func TestRender_IssueList(t *testing.T) {
 | 
			
		||||
		rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{
 | 
			
		||||
			"user": "test-user", "repo": "test-repo",
 | 
			
		||||
			"markupAllowShortIssuePattern": "true",
 | 
			
		||||
			"footnoteContextId":            "12345",
 | 
			
		||||
		})
 | 
			
		||||
		out, err := markdown.RenderString(rctx, input)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
@@ -69,4 +70,22 @@ func TestRender_IssueList(t *testing.T) {
 | 
			
		||||
</ul>`,
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("IssueFootnote", func(t *testing.T) {
 | 
			
		||||
		test(
 | 
			
		||||
			"foo[^1][^2]\n\n[^1]: bar\n[^2]: baz",
 | 
			
		||||
			`<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">1 </a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">2 </a></sup></p>
 | 
			
		||||
<div>
 | 
			
		||||
<hr/>
 | 
			
		||||
<ol>
 | 
			
		||||
<li id="fn:user-content-1-12345">
 | 
			
		||||
<p>bar <a href="#fnref:user-content-1-12345" rel="nofollow">↩︎</a></p>
 | 
			
		||||
</li>
 | 
			
		||||
<li id="fn:user-content-2-12345">
 | 
			
		||||
<p>baz <a href="#fnref:user-content-2-12345" rel="nofollow">↩︎</a></p>
 | 
			
		||||
</li>
 | 
			
		||||
</ol>
 | 
			
		||||
</div>`,
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user