mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			84 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | 
							
								
								
									
										2
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										443
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										443
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,449 @@ 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.1](https://github.com/go-gitea/gitea/releases/tag/1.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/1.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/1.23.8) - 2025-05-11
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
 | 
			
		||||
  * Update net package (#34228) (#34232)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix releases sidebar navigation link (#34436) #34439
 | 
			
		||||
  * Fix bug webhook milestone is not right. (#34419) #34429
 | 
			
		||||
  * Fix two missed null value checks on the wiki page. (#34205) (#34215)
 | 
			
		||||
  * Swift files can be passed either as file or as form value (#34068) (#34236)
 | 
			
		||||
  * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
 | 
			
		||||
  * Upgrade github v61 -> v71 to fix migrating bug (#34389)
 | 
			
		||||
  * Fix bug when visiting comparation page (#34334) (#34364)
 | 
			
		||||
  * Fix wrong review requests when updating the pull request (#34286) (#34304)
 | 
			
		||||
  * Fix github migration error when using multiple tokens (#34144) (#34302)
 | 
			
		||||
  * Explicitly not update indexes when sync database schemas (#34281) (#34295)
 | 
			
		||||
  * Fix panic when comment is nil (#34257) (#34277)
 | 
			
		||||
  * Fix project board links to related Pull Requests (#34213) (#34222)
 | 
			
		||||
  * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
 | 
			
		||||
* DOCUMENTATION
 | 
			
		||||
  * Update token creation API swagger documentation (#34288) (#34296)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Fix CI Build (#34315)
 | 
			
		||||
  * Add riscv64 support (#34199) (#34204)
 | 
			
		||||
  * Bump go version in go.mod (#34160)
 | 
			
		||||
  * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
 | 
			
		||||
 | 
			
		||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
 | 
			
		||||
 | 
			
		||||
* Enhancements
 | 
			
		||||
  * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
 | 
			
		||||
  * Also check default ssh-cert location for host (#34099) (#34100) (#34116)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
 | 
			
		||||
  * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
 | 
			
		||||
  * Fix invalid version in RPM package path (#34112) (#34115)
 | 
			
		||||
  * Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
 | 
			
		||||
  * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
 | 
			
		||||
  * Try to fix check-attr bug (#34029) (#34033)
 | 
			
		||||
  * Git client will follow 301 but 307 (#34005) (#34010)
 | 
			
		||||
  * Fix block expensive for 1.23 (#34127)
 | 
			
		||||
  * Fix markdown frontmatter rendering (#34102) (#34107)
 | 
			
		||||
  * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
 | 
			
		||||
  * Do not show 500 error when default branch doesn't exist (#34096) (#34097)
 | 
			
		||||
  * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
 | 
			
		||||
  * Simplify emoji rendering (#34048) (#34049)
 | 
			
		||||
  * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
 | 
			
		||||
  * Pull request updates will also trigger code owners review requests (#33744) (#34045)
 | 
			
		||||
  * Fix org repo creation being limited by user limits (#34030) (#34044)
 | 
			
		||||
  * Fix git client accessing renamed repo (#34034) (#34043)
 | 
			
		||||
  * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
 | 
			
		||||
  * Polyfill WeakRef (#34025) (#34028)
 | 
			
		||||
 | 
			
		||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								cmd/serv.go
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								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,21 +258,22 @@ func runServ(c *cli.Context) error {
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if allowedCommands.Contains(verb) {
 | 
			
		||||
		if allowedCommandsLfs.Contains(verb) {
 | 
			
		||||
	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 == verbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
		if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
			return fail(ctx, "LFS SSH transfer is not enabled", "")
 | 
			
		||||
		}
 | 
			
		||||
			if len(words) > 2 {
 | 
			
		||||
				lfsVerb = words[2]
 | 
			
		||||
		if len(sshCmdArgs) > 2 {
 | 
			
		||||
			lfsVerb = sshCmdArgs[2]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	} else {
 | 
			
		||||
		return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestedMode := getAccessMode(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": 1747179050,
 | 
			
		||||
        "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
 | 
			
		||||
        "owner": "nixos",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
 | 
			
		||||
        "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								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=
 | 
			
		||||
@@ -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=
 | 
			
		||||
 
 | 
			
		||||
@@ -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}),
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
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"}).GroupBy("issue.repo_id").
 | 
			
		||||
			Find(&ids); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		Where(builder.IsNull{"repository.id"}).
 | 
			
		||||
		Find(&repoIDs); err != nil {
 | 
			
		||||
		return nil, 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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
}
 | 
			
		||||
@@ -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})
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
		// 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)
 | 
			
		||||
		rctx = rctx.WithMetas(map[string]string{
 | 
			
		||||
			"user": helper.opts.DeprecatedOwnerName,
 | 
			
		||||
			"repo": helper.opts.DeprecatedRepoName,
 | 
			
		||||
 | 
			
		||||
			"markdownNewLineHardBreak":     "true",
 | 
			
		||||
			"markupAllowShortIssuePattern": "true",
 | 
			
		||||
		})
 | 
			
		||||
			metas["user"] = helper.opts.DeprecatedOwnerName
 | 
			
		||||
			metas["repo"] = helper.opts.DeprecatedRepoName
 | 
			
		||||
		}
 | 
			
		||||
	rctx = rctx.WithHelper(helper)
 | 
			
		||||
		metas["markdownNewLineHardBreak"] = "true"
 | 
			
		||||
		metas["markupAllowShortIssuePattern"] = "true"
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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]).
 | 
			
		||||
		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
 | 
			
		||||
 
 | 
			
		||||
@@ -67,15 +67,15 @@ type globalVarsStruct struct {
 | 
			
		||||
	validRepoNamePattern     *regexp.Regexp
 | 
			
		||||
	invalidRepoNamePattern   *regexp.Regexp
 | 
			
		||||
	reservedRepoNames        []string
 | 
			
		||||
	reservedRepoPatterns   []string
 | 
			
		||||
	reservedRepoNamePatterns []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
 | 
			
		||||
	return &globalVarsStruct{
 | 
			
		||||
		validRepoNamePattern:   regexp.MustCompile(`[-.\w]+`),
 | 
			
		||||
		validRepoNamePattern:     regexp.MustCompile(`^[-.\w]+$`),
 | 
			
		||||
		invalidRepoNamePattern:   regexp.MustCompile(`[.]{2,}`),
 | 
			
		||||
		reservedRepoNames:        []string{".", "..", "-"},
 | 
			
		||||
		reservedRepoPatterns:   []string{"*.git", "*.wiki", "*.rss", "*.atom"},
 | 
			
		||||
		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,7 +1161,15 @@ 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
 | 
			
		||||
	}
 | 
			
		||||
@@ -1171,7 +1179,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
 | 
			
		||||
	for _, email := range emails {
 | 
			
		||||
		if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
 | 
			
		||||
			username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
 | 
			
		||||
			needCheckUserNames.Add(username)
 | 
			
		||||
			needCheckUserNames.Add(strings.ToLower(username))
 | 
			
		||||
		} else {
 | 
			
		||||
			needCheckEmails.Add(strings.ToLower(email))
 | 
			
		||||
		}
 | 
			
		||||
@@ -1198,7 +1206,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 +1216,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,43 @@ 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())
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
		}
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
				break readLoop
 | 
			
		||||
			}
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if pgpsig {
 | 
			
		||||
			if len(line) > 0 && line[0] == ' ' {
 | 
			
		||||
				_, _ = signatureSB.Write(line[1:])
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			pgpsig = false
 | 
			
		||||
		if len(line) == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !message {
 | 
			
		||||
			// This is probably not correct but is copied from go-gits interpretation...
 | 
			
		||||
			trimmed := bytes.TrimSpace(line)
 | 
			
		||||
			if len(trimmed) == 0 {
 | 
			
		||||
				message = true
 | 
			
		||||
				_, _ = payloadSB.Write(line)
 | 
			
		||||
				continue
 | 
			
		||||
		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)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
			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)))
 | 
			
		||||
				headerKey = string(k) // it also resets the headerValue to empty string if not inHeader
 | 
			
		||||
				headerValue = v
 | 
			
		||||
			} else {
 | 
			
		||||
				headerValue = append(headerValue, v...)
 | 
			
		||||
			}
 | 
			
		||||
			if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 {
 | 
			
		||||
				_, _ = 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>`,
 | 
			
		||||
		)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,14 @@ func isAnchorIDUserContent(s string) bool {
 | 
			
		||||
	return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isAnchorIDFootnote(s string) bool {
 | 
			
		||||
	return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isAnchorHrefFootnote(s string) bool {
 | 
			
		||||
	return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processNodeAttrID(node *html.Node) {
 | 
			
		||||
	// Add user-content- to IDs and "#" links if they don't already have them,
 | 
			
		||||
	// and convert the link href to a relative link to the host root
 | 
			
		||||
@@ -27,6 +35,18 @@ func processNodeAttrID(node *html.Node) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processFootnoteNode(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	for idx, attr := range node.Attr {
 | 
			
		||||
		if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) ||
 | 
			
		||||
			(attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) {
 | 
			
		||||
			if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" {
 | 
			
		||||
				node.Attr[idx].Val = attr.Val + "-" + footnoteContextID
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processNodeA(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	for idx, attr := range node.Attr {
 | 
			
		||||
		if attr.Key == "href" {
 | 
			
		||||
 
 | 
			
		||||
@@ -525,6 +525,10 @@ func TestPostProcess(t *testing.T) {
 | 
			
		||||
	test("<script>a</script>", `<script>a</script>`)
 | 
			
		||||
	test("<STYLE>a", `<STYLE>a`)
 | 
			
		||||
	test("<style>a</STYLE>", `<style>a</STYLE>`)
 | 
			
		||||
 | 
			
		||||
	// other special tags, our special behavior
 | 
			
		||||
	test("<?php\nfoo", "<?php\nfoo")
 | 
			
		||||
	test("<%asp\nfoo", "<%asp\nfoo")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIssue16020(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
package container
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -83,7 +84,8 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
 | 
			
		||||
 | 
			
		||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
 | 
			
		||||
	var image oci.Image
 | 
			
		||||
	if err := json.NewDecoder(r).Decode(&image); err != nil {
 | 
			
		||||
	// EOF means empty input, still use the default data
 | 
			
		||||
	if err := json.NewDecoder(r).Decode(&image); err != nil && !errors.Is(err, io.EOF) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	oci "github.com/opencontainers/image-spec/specs-go/v1"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParseImageConfig(t *testing.T) {
 | 
			
		||||
@@ -59,3 +60,9 @@ func TestParseImageConfig(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, projectURL, metadata.ProjectURL)
 | 
			
		||||
	assert.Equal(t, repositoryURL, metadata.RepositoryURL)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseOCIImageConfig(t *testing.T) {
 | 
			
		||||
	metadata, err := parseOCIImageConfig(strings.NewReader(""))
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, &Metadata{Type: TypeOCI, Platform: DefaultPlatform, ImageLayers: []string{}}, metadata)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,18 +46,16 @@ type ServCommandResults struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServCommand preps for a serv call
 | 
			
		||||
func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) {
 | 
			
		||||
func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verb, lfsVerb string) (*ServCommandResults, ResponseExtra) {
 | 
			
		||||
	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d",
 | 
			
		||||
		keyID,
 | 
			
		||||
		url.PathEscape(ownerName),
 | 
			
		||||
		url.PathEscape(repoName),
 | 
			
		||||
		mode,
 | 
			
		||||
	)
 | 
			
		||||
	for _, verb := range verbs {
 | 
			
		||||
		if verb != "" {
 | 
			
		||||
	reqURL += "&verb=" + url.QueryEscape(verb)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// reqURL += "&lfs_verb=" + url.QueryEscape(lfsVerb) // TODO: actually there is no use of this parameter. In the future, the URL construction should be more flexible
 | 
			
		||||
	_ = lfsVerb
 | 
			
		||||
	req := newInternalRequestAPI(ctx, reqURL, "GET")
 | 
			
		||||
	return requestJSONResp(req, &ServCommandResults{})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,10 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/lfs"
 | 
			
		||||
@@ -59,118 +56,6 @@ func SyncRepoTags(ctx context.Context, repoID int64) error {
 | 
			
		||||
	return SyncReleasesWithTags(ctx, repo, gitRepo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncReleasesWithTags synchronizes release table with repository tags
 | 
			
		||||
func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
 | 
			
		||||
	log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
 | 
			
		||||
	// optimized procedure for pull-mirrors which saves a lot of time (in
 | 
			
		||||
	// particular for repos with many tags).
 | 
			
		||||
	if repo.IsMirror {
 | 
			
		||||
		return pullMirrorReleaseSync(ctx, repo, gitRepo)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	existingRelTags := make(container.Set[string])
 | 
			
		||||
	opts := repo_model.FindReleasesOptions{
 | 
			
		||||
		IncludeDrafts: true,
 | 
			
		||||
		IncludeTags:   true,
 | 
			
		||||
		ListOptions:   db.ListOptions{PageSize: 50},
 | 
			
		||||
		RepoID:        repo.ID,
 | 
			
		||||
	}
 | 
			
		||||
	for page := 1; ; page++ {
 | 
			
		||||
		opts.Page = page
 | 
			
		||||
		rels, err := db.Find[repo_model.Release](gitRepo.Ctx, opts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(rels) == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		for _, rel := range rels {
 | 
			
		||||
			if rel.IsDraft {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			commitID, err := gitRepo.GetTagCommitID(rel.TagName)
 | 
			
		||||
			if err != nil && !git.IsErrNotExist(err) {
 | 
			
		||||
				return fmt.Errorf("unable to GetTagCommitID for %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
			}
 | 
			
		||||
			if git.IsErrNotExist(err) || commitID != rel.Sha1 {
 | 
			
		||||
				if err := repo_model.PushUpdateDeleteTag(ctx, repo, rel.TagName); err != nil {
 | 
			
		||||
					return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				existingRelTags.Add(strings.ToLower(rel.TagName))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error {
 | 
			
		||||
		tagName := strings.TrimPrefix(refname, git.TagPrefix)
 | 
			
		||||
		if existingRelTags.Contains(strings.ToLower(tagName)) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil {
 | 
			
		||||
			// sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11
 | 
			
		||||
			// this is a tree object, not a tag object which created before git
 | 
			
		||||
			log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushUpdateAddTag must be called for any push actions to add tag
 | 
			
		||||
func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tagName, sha1, refname string) error {
 | 
			
		||||
	tag, err := gitRepo.GetTagWithID(sha1, tagName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to GetTag: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	commit, err := gitRepo.GetTagCommit(tag.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to get tag Commit: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sig := tag.Tagger
 | 
			
		||||
	if sig == nil {
 | 
			
		||||
		sig = commit.Author
 | 
			
		||||
	}
 | 
			
		||||
	if sig == nil {
 | 
			
		||||
		sig = commit.Committer
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var author *user_model.User
 | 
			
		||||
	createdAt := time.Unix(1, 0)
 | 
			
		||||
 | 
			
		||||
	if sig != nil {
 | 
			
		||||
		author, err = user_model.GetUserByEmail(ctx, sig.Email)
 | 
			
		||||
		if err != nil && !user_model.IsErrUserNotExist(err) {
 | 
			
		||||
			return fmt.Errorf("unable to GetUserByEmail for %q: %w", sig.Email, err)
 | 
			
		||||
		}
 | 
			
		||||
		createdAt = sig.When
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commitsCount, err := commit.CommitsCount()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to get CommitsCount: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rel := repo_model.Release{
 | 
			
		||||
		RepoID:       repo.ID,
 | 
			
		||||
		TagName:      tagName,
 | 
			
		||||
		LowerTagName: strings.ToLower(tagName),
 | 
			
		||||
		Sha1:         commit.ID.String(),
 | 
			
		||||
		NumCommits:   commitsCount,
 | 
			
		||||
		CreatedUnix:  timeutil.TimeStamp(createdAt.Unix()),
 | 
			
		||||
		IsTag:        true,
 | 
			
		||||
	}
 | 
			
		||||
	if author != nil {
 | 
			
		||||
		rel.PublisherID = author.ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repo_model.SaveOrUpdateTag(ctx, repo, &rel)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StoreMissingLfsObjectsInRepository downloads missing LFS objects
 | 
			
		||||
func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error {
 | 
			
		||||
	contentStore := lfs.NewContentStore()
 | 
			
		||||
@@ -286,18 +171,19 @@ func (shortRelease) TableName() string {
 | 
			
		||||
	return "release"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pullMirrorReleaseSync is a pull-mirror specific tag<->release table
 | 
			
		||||
// SyncReleasesWithTags is a tag<->release table
 | 
			
		||||
// synchronization which overwrites all Releases from the repository tags. This
 | 
			
		||||
// can be relied on since a pull-mirror is always identical to its
 | 
			
		||||
// upstream. Hence, after each sync we want the pull-mirror release set to be
 | 
			
		||||
// upstream. Hence, after each sync we want the release set to be
 | 
			
		||||
// identical to the upstream tag set. This is much more efficient for
 | 
			
		||||
// repositories like https://github.com/vim/vim (with over 13000 tags).
 | 
			
		||||
func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
 | 
			
		||||
	log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
	tags, numTags, err := gitRepo.GetTagInfos(0, 0)
 | 
			
		||||
func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
 | 
			
		||||
	log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
	tags, _, err := gitRepo.GetTagInfos(0, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
	var added, deleted, updated int
 | 
			
		||||
	err = db.WithTx(ctx, func(ctx context.Context) error {
 | 
			
		||||
		dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{
 | 
			
		||||
			RepoID:        repo.ID,
 | 
			
		||||
@@ -318,9 +204,7 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git
 | 
			
		||||
				TagName:      tag.Name,
 | 
			
		||||
				LowerTagName: strings.ToLower(tag.Name),
 | 
			
		||||
				Sha1:         tag.Object.String(),
 | 
			
		||||
				// NOTE: ignored, since NumCommits are unused
 | 
			
		||||
				// for pull-mirrors (only relevant when
 | 
			
		||||
				// displaying releases, IsTag: false)
 | 
			
		||||
				// NOTE: ignored, The NumCommits value is calculated and cached on demand when the UI requires it.
 | 
			
		||||
				NumCommits:  -1,
 | 
			
		||||
				CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()),
 | 
			
		||||
				IsTag:       true,
 | 
			
		||||
@@ -349,13 +233,14 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git
 | 
			
		||||
				return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		added, deleted, updated = len(deletes), len(updates), len(inserts)
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags)
 | 
			
		||||
	log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -357,7 +357,7 @@ type MigrateRepoOptions struct {
 | 
			
		||||
	// required: true
 | 
			
		||||
	RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
 | 
			
		||||
 | 
			
		||||
	// enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase
 | 
			
		||||
	// enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit
 | 
			
		||||
	Service      string `json:"service"`
 | 
			
		||||
	AuthUsername string `json:"auth_username"`
 | 
			
		||||
	AuthPassword string `json:"auth_password"`
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ type Tag struct {
 | 
			
		||||
	Message    string      `json:"message"`
 | 
			
		||||
	ID         string      `json:"id"`
 | 
			
		||||
	Commit     *CommitMeta `json:"commit"`
 | 
			
		||||
	ZipballURL string      `json:"zipball_url"`
 | 
			
		||||
	TarballURL string      `json:"tarball_url"`
 | 
			
		||||
	ZipballURL string      `json:"zipball_url,omitempty"`
 | 
			
		||||
	TarballURL string      `json:"tarball_url,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AnnotatedTag represents an annotated tag
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@ type AccessToken struct {
 | 
			
		||||
	Token          string    `json:"sha1"`
 | 
			
		||||
	TokenLastEight string    `json:"token_last_eight"`
 | 
			
		||||
	Scopes         []string  `json:"scopes"`
 | 
			
		||||
	Created        time.Time `json:"created_at"`
 | 
			
		||||
	Updated        time.Time `json:"last_used_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AccessTokenList represents a list of API access token.
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ type PublicKey struct {
 | 
			
		||||
	Fingerprint string `json:"fingerprint,omitempty"`
 | 
			
		||||
	// swagger:strfmt date-time
 | 
			
		||||
	Created  time.Time `json:"created_at,omitempty"`
 | 
			
		||||
	Updated  time.Time `json:"last_used_at,omitempty"`
 | 
			
		||||
	Owner    *User     `json:"user,omitempty"`
 | 
			
		||||
	ReadOnly bool      `json:"read_only,omitempty"`
 | 
			
		||||
	KeyType  string    `json:"key_type,omitempty"`
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import (
 | 
			
		||||
func TestDateTime(t *testing.T) {
 | 
			
		||||
	testTz, _ := time.LoadLocation("America/New_York")
 | 
			
		||||
	defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
 | 
			
		||||
	defer test.MockVariableValue(&setting.IsProd, true)()
 | 
			
		||||
	defer test.MockVariableValue(&setting.IsInTesting, false)()
 | 
			
		||||
 | 
			
		||||
	du := NewDateUtils()
 | 
			
		||||
@@ -53,6 +54,7 @@ func TestDateTime(t *testing.T) {
 | 
			
		||||
func TestTimeSince(t *testing.T) {
 | 
			
		||||
	testTz, _ := time.LoadLocation("America/New_York")
 | 
			
		||||
	defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
 | 
			
		||||
	defer test.MockVariableValue(&setting.IsProd, true)()
 | 
			
		||||
	defer test.MockVariableValue(&setting.IsInTesting, false)()
 | 
			
		||||
 | 
			
		||||
	du := NewDateUtils()
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,8 @@ import (
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/models/renderhelper"
 | 
			
		||||
	"code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/emoji"
 | 
			
		||||
	"code.gitea.io/gitea/modules/htmlutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -34,25 +36,25 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderCommitMessage renders commit message with XSS-safe and special links.
 | 
			
		||||
func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string) template.HTML {
 | 
			
		||||
func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML {
 | 
			
		||||
	cleanMsg := template.HTMLEscapeString(msg)
 | 
			
		||||
	// we can safely assume that it will not return any error, since there
 | 
			
		||||
	// shouldn't be any special HTML.
 | 
			
		||||
	fullMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg)
 | 
			
		||||
	// we can safely assume that it will not return any error, since there shouldn't be any special HTML.
 | 
			
		||||
	// "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed.
 | 
			
		||||
	fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("PostProcessCommitMessage: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
 | 
			
		||||
	if len(msgLines) == 0 {
 | 
			
		||||
		return template.HTML("")
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return renderCodeBlock(template.HTML(msgLines[0]))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
 | 
			
		||||
// the provided default url, handling for special links without email to links.
 | 
			
		||||
func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, metas map[string]string) template.HTML {
 | 
			
		||||
func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) template.HTML {
 | 
			
		||||
	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
 | 
			
		||||
	lineEnd := strings.IndexByte(msgLine, '\n')
 | 
			
		||||
	if lineEnd > 0 {
 | 
			
		||||
@@ -63,9 +65,8 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we can safely assume that it will not return any error, since there
 | 
			
		||||
	// shouldn't be any special HTML.
 | 
			
		||||
	renderedMessage, err := markup.PostProcessCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine))
 | 
			
		||||
	// we can safely assume that it will not return any error, since there shouldn't be any special HTML.
 | 
			
		||||
	renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("PostProcessCommitMessageSubject: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
@@ -74,7 +75,7 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderCommitBody extracts the body of a commit message without its title.
 | 
			
		||||
func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) template.HTML {
 | 
			
		||||
func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML {
 | 
			
		||||
	msgLine := strings.TrimSpace(msg)
 | 
			
		||||
	lineEnd := strings.IndexByte(msgLine, '\n')
 | 
			
		||||
	if lineEnd > 0 {
 | 
			
		||||
@@ -87,7 +88,7 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderedMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine))
 | 
			
		||||
	renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("PostProcessCommitMessage: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
@@ -105,8 +106,8 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderIssueTitle renders issue/pull title with defined post processors
 | 
			
		||||
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
 | 
			
		||||
	renderedText, err := markup.PostProcessIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text))
 | 
			
		||||
func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML {
 | 
			
		||||
	renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(text))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("PostProcessIssueTitle: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
 
 | 
			
		||||
@@ -32,22 +32,22 @@ func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).MarkdownToHtml(input)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML {
 | 
			
		||||
func renderCommitMessageLegacy(ctx context.Context, msg string, _ map[string]string) template.HTML {
 | 
			
		||||
	panicIfDevOrTesting()
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, metas)
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
 | 
			
		||||
func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, _ map[string]string) template.HTML {
 | 
			
		||||
	panicIfDevOrTesting()
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, metas)
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML {
 | 
			
		||||
func renderIssueTitleLegacy(ctx context.Context, text string, _ map[string]string) template.HTML {
 | 
			
		||||
	panicIfDevOrTesting()
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, metas)
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML {
 | 
			
		||||
func renderCommitBodyLegacy(ctx context.Context, msg string, _ map[string]string) template.HTML {
 | 
			
		||||
	panicIfDevOrTesting()
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, metas)
 | 
			
		||||
	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, nil)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,11 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/reqctx"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
 | 
			
		||||
@@ -47,19 +47,8 @@ mail@domain.com
 | 
			
		||||
	return strings.ReplaceAll(s, "<SPACE>", " ")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var testMetas = map[string]string{
 | 
			
		||||
	"user":                         "user13",
 | 
			
		||||
	"repo":                         "repo11",
 | 
			
		||||
	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/",
 | 
			
		||||
	"markdownNewLineHardBreak":     "true",
 | 
			
		||||
	"markupAllowShortIssuePattern": "true",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	unittest.InitSettingsForTesting()
 | 
			
		||||
	if err := git.InitSimple(context.Background()); err != nil {
 | 
			
		||||
		log.Fatal("git init failed, err: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	setting.Markdown.RenderOptionsComment.ShortIssuePattern = true
 | 
			
		||||
	markup.Init(&markup.RenderHelperFuncs{
 | 
			
		||||
		IsUsernameMentionable: func(ctx context.Context, username string) bool {
 | 
			
		||||
			return username == "mention-user"
 | 
			
		||||
@@ -74,7 +63,13 @@ func newTestRenderUtils(t *testing.T) *RenderUtils {
 | 
			
		||||
	return NewRenderUtils(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRenderCommitBody(t *testing.T) {
 | 
			
		||||
func TestRenderRepoComment(t *testing.T) {
 | 
			
		||||
	mockRepo := &repo.Repository{
 | 
			
		||||
		ID: 1, OwnerName: "user13", Name: "repo11",
 | 
			
		||||
		Owner: &user_model.User{ID: 13, Name: "user13"},
 | 
			
		||||
		Units: []*repo.RepoUnit{},
 | 
			
		||||
	}
 | 
			
		||||
	t.Run("RenderCommitBody", func(t *testing.T) {
 | 
			
		||||
		defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
 | 
			
		||||
		type args struct {
 | 
			
		||||
			msg string
 | 
			
		||||
@@ -109,7 +104,7 @@ func TestRenderCommitBody(t *testing.T) {
 | 
			
		||||
		ut := newTestRenderUtils(t)
 | 
			
		||||
		for _, tt := range tests {
 | 
			
		||||
			t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil)
 | 
			
		||||
				assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, mockRepo), "RenderCommitBody(%v, %v)", tt.args.msg, nil)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -132,20 +127,20 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 | 
			
		||||
<a href="/mention-user">@mention-user</a> test
 | 
			
		||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
 | 
			
		||||
  space`
 | 
			
		||||
	assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), testMetas)))
 | 
			
		||||
}
 | 
			
		||||
		assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), mockRepo)))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func TestRenderCommitMessage(t *testing.T) {
 | 
			
		||||
	t.Run("RenderCommitMessage", func(t *testing.T) {
 | 
			
		||||
		expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a>  `
 | 
			
		||||
	assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), testMetas))
 | 
			
		||||
}
 | 
			
		||||
		assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func TestRenderCommitMessageLinkSubject(t *testing.T) {
 | 
			
		||||
	t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) {
 | 
			
		||||
		expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>`
 | 
			
		||||
	assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
 | 
			
		||||
}
 | 
			
		||||
		assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func TestRenderIssueTitle(t *testing.T) {
 | 
			
		||||
	t.Run("RenderIssueTitle", func(t *testing.T) {
 | 
			
		||||
		defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
 | 
			
		||||
		expected := `  space @mention-user<SPACE><SPACE>
 | 
			
		||||
/just/a/path.bin
 | 
			
		||||
@@ -169,7 +164,8 @@ mail@domain.com
 | 
			
		||||
  space<SPACE><SPACE>
 | 
			
		||||
`
 | 
			
		||||
		expected = strings.ReplaceAll(expected, "<SPACE>", " ")
 | 
			
		||||
	assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), testMetas)))
 | 
			
		||||
		assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), mockRepo)))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRenderMarkdownToHtml(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,10 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
 | 
			
		||||
			status = v.WrittenStatus()
 | 
			
		||||
		}
 | 
			
		||||
		logf := logInfo
 | 
			
		||||
		if strings.HasPrefix(req.RequestURI, "/assets/") {
 | 
			
		||||
		// lower the log level for some specific requests, in most cases these logs are not useful
 | 
			
		||||
		if strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ ||
 | 
			
		||||
			req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ ||
 | 
			
		||||
			req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ {
 | 
			
		||||
			logf = logTrace
 | 
			
		||||
		}
 | 
			
		||||
		message := completedMessage
 | 
			
		||||
 
 | 
			
		||||
@@ -130,6 +130,7 @@ pin = Pin
 | 
			
		||||
unpin = Unpin
 | 
			
		||||
 | 
			
		||||
artifacts = Artifacts
 | 
			
		||||
expired = Expired
 | 
			
		||||
confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ?
 | 
			
		||||
 | 
			
		||||
archived = Archived
 | 
			
		||||
@@ -3722,13 +3723,18 @@ owner.settings.chef.keypair.description = A key pair is necessary to authenticat
 | 
			
		||||
secrets = Secrets
 | 
			
		||||
description = Secrets will be passed to certain actions and cannot be read otherwise.
 | 
			
		||||
none = There are no secrets yet.
 | 
			
		||||
creation = Add Secret
 | 
			
		||||
 | 
			
		||||
; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
 | 
			
		||||
creation.description = Description
 | 
			
		||||
creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_
 | 
			
		||||
creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted.
 | 
			
		||||
creation.description_placeholder = Enter short description (optional).
 | 
			
		||||
creation.success = The secret "%s" has been added.
 | 
			
		||||
creation.failed = Failed to add secret.
 | 
			
		||||
 | 
			
		||||
save_success = The secret "%s" has been saved.
 | 
			
		||||
save_failed = Failed to save secret.
 | 
			
		||||
 | 
			
		||||
add_secret = Add secret
 | 
			
		||||
edit_secret = Edit secret
 | 
			
		||||
deletion = Remove secret
 | 
			
		||||
deletion.description = Removing a secret is permanent and cannot be undone. Continue?
 | 
			
		||||
deletion.success = The secret has been removed.
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/httplib"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	container_module "code.gitea.io/gitea/modules/packages/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
@@ -50,7 +51,7 @@ type containerHeaders struct {
 | 
			
		||||
	Range         string
 | 
			
		||||
	Location      string
 | 
			
		||||
	ContentType   string
 | 
			
		||||
	ContentLength int64
 | 
			
		||||
	ContentLength optional.Option[int64]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
 | 
			
		||||
@@ -64,8 +65,8 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
 | 
			
		||||
	if h.ContentType != "" {
 | 
			
		||||
		resp.Header().Set("Content-Type", h.ContentType)
 | 
			
		||||
	}
 | 
			
		||||
	if h.ContentLength != 0 {
 | 
			
		||||
		resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
 | 
			
		||||
	if h.ContentLength.Has() {
 | 
			
		||||
		resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10))
 | 
			
		||||
	}
 | 
			
		||||
	if h.UploadUUID != "" {
 | 
			
		||||
		resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
 | 
			
		||||
@@ -312,13 +313,12 @@ func InitiateUploadBlob(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
 | 
			
		||||
		Range:      "0-0",
 | 
			
		||||
		UploadUUID: upload.ID,
 | 
			
		||||
		Status:     http.StatusAccepted,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://docs.docker.com/registry/spec/api/#get-blob-upload
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
			
		||||
func GetUploadBlob(ctx *context.Context) {
 | 
			
		||||
	uuid := ctx.PathParam("uuid")
 | 
			
		||||
 | 
			
		||||
@@ -332,13 +332,18 @@ func GetUploadBlob(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		Range:      fmt.Sprintf("0-%d", upload.BytesReceived),
 | 
			
		||||
	// FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
 | 
			
		||||
	respHeaders := &containerHeaders{
 | 
			
		||||
		UploadUUID: upload.ID,
 | 
			
		||||
		Status:     http.StatusNoContent,
 | 
			
		||||
	})
 | 
			
		||||
	}
 | 
			
		||||
	if upload.BytesReceived > 0 {
 | 
			
		||||
		respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1)
 | 
			
		||||
	}
 | 
			
		||||
	setResponseHeaders(ctx.Resp, respHeaders)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
			
		||||
func UploadBlob(ctx *context.Context) {
 | 
			
		||||
	image := ctx.PathParam("image")
 | 
			
		||||
@@ -376,12 +381,15 @@ func UploadBlob(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
	respHeaders := &containerHeaders{
 | 
			
		||||
		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
 | 
			
		||||
		Range:      fmt.Sprintf("0-%d", uploader.Size()-1),
 | 
			
		||||
		UploadUUID: uploader.ID,
 | 
			
		||||
		Status:     http.StatusAccepted,
 | 
			
		||||
	})
 | 
			
		||||
	}
 | 
			
		||||
	if contentRange != "" {
 | 
			
		||||
		respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
 | 
			
		||||
	}
 | 
			
		||||
	setResponseHeaders(ctx.Resp, respHeaders)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
			
		||||
@@ -403,12 +411,7 @@ func EndUploadBlob(ctx *context.Context) {
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	doClose := true
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if doClose {
 | 
			
		||||
			uploader.Close()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	defer uploader.Close()
 | 
			
		||||
 | 
			
		||||
	if ctx.Req.Body != nil {
 | 
			
		||||
		if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
 | 
			
		||||
@@ -441,11 +444,10 @@ func EndUploadBlob(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := uploader.Close(); err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	doClose = false
 | 
			
		||||
	// There was a strange bug: the "Close" fails with error "close .../tmp/package-upload/....: file already closed"
 | 
			
		||||
	// AFAIK there should be no other "Close" call to the uploader between NewBlobUploader and this line.
 | 
			
		||||
	// At least it's safe to call Close twice, so ignore the error.
 | 
			
		||||
	_ = uploader.Close()
 | 
			
		||||
 | 
			
		||||
	if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
@@ -511,7 +513,7 @@ func HeadBlob(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
 | 
			
		||||
		ContentLength: blob.Blob.Size,
 | 
			
		||||
		ContentLength: optional.Some(blob.Blob.Size),
 | 
			
		||||
		Status:        http.StatusOK,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -650,7 +652,7 @@ func HeadManifest(ctx *context.Context) {
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
 | 
			
		||||
		ContentType:   manifest.Properties.GetByName(container_module.PropertyMediaType),
 | 
			
		||||
		ContentLength: manifest.Blob.Size,
 | 
			
		||||
		ContentLength: optional.Some(manifest.Blob.Size),
 | 
			
		||||
		Status:        http.StatusOK,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -714,14 +716,14 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor)
 | 
			
		||||
	headers := &containerHeaders{
 | 
			
		||||
		ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
 | 
			
		||||
		ContentType:   pfd.Properties.GetByName(container_module.PropertyMediaType),
 | 
			
		||||
		ContentLength: pfd.Blob.Size,
 | 
			
		||||
		ContentLength: optional.Some(pfd.Blob.Size),
 | 
			
		||||
		Status:        http.StatusOK,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if u != nil {
 | 
			
		||||
		headers.Status = http.StatusTemporaryRedirect
 | 
			
		||||
		headers.Location = u.String()
 | 
			
		||||
 | 
			
		||||
		headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses
 | 
			
		||||
		setResponseHeaders(ctx.Resp, headers)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) {
 | 
			
		||||
		Location:                optional.FromPtr(form.Location),
 | 
			
		||||
		Description:             optional.FromPtr(form.Description),
 | 
			
		||||
		IsActive:                optional.FromPtr(form.Active),
 | 
			
		||||
		IsAdmin:                 optional.FromPtr(form.Admin),
 | 
			
		||||
		IsAdmin:                 user_service.UpdateOptionFieldFromPtr(form.Admin),
 | 
			
		||||
		Visibility:              optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
 | 
			
		||||
		AllowGitHook:            optional.FromPtr(form.AllowGitHook),
 | 
			
		||||
		AllowImportLocal:        optional.FromPtr(form.AllowImportLocal),
 | 
			
		||||
 
 | 
			
		||||
@@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
 | 
			
		||||
		issue.MilestoneID != *form.Milestone {
 | 
			
		||||
		oldMilestoneID := issue.MilestoneID
 | 
			
		||||
		issue.MilestoneID = *form.Milestone
 | 
			
		||||
		if issue.MilestoneID > 0 {
 | 
			
		||||
			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.APIErrorInternal(err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			issue.Milestone = nil
 | 
			
		||||
		}
 | 
			
		||||
		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
			
		||||
			ctx.APIErrorInternal(err)
 | 
			
		||||
			return
 | 
			
		||||
 
 | 
			
		||||
@@ -609,6 +609,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if form.Body != comment.Content {
 | 
			
		||||
		oldContent := comment.Content
 | 
			
		||||
		comment.Content = form.Body
 | 
			
		||||
		if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
 | 
			
		||||
@@ -619,6 +620,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
@@ -73,7 +74,7 @@ func ListPullRequests(ctx *context.APIContext) {
 | 
			
		||||
	//   in: query
 | 
			
		||||
	//   description: Type of sort
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
 | 
			
		||||
	//   enum: [oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, priority]
 | 
			
		||||
	// - name: milestone
 | 
			
		||||
	//   in: query
 | 
			
		||||
	//   description: ID of the milestone
 | 
			
		||||
@@ -706,6 +707,11 @@ func EditPullRequest(ctx *context.APIContext) {
 | 
			
		||||
		issue.MilestoneID != form.Milestone {
 | 
			
		||||
		oldMilestoneID := issue.MilestoneID
 | 
			
		||||
		issue.MilestoneID = form.Milestone
 | 
			
		||||
		issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.APIErrorInternal(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
			
		||||
			ctx.APIErrorInternal(err)
 | 
			
		||||
			return
 | 
			
		||||
@@ -1296,7 +1302,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
 | 
			
		||||
	// default merge commit message
 | 
			
		||||
	message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
 | 
			
		||||
 | 
			
		||||
	if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase); err != nil {
 | 
			
		||||
	if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil {
 | 
			
		||||
		if pull_service.IsErrMergeConflicts(err) {
 | 
			
		||||
			ctx.APIError(http.StatusConflict, "merge failed because of conflict")
 | 
			
		||||
			return
 | 
			
		||||
@@ -1632,7 +1638,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 | 
			
		||||
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, limit)
 | 
			
		||||
	for i := start; i < start+limit; i++ {
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
 | 
			
		||||
		// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
 | 
			
		||||
		// The head repository might have been deleted, so we should not rely on it here.
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
 | 
			
		||||
 
 | 
			
		||||
@@ -67,6 +67,28 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
 | 
			
		||||
	ctx.JSON(http.StatusOK, &res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*actions_model.ActionRunner, bool) {
 | 
			
		||||
	if ownerID != 0 && repoID != 0 {
 | 
			
		||||
		setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			ctx.APIErrorNotFound("Runner not found")
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.APIErrorInternal(err)
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !runner.EditableInContext(ownerID, repoID) {
 | 
			
		||||
		ctx.APIErrorNotFound("No permission to access this runner")
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
	return runner, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRunner get the runner for api route validated ownerID and repoID
 | 
			
		||||
// ownerID == 0 and repoID == 0 means any runner including global runners
 | 
			
		||||
// ownerID == 0 and repoID != 0 means any runner for the given repo
 | 
			
		||||
@@ -77,13 +99,8 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
 | 
			
		||||
	if ownerID != 0 && repoID != 0 {
 | 
			
		||||
		setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
 | 
			
		||||
	}
 | 
			
		||||
	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.APIErrorNotFound(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !runner.EditableInContext(ownerID, repoID) {
 | 
			
		||||
		ctx.APIErrorNotFound("No permission to get this runner")
 | 
			
		||||
	runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
 | 
			
		||||
@@ -96,20 +113,12 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
 | 
			
		||||
// ownerID != 0 and repoID != 0 undefined behavior
 | 
			
		||||
// Access rights are checked at the API route level
 | 
			
		||||
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
 | 
			
		||||
	if ownerID != 0 && repoID != 0 {
 | 
			
		||||
		setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
 | 
			
		||||
	}
 | 
			
		||||
	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.APIErrorInternal(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !runner.EditableInContext(ownerID, repoID) {
 | 
			
		||||
		ctx.APIErrorNotFound("No permission to delete this runner")
 | 
			
		||||
	runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = actions_model.DeleteRunner(ctx, runner.ID)
 | 
			
		||||
	err := actions_model.DeleteRunner(ctx, runner.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.APIErrorInternal(err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,8 @@ func ListAccessTokens(ctx *context.APIContext) {
 | 
			
		||||
			Name:           tokens[i].Name,
 | 
			
		||||
			TokenLastEight: tokens[i].TokenLastEight,
 | 
			
		||||
			Scopes:         tokens[i].Scope.StringSlice(),
 | 
			
		||||
			Created:        tokens[i].CreatedUnix.AsTime(),
 | 
			
		||||
			Updated:        tokens[i].UpdatedUnix.AsTime(),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
	webhook_module "code.gitea.io/gitea/modules/webhook"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	webhook_service "code.gitea.io/gitea/services/webhook"
 | 
			
		||||
@@ -92,6 +93,10 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
 | 
			
		||||
		ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if !validation.IsValidURL(form.Config["url"]) {
 | 
			
		||||
		ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -154,6 +159,41 @@ func pullHook(events []string, event string) bool {
 | 
			
		||||
	return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateHookEvents(events []string) webhook_module.HookEvents {
 | 
			
		||||
	if len(events) == 0 {
 | 
			
		||||
		events = []string{"push"}
 | 
			
		||||
	}
 | 
			
		||||
	hookEvents := make(webhook_module.HookEvents)
 | 
			
		||||
	hookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(events, string(webhook_module.HookEventCreate), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventPush] = util.SliceContainsString(events, string(webhook_module.HookEventPush), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(events, string(webhook_module.HookEventDelete), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventFork] = util.SliceContainsString(events, string(webhook_module.HookEventFork), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(events, string(webhook_module.HookEventRepository), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(events, string(webhook_module.HookEventWiki), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true)
 | 
			
		||||
	hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true)
 | 
			
		||||
 | 
			
		||||
	// Issues
 | 
			
		||||
	hookEvents[webhook_module.HookEventIssues] = issuesHook(events, "issues_only")
 | 
			
		||||
	hookEvents[webhook_module.HookEventIssueAssign] = issuesHook(events, string(webhook_module.HookEventIssueAssign))
 | 
			
		||||
	hookEvents[webhook_module.HookEventIssueLabel] = issuesHook(events, string(webhook_module.HookEventIssueLabel))
 | 
			
		||||
	hookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(events, string(webhook_module.HookEventIssueMilestone))
 | 
			
		||||
	hookEvents[webhook_module.HookEventIssueComment] = issuesHook(events, string(webhook_module.HookEventIssueComment))
 | 
			
		||||
 | 
			
		||||
	// Pull requests
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequest] = pullHook(events, "pull_request_only")
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(events, string(webhook_module.HookEventPullRequestAssign))
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(events, string(webhook_module.HookEventPullRequestLabel))
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(events, string(webhook_module.HookEventPullRequestMilestone))
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestComment] = pullHook(events, string(webhook_module.HookEventPullRequestComment))
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestReview] = pullHook(events, "pull_request_review")
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(events, string(webhook_module.HookEventPullRequestReviewRequest))
 | 
			
		||||
	hookEvents[webhook_module.HookEventPullRequestSync] = pullHook(events, string(webhook_module.HookEventPullRequestSync))
 | 
			
		||||
	return hookEvents
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
 | 
			
		||||
// an error, write to `ctx` accordingly. Return (webhook, ok)
 | 
			
		||||
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
 | 
			
		||||
@@ -162,9 +202,6 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(form.Events) == 0 {
 | 
			
		||||
		form.Events = []string{"push"}
 | 
			
		||||
	}
 | 
			
		||||
	if form.Config["is_system_webhook"] != "" {
 | 
			
		||||
		sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -183,31 +220,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
 | 
			
		||||
		IsSystemWebhook: isSystemWebhook,
 | 
			
		||||
		HookEvent: &webhook_module.HookEvent{
 | 
			
		||||
			ChooseEvents: true,
 | 
			
		||||
			HookEvents: webhook_module.HookEvents{
 | 
			
		||||
				webhook_module.HookEventCreate:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
 | 
			
		||||
				webhook_module.HookEventDelete:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
 | 
			
		||||
				webhook_module.HookEventFork:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
 | 
			
		||||
				webhook_module.HookEventIssues:                   issuesHook(form.Events, "issues_only"),
 | 
			
		||||
				webhook_module.HookEventIssueAssign:              issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
 | 
			
		||||
				webhook_module.HookEventIssueLabel:               issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
 | 
			
		||||
				webhook_module.HookEventIssueMilestone:           issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
 | 
			
		||||
				webhook_module.HookEventIssueComment:             issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
 | 
			
		||||
				webhook_module.HookEventPush:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
 | 
			
		||||
				webhook_module.HookEventPullRequest:              pullHook(form.Events, "pull_request_only"),
 | 
			
		||||
				webhook_module.HookEventPullRequestAssign:        pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
 | 
			
		||||
				webhook_module.HookEventPullRequestLabel:         pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
 | 
			
		||||
				webhook_module.HookEventPullRequestMilestone:     pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
 | 
			
		||||
				webhook_module.HookEventPullRequestComment:       pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
 | 
			
		||||
				webhook_module.HookEventPullRequestReview:        pullHook(form.Events, "pull_request_review"),
 | 
			
		||||
				webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
 | 
			
		||||
				webhook_module.HookEventPullRequestSync:          pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
 | 
			
		||||
				webhook_module.HookEventWiki:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
 | 
			
		||||
				webhook_module.HookEventRepository:               util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
 | 
			
		||||
				webhook_module.HookEventRelease:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
 | 
			
		||||
				webhook_module.HookEventPackage:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
 | 
			
		||||
				webhook_module.HookEventStatus:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
 | 
			
		||||
				webhook_module.HookEventWorkflowJob:              util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true),
 | 
			
		||||
			},
 | 
			
		||||
			HookEvents:   updateHookEvents(form.Events),
 | 
			
		||||
			BranchFilter: form.BranchFilter,
 | 
			
		||||
		},
 | 
			
		||||
		IsActive: form.Active,
 | 
			
		||||
@@ -324,6 +337,10 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6
 | 
			
		||||
func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
 | 
			
		||||
	if form.Config != nil {
 | 
			
		||||
		if url, ok := form.Config["url"]; ok {
 | 
			
		||||
			if !validation.IsValidURL(url) {
 | 
			
		||||
				ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			w.URL = url
 | 
			
		||||
		}
 | 
			
		||||
		if ct, ok := form.Config["content_type"]; ok {
 | 
			
		||||
@@ -352,19 +369,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update events
 | 
			
		||||
	if len(form.Events) == 0 {
 | 
			
		||||
		form.Events = []string{"push"}
 | 
			
		||||
	}
 | 
			
		||||
	w.HookEvents = updateHookEvents(form.Events)
 | 
			
		||||
	w.PushOnly = false
 | 
			
		||||
	w.SendEverything = false
 | 
			
		||||
	w.ChooseEvents = true
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
 | 
			
		||||
	w.BranchFilter = form.BranchFilter
 | 
			
		||||
 | 
			
		||||
	err := w.SetHeaderAuthorization(form.AuthorizationHeader)
 | 
			
		||||
@@ -373,23 +381,6 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Issues
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
 | 
			
		||||
 | 
			
		||||
	// Pull requests
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestComment] = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
 | 
			
		||||
	w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
 | 
			
		||||
 | 
			
		||||
	if err := w.UpdateEvent(); err != nil {
 | 
			
		||||
		ctx.APIErrorInternal(err)
 | 
			
		||||
		return false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								routers/api/v1/utils/hook_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								routers/api/v1/utils/hook_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/services/contexttest"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestTestHookValidation(t *testing.T) {
 | 
			
		||||
	unittest.PrepareTestEnv(t)
 | 
			
		||||
 | 
			
		||||
	t.Run("Test Validation", func(t *testing.T) {
 | 
			
		||||
		ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
 | 
			
		||||
		contexttest.LoadRepo(t, ctx, 1)
 | 
			
		||||
		contexttest.LoadRepoCommit(t, ctx)
 | 
			
		||||
		contexttest.LoadUser(t, ctx, 2)
 | 
			
		||||
 | 
			
		||||
		checkCreateHookOption(ctx, &structs.CreateHookOption{
 | 
			
		||||
			Type: "gitea",
 | 
			
		||||
			Config: map[string]string{
 | 
			
		||||
				"content_type": "json",
 | 
			
		||||
				"url":          "https://example.com/webhook",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		assert.Equal(t, 0, ctx.Resp.WrittenStatus()) // not written yet
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Test Validation with invalid URL", func(t *testing.T) {
 | 
			
		||||
		ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
 | 
			
		||||
		contexttest.LoadRepo(t, ctx, 1)
 | 
			
		||||
		contexttest.LoadRepoCommit(t, ctx)
 | 
			
		||||
		contexttest.LoadUser(t, ctx, 2)
 | 
			
		||||
 | 
			
		||||
		checkCreateHookOption(ctx, &structs.CreateHookOption{
 | 
			
		||||
			Type: "gitea",
 | 
			
		||||
			Config: map[string]string{
 | 
			
		||||
				"content_type": "json",
 | 
			
		||||
				"url":          "example.com/webhook",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Test Validation with invalid webhook type", func(t *testing.T) {
 | 
			
		||||
		ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
 | 
			
		||||
		contexttest.LoadRepo(t, ctx, 1)
 | 
			
		||||
		contexttest.LoadRepoCommit(t, ctx)
 | 
			
		||||
		contexttest.LoadUser(t, ctx, 2)
 | 
			
		||||
 | 
			
		||||
		checkCreateHookOption(ctx, &structs.CreateHookOption{
 | 
			
		||||
			Type: "unknown",
 | 
			
		||||
			Config: map[string]string{
 | 
			
		||||
				"content_type": "json",
 | 
			
		||||
				"url":          "example.com/webhook",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Test Validation with empty content type", func(t *testing.T) {
 | 
			
		||||
		ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
 | 
			
		||||
		contexttest.LoadRepo(t, ctx, 1)
 | 
			
		||||
		contexttest.LoadRepoCommit(t, ctx)
 | 
			
		||||
		contexttest.LoadUser(t, ctx, 2)
 | 
			
		||||
 | 
			
		||||
		checkCreateHookOption(ctx, &structs.CreateHookOption{
 | 
			
		||||
			Type: "unknown",
 | 
			
		||||
			Config: map[string]string{
 | 
			
		||||
				"url": "https://example.com/webhook",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								routers/api/v1/utils/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								routers/api/v1/utils/main_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
// Copyright 2018 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	webhook_service "code.gitea.io/gitea/services/webhook"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	unittest.MainTest(m, &unittest.TestOptions{
 | 
			
		||||
		SetUp: func() error {
 | 
			
		||||
			setting.LoadQueueSettings()
 | 
			
		||||
			return webhook_service.Init()
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
 | 
			
		||||
		})
 | 
			
		||||
		rctx = rctx.WithMarkupType(markdown.MarkupName)
 | 
			
		||||
	case "comment":
 | 
			
		||||
		rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
 | 
			
		||||
		rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{
 | 
			
		||||
			DeprecatedOwnerName: repoOwnerName,
 | 
			
		||||
			DeprecatedRepoName:  repoName,
 | 
			
		||||
			FootnoteContextID:   "preview",
 | 
			
		||||
		})
 | 
			
		||||
		rctx = rctx.WithMarkupType(markdown.MarkupName)
 | 
			
		||||
	case "wiki":
 | 
			
		||||
		rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
 | 
			
		||||
 
 | 
			
		||||
@@ -220,7 +220,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(cols) > 0 {
 | 
			
		||||
			if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
 | 
			
		||||
			if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, cols...); err != nil {
 | 
			
		||||
				log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
 | 
			
		||||
				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
 | 
			
		||||
					Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) {
 | 
			
		||||
	ownerName := ctx.PathParam("owner")
 | 
			
		||||
	repoName := ctx.PathParam("repo")
 | 
			
		||||
	mode := perm.AccessMode(ctx.FormInt("mode"))
 | 
			
		||||
	verb := ctx.FormString("verb")
 | 
			
		||||
 | 
			
		||||
	// Set the basic parts of the results to return
 | 
			
		||||
	results := private.ServCommandResults{
 | 
			
		||||
@@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// Because of the special ref "refs/for" we will need to delay write permission check
 | 
			
		||||
			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode {
 | 
			
		||||
			// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
 | 
			
		||||
			// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
 | 
			
		||||
			// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
 | 
			
		||||
			// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
 | 
			
		||||
			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == git.CmdVerbReceivePack {
 | 
			
		||||
				mode = perm.AccessModeRead
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -431,7 +431,7 @@ func EditUserPost(ctx *context.Context) {
 | 
			
		||||
		Website:                 optional.Some(form.Website),
 | 
			
		||||
		Location:                optional.Some(form.Location),
 | 
			
		||||
		IsActive:                optional.Some(form.Active),
 | 
			
		||||
		IsAdmin:                 optional.Some(form.Admin),
 | 
			
		||||
		IsAdmin:                 user_service.UpdateOptionFieldFromValue(form.Admin),
 | 
			
		||||
		AllowGitHook:            optional.Some(form.AllowGitHook),
 | 
			
		||||
		AllowImportLocal:        optional.Some(form.AllowImportLocal),
 | 
			
		||||
		MaxRepoCreation:         optional.Some(form.MaxRepoCreation),
 | 
			
		||||
 
 | 
			
		||||
@@ -613,7 +613,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 | 
			
		||||
	if user_model.CountUsers(ctx, nil) == 1 {
 | 
			
		||||
		opts := &user_service.UpdateOptions{
 | 
			
		||||
			IsActive:     optional.Some(true),
 | 
			
		||||
			IsAdmin:      optional.Some(true),
 | 
			
		||||
			IsAdmin:      user_service.UpdateOptionFieldFromValue(true),
 | 
			
		||||
			SetLastLogin: true,
 | 
			
		||||
		}
 | 
			
		||||
		if err := user_service.UpdateUser(ctx, u, opts); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -193,8 +193,8 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
			
		||||
			source := authSource.Cfg.(*oauth2.Source)
 | 
			
		||||
 | 
			
		||||
			isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
 | 
			
		||||
			u.IsAdmin = isAdmin.ValueOrDefault(false)
 | 
			
		||||
			u.IsRestricted = isRestricted.ValueOrDefault(false)
 | 
			
		||||
			u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
 | 
			
		||||
			u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
 | 
			
		||||
 | 
			
		||||
			if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
 | 
			
		||||
				// error already handled
 | 
			
		||||
@@ -258,11 +258,11 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
 | 
			
		||||
	return claimValueToStringSet(groupClaims)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
 | 
			
		||||
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin optional.Option[user_service.UpdateOptionField[bool]], isRestricted optional.Option[bool]) {
 | 
			
		||||
	groups := getClaimedGroups(source, gothUser)
 | 
			
		||||
 | 
			
		||||
	if source.AdminGroup != "" {
 | 
			
		||||
		isAdmin = optional.Some(groups.Contains(source.AdminGroup))
 | 
			
		||||
		isAdmin = user_service.UpdateOptionFieldFromSync(groups.Contains(source.AdminGroup))
 | 
			
		||||
	}
 | 
			
		||||
	if source.RestrictedGroup != "" {
 | 
			
		||||
		isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
 | 
			
		||||
 
 | 
			
		||||
@@ -94,6 +94,16 @@ func MockActionsRunsJobs(ctx *context.Context) {
 | 
			
		||||
		Size:   1024 * 1024,
 | 
			
		||||
		Status: "completed",
 | 
			
		||||
	})
 | 
			
		||||
	resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
 | 
			
		||||
		Name:   "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
 | 
			
		||||
		Size:   100 * 1024,
 | 
			
		||||
		Status: "expired",
 | 
			
		||||
	})
 | 
			
		||||
	resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
 | 
			
		||||
		Name:   "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
 | 
			
		||||
		Size:   1024 * 1024,
 | 
			
		||||
		Status: "completed",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
 | 
			
		||||
		ID:       runID * 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -201,7 +201,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 | 
			
		||||
			switch act.OpType {
 | 
			
		||||
			case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
 | 
			
		||||
				push := templates.ActionContent2Commits(act)
 | 
			
		||||
 | 
			
		||||
				_ = act.LoadRepo(ctx)
 | 
			
		||||
				for _, commit := range push.Commits {
 | 
			
		||||
					if len(desc) != 0 {
 | 
			
		||||
						desc += "\n\n"
 | 
			
		||||
@@ -209,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 | 
			
		||||
					desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s",
 | 
			
		||||
						html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
 | 
			
		||||
						commit.Sha1,
 | 
			
		||||
						renderUtils.RenderCommitMessage(commit.Message, nil),
 | 
			
		||||
						renderUtils.RenderCommitMessage(commit.Message, act.Repo),
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -200,13 +200,9 @@ func ViewPost(ctx *context_module.Context) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: "ComposeCommentMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
 | 
			
		||||
	// need to be refactored together in the future
 | 
			
		||||
	metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
 | 
			
		||||
 | 
			
		||||
	// the title for the "run" is from the commit message
 | 
			
		||||
	resp.State.Run.Title = run.Title
 | 
			
		||||
	resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas)
 | 
			
		||||
	resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository)
 | 
			
		||||
	resp.State.Run.Link = run.Link()
 | 
			
		||||
	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 | 
			
		||||
	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
 | 
			
		||||
@@ -588,20 +584,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
 | 
			
		||||
	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			ctx.HTTPError(http.StatusNotFound, err.Error())
 | 
			
		||||
			ctx.NotFound(nil)
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		ctx.HTTPError(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		ctx.ServerError("GetRunByIndex", err)
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	run.Repo = ctx.Repo.Repository
 | 
			
		||||
	jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.HTTPError(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		ctx.ServerError("GetRunJobsByRunID", err)
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	if len(jobs) == 0 {
 | 
			
		||||
		ctx.HTTPError(http.StatusNotFound)
 | 
			
		||||
		ctx.NotFound(nil)
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -401,12 +401,11 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
 | 
			
		||||
		ci.HeadRepo = ctx.Repo.Repository
 | 
			
		||||
		ci.HeadGitRepo = ctx.Repo.GitRepo
 | 
			
		||||
	} else if has {
 | 
			
		||||
		ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
 | 
			
		||||
		ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("OpenRepository", err)
 | 
			
		||||
			ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		defer ci.HeadGitRepo.Close()
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.NotFound(nil)
 | 
			
		||||
		return nil
 | 
			
		||||
@@ -575,7 +574,13 @@ func PrepareCompareDiff(
 | 
			
		||||
 | 
			
		||||
	ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
 | 
			
		||||
	ctx.Data["AfterCommitID"] = headCommitID
 | 
			
		||||
	ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand")
 | 
			
		||||
 | 
			
		||||
	// follow GitHub's behavior: autofill the form and expand
 | 
			
		||||
	newPrFormTitle := ctx.FormTrim("title")
 | 
			
		||||
	newPrFormBody := ctx.FormTrim("body")
 | 
			
		||||
	ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != ""
 | 
			
		||||
	ctx.Data["TitleQuery"] = newPrFormTitle
 | 
			
		||||
	ctx.Data["BodyQuery"] = newPrFormBody
 | 
			
		||||
 | 
			
		||||
	if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
 | 
			
		||||
		headCommitID == ci.CompareInfo.BaseCommitID {
 | 
			
		||||
@@ -721,11 +726,6 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 | 
			
		||||
// CompareDiff show different from one commit to another commit
 | 
			
		||||
func CompareDiff(ctx *context.Context) {
 | 
			
		||||
	ci := ParseCompareInfo(ctx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if ci != nil && ci.HeadGitRepo != nil {
 | 
			
		||||
			ci.HeadGitRepo.Close()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/utils"
 | 
			
		||||
@@ -151,9 +150,13 @@ func editFile(ctx *context.Context, isNewFile bool) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dataRc, err := blob.DataAsync()
 | 
			
		||||
		buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if git.IsErrNotExist(err) {
 | 
			
		||||
				ctx.NotFound(err)
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.ServerError("getFileReader", err)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -161,12 +164,8 @@ func editFile(ctx *context.Context, isNewFile bool) {
 | 
			
		||||
 | 
			
		||||
		ctx.Data["FileSize"] = blob.Size()
 | 
			
		||||
 | 
			
		||||
		buf := make([]byte, 1024)
 | 
			
		||||
		n, _ := util.ReadAtMost(dataRc, buf)
 | 
			
		||||
		buf = buf[:n]
 | 
			
		||||
 | 
			
		||||
		// Only some file types are editable online as text.
 | 
			
		||||
		if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
 | 
			
		||||
		if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile {
 | 
			
		||||
			ctx.NotFound(nil)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
@@ -377,12 +376,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
 | 
			
		||||
			_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -791,7 +784,7 @@ func UploadFilePost(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
 | 
			
		||||
			_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
 | 
			
		||||
			_ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["ContextUser"] = ctxUser
 | 
			
		||||
 | 
			
		||||
	if ctx.HasError() {
 | 
			
		||||
		ctx.HTML(http.StatusOK, tplFork)
 | 
			
		||||
		ctx.JSONError(ctx.GetErrMsg())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -159,12 +159,12 @@ func ForkPost(ctx *context.Context) {
 | 
			
		||||
	traverseParentRepo := forkRepo
 | 
			
		||||
	for {
 | 
			
		||||
		if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
 | 
			
		||||
			ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
 | 
			
		||||
		if repo != nil {
 | 
			
		||||
			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
 | 
			
		||||
			ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !traverseParentRepo.IsFork {
 | 
			
		||||
@@ -201,26 +201,26 @@ func ForkPost(ctx *context.Context) {
 | 
			
		||||
		case repo_model.IsErrReachLimitOfRepo(err):
 | 
			
		||||
			maxCreationLimit := ctxUser.MaxCreationLimit()
 | 
			
		||||
			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
 | 
			
		||||
			ctx.RenderWithErr(msg, tplFork, &form)
 | 
			
		||||
			ctx.JSONError(msg)
 | 
			
		||||
		case repo_model.IsErrRepoAlreadyExist(err):
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
 | 
			
		||||
			ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
 | 
			
		||||
		case repo_model.IsErrRepoFilesAlreadyExist(err):
 | 
			
		||||
			switch {
 | 
			
		||||
			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
 | 
			
		||||
				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
 | 
			
		||||
				ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
 | 
			
		||||
			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
 | 
			
		||||
				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
 | 
			
		||||
				ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
 | 
			
		||||
			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
 | 
			
		||||
				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
 | 
			
		||||
				ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
 | 
			
		||||
			default:
 | 
			
		||||
				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
 | 
			
		||||
				ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
 | 
			
		||||
			}
 | 
			
		||||
		case db.IsErrNameReserved(err):
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
 | 
			
		||||
			ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name))
 | 
			
		||||
		case db.IsErrNamePatternNotAllowed(err):
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
 | 
			
		||||
			ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern))
 | 
			
		||||
		case errors.Is(err, user_model.ErrBlockedUser):
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
 | 
			
		||||
			ctx.JSONError(ctx.Tr("repo.fork.blocked_user"))
 | 
			
		||||
		default:
 | 
			
		||||
			ctx.ServerError("ForkPost", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -228,5 +228,5 @@ func ForkPost(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
 | 
			
		||||
	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
 | 
			
		||||
	ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user