mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			69 Commits
		
	
	
		
			v1.23.0-de
			...
			v1.18.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					53db977e7e | ||
| 
						 | 
					4fdd4fb2c4 | ||
| 
						 | 
					900e158064 | ||
| 
						 | 
					e9bc2c77c3 | ||
| 
						 | 
					9b4da56963 | ||
| 
						 | 
					5583eaa904 | ||
| 
						 | 
					2a5e7f8f92 | ||
| 
						 | 
					d2777444d9 | ||
| 
						 | 
					198342efe4 | ||
| 
						 | 
					f7258aa42b | ||
| 
						 | 
					9a0a4086e2 | ||
| 
						 | 
					145e11bc39 | ||
| 
						 | 
					72524adf3f | ||
| 
						 | 
					2d4083f03c | ||
| 
						 | 
					56bded9d8d | ||
| 
						 | 
					e88218f4be | ||
| 
						 | 
					4297aced93 | ||
| 
						 | 
					dd2343d01f | ||
| 
						 | 
					9e49270676 | ||
| 
						 | 
					194b780cd7 | ||
| 
						 | 
					1409b348c6 | ||
| 
						 | 
					c36a1bc766 | ||
| 
						 | 
					079ef56824 | ||
| 
						 | 
					b54c064f89 | ||
| 
						 | 
					c0ca9c612b | ||
| 
						 | 
					e39bb2d05a | ||
| 
						 | 
					ac54331549 | ||
| 
						 | 
					35fc9ad984 | ||
| 
						 | 
					6e4ba04843 | ||
| 
						 | 
					09794b4259 | ||
| 
						 | 
					757b49ec5e | ||
| 
						 | 
					9819a47717 | ||
| 
						 | 
					c7770fa502 | ||
| 
						 | 
					da956b863b | ||
| 
						 | 
					888384a631 | ||
| 
						 | 
					cddceb9dca | ||
| 
						 | 
					b56d269cf8 | ||
| 
						 | 
					ff4e292b3f | ||
| 
						 | 
					9ba4ef93ff | ||
| 
						 | 
					9bccc60cf5 | ||
| 
						 | 
					16772ffde3 | ||
| 
						 | 
					c844c4ff88 | ||
| 
						 | 
					f4ec03a4e5 | ||
| 
						 | 
					b2369830bb | ||
| 
						 | 
					ef08998bf6 | ||
| 
						 | 
					7a004ad7eb | ||
| 
						 | 
					af8b2250c4 | ||
| 
						 | 
					8917af8701 | ||
| 
						 | 
					0d25292fbc | ||
| 
						 | 
					ac409fcfba | ||
| 
						 | 
					df512f77b7 | ||
| 
						 | 
					e4bf9cad1e | ||
| 
						 | 
					169eeee101 | ||
| 
						 | 
					3aacc9b4ac | ||
| 
						 | 
					87d05d376d | ||
| 
						 | 
					b9dcf991b9 | ||
| 
						 | 
					a2a42cd5de | ||
| 
						 | 
					805a14cc91 | ||
| 
						 | 
					69a54545a8 | ||
| 
						 | 
					e054f80fe0 | ||
| 
						 | 
					89d52922d0 | ||
| 
						 | 
					3a0d000b94 | ||
| 
						 | 
					fd4e7447e7 | ||
| 
						 | 
					7a8e34b255 | ||
| 
						 | 
					e4a10f8c78 | ||
| 
						 | 
					6dba648e5d | ||
| 
						 | 
					4d39fd8aae | ||
| 
						 | 
					4869f9c3c8 | ||
| 
						 | 
					79275d9db4 | 
@@ -5,6 +5,6 @@ tmp_dir = ".air"
 | 
			
		||||
cmd = "make backend"
 | 
			
		||||
bin = "gitea"
 | 
			
		||||
include_ext = ["go", "tmpl"]
 | 
			
		||||
exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata"]
 | 
			
		||||
include_dir = ["cmd", "models", "modules", "options", "routers", "services", "templates"]
 | 
			
		||||
exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
 | 
			
		||||
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
 | 
			
		||||
exclude_regex = ["_test.go$", "_gen.go$"]
 | 
			
		||||
 
 | 
			
		||||
@@ -941,7 +941,8 @@ steps:
 | 
			
		||||
    image: plugins/hugo:latest
 | 
			
		||||
    pull: always
 | 
			
		||||
    commands:
 | 
			
		||||
      - apk add --no-cache make bash curl
 | 
			
		||||
      # https://github.com/drone-plugins/drone-hugo/issues/36
 | 
			
		||||
      - apk upgrade --no-cache libcurl && apk add --no-cache make bash curl
 | 
			
		||||
      - cd docs
 | 
			
		||||
      - make trans-copy clean build
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										250
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										250
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,256 @@ This changelog goes through all 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.io).
 | 
			
		||||
 | 
			
		||||
## [1.18.0](https://github.com/go-gitea/gitea/releases/tag/1.18.0) - 2022-12-22
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Remove ReverseProxy authentication from the API (#22219) (#22251)
 | 
			
		||||
  * Support Go Vulnerability Management (#21139)
 | 
			
		||||
  * Forbid HTML string tooltips (#20935)
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Rework mailer settings (#18982)
 | 
			
		||||
  * Remove U2F support (#20141)
 | 
			
		||||
  * Refactor `i18n` to `locale` (#20153)
 | 
			
		||||
  * Enable contenthash in filename for dynamic assets (#20813)
 | 
			
		||||
* FEATURES
 | 
			
		||||
  * Add color previews in markdown (#21474)
 | 
			
		||||
  * Allow package version sorting (#21453)
 | 
			
		||||
  * Add support for Chocolatey/NuGet v2 API (#21393)
 | 
			
		||||
  * Add API endpoint to get changed files of a PR (#21177)
 | 
			
		||||
  * Add filetree on left of diff view (#21012)
 | 
			
		||||
  * Support Issue forms and PR forms (#20987)
 | 
			
		||||
  * Add support for Vagrant packages (#20930)
 | 
			
		||||
  * Add support for `npm unpublish` (#20688)
 | 
			
		||||
  * Add badge capabilities to users (#20607)
 | 
			
		||||
  * Add issue filter for Author (#20578)
 | 
			
		||||
  * Add KaTeX rendering to Markdown. (#20571)
 | 
			
		||||
  * Add support for Pub packages (#20560)
 | 
			
		||||
  * Support localized README (#20508)
 | 
			
		||||
  * Add support mCaptcha as captcha provider (#20458)
 | 
			
		||||
  * Add team member invite by email (#20307)
 | 
			
		||||
  * Added email notification option to receive all own messages (#20179)
 | 
			
		||||
  * Switch Unicode Escaping to a VSCode-like system (#19990)
 | 
			
		||||
  * Add user/organization code search (#19977)
 | 
			
		||||
  * Only show relevant repositories on explore page (#19361)
 | 
			
		||||
  * User keypairs and HTTP signatures for ActivityPub federation using go-ap (#19133)
 | 
			
		||||
  * Add sitemap support (#18407)
 | 
			
		||||
  * Allow creation of OAuth2 applications for orgs (#18084)
 | 
			
		||||
  * Add system setting table with cache and also add cache supports for user setting (#18058)
 | 
			
		||||
  * Add pages to view watched repos and subscribed issues/PRs (#17156)
 | 
			
		||||
  * Support Proxy protocol (#12527)
 | 
			
		||||
  * Implement sync push mirror on commit (#19411)
 | 
			
		||||
* API
 | 
			
		||||
  * Allow empty assignees on pull request edit (#22150) (#22214)
 | 
			
		||||
  * Make external issue tracker regexp configurable via API (#21338)
 | 
			
		||||
  * Add name field for org api (#21270)
 | 
			
		||||
  * Show teams with no members if user is admin (#21204)
 | 
			
		||||
  * Add latest commit's SHA to content response (#20398)
 | 
			
		||||
  * Add allow_rebase_update, default_delete_branch_after_merge to repository api response (#20079)
 | 
			
		||||
  * Add new endpoints for push mirrors management (#19841)
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Add setting to disable the git apply step in test patch (#22130) (#22170)
 | 
			
		||||
  * Multiple improvements for comment edit diff (#21990) (#22007)
 | 
			
		||||
  * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21928)
 | 
			
		||||
  * Fix flex layout for repo list icons (#21896) (#21920)
 | 
			
		||||
  * Fix vertical align of committer avatar rendered by email address (#21884) (#21918)
 | 
			
		||||
  * Fix setting HTTP headers after write (#21833) (#21877)
 | 
			
		||||
  * Color and Style enhancements (#21784, #21799) (#21868)
 | 
			
		||||
  * Ignore line anchor links with leading zeroes (#21728) (#21776)
 | 
			
		||||
  * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
 | 
			
		||||
  * Use CSS color-scheme instead of invert (#21616) (#21623)
 | 
			
		||||
  * Respect user's locale when rendering the date range in the repo activity page (#21410)
 | 
			
		||||
  * Change `commits-table` column width (#21564)
 | 
			
		||||
  * Refactor git command arguments and make all arguments to be safe to be used (#21535)
 | 
			
		||||
  * CSS color enhancements (#21534)
 | 
			
		||||
  * Add link to user profile in markdown mention only if user exists (#21533, #21554)
 | 
			
		||||
  * Add option to skip index dirs (#21501)
 | 
			
		||||
  * Diff file tree tweaks (#21446)
 | 
			
		||||
  * Localize all timestamps (#21440)
 | 
			
		||||
  * Add `code` highlighting in issue titles (#21432)
 | 
			
		||||
  * Use Name instead of DisplayName in LFS Lock (#21415)
 | 
			
		||||
  * Consolidate more CSS colors into variables (#21402)
 | 
			
		||||
  * Redirect to new repository owner (#21398)
 | 
			
		||||
  * Use ISO date format instead of hard-coded English date format for date range in repo activity page (#21396)
 | 
			
		||||
  * Use weighted algorithm for string matching when finding files in repo (#21370)
 | 
			
		||||
  * Show private data in feeds (#21369)
 | 
			
		||||
  * Refactor parseTreeEntries, speed up tree list (#21368)
 | 
			
		||||
  * Add GET and DELETE endpoints for Docker blob uploads (#21367)
 | 
			
		||||
  * Add nicer error handling on template compile errors (#21350)
 | 
			
		||||
  * Add `stat` to `ToCommit` function for speed (#21337)
 | 
			
		||||
  * Support instance-wide OAuth2 applications (#21335)
 | 
			
		||||
  * Record OAuth client type at registration (#21316)
 | 
			
		||||
  * Add new CSS variables --color-accent and --color-small-accent (#21305)
 | 
			
		||||
  * Improve error descriptions for unauthorized_client (#21292)
 | 
			
		||||
  * Case-insensitive "find files in repo" (#21269)
 | 
			
		||||
  * Consolidate more CSS rules, fix inline code on arc-green (#21260)
 | 
			
		||||
  * Log real ip of requests from ssh (#21216)
 | 
			
		||||
  * Save files in local storage as group readable (#21198)
 | 
			
		||||
  * Enable fluid page layout on medium size viewports (#21178)
 | 
			
		||||
  * File header tweaks (#21175)
 | 
			
		||||
  * Added missing headers on user packages page (#21172)
 | 
			
		||||
  * Display image digest for container packages (#21170)
 | 
			
		||||
  * Skip dirty check for team forms (#21154)
 | 
			
		||||
  * Keep path when creating a new branch (#21153)
 | 
			
		||||
  * Remove fomantic image module (#21145)
 | 
			
		||||
  * Make labels clickable in the comments section. (#21137)
 | 
			
		||||
  * Sort branches and tags by date descending (#21136)
 | 
			
		||||
  * Better repo API unit checks (#21130)
 | 
			
		||||
  * Improve commit status icons (#21124)
 | 
			
		||||
  * Limit length of repo description and repo url input fields (#21119)
 | 
			
		||||
  * Show .editorconfig errors in frontend (#21088)
 | 
			
		||||
  * Allow poster to choose reviewers (#21084)
 | 
			
		||||
  * Remove black labels and CSS cleanup (#21003)
 | 
			
		||||
  * Make e-mail sanity check more precise (#20991)
 | 
			
		||||
  * Use native inputs in whitespace dropdown (#20980)
 | 
			
		||||
  * Enhance package date display (#20928)
 | 
			
		||||
  * Display total blob size of a package version (#20927)
 | 
			
		||||
  * Show language name on hover (#20923)
 | 
			
		||||
  * Show instructions for all generic package files (#20917)
 | 
			
		||||
  * Refactor AssertExistsAndLoadBean to use generics (#20797)
 | 
			
		||||
  * Move the official website link at the footer of gitea (#20777)
 | 
			
		||||
  * Add support for full name in reverse proxy auth (#20776)
 | 
			
		||||
  * Remove useless JS operation for relative time tooltips (#20756)
 | 
			
		||||
  * Replace some icons with SVG (#20741)
 | 
			
		||||
  * Change commit status icons to SVG (#20736)
 | 
			
		||||
  * Improve single repo action for issue and pull requests (#20730)
 | 
			
		||||
  * Allow multiple files in generic packages (#20661)
 | 
			
		||||
  * Add option to create new issue from /issues page (#20650)
 | 
			
		||||
  * Background color of private list-items updated (#20630)
 | 
			
		||||
  * Added search input field to issue filter (#20623)
 | 
			
		||||
  * Increase default item listing size `ISSUE_PAGING_NUM` to 20 (#20547)
 | 
			
		||||
  * Modify milestone search keywords to be case insensitive again (#20513)
 | 
			
		||||
  * Show hint to link package to repo when viewing empty repo package list (#20504)
 | 
			
		||||
  * Add Tar ZSTD support (#20493)
 | 
			
		||||
  * Make code review checkboxes clickable (#20481)
 | 
			
		||||
  * Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (#20438)
 | 
			
		||||
  * Display project in issue list (#20434)
 | 
			
		||||
  * Prepend commit message to template content when opening a new PR (#20429)
 | 
			
		||||
  * Replace fomantic popup module with tippy.js (#20428)
 | 
			
		||||
  * Allow to specify colors for text in markup (#20363)
 | 
			
		||||
  * Allow access to the Public Organization Member lists with minimal permissions (#20330)
 | 
			
		||||
  * Use default values when provided values are empty (#20318)
 | 
			
		||||
  * Vertical align navbar avatar at middle (#20302)
 | 
			
		||||
  * Delete cancel button in repo creation page (#21381)
 | 
			
		||||
  * Include login_name in adminCreateUser response (#20283)
 | 
			
		||||
  * fix: icon margin in user/settings/repos (#20281)
 | 
			
		||||
  * Remove blue text on migrate page (#20273)
 | 
			
		||||
  * Modify milestone search keywords to be case insensitive (#20266)
 | 
			
		||||
  * Move some files into models' sub packages (#20262)
 | 
			
		||||
  * Add tooltip to repo icons in explore page (#20241)
 | 
			
		||||
  * Remove deprecated licenses (#20222)
 | 
			
		||||
  * Webhook for Wiki changes (#20219)
 | 
			
		||||
  * Share HTML template renderers and create a watcher framework (#20218)
 | 
			
		||||
  * Allow enable LDAP source and disable user sync via CLI (#20206)
 | 
			
		||||
  * Adds a checkbox to select all issues/PRs (#20177)
 | 
			
		||||
  * Refactor `i18n` to `locale` (#20153)
 | 
			
		||||
  * Disable status checks in template if none found (#20088)
 | 
			
		||||
  * Allow manager logging to set SQL (#20064)
 | 
			
		||||
  * Add order by for assignee no sort issue (#20053)
 | 
			
		||||
  * Take a stab at porting existing components to Vue3 (#20044)
 | 
			
		||||
  * Add doctor command to write commit-graphs (#20007)
 | 
			
		||||
  * Add support for authentication based on reverse proxy email (#19949)
 | 
			
		||||
  * Enable spellcheck for EasyMDE, use contenteditable mode (#19776)
 | 
			
		||||
  * Allow specifying SECRET_KEY_URI, similar to INTERNAL_TOKEN_URI (#19663)
 | 
			
		||||
  * Rework mailer settings (#18982)
 | 
			
		||||
  * Add option to purge users (#18064)
 | 
			
		||||
  * Add author search input (#21246)
 | 
			
		||||
  * Make rss/atom identifier globally unique (#21550)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Auth interface return error when verify failure (#22119) (#22259)
 | 
			
		||||
  * Use complete SHA to create and query commit status (#22244) (#22257)
 | 
			
		||||
  * Update bleve and zapx to fix unaligned atomic (#22031) (#22218)
 | 
			
		||||
  * Prevent panic in doctor command when running default checks (#21791) (#21807)
 | 
			
		||||
  * Load GitRepo in API before deleting issue (#21720) (#21796)
 | 
			
		||||
  * Ignore line anchor links with leading zeroes (#21728) (#21776)
 | 
			
		||||
  * Set last login when activating account (#21731) (#21755)
 | 
			
		||||
  * Fix UI language switching bug (#21597) (#21749)
 | 
			
		||||
  * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
 | 
			
		||||
  * Allow local package identifiers for PyPI packages (#21690) (#21727)
 | 
			
		||||
  * Deal with markdown template without metadata (#21639) (#21654)
 | 
			
		||||
  * Fix opaque background on mermaid diagrams (#21642) (#21652)
 | 
			
		||||
  * Fix repository adoption on Windows (#21646) (#21650)
 | 
			
		||||
  * Sync git hooks when config file path changed (#21619) (#21626)
 | 
			
		||||
  * Fix 500 on PR files API (#21602) (#21607)
 | 
			
		||||
  * Fix `Timestamp.IsZero` (#21593) (#21603)
 | 
			
		||||
  * Fix viewing user subscriptions (#21482)
 | 
			
		||||
  * Fix mermaid-related bugs (#21431)
 | 
			
		||||
  * Fix branch dropdown shifting on page load (#21428)
 | 
			
		||||
  * Fix default theme-auto selector when nologin (#21346)
 | 
			
		||||
  * Fix and improve incorrect error messages (#21342)
 | 
			
		||||
  * Fix formatted link for PR review notifications to matrix (#21319)
 | 
			
		||||
  * Center-aligning content of WebAuthN page (#21127)
 | 
			
		||||
  * Remove follow from commits by file (#20765)
 | 
			
		||||
  * Fix commit status popup (#20737)
 | 
			
		||||
  * Fix init mail render logic (#20704)
 | 
			
		||||
  * Use correct page size for link header pagination (#20546)
 | 
			
		||||
  * Preserve unix socket file (#20499)
 | 
			
		||||
  * Use tippy.js for context popup (#20393)
 | 
			
		||||
  * Add missing parameter for error in log message (#20144)
 | 
			
		||||
  * Do not allow organisation owners add themselves as collaborator (#20043)
 | 
			
		||||
  * Rework file highlight rendering and fix yaml copy-paste (#19967)
 | 
			
		||||
  * Improve code diff highlight, fix incorrect rendered diff result (#19958)
 | 
			
		||||
* TESTING
 | 
			
		||||
  * Improve OAuth integration tests (#21390)
 | 
			
		||||
  * Add playwright tests (#20123)
 | 
			
		||||
* BUILD
 | 
			
		||||
  * Switch to building with go1.19 (#20695)
 | 
			
		||||
  * Update JS dependencies, adjust eslint (#20659)
 | 
			
		||||
  * Add more linters to improve code readability (#19989)
 | 
			
		||||
 | 
			
		||||
## [1.17.4](https://github.com/go-gitea/gitea/releases/tag/1.17.4) - 2022-12-21
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Do not allow Ghost access to limited visible user/org (#21849) (#21875)
 | 
			
		||||
  * Fix package access for admins and inactive users (#21580) (#21592)
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21927)
 | 
			
		||||
  * Fix vertical align of committer avatar rendered by email address (#21884) (#21919)
 | 
			
		||||
  * Fix setting HTTP headers after write (#21833) (#21874)
 | 
			
		||||
  * Ignore line anchor links with leading zeroes (#21728) (#21777)
 | 
			
		||||
  * Enable Monaco automaticLayout (#21516)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Do not list active repositories as unadopted (#22034) (#22167)
 | 
			
		||||
  * Correctly handle moved files in apply patch (#22118) (#22136)
 | 
			
		||||
  * Fix condition for is_internal (#22095) (#22131)
 | 
			
		||||
  * Fix permission check on issue/pull lock (#22114)
 | 
			
		||||
  * Fix sorting admin user list by last login (#22081) (#22106)
 | 
			
		||||
  * Workaround for container registry push/pull errors (#21862) (#22069)
 | 
			
		||||
  * Fix issue/PR numbers (#22037) (#22045)
 | 
			
		||||
  * Handle empty author names (#21902) (#22028)
 | 
			
		||||
  * Fix ListBranches to handle empty case (#21921) (#22025)
 | 
			
		||||
  * Fix enabling partial clones on 1.17 (#21809)
 | 
			
		||||
  * Prevent panic in doctor command when running default checks (#21791) (#21808)
 | 
			
		||||
  * Upgrade golang.org/x/crypto (#21792) (#21794)
 | 
			
		||||
  * Init git module before database migration (#21764) (#21766)
 | 
			
		||||
  * Set last login when activating account (#21731) (#21754)
 | 
			
		||||
  * Add HEAD fix to gitea doctor (#21352) (#21751)
 | 
			
		||||
  * Fix UI language switching bug (#21597) (#21748)
 | 
			
		||||
  * Remove semver compatible flag and change pypi to an array of test cases (#21708) (#21729)
 | 
			
		||||
  * Allow local package identifiers for PyPI packages (#21690) (#21726)
 | 
			
		||||
  * Fix repository adoption on Windows (#21646) (#21651)
 | 
			
		||||
  * Sync git hooks when config file path changed (#21619) (#21625)
 | 
			
		||||
  * Added check for disabled Packages (#21540) (#21614)
 | 
			
		||||
  * Fix `Timestamp.IsZero` (#21593) (#21604)
 | 
			
		||||
  * Fix issues count bug (#21600)
 | 
			
		||||
  * Support binary deploy in npm packages (#21589)
 | 
			
		||||
  * Update milestone counters when issue is deleted (#21459) (#21586)
 | 
			
		||||
  * SessionUser protection against nil pointer dereference (#21581)
 | 
			
		||||
  * Case-insensitive NuGet symbol file GUID (#21409) (#21575)
 | 
			
		||||
  * Suppress `ExternalLoginUserNotExist` error (#21504) (#21572)
 | 
			
		||||
  * Prevent Authorization header for presigned LFS urls (#21531) (#21569)
 | 
			
		||||
  * Update binding to fix bugs (#21560)
 | 
			
		||||
  * Fix generating compare link (#21519) (#21530)
 | 
			
		||||
  * Ignore error when retrieving changed PR review files (#21487) (#21524)
 | 
			
		||||
  * Fix incorrect notification commit url (#21479) (#21483)
 | 
			
		||||
  * Display total commit count in hook message (#21400) (#21481)
 | 
			
		||||
  * Enforce grouped NuGet search results (#21442) (#21480)
 | 
			
		||||
  * Return 404 when user is not found on avatar (#21476) (#21477)
 | 
			
		||||
  * Normalize NuGet package version on upload (#22186) (#22201)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Check for zero time instant in TimeStamp.IsZero() (#22171) (#22173)
 | 
			
		||||
  * Fix warn in database structs sync (#22111)
 | 
			
		||||
  * Allow for resolution of NPM registry paths that match upstream (#21568) (#21723)
 | 
			
		||||
 | 
			
		||||
## [1.17.3](https://github.com/go-gitea/gitea/releases/tag/v1.17.3) - 2022-10-15
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ EXPOSE 2222 3000
 | 
			
		||||
RUN apk --no-cache add \
 | 
			
		||||
    bash \
 | 
			
		||||
    ca-certificates \
 | 
			
		||||
    dumb-init \
 | 
			
		||||
    gettext \
 | 
			
		||||
    git \
 | 
			
		||||
    curl \
 | 
			
		||||
@@ -68,6 +69,6 @@ ENV HOME "/var/lib/gitea/git"
 | 
			
		||||
VOLUME ["/var/lib/gitea", "/etc/gitea"]
 | 
			
		||||
WORKDIR /var/lib/gitea
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
 | 
			
		||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
 | 
			
		||||
CMD []
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@@ -358,7 +358,7 @@ watch-frontend: node-check node_modules
 | 
			
		||||
 | 
			
		||||
.PHONY: watch-backend
 | 
			
		||||
watch-backend: go-check
 | 
			
		||||
	$(GO) run $(AIR_PACKAGE) -c .air.toml
 | 
			
		||||
	GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
 | 
			
		||||
 | 
			
		||||
.PHONY: test
 | 
			
		||||
test: test-frontend test-backend
 | 
			
		||||
 
 | 
			
		||||
@@ -413,9 +413,9 @@ var (
 | 
			
		||||
			Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
 | 
			
		||||
		},
 | 
			
		||||
		cli.StringFlag{
 | 
			
		||||
			Name:  "addr",
 | 
			
		||||
			Name:  "host",
 | 
			
		||||
			Value: "",
 | 
			
		||||
			Usage: "SMTP Addr",
 | 
			
		||||
			Usage: "SMTP Host",
 | 
			
		||||
		},
 | 
			
		||||
		cli.IntFlag{
 | 
			
		||||
			Name:  "port",
 | 
			
		||||
@@ -955,8 +955,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
 | 
			
		||||
		}
 | 
			
		||||
		conf.Auth = c.String("auth-type")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("addr") {
 | 
			
		||||
		conf.Addr = c.String("addr")
 | 
			
		||||
	if c.IsSet("host") {
 | 
			
		||||
		conf.Host = c.String("host")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("port") {
 | 
			
		||||
		conf.Port = c.Int("port")
 | 
			
		||||
 
 | 
			
		||||
@@ -996,6 +996,9 @@ ROUTER = console
 | 
			
		||||
;;
 | 
			
		||||
;; Add co-authored-by and co-committed-by trailers if committer does not match author
 | 
			
		||||
;ADD_CO_COMMITTER_TRAILERS = true
 | 
			
		||||
;;
 | 
			
		||||
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
 | 
			
		||||
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = true
 | 
			
		||||
 | 
			
		||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 | 
			
		||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 | 
			
		||||
@@ -1550,7 +1553,7 @@ ROUTER = console
 | 
			
		||||
;; Prefix displayed before subject in mail
 | 
			
		||||
;SUBJECT_PREFIX =
 | 
			
		||||
;;
 | 
			
		||||
;; Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy".
 | 
			
		||||
;; Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy".
 | 
			
		||||
;; - sendmail: use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
 | 
			
		||||
;; - dummy: send email messages to the log as a testing phase.
 | 
			
		||||
;; If your provider does not explicitly say which protocol it uses but does provide a port,
 | 
			
		||||
 
 | 
			
		||||
@@ -101,6 +101,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		||||
- `DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY`: **true**: In default merge messages only include approvers who are officially allowed to review.
 | 
			
		||||
- `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request.
 | 
			
		||||
- `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author.
 | 
			
		||||
- `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **true**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required.
 | 
			
		||||
 | 
			
		||||
### Repository - Issue (`repository.issue`)
 | 
			
		||||
 | 
			
		||||
@@ -672,7 +673,7 @@ and
 | 
			
		||||
[Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
 | 
			
		||||
 | 
			
		||||
- `ENABLED`: **false**: Enable to use a mail service.
 | 
			
		||||
- `PROTOCOL`: **\<empty\>**: Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
 | 
			
		||||
- `PROTOCOL`: **\<empty\>**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
 | 
			
		||||
  - SMTP family, if your provider does not explicitly say which protocol it uses but does provide a port, you can set SMTP_PORT instead and this will be inferred.
 | 
			
		||||
  - **sendmail** Use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
 | 
			
		||||
  - **dummy** Send email messages to the log as a testing phase.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								go.mod
									
									
									
									
									
								
							@@ -15,8 +15,8 @@ require (
 | 
			
		||||
	github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
 | 
			
		||||
	github.com/NYTimes/gziphandler v1.1.1
 | 
			
		||||
	github.com/PuerkitoBio/goquery v1.8.0
 | 
			
		||||
	github.com/alecthomas/chroma/v2 v2.3.0
 | 
			
		||||
	github.com/blevesearch/bleve/v2 v2.3.4
 | 
			
		||||
	github.com/alecthomas/chroma/v2 v2.4.0
 | 
			
		||||
	github.com/blevesearch/bleve/v2 v2.3.5
 | 
			
		||||
	github.com/buildkite/terminal-to-html/v3 v3.7.0
 | 
			
		||||
	github.com/caddyserver/certmagic v0.17.2
 | 
			
		||||
	github.com/chi-middleware/proxy v1.1.1
 | 
			
		||||
@@ -94,11 +94,11 @@ require (
 | 
			
		||||
	github.com/yuin/goldmark-meta v1.1.0
 | 
			
		||||
	go.jolheiser.com/hcaptcha v0.0.4
 | 
			
		||||
	go.jolheiser.com/pwn v0.0.3
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
 | 
			
		||||
	golang.org/x/net v0.0.0-20220927171203-f486391704dc
 | 
			
		||||
	golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891
 | 
			
		||||
	golang.org/x/net v0.2.0
 | 
			
		||||
	golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
 | 
			
		||||
	golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
 | 
			
		||||
	golang.org/x/text v0.3.8
 | 
			
		||||
	golang.org/x/sys v0.2.0
 | 
			
		||||
	golang.org/x/text v0.4.0
 | 
			
		||||
	golang.org/x/tools v0.1.12
 | 
			
		||||
	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 | 
			
		||||
	gopkg.in/ini.v1 v1.67.0
 | 
			
		||||
@@ -129,21 +129,21 @@ require (
 | 
			
		||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
			
		||||
	github.com/bgentry/speakeasy v0.1.0 // indirect
 | 
			
		||||
	github.com/bits-and-blooms/bitset v1.3.3 // indirect
 | 
			
		||||
	github.com/blevesearch/bleve_index_api v1.0.3 // indirect
 | 
			
		||||
	github.com/blevesearch/geo v0.1.14 // indirect
 | 
			
		||||
	github.com/blevesearch/bleve_index_api v1.0.4 // indirect
 | 
			
		||||
	github.com/blevesearch/geo v0.1.15 // indirect
 | 
			
		||||
	github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
 | 
			
		||||
	github.com/blevesearch/gtreap v0.1.1 // indirect
 | 
			
		||||
	github.com/blevesearch/mmap-go v1.0.4 // indirect
 | 
			
		||||
	github.com/blevesearch/scorch_segment_api/v2 v2.1.2 // indirect
 | 
			
		||||
	github.com/blevesearch/scorch_segment_api/v2 v2.1.3 // indirect
 | 
			
		||||
	github.com/blevesearch/segment v0.9.0 // indirect
 | 
			
		||||
	github.com/blevesearch/snowballstem v0.9.0 // indirect
 | 
			
		||||
	github.com/blevesearch/upsidedown_store_api v1.0.1 // indirect
 | 
			
		||||
	github.com/blevesearch/vellum v1.0.8 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v11 v11.3.5 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v12 v12.3.5 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v13 v13.3.5 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v14 v14.3.5 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v15 v15.3.5 // indirect
 | 
			
		||||
	github.com/blevesearch/vellum v1.0.9 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v11 v11.3.6 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v12 v12.3.6 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v13 v13.3.6 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v14 v14.3.6 // indirect
 | 
			
		||||
	github.com/blevesearch/zapx/v15 v15.3.6 // indirect
 | 
			
		||||
	github.com/boombuler/barcode v1.0.1 // indirect
 | 
			
		||||
	github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
 | 
			
		||||
	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
 | 
			
		||||
@@ -302,6 +302,8 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142
 | 
			
		||||
 | 
			
		||||
replace github.com/satori/go.uuid v1.2.0 => github.com/gofrs/uuid v4.2.0+incompatible
 | 
			
		||||
 | 
			
		||||
replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix
 | 
			
		||||
 | 
			
		||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
 | 
			
		||||
 | 
			
		||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								go.sum
									
									
									
									
									
								
							@@ -149,7 +149,6 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
 | 
			
		||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
			
		||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
 | 
			
		||||
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
 | 
			
		||||
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
 | 
			
		||||
github.com/RoaringBitmap/roaring v1.2.1 h1:58/LJlg/81wfEHd5L9qsHduznOIhyv4qb1yWcSvVq9A=
 | 
			
		||||
github.com/RoaringBitmap/roaring v1.2.1/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
 | 
			
		||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 | 
			
		||||
@@ -160,9 +159,10 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
 | 
			
		||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 | 
			
		||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 | 
			
		||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.3.0 h1:83xfxrnjv8eK+Cf8qZDzNo3PPF9IbTWHs7z28GY6D0U=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.3.0/go.mod h1:mZxeWZlxP2Dy+/8cBob2PYd8O2DwNAzave5AY7A2eQw=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ=
 | 
			
		||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
 | 
			
		||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 | 
			
		||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
 | 
			
		||||
@@ -225,52 +225,47 @@ github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
 | 
			
		||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 | 
			
		||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 | 
			
		||||
github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM=
 | 
			
		||||
github.com/blevesearch/bleve/v2 v2.3.4 h1:SSb7/cwGzo85LWX1jchIsXM8ZiNNMX3shT5lROM63ew=
 | 
			
		||||
github.com/blevesearch/bleve/v2 v2.3.4/go.mod h1:Ot0zYum8XQRfPcwhae8bZmNyYubynsoMjVvl1jPqL30=
 | 
			
		||||
github.com/blevesearch/bleve/v2 v2.3.5 h1:1wuR7eB8Fk9UaCaBUfnQt5V7zIpi4VDok9ExN7Rl+/8=
 | 
			
		||||
github.com/blevesearch/bleve/v2 v2.3.5/go.mod h1:FneKGHMRrCLrp4X9+iy3wlBqgM2ALucg7bp8jUuAi/s=
 | 
			
		||||
github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
 | 
			
		||||
github.com/blevesearch/bleve_index_api v1.0.3 h1:DDSWaPXOZZJ2BB73ZTWjKxydAugjwywcqU+91AAqcAg=
 | 
			
		||||
github.com/blevesearch/bleve_index_api v1.0.3/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
 | 
			
		||||
github.com/blevesearch/geo v0.1.13/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
 | 
			
		||||
github.com/blevesearch/geo v0.1.14 h1:TTDpJN6l9ck/cUYbXSn4aCElNls0Whe44rcQKsB7EfU=
 | 
			
		||||
github.com/blevesearch/geo v0.1.14/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
 | 
			
		||||
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
 | 
			
		||||
github.com/blevesearch/bleve_index_api v1.0.4 h1:mtlzsyJjMIlDngqqB1mq8kPryUMIuEVVbRbJHOWEexU=
 | 
			
		||||
github.com/blevesearch/bleve_index_api v1.0.4/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
 | 
			
		||||
github.com/blevesearch/geo v0.1.15 h1:0NybEduqE5fduFRYiUKF0uqybAIFKXYjkBdXKYn7oA4=
 | 
			
		||||
github.com/blevesearch/geo v0.1.15/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
 | 
			
		||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
 | 
			
		||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
 | 
			
		||||
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
 | 
			
		||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
 | 
			
		||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
 | 
			
		||||
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
 | 
			
		||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
 | 
			
		||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
 | 
			
		||||
github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU=
 | 
			
		||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.2 h1:TAte9VZLWda5WAVlZTTZ+GCzEHqGJb4iB2aiZSA6Iv8=
 | 
			
		||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.2/go.mod h1:rvoQXZGq8drq7vXbNeyiRzdEOwZkjkiYGf1822i6CRA=
 | 
			
		||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.3 h1:2UzpR2dR5DvSZk8tVJkcQ7D5xhoK/UBelYw8ttBHrRQ=
 | 
			
		||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.3/go.mod h1:eZrfp1y+lUh+DzFjUcTBUSnKGuunyFIpBIvqYVzJfvc=
 | 
			
		||||
github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
 | 
			
		||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
 | 
			
		||||
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
 | 
			
		||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
 | 
			
		||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
 | 
			
		||||
github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU=
 | 
			
		||||
github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.8 h1:iMGh4lfxza4BnWO/UJTMPlI3HsK9YawjPv+TteVa9ck=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.8/go.mod h1:+cpRi/tqq49xUYSQN2P7A5zNSNrS+MscLeeaZ3J46UA=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.9 h1:PL+NWVk3dDGPCV0hoDu9XLLJgqU4E5s/dOeEJByQ2uQ=
 | 
			
		||||
github.com/blevesearch/vellum v1.0.9/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
 | 
			
		||||
github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM=
 | 
			
		||||
github.com/blevesearch/zapx/v11 v11.3.5 h1:eBQWQ7huA+mzm0sAGnZDwgGGli7S45EO+N+ObFWssbI=
 | 
			
		||||
github.com/blevesearch/zapx/v11 v11.3.5/go.mod h1:5UdIa/HRMdeRCiLQOyFESsnqBGiip7vQmYReA9toevU=
 | 
			
		||||
github.com/blevesearch/zapx/v11 v11.3.6 h1:50jET4HUJ6eCqGxdhUt+mjybMvEX2MWyqLGtCx3yUgc=
 | 
			
		||||
github.com/blevesearch/zapx/v11 v11.3.6/go.mod h1:B0CzJRj/pS7hJIroflRtFsa9mRHpMSucSgre0FVINns=
 | 
			
		||||
github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A=
 | 
			
		||||
github.com/blevesearch/zapx/v12 v12.3.5 h1:5pX2hU+R1aZihT7ac1dNWh1n4wqkIM9pZzWp0ANED9s=
 | 
			
		||||
github.com/blevesearch/zapx/v12 v12.3.5/go.mod h1:ANcthYRZQycpbRut/6ArF5gP5HxQyJqiFcuJCBju/ss=
 | 
			
		||||
github.com/blevesearch/zapx/v12 v12.3.6 h1:G304NHBLgQeZ+IHK/XRCM0nhHqAts8MEvHI6LhoDNM4=
 | 
			
		||||
github.com/blevesearch/zapx/v12 v12.3.6/go.mod h1:iYi7tIKpauwU5os5wTxJITixr5Km21Hl365otMwdaP0=
 | 
			
		||||
github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ=
 | 
			
		||||
github.com/blevesearch/zapx/v13 v13.3.5 h1:eJ3gbD+Nu8p36/O6lhfdvWQ4pxsGYSuTOBrLLPVWJ74=
 | 
			
		||||
github.com/blevesearch/zapx/v13 v13.3.5/go.mod h1:FV+dRnScFgKnRDIp08RQL4JhVXt1x2HE3AOzqYa6fjo=
 | 
			
		||||
github.com/blevesearch/zapx/v13 v13.3.6 h1:vavltQHNdjQezhLZs5nIakf+w/uOa1oqZxB58Jy/3Ig=
 | 
			
		||||
github.com/blevesearch/zapx/v13 v13.3.6/go.mod h1:X+FsTwCU8qOHtK0d/ArvbOH7qiIgViSQ1GQvcR6LSkI=
 | 
			
		||||
github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g=
 | 
			
		||||
github.com/blevesearch/zapx/v14 v14.3.5 h1:hEvVjZaagFCvOUJrlFQ6/Z6Jjy0opM3g7TMEo58TwP4=
 | 
			
		||||
github.com/blevesearch/zapx/v14 v14.3.5/go.mod h1:954A/eKFb+pg/ncIYWLWCKY+mIjReM9FGTGIO2Wu1cU=
 | 
			
		||||
github.com/blevesearch/zapx/v14 v14.3.6 h1:b9lub7TvcwUyJxK/cQtnN79abngKxsI7zMZnICU0WhE=
 | 
			
		||||
github.com/blevesearch/zapx/v14 v14.3.6/go.mod h1:9X8W3XoikagU0rwcTqwZho7p9cC7m7zhPZO94S4wUvM=
 | 
			
		||||
github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A=
 | 
			
		||||
github.com/blevesearch/zapx/v15 v15.3.5 h1:NVD0qq8vRk66ImJn1KloXT5ckqPDUZT7VbVJs9jKlac=
 | 
			
		||||
github.com/blevesearch/zapx/v15 v15.3.5/go.mod h1:QMUh2hXCaYIWFKPYGavq/Iga2zbHWZ9DZAa9uFbWyvg=
 | 
			
		||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 | 
			
		||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
 | 
			
		||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 | 
			
		||||
@@ -361,7 +356,6 @@ github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFl
 | 
			
		||||
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 h1:4KDlx3vjalrHD/EfsjCpV91HNX3JPaIqRtt83zZ7x+Y=
 | 
			
		||||
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
 | 
			
		||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
 | 
			
		||||
github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
 | 
			
		||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 | 
			
		||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 | 
			
		||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 | 
			
		||||
@@ -830,6 +824,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 | 
			
		||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 | 
			
		||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 | 
			
		||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 | 
			
		||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 | 
			
		||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
 | 
			
		||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
 | 
			
		||||
@@ -1487,6 +1482,8 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.m
 | 
			
		||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
 | 
			
		||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
 | 
			
		||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 | 
			
		||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix h1:fKZ9OxEDoJKgM0KBXRbSb5IgKUEXis6C3zEIiMtzzQ0=
 | 
			
		||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix/go.mod h1:5DbhhDTGtuQSns1tS2aJxJLPc91boXCvjOMeCLD1saM=
 | 
			
		||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
 | 
			
		||||
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
 | 
			
		||||
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
 | 
			
		||||
@@ -1608,8 +1605,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891 h1:WhEPFM1Ck5gaKybeSWvzI7Y/cd8K9K5tJGRxXMACOBA=
 | 
			
		||||
golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
			
		||||
@@ -1721,8 +1718,8 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su
 | 
			
		||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
			
		||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
 | 
			
		||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
			
		||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
 | 
			
		||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
@@ -1876,13 +1873,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
			
		||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
 | 
			
		||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
			
		||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
 | 
			
		||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
			
		||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
@@ -1892,8 +1889,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
			
		||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
 | 
			
		||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 | 
			
		||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
 | 
			
		||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
 
 | 
			
		||||
@@ -272,7 +272,7 @@ func (a *Action) GetRefLink() string {
 | 
			
		||||
		return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix))
 | 
			
		||||
	case strings.HasPrefix(a.RefName, git.TagPrefix):
 | 
			
		||||
		return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix))
 | 
			
		||||
	case len(a.RefName) == 40 && git.IsValidSHAPattern(a.RefName):
 | 
			
		||||
	case len(a.RefName) == git.SHAFullLength && git.IsValidSHAPattern(a.RefName):
 | 
			
		||||
		return a.GetRepoLink() + "/src/commit/" + a.RefName
 | 
			
		||||
	default:
 | 
			
		||||
		// FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here.
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,12 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 | 
			
		||||
const DefaultAvatarPixelSize = 28
 | 
			
		||||
const (
 | 
			
		||||
	// DefaultAvatarClass is the default class of a rendered avatar
 | 
			
		||||
	DefaultAvatarClass = "ui avatar vm"
 | 
			
		||||
	// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 | 
			
		||||
	DefaultAvatarPixelSize = 28
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
 | 
			
		||||
type EmailHash struct {
 | 
			
		||||
@@ -150,10 +154,11 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return DefaultAvatarLink()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatarSetting, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatar := enableFederatedAvatarSetting.GetValueBool()
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil {
 | 
			
		||||
	if enableFederatedAvatar && system_model.LibravatarService != nil {
 | 
			
		||||
		emailHash := saveEmailHash(email)
 | 
			
		||||
		if final {
 | 
			
		||||
			// for final link, we can spend more time on slow external query
 | 
			
		||||
@@ -171,8 +176,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return urlStr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	if disableGravatar != nil && !disableGravatar.GetValueBool() {
 | 
			
		||||
	disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
 | 
			
		||||
	disableGravatar := disableGravatarSetting.GetValueBool()
 | 
			
		||||
	if !disableGravatar {
 | 
			
		||||
		// copy GravatarSourceURL, because we will modify its Path.
 | 
			
		||||
		avatarURLCopy := *system_model.GravatarSourceURL
 | 
			
		||||
		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,9 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ResourceIndex represents a resource index which could be used as issue/release and others
 | 
			
		||||
@@ -24,11 +27,6 @@ var (
 | 
			
		||||
	ErrGetResourceIndexFailed = errors.New("get resource index failed")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// MaxDupIndexAttempts max retry times to create index
 | 
			
		||||
	MaxDupIndexAttempts = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SyncMaxResourceIndex sync the max index with the resource
 | 
			
		||||
func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) {
 | 
			
		||||
	e := GetEngine(ctx)
 | 
			
		||||
@@ -61,8 +59,25 @@ func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxInd
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
 | 
			
		||||
	res, err := GetEngine(ctx).Query(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
 | 
			
		||||
		"VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1 RETURNING max_index",
 | 
			
		||||
		tableName, tableName), groupID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(res) == 0 {
 | 
			
		||||
		return 0, ErrGetResourceIndexFailed
 | 
			
		||||
	}
 | 
			
		||||
	return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
 | 
			
		||||
func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
 | 
			
		||||
	if setting.Database.UsePostgreSQL {
 | 
			
		||||
		return postgresGetNextResourceIndex(ctx, tableName, groupID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e := GetEngine(ctx)
 | 
			
		||||
 | 
			
		||||
	// try to update the max_index to next value, and acquire the write-lock for the record
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,10 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -49,79 +51,67 @@ func init() {
 | 
			
		||||
	db.RegisterModel(new(CommitStatusIndex))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error.
 | 
			
		||||
func upsertCommitStatusIndex(ctx context.Context, repoID int64, sha string) (err error) {
 | 
			
		||||
	// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
 | 
			
		||||
	// that ensures that the key is actually locked.
 | 
			
		||||
	switch {
 | 
			
		||||
	case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL:
 | 
			
		||||
		_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
 | 
			
		||||
			"VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1",
 | 
			
		||||
			repoID, sha)
 | 
			
		||||
	case setting.Database.UseMySQL:
 | 
			
		||||
		_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
 | 
			
		||||
			"VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1",
 | 
			
		||||
			repoID, sha)
 | 
			
		||||
	case setting.Database.UseMSSQL:
 | 
			
		||||
		// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
 | 
			
		||||
		_, err = db.Exec(ctx, "MERGE `commit_status_index` WITH (HOLDLOCK) as target "+
 | 
			
		||||
			"USING (SELECT ? AS repo_id, ? AS sha) AS src "+
 | 
			
		||||
			"ON src.repo_id = target.repo_id AND src.sha = target.sha "+
 | 
			
		||||
			"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+
 | 
			
		||||
			"WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) "+
 | 
			
		||||
			"VALUES (src.repo_id, src.sha, 1);",
 | 
			
		||||
			repoID, sha)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("database type not supported")
 | 
			
		||||
func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
 | 
			
		||||
	res, err := db.GetEngine(ctx).Query("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
 | 
			
		||||
		"VALUES (?,?,1) ON CONFLICT (repo_id, sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1 RETURNING max_index",
 | 
			
		||||
		repoID, sha)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
	if len(res) == 0 {
 | 
			
		||||
		return 0, db.ErrGetResourceIndexFailed
 | 
			
		||||
	}
 | 
			
		||||
	return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNextCommitStatusIndex retried 3 times to generate a resource index
 | 
			
		||||
func GetNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
 | 
			
		||||
	for i := 0; i < db.MaxDupIndexAttempts; i++ {
 | 
			
		||||
		idx, err := getNextCommitStatusIndex(repoID, sha)
 | 
			
		||||
		if err == db.ErrResouceOutdated {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
 | 
			
		||||
	if setting.Database.UsePostgreSQL {
 | 
			
		||||
		return postgresGetCommitStatusIndex(ctx, repoID, sha)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e := db.GetEngine(ctx)
 | 
			
		||||
 | 
			
		||||
	// try to update the max_index to next value, and acquire the write-lock for the record
 | 
			
		||||
	res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	affected, err := res.RowsAffected()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	if affected == 0 {
 | 
			
		||||
		// this slow path is only for the first time of creating a resource index
 | 
			
		||||
		_, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha)
 | 
			
		||||
		res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		return idx, nil
 | 
			
		||||
	}
 | 
			
		||||
	return 0, db.ErrGetResourceIndexFailed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getNextCommitStatusIndex return the next index
 | 
			
		||||
func getNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
 | 
			
		||||
	ctx, commiter, err := db.TxContext()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	defer commiter.Close()
 | 
			
		||||
 | 
			
		||||
	var preIdx int64
 | 
			
		||||
	_, err = db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", repoID, sha).Get(&preIdx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
		affected, err = res.RowsAffected()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		// if the update still can not update any records, the record must not exist and there must be some errors (insert error)
 | 
			
		||||
		if affected == 0 {
 | 
			
		||||
			if errIns == nil {
 | 
			
		||||
				return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated")
 | 
			
		||||
			}
 | 
			
		||||
			return 0, errIns
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := upsertCommitStatusIndex(ctx, repoID, sha); err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var curIdx int64
 | 
			
		||||
	has, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ? AND max_index=?", repoID, sha, preIdx+1).Get(&curIdx)
 | 
			
		||||
	// now, the new index is in database (protected by the transaction and write-lock)
 | 
			
		||||
	var newIdx int64
 | 
			
		||||
	has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	if !has {
 | 
			
		||||
		return 0, db.ErrResouceOutdated
 | 
			
		||||
		return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected")
 | 
			
		||||
	}
 | 
			
		||||
	if err := commiter.Commit(); err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return curIdx, nil
 | 
			
		||||
	return newIdx, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
 | 
			
		||||
@@ -291,10 +281,8 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
 | 
			
		||||
		return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the next Status Index
 | 
			
		||||
	idx, err := GetNextCommitStatusIndex(opts.Repo.ID, opts.SHA)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("generate commit status index failed: %w", err)
 | 
			
		||||
	if _, err := git.NewIDFromString(opts.SHA); err != nil {
 | 
			
		||||
		return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, committer, err := db.TxContext()
 | 
			
		||||
@@ -303,6 +291,12 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
 | 
			
		||||
	}
 | 
			
		||||
	defer committer.Close()
 | 
			
		||||
 | 
			
		||||
	// Get the next Status Index
 | 
			
		||||
	idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("generate commit status index failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
 | 
			
		||||
	opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
 | 
			
		||||
	opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
 | 
			
		||||
@@ -316,7 +310,7 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
 | 
			
		||||
 | 
			
		||||
	// Insert new CommitStatus
 | 
			
		||||
	if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil {
 | 
			
		||||
		return fmt.Errorf("Insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
 | 
			
		||||
		return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return committer.Commit()
 | 
			
		||||
 
 | 
			
		||||
@@ -1010,12 +1010,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.IsPull {
 | 
			
		||||
		_, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
 | 
			
		||||
	} else {
 | 
			
		||||
		_, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package issues
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -47,33 +48,42 @@ func (t *TrackedTime) LoadAttributes() (err error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) {
 | 
			
		||||
	// Load the issue
 | 
			
		||||
	if t.Issue == nil {
 | 
			
		||||
		t.Issue, err = GetIssueByID(ctx, t.IssueID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = t.Issue.LoadRepo(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if t.User == nil {
 | 
			
		||||
		t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadAttributes load Issue, User
 | 
			
		||||
func (tl TrackedTimeList) LoadAttributes() (err error) {
 | 
			
		||||
	for _, t := range tl {
 | 
			
		||||
		if err = t.LoadAttributes(); err != nil {
 | 
			
		||||
		if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
	// Now load the repo for the issue (which we may have just loaded)
 | 
			
		||||
	if t.Issue != nil {
 | 
			
		||||
		err = t.Issue.LoadRepo(ctx)
 | 
			
		||||
		if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Load the user
 | 
			
		||||
	if t.User == nil {
 | 
			
		||||
		t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if !errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			t.User = user_model.NewGhostUser()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadAttributes load Issue, User
 | 
			
		||||
func (tl TrackedTimeList) LoadAttributes() error {
 | 
			
		||||
	for _, t := range tl {
 | 
			
		||||
		if err := t.LoadAttributes(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
@@ -513,6 +514,13 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Some migration tasks depend on the git command
 | 
			
		||||
	if git.DefaultContext == nil {
 | 
			
		||||
		if err = git.InitSimple(context.Background()); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Migrate
 | 
			
		||||
	for i, m := range migrations[v-minDBVersion:] {
 | 
			
		||||
		log.Info("Migration[%d]: %s", v+int64(i), m.Description())
 | 
			
		||||
 
 | 
			
		||||
@@ -458,8 +458,9 @@ func CountOrgs(opts FindOrgOptions) (int64, error) {
 | 
			
		||||
 | 
			
		||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
 | 
			
		||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
 | 
			
		||||
	// Not SignedUser
 | 
			
		||||
	if user == nil {
 | 
			
		||||
	// If user is nil, it's an anonymous user/request.
 | 
			
		||||
	// The Ghost user is handled like an anonymous user.
 | 
			
		||||
	if user == nil || user.IsGhost() {
 | 
			
		||||
		return orgOrUser.Visibility == structs.VisibleTypePublic
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -305,7 +305,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
 | 
			
		||||
 | 
			
		||||
	sess := db.GetEngine(ctx).
 | 
			
		||||
		Table("package_version").
 | 
			
		||||
		Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))").
 | 
			
		||||
		Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND pv2.is_internal = ? AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))", false).
 | 
			
		||||
		Join("INNER", "package", "package.id = package_version.package_id").
 | 
			
		||||
		Where(cond)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -444,7 +444,7 @@ func CheckRepoStats(ctx context.Context) error {
 | 
			
		||||
		},
 | 
			
		||||
		// Repository.NumIssues
 | 
			
		||||
		{
 | 
			
		||||
			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, false),
 | 
			
		||||
			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", false),
 | 
			
		||||
			repoStatsCorrectNumIssues,
 | 
			
		||||
			"repository count 'num_issues'",
 | 
			
		||||
		},
 | 
			
		||||
@@ -456,7 +456,7 @@ func CheckRepoStats(ctx context.Context) error {
 | 
			
		||||
		},
 | 
			
		||||
		// Repository.NumPulls
 | 
			
		||||
		{
 | 
			
		||||
			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, true),
 | 
			
		||||
			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", true),
 | 
			
		||||
			repoStatsCorrectNumPulls,
 | 
			
		||||
			"repository count 'num_pulls'",
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,10 @@ func (s *Setting) TableName() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Setting) GetValueBool() bool {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b, _ := strconv.ParseBool(s.SettingValue)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
@@ -75,8 +80,8 @@ func IsErrDataExpired(err error) bool {
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns specific setting
 | 
			
		||||
func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
// GetSettingNoCache returns specific setting without using the cache
 | 
			
		||||
func GetSettingNoCache(key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetSettings([]string{key})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -87,6 +92,24 @@ func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
	return v[key], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
	return cache.Get(genSettingCacheKey(key), func() (*Setting, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return res, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingBool return bool value of setting,
 | 
			
		||||
// none existing keys and errors are ignored and result in false
 | 
			
		||||
func GetSettingBool(key string) bool {
 | 
			
		||||
	s, _ := GetSetting(key)
 | 
			
		||||
	return s.GetValueBool()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettings returns specific settings
 | 
			
		||||
func GetSettings(keys []string) (map[string]*Setting, error) {
 | 
			
		||||
	for i := 0; i < len(keys); i++ {
 | 
			
		||||
@@ -139,12 +162,13 @@ func GetAllSettings() (AllSettings, error) {
 | 
			
		||||
 | 
			
		||||
// DeleteSetting deletes a specific setting for a user
 | 
			
		||||
func DeleteSetting(setting *Setting) error {
 | 
			
		||||
	cache.Remove(genSettingCacheKey(setting.SettingKey))
 | 
			
		||||
	_, err := db.GetEngine(db.DefaultContext).Delete(setting)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetSettingNoVersion(key, value string) error {
 | 
			
		||||
	s, err := GetSetting(key)
 | 
			
		||||
	s, err := GetSettingNoCache(key)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		return SetSetting(&Setting{
 | 
			
		||||
			SettingKey:   key,
 | 
			
		||||
@@ -160,9 +184,13 @@ func SetSettingNoVersion(key, value string) error {
 | 
			
		||||
 | 
			
		||||
// SetSetting updates a users' setting for a specific key
 | 
			
		||||
func SetSetting(setting *Setting) error {
 | 
			
		||||
	if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
 | 
			
		||||
	_, err := cache.Set(genSettingCacheKey(setting.SettingKey), func() (*Setting, error) {
 | 
			
		||||
		return setting, upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting.Version++
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -213,7 +241,7 @@ var (
 | 
			
		||||
 | 
			
		||||
func Init() error {
 | 
			
		||||
	var disableGravatar bool
 | 
			
		||||
	disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar)
 | 
			
		||||
	disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		disableGravatar = setting.GetDefaultDisableGravatar()
 | 
			
		||||
		disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
 | 
			
		||||
@@ -224,7 +252,7 @@ func Init() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var enableFederatedAvatar bool
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar)
 | 
			
		||||
		enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,3 +9,8 @@ const (
 | 
			
		||||
	KeyPictureDisableGravatar       = "picture.disable_gravatar"
 | 
			
		||||
	KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// genSettingCacheKey returns the cache key for some configuration
 | 
			
		||||
func genSettingCacheKey(key string) string {
 | 
			
		||||
	return "system.setting." + key
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 | 
			
		||||
		&user_model.UserBadge{UserID: u.ID},
 | 
			
		||||
		&pull_model.AutoMerge{DoerID: u.ID},
 | 
			
		||||
		&pull_model.ReviewState{UserID: u.ID},
 | 
			
		||||
		&user_model.Redirect{RedirectUserID: u.ID},
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		return fmt.Errorf("deleteBeans: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,11 +68,9 @@ func (u *User) AvatarLinkWithSize(size int) string {
 | 
			
		||||
	useLocalAvatar := false
 | 
			
		||||
	autoGenerateAvatar := false
 | 
			
		||||
 | 
			
		||||
	var disableGravatar bool
 | 
			
		||||
	disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	if disableGravatarSetting != nil {
 | 
			
		||||
		disableGravatar = disableGravatarSetting.GetValueBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disableGravatar := disableGravatarSetting.GetValueBool()
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case u.UseCustomAvatar:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
@@ -47,9 +48,25 @@ func IsErrUserSettingIsNotExist(err error) bool {
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns specific setting
 | 
			
		||||
// genSettingCacheKey returns the cache key for some configuration
 | 
			
		||||
func genSettingCacheKey(userID int64, key string) string {
 | 
			
		||||
	return fmt.Sprintf("user_%d.setting.%s", userID, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetUserSettings(uid, []string{key})
 | 
			
		||||
	return cache.Get(genSettingCacheKey(uid, key), func() (*Setting, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(uid, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return res, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingNoCache returns specific setting without using the cache
 | 
			
		||||
func GetSettingNoCache(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetSettings(uid, []string{key})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -59,8 +76,8 @@ func GetSetting(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	return v[key], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserSettings returns specific settings from user
 | 
			
		||||
func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) {
 | 
			
		||||
// GetSettings returns specific settings from user
 | 
			
		||||
func GetSettings(uid int64, keys []string) (map[string]*Setting, error) {
 | 
			
		||||
	settings := make([]*Setting, 0, len(keys))
 | 
			
		||||
	if err := db.GetEngine(db.DefaultContext).
 | 
			
		||||
		Where("user_id=?", uid).
 | 
			
		||||
@@ -105,6 +122,7 @@ func GetUserSetting(userID int64, key string, def ...string) (string, error) {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting := &Setting{UserID: userID, SettingKey: key}
 | 
			
		||||
	has, err := db.GetEngine(db.DefaultContext).Get(setting)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -124,7 +142,10 @@ func DeleteUserSetting(userID int64, key string) error {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cache.Remove(genSettingCacheKey(userID, key))
 | 
			
		||||
	_, err := db.GetEngine(db.DefaultContext).Delete(&Setting{UserID: userID, SettingKey: key})
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -133,7 +154,12 @@ func SetUserSetting(userID int64, key, value string) error {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return upsertUserSettingValue(userID, key, value)
 | 
			
		||||
 | 
			
		||||
	_, err := cache.Set(genSettingCacheKey(userID, key), func() (string, error) {
 | 
			
		||||
		return value, upsertUserSettingValue(userID, key, value)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func upsertUserSettingValue(userID int64, key, value string) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ func TestSettings(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// get specific setting
 | 
			
		||||
	settings, err := user_model.GetUserSettings(99, []string{keyName})
 | 
			
		||||
	settings, err := user_model.GetSettings(99, []string{keyName})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, settings, 1)
 | 
			
		||||
	assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import (
 | 
			
		||||
// GetKeyPair function returns a user's private and public keys
 | 
			
		||||
func GetKeyPair(user *user_model.User) (pub, priv string, err error) {
 | 
			
		||||
	var settings map[string]*user_model.Setting
 | 
			
		||||
	settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
 | 
			
		||||
	settings, err = user_model.GetSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	} else if len(settings) == 0 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										131
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										131
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							@@ -46,32 +46,64 @@ func GetCache() mc.Cache {
 | 
			
		||||
	return conn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get returns the key value from cache with callback when no key exists in cache
 | 
			
		||||
func Get[V interface{}](key string, getFunc func() (V, error)) (V, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
	if value, ok := cached.(V); ok {
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, err := getFunc()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return value, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set updates and returns the key value in the cache with callback. The old value is only removed if the updateFunc() is successful
 | 
			
		||||
func Set[V interface{}](key string, valueFunc func() (V, error)) (V, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return valueFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, err := valueFunc()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return value, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetString returns the key value from cache with callback when no key exists in cache
 | 
			
		||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value string
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	value := conn.Get(key)
 | 
			
		||||
	if v, ok := value.(string); ok {
 | 
			
		||||
		return v, nil
 | 
			
		||||
 | 
			
		||||
	if value, ok := cached.(string); ok {
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
	if v, ok := value.(fmt.Stringer); ok {
 | 
			
		||||
		return v.String(), nil
 | 
			
		||||
 | 
			
		||||
	if stringer, ok := cached.(fmt.Stringer); ok {
 | 
			
		||||
		return stringer.String(), nil
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s", conn.Get(key)), nil
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s", cached), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetInt returns key value from cache with callback when no key exists in cache
 | 
			
		||||
@@ -79,30 +111,33 @@ func GetInt(key string, getFunc func() (int, error)) (int, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value int
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	switch value := conn.Get(key).(type) {
 | 
			
		||||
 | 
			
		||||
	switch v := cached.(type) {
 | 
			
		||||
	case int:
 | 
			
		||||
		return value, nil
 | 
			
		||||
		return v, nil
 | 
			
		||||
	case string:
 | 
			
		||||
		v, err := strconv.Atoi(value)
 | 
			
		||||
		value, err := strconv.Atoi(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		return v, nil
 | 
			
		||||
		return value, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("Unsupported cached value type: %v", value)
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -111,30 +146,34 @@ func GetInt64(key string, getFunc func() (int64, error)) (int64, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value int64
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	switch value := conn.Get(key).(type) {
 | 
			
		||||
 | 
			
		||||
	switch v := conn.Get(key).(type) {
 | 
			
		||||
	case int64:
 | 
			
		||||
		return value, nil
 | 
			
		||||
		return v, nil
 | 
			
		||||
	case string:
 | 
			
		||||
		v, err := strconv.ParseInt(value, 10, 64)
 | 
			
		||||
		value, err := strconv.ParseInt(v, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		return v, nil
 | 
			
		||||
		return value, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("Unsupported cached value type: %v", value)
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,12 @@ func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable {
 | 
			
		||||
			key = key[:idx]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if table == nil && (locale.Language() == "zh-CN" || locale.Language() == "zh_CN") {
 | 
			
		||||
		table = AmbiguousCharacters["zh-hans"]
 | 
			
		||||
	}
 | 
			
		||||
	if table == nil && strings.HasPrefix(locale.Language(), "zh") {
 | 
			
		||||
		table = AmbiguousCharacters["zh-hant"]
 | 
			
		||||
	}
 | 
			
		||||
	if table == nil {
 | 
			
		||||
		table = AmbiguousCharacters["_default"]
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
package charset
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +33,7 @@ func EscapeControlHTML(text string, locale translation.Locale, allowed ...rune)
 | 
			
		||||
	return streamer.escaped, sb.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EscapeControlReaders escapes the unicode control sequences in a provider reader and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
 | 
			
		||||
// EscapeControlReaders escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
 | 
			
		||||
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
 | 
			
		||||
	outputStream := &HTMLStreamerWriter{Writer: writer}
 | 
			
		||||
	streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
 | 
			
		||||
@@ -44,6 +45,35 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
 | 
			
		||||
	return streamer.escaped, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
 | 
			
		||||
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
 | 
			
		||||
	bufRd := bufio.NewReader(reader)
 | 
			
		||||
	outputStream := &HTMLStreamerWriter{Writer: writer}
 | 
			
		||||
	streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		line, rdErr := bufRd.ReadString('\n')
 | 
			
		||||
		if len(line) > 0 {
 | 
			
		||||
			if err := streamer.Text(line); err != nil {
 | 
			
		||||
				streamer.escaped.HasError = true
 | 
			
		||||
				log.Error("Error whilst escaping: %v", err)
 | 
			
		||||
				return streamer.escaped, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if rdErr != nil {
 | 
			
		||||
			if rdErr != io.EOF {
 | 
			
		||||
				err = rdErr
 | 
			
		||||
			}
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if err := streamer.SelfClosingTag("br"); err != nil {
 | 
			
		||||
			streamer.escaped.HasError = true
 | 
			
		||||
			return streamer.escaped, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return streamer.escaped, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EscapeControlString escapes the unicode control sequences in a provided string and returns the findings as an EscapeStatus and the escaped string
 | 
			
		||||
func EscapeControlString(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
 | 
			
		||||
	sb := &strings.Builder{}
 | 
			
		||||
 
 | 
			
		||||
@@ -220,7 +220,13 @@ func (ctx *APIContext) CheckForOTP() {
 | 
			
		||||
func APIAuth(authMethod auth_service.Method) func(*APIContext) {
 | 
			
		||||
	return func(ctx *APIContext) {
 | 
			
		||||
		// Get user from session if logged in.
 | 
			
		||||
		ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "APIAuth", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ctx.Doer != nil {
 | 
			
		||||
			if ctx.Locale.Language() != ctx.Doer.Language {
 | 
			
		||||
				ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
 | 
			
		||||
@@ -388,7 +394,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
 | 
			
		||||
		} else if len(refName) == 40 {
 | 
			
		||||
		} else if len(refName) == git.SHAFullLength {
 | 
			
		||||
			ctx.Repo.CommitID = refName
 | 
			
		||||
			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth"
 | 
			
		||||
@@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
 | 
			
		||||
	if statusPrefix == 4 || statusPrefix == 5 {
 | 
			
		||||
		log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Resp.WriteHeader(status)
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
 | 
			
		||||
	ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
	ctx.Resp.WriteHeader(status)
 | 
			
		||||
	if _, err := ctx.Resp.Write(bs); err != nil {
 | 
			
		||||
		log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -345,34 +346,61 @@ func (ctx *Context) RespHeader() http.Header {
 | 
			
		||||
	return ctx.Resp.Header()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ServeHeaderOptions struct {
 | 
			
		||||
	ContentType        string // defaults to "application/octet-stream"
 | 
			
		||||
	ContentTypeCharset string
 | 
			
		||||
	ContentLength      *int64
 | 
			
		||||
	Disposition        string // defaults to "attachment"
 | 
			
		||||
	Filename           string
 | 
			
		||||
	CacheDuration      time.Duration // defaults to 5 minutes
 | 
			
		||||
	LastModified       time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetServeHeaders sets necessary content serve headers
 | 
			
		||||
func (ctx *Context) SetServeHeaders(filename string) {
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Description", "File Transfer")
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename)
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
 | 
			
		||||
	ctx.Resp.Header().Set("Expires", "0")
 | 
			
		||||
	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
 | 
			
		||||
	ctx.Resp.Header().Set("Pragma", "public")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
 | 
			
		||||
	header := ctx.Resp.Header()
 | 
			
		||||
 | 
			
		||||
	contentType := typesniffer.ApplicationOctetStream
 | 
			
		||||
	if opts.ContentType != "" {
 | 
			
		||||
		if opts.ContentTypeCharset != "" {
 | 
			
		||||
			contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
 | 
			
		||||
		} else {
 | 
			
		||||
			contentType = opts.ContentType
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	header.Set("Content-Type", contentType)
 | 
			
		||||
	header.Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
 | 
			
		||||
	if opts.ContentLength != nil {
 | 
			
		||||
		header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Filename != "" {
 | 
			
		||||
		disposition := opts.Disposition
 | 
			
		||||
		if disposition == "" {
 | 
			
		||||
			disposition = "attachment"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
 | 
			
		||||
		header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
 | 
			
		||||
		header.Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	duration := opts.CacheDuration
 | 
			
		||||
	if duration == 0 {
 | 
			
		||||
		duration = 5 * time.Minute
 | 
			
		||||
	}
 | 
			
		||||
	httpcache.AddCacheControlToHeader(header, duration)
 | 
			
		||||
 | 
			
		||||
	if !opts.LastModified.IsZero() {
 | 
			
		||||
		header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeContent serves content to http request
 | 
			
		||||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
 | 
			
		||||
	ctx.SetServeHeaders(name)
 | 
			
		||||
	http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeFile serves given file to response.
 | 
			
		||||
func (ctx *Context) ServeFile(file string, names ...string) {
 | 
			
		||||
	var name string
 | 
			
		||||
	if len(names) > 0 {
 | 
			
		||||
		name = names[0]
 | 
			
		||||
	} else {
 | 
			
		||||
		name = path.Base(file)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.SetServeHeaders(name)
 | 
			
		||||
	http.ServeFile(ctx.Resp, ctx.Req, file)
 | 
			
		||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
	http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadStream returns the request body or the first form file
 | 
			
		||||
@@ -635,7 +663,13 @@ func getCsrfOpts() CsrfOptions {
 | 
			
		||||
// Auth converts auth.Auth as a middleware
 | 
			
		||||
func Auth(authMethod auth.Method) func(*Context) {
 | 
			
		||||
	return func(ctx *Context) {
 | 
			
		||||
		ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err)
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "Verify")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.Doer != nil {
 | 
			
		||||
			if ctx.Locale.Language() != ctx.Doer.Language {
 | 
			
		||||
				ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
 | 
			
		||||
 
 | 
			
		||||
@@ -816,7 +816,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
 | 
			
		||||
		}
 | 
			
		||||
		// For legacy and API support only full commit sha
 | 
			
		||||
		parts := strings.Split(path, "/")
 | 
			
		||||
		if len(parts) > 0 && len(parts[0]) == 40 {
 | 
			
		||||
		if len(parts) > 0 && len(parts[0]) == git.SHAFullLength {
 | 
			
		||||
			ctx.Repo.TreePath = strings.Join(parts[1:], "/")
 | 
			
		||||
			return parts[0]
 | 
			
		||||
		}
 | 
			
		||||
@@ -852,7 +852,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
 | 
			
		||||
		return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist)
 | 
			
		||||
	case RepoRefCommit:
 | 
			
		||||
		parts := strings.Split(path, "/")
 | 
			
		||||
		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= 40 {
 | 
			
		||||
		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength {
 | 
			
		||||
			ctx.Repo.TreePath = strings.Join(parts[1:], "/")
 | 
			
		||||
			return parts[0]
 | 
			
		||||
		}
 | 
			
		||||
@@ -961,7 +961,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
 | 
			
		||||
			} else if len(refName) >= 7 && len(refName) <= 40 {
 | 
			
		||||
			} else if len(refName) >= 7 && len(refName) <= git.SHAFullLength {
 | 
			
		||||
				ctx.Repo.IsViewCommit = true
 | 
			
		||||
				ctx.Repo.CommitID = refName
 | 
			
		||||
 | 
			
		||||
@@ -971,7 +971,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				// If short commit ID add canonical link header
 | 
			
		||||
				if len(refName) < 40 {
 | 
			
		||||
				if len(refName) < git.SHAFullLength {
 | 
			
		||||
					ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
 | 
			
		||||
						util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -110,12 +110,11 @@ func ToAPIIssueList(il issues_model.IssueList) []*api.Issue {
 | 
			
		||||
// ToTrackedTime converts TrackedTime to API format
 | 
			
		||||
func ToTrackedTime(t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
 | 
			
		||||
	apiT = &api.TrackedTime{
 | 
			
		||||
		ID:       t.ID,
 | 
			
		||||
		IssueID:  t.IssueID,
 | 
			
		||||
		UserID:   t.UserID,
 | 
			
		||||
		UserName: t.User.Name,
 | 
			
		||||
		Time:     t.Time,
 | 
			
		||||
		Created:  t.Created,
 | 
			
		||||
		ID:      t.ID,
 | 
			
		||||
		IssueID: t.IssueID,
 | 
			
		||||
		UserID:  t.UserID,
 | 
			
		||||
		Time:    t.Time,
 | 
			
		||||
		Created: t.Created,
 | 
			
		||||
	}
 | 
			
		||||
	if t.Issue != nil {
 | 
			
		||||
		apiT.Issue = ToAPIIssue(t.Issue)
 | 
			
		||||
 
 | 
			
		||||
@@ -205,6 +205,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 | 
			
		||||
		// find stopwatches without existing issue
 | 
			
		||||
		genericOrphanCheck("Orphaned Stopwatches without existing Issue",
 | 
			
		||||
			"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
 | 
			
		||||
		// find redirects without existing user.
 | 
			
		||||
		genericOrphanCheck("Orphaned Redirects without existing redirect user",
 | 
			
		||||
			"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for _, c := range consistencyChecks {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,9 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
	numReposUpdated := 0
 | 
			
		||||
	err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
 | 
			
		||||
		numRepos++
 | 
			
		||||
		runOpts := &git.RunOpts{Dir: repo.RepoPath()}
 | 
			
		||||
		_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
 | 
			
		||||
		_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(runOpts)
 | 
			
		||||
 | 
			
		||||
		head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts)
 | 
			
		||||
		head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
 | 
			
		||||
		// what we expect: default branch is valid, and HEAD points to it
 | 
			
		||||
		if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch {
 | 
			
		||||
@@ -49,7 +47,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// otherwise, let's try fixing HEAD
 | 
			
		||||
		err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", repo.DefaultBranch).Run(runOpts)
 | 
			
		||||
		err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", git.BranchPrefix+repo.DefaultBranch).Run(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err)
 | 
			
		||||
			return nil
 | 
			
		||||
@@ -65,7 +63,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
		logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated)
 | 
			
		||||
	} else {
 | 
			
		||||
		if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 {
 | 
			
		||||
			logger.Info("All %d repos have their HEADs in the correct state")
 | 
			
		||||
			logger.Info("All %d repos have their HEADs in the correct state", numRepos)
 | 
			
		||||
		} else {
 | 
			
		||||
			if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 {
 | 
			
		||||
				logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos)
 | 
			
		||||
 
 | 
			
		||||
@@ -202,8 +202,11 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		||||
	if opts == nil {
 | 
			
		||||
		opts = &RunOpts{}
 | 
			
		||||
	}
 | 
			
		||||
	if opts.Timeout <= 0 {
 | 
			
		||||
		opts.Timeout = defaultCommandExecutionTimeout
 | 
			
		||||
 | 
			
		||||
	// We must not change the provided options
 | 
			
		||||
	timeout := opts.Timeout
 | 
			
		||||
	if timeout <= 0 {
 | 
			
		||||
		timeout = defaultCommandExecutionTimeout
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(opts.Dir) == 0 {
 | 
			
		||||
@@ -238,7 +241,7 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		||||
	if opts.UseContextTimeout {
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc)
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc)
 | 
			
		||||
	}
 | 
			
		||||
	defer finished()
 | 
			
		||||
 | 
			
		||||
@@ -339,9 +342,20 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
 | 
			
		||||
	}
 | 
			
		||||
	stdoutBuf := &bytes.Buffer{}
 | 
			
		||||
	stderrBuf := &bytes.Buffer{}
 | 
			
		||||
	opts.Stdout = stdoutBuf
 | 
			
		||||
	opts.Stderr = stderrBuf
 | 
			
		||||
	err := c.Run(opts)
 | 
			
		||||
 | 
			
		||||
	// We must not change the provided options as it could break future calls - therefore make a copy.
 | 
			
		||||
	newOpts := &RunOpts{
 | 
			
		||||
		Env:               opts.Env,
 | 
			
		||||
		Timeout:           opts.Timeout,
 | 
			
		||||
		UseContextTimeout: opts.UseContextTimeout,
 | 
			
		||||
		Dir:               opts.Dir,
 | 
			
		||||
		Stdout:            stdoutBuf,
 | 
			
		||||
		Stderr:            stderrBuf,
 | 
			
		||||
		Stdin:             opts.Stdin,
 | 
			
		||||
		PipelineFunc:      opts.PipelineFunc,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := c.Run(newOpts)
 | 
			
		||||
	stderr = stderrBuf.Bytes()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)}
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ func (repo *Repository) IsReferenceExist(name string) bool {
 | 
			
		||||
 | 
			
		||||
// IsBranchExist returns true if given branch exists in current repository.
 | 
			
		||||
func (repo *Repository) IsBranchExist(name string) bool {
 | 
			
		||||
	if name == "" {
 | 
			
		||||
	if repo == nil || name == "" {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ func (repo *Repository) RemoveReference(name string) error {
 | 
			
		||||
 | 
			
		||||
// ConvertToSHA1 returns a Hash object from a potential ID string
 | 
			
		||||
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
 | 
			
		||||
	if len(commitID) == 40 {
 | 
			
		||||
	if len(commitID) == SHAFullLength {
 | 
			
		||||
		sha1, err := NewIDFromString(commitID)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return sha1, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co
 | 
			
		||||
 | 
			
		||||
// ConvertToSHA1 returns a Hash object from a potential ID string
 | 
			
		||||
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
 | 
			
		||||
	if len(commitID) == 40 && IsValidSHAPattern(commitID) {
 | 
			
		||||
	if len(commitID) == SHAFullLength && IsValidSHAPattern(commitID) {
 | 
			
		||||
		sha1, err := NewIDFromString(commitID)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return sha1, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// ReadTreeToIndex reads a treeish to the index
 | 
			
		||||
func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) error {
 | 
			
		||||
	if len(treeish) != 40 {
 | 
			
		||||
	if len(treeish) != SHAFullLength {
 | 
			
		||||
		res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(&RunOpts{Dir: repo.Path})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// IsTagExist returns true if given tag exists in the repository.
 | 
			
		||||
func (repo *Repository) IsTagExist(name string) bool {
 | 
			
		||||
	if name == "" {
 | 
			
		||||
	if repo == nil || name == "" {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ func (repo *Repository) getTree(id SHA1) (*Tree, error) {
 | 
			
		||||
 | 
			
		||||
// GetTree find the tree object in the repository.
 | 
			
		||||
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
 | 
			
		||||
	if len(idStr) != 40 {
 | 
			
		||||
	if len(idStr) != SHAFullLength {
 | 
			
		||||
		res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ func (repo *Repository) getTree(id SHA1) (*Tree, error) {
 | 
			
		||||
 | 
			
		||||
// GetTree find the tree object in the repository.
 | 
			
		||||
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
 | 
			
		||||
	if len(idStr) != 40 {
 | 
			
		||||
	if len(idStr) != SHAFullLength {
 | 
			
		||||
		res, err := repo.GetRefCommitID(idStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,9 @@ const EmptySHA = "0000000000000000000000000000000000000000"
 | 
			
		||||
// EmptyTreeSHA is the SHA of an empty tree
 | 
			
		||||
const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
 | 
			
		||||
 | 
			
		||||
// SHAFullLength is the full length of a git SHA
 | 
			
		||||
const SHAFullLength = 40
 | 
			
		||||
 | 
			
		||||
// SHAPattern can be used to determine if a string is an valid sha
 | 
			
		||||
var shaPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +54,7 @@ func MustIDFromString(s string) SHA1 {
 | 
			
		||||
func NewIDFromString(s string) (SHA1, error) {
 | 
			
		||||
	var id SHA1
 | 
			
		||||
	s = strings.TrimSpace(s)
 | 
			
		||||
	if len(s) != 40 {
 | 
			
		||||
	if len(s) != SHAFullLength {
 | 
			
		||||
		return id, fmt.Errorf("Length must be 40: %s", s)
 | 
			
		||||
	}
 | 
			
		||||
	b, err := hex.DecodeString(s)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/object"
 | 
			
		||||
@@ -30,7 +31,9 @@ type Signature = object.Signature
 | 
			
		||||
func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
 | 
			
		||||
	sig := new(Signature)
 | 
			
		||||
	emailStart := bytes.IndexByte(line, '<')
 | 
			
		||||
	sig.Name = string(line[:emailStart-1])
 | 
			
		||||
	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
 | 
			
		||||
		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
 | 
			
		||||
	}
 | 
			
		||||
	emailEnd := bytes.IndexByte(line, '>')
 | 
			
		||||
	sig.Email = string(line[emailStart+1 : emailEnd])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +52,9 @@ func newSignatureFromCommitline(line []byte) (sig *Signature, err error) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sig.Name = string(line[:emailStart-1])
 | 
			
		||||
	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
 | 
			
		||||
		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
 | 
			
		||||
	}
 | 
			
		||||
	sig.Email = string(line[emailStart+1 : emailEnd])
 | 
			
		||||
 | 
			
		||||
	hasTime := emailEnd+2 < len(line)
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,7 @@ func validateOptions(field *api.IssueFormField, idx int) error {
 | 
			
		||||
				return position.Errorf("should be a string")
 | 
			
		||||
			}
 | 
			
		||||
		case api.IssueFormFieldTypeCheckboxes:
 | 
			
		||||
			opt, ok := option.(map[interface{}]interface{})
 | 
			
		||||
			opt, ok := option.(map[string]interface{})
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return position.Errorf("should be a dictionary")
 | 
			
		||||
			}
 | 
			
		||||
@@ -351,7 +351,7 @@ func (o *valuedOption) Label() string {
 | 
			
		||||
			return label
 | 
			
		||||
		}
 | 
			
		||||
	case api.IssueFormFieldTypeCheckboxes:
 | 
			
		||||
		if vs, ok := o.data.(map[interface{}]interface{}); ok {
 | 
			
		||||
		if vs, ok := o.data.(map[string]interface{}); ok {
 | 
			
		||||
			if v, ok := vs["label"].(string); ok {
 | 
			
		||||
				return v
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,18 +6,21 @@ package template
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestValidate(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		content string
 | 
			
		||||
		wantErr string
 | 
			
		||||
		name     string
 | 
			
		||||
		filename string
 | 
			
		||||
		content  string
 | 
			
		||||
		want     *api.IssueTemplate
 | 
			
		||||
		wantErr  string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:    "miss name",
 | 
			
		||||
@@ -316,21 +319,9 @@ body:
 | 
			
		||||
`,
 | 
			
		||||
			wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			tmpl, err := unmarshal("test.yaml", []byte(tt.content))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := Validate(tmpl); (err == nil) != (tt.wantErr == "") || err != nil && err.Error() != tt.wantErr {
 | 
			
		||||
				t.Errorf("Validate() error = %v, wantErr %q", err, tt.wantErr)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("valid", func(t *testing.T) {
 | 
			
		||||
		content := `
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid",
 | 
			
		||||
			content: `
 | 
			
		||||
name: Name
 | 
			
		||||
title: Title
 | 
			
		||||
about: About
 | 
			
		||||
@@ -386,96 +377,227 @@ body:
 | 
			
		||||
          required: false
 | 
			
		||||
        - label: Option 3 of checkboxes
 | 
			
		||||
          required: true
 | 
			
		||||
`
 | 
			
		||||
		want := &api.IssueTemplate{
 | 
			
		||||
			Name:   "Name",
 | 
			
		||||
			Title:  "Title",
 | 
			
		||||
			About:  "About",
 | 
			
		||||
			Labels: []string{"label1", "label2"},
 | 
			
		||||
			Ref:    "Ref",
 | 
			
		||||
			Fields: []*api.IssueFormField{
 | 
			
		||||
				{
 | 
			
		||||
					Type: "markdown",
 | 
			
		||||
					ID:   "id1",
 | 
			
		||||
					Attributes: map[string]interface{}{
 | 
			
		||||
						"value": "Value of the markdown",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Type: "textarea",
 | 
			
		||||
					ID:   "id2",
 | 
			
		||||
					Attributes: map[string]interface{}{
 | 
			
		||||
						"label":       "Label of textarea",
 | 
			
		||||
						"description": "Description of textarea",
 | 
			
		||||
						"placeholder": "Placeholder of textarea",
 | 
			
		||||
						"value":       "Value of textarea",
 | 
			
		||||
						"render":      "bash",
 | 
			
		||||
					},
 | 
			
		||||
					Validations: map[string]interface{}{
 | 
			
		||||
						"required": true,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Type: "input",
 | 
			
		||||
					ID:   "id3",
 | 
			
		||||
					Attributes: map[string]interface{}{
 | 
			
		||||
						"label":       "Label of input",
 | 
			
		||||
						"description": "Description of input",
 | 
			
		||||
						"placeholder": "Placeholder of input",
 | 
			
		||||
						"value":       "Value of input",
 | 
			
		||||
					},
 | 
			
		||||
					Validations: map[string]interface{}{
 | 
			
		||||
						"required":  true,
 | 
			
		||||
						"is_number": true,
 | 
			
		||||
						"regex":     "[a-zA-Z0-9]+",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Type: "dropdown",
 | 
			
		||||
					ID:   "id4",
 | 
			
		||||
					Attributes: map[string]interface{}{
 | 
			
		||||
						"label":       "Label of dropdown",
 | 
			
		||||
						"description": "Description of dropdown",
 | 
			
		||||
						"multiple":    true,
 | 
			
		||||
						"options": []interface{}{
 | 
			
		||||
							"Option 1 of dropdown",
 | 
			
		||||
							"Option 2 of dropdown",
 | 
			
		||||
							"Option 3 of dropdown",
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
				Title:  "Title",
 | 
			
		||||
				About:  "About",
 | 
			
		||||
				Labels: []string{"label1", "label2"},
 | 
			
		||||
				Ref:    "Ref",
 | 
			
		||||
				Fields: []*api.IssueFormField{
 | 
			
		||||
					{
 | 
			
		||||
						Type: "markdown",
 | 
			
		||||
						ID:   "id1",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					Validations: map[string]interface{}{
 | 
			
		||||
						"required": true,
 | 
			
		||||
					{
 | 
			
		||||
						Type: "textarea",
 | 
			
		||||
						ID:   "id2",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"label":       "Label of textarea",
 | 
			
		||||
							"description": "Description of textarea",
 | 
			
		||||
							"placeholder": "Placeholder of textarea",
 | 
			
		||||
							"value":       "Value of textarea",
 | 
			
		||||
							"render":      "bash",
 | 
			
		||||
						},
 | 
			
		||||
						Validations: map[string]interface{}{
 | 
			
		||||
							"required": true,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Type: "checkboxes",
 | 
			
		||||
					ID:   "id5",
 | 
			
		||||
					Attributes: map[string]interface{}{
 | 
			
		||||
						"label":       "Label of checkboxes",
 | 
			
		||||
						"description": "Description of checkboxes",
 | 
			
		||||
						"options": []interface{}{
 | 
			
		||||
							map[interface{}]interface{}{"label": "Option 1 of checkboxes", "required": true},
 | 
			
		||||
							map[interface{}]interface{}{"label": "Option 2 of checkboxes", "required": false},
 | 
			
		||||
							map[interface{}]interface{}{"label": "Option 3 of checkboxes", "required": true},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "input",
 | 
			
		||||
						ID:   "id3",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"label":       "Label of input",
 | 
			
		||||
							"description": "Description of input",
 | 
			
		||||
							"placeholder": "Placeholder of input",
 | 
			
		||||
							"value":       "Value of input",
 | 
			
		||||
						},
 | 
			
		||||
						Validations: map[string]interface{}{
 | 
			
		||||
							"required":  true,
 | 
			
		||||
							"is_number": true,
 | 
			
		||||
							"regex":     "[a-zA-Z0-9]+",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "dropdown",
 | 
			
		||||
						ID:   "id4",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"label":       "Label of dropdown",
 | 
			
		||||
							"description": "Description of dropdown",
 | 
			
		||||
							"multiple":    true,
 | 
			
		||||
							"options": []interface{}{
 | 
			
		||||
								"Option 1 of dropdown",
 | 
			
		||||
								"Option 2 of dropdown",
 | 
			
		||||
								"Option 3 of dropdown",
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
						Validations: map[string]interface{}{
 | 
			
		||||
							"required": true,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Type: "checkboxes",
 | 
			
		||||
						ID:   "id5",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"label":       "Label of checkboxes",
 | 
			
		||||
							"description": "Description of checkboxes",
 | 
			
		||||
							"options": []interface{}{
 | 
			
		||||
								map[string]interface{}{"label": "Option 1 of checkboxes", "required": true},
 | 
			
		||||
								map[string]interface{}{"label": "Option 2 of checkboxes", "required": false},
 | 
			
		||||
								map[string]interface{}{"label": "Option 3 of checkboxes", "required": true},
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
			},
 | 
			
		||||
			FileName: "test.yaml",
 | 
			
		||||
		}
 | 
			
		||||
		got, err := unmarshal("test.yaml", []byte(content))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if err := Validate(got); err != nil {
 | 
			
		||||
			t.Errorf("Validate() error = %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		if !reflect.DeepEqual(want, got) {
 | 
			
		||||
			jsonWant, _ := json.Marshal(want)
 | 
			
		||||
			jsonGot, _ := json.Marshal(got)
 | 
			
		||||
			t.Errorf("want:\n%s\ngot:\n%s", jsonWant, jsonGot)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "single label",
 | 
			
		||||
			content: `
 | 
			
		||||
name: Name
 | 
			
		||||
title: Title
 | 
			
		||||
about: About
 | 
			
		||||
labels: label1
 | 
			
		||||
ref: Ref
 | 
			
		||||
body:
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    id: id1
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
				Title:  "Title",
 | 
			
		||||
				About:  "About",
 | 
			
		||||
				Labels: []string{"label1"},
 | 
			
		||||
				Ref:    "Ref",
 | 
			
		||||
				Fields: []*api.IssueFormField{
 | 
			
		||||
					{
 | 
			
		||||
						Type: "markdown",
 | 
			
		||||
						ID:   "id1",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "comma-delimited labels",
 | 
			
		||||
			content: `
 | 
			
		||||
name: Name
 | 
			
		||||
title: Title
 | 
			
		||||
about: About
 | 
			
		||||
labels: label1,label2,,label3 ,,
 | 
			
		||||
ref: Ref
 | 
			
		||||
body:
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    id: id1
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
				Title:  "Title",
 | 
			
		||||
				About:  "About",
 | 
			
		||||
				Labels: []string{"label1", "label2", "label3"},
 | 
			
		||||
				Ref:    "Ref",
 | 
			
		||||
				Fields: []*api.IssueFormField{
 | 
			
		||||
					{
 | 
			
		||||
						Type: "markdown",
 | 
			
		||||
						ID:   "id1",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty string as labels",
 | 
			
		||||
			content: `
 | 
			
		||||
name: Name
 | 
			
		||||
title: Title
 | 
			
		||||
about: About
 | 
			
		||||
labels: ''
 | 
			
		||||
ref: Ref
 | 
			
		||||
body:
 | 
			
		||||
  - type: markdown
 | 
			
		||||
    id: id1
 | 
			
		||||
    attributes:
 | 
			
		||||
      value: Value of the markdown
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:   "Name",
 | 
			
		||||
				Title:  "Title",
 | 
			
		||||
				About:  "About",
 | 
			
		||||
				Labels: nil,
 | 
			
		||||
				Ref:    "Ref",
 | 
			
		||||
				Fields: []*api.IssueFormField{
 | 
			
		||||
					{
 | 
			
		||||
						Type: "markdown",
 | 
			
		||||
						ID:   "id1",
 | 
			
		||||
						Attributes: map[string]interface{}{
 | 
			
		||||
							"value": "Value of the markdown",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				FileName: "test.yaml",
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "comma delimited labels in markdown",
 | 
			
		||||
			filename: "test.md",
 | 
			
		||||
			content: `---
 | 
			
		||||
name: Name
 | 
			
		||||
title: Title
 | 
			
		||||
about: About
 | 
			
		||||
labels: label1,label2,,label3 ,,
 | 
			
		||||
ref: Ref
 | 
			
		||||
---
 | 
			
		||||
Content
 | 
			
		||||
`,
 | 
			
		||||
			want: &api.IssueTemplate{
 | 
			
		||||
				Name:     "Name",
 | 
			
		||||
				Title:    "Title",
 | 
			
		||||
				About:    "About",
 | 
			
		||||
				Labels:   []string{"label1", "label2", "label3"},
 | 
			
		||||
				Ref:      "Ref",
 | 
			
		||||
				Fields:   nil,
 | 
			
		||||
				Content:  "Content\n",
 | 
			
		||||
				FileName: "test.md",
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			filename := "test.yaml"
 | 
			
		||||
			if tt.filename != "" {
 | 
			
		||||
				filename = tt.filename
 | 
			
		||||
			}
 | 
			
		||||
			tmpl, err := unmarshal(filename, []byte(tt.content))
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				require.EqualError(t, Validate(tmpl), tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				require.NoError(t, Validate(tmpl))
 | 
			
		||||
				want, _ := json.Marshal(tt.want)
 | 
			
		||||
				got, _ := json.Marshal(tmpl)
 | 
			
		||||
				require.JSONEq(t, string(want), string(got))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRenderToMarkdown(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,15 +7,16 @@ package template
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/yaml.v2"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CouldBe indicates a file with the filename could be a template,
 | 
			
		||||
@@ -43,7 +44,7 @@ func Unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
 | 
			
		||||
 | 
			
		||||
// UnmarshalFromEntry parses out a valid template from the blob in entry
 | 
			
		||||
func UnmarshalFromEntry(entry *git.TreeEntry, dir string) (*api.IssueTemplate, error) {
 | 
			
		||||
	return unmarshalFromEntry(entry, filepath.Join(dir, entry.Name()))
 | 
			
		||||
	return unmarshalFromEntry(entry, path.Join(dir, entry.Name())) // Filepaths in Git are ALWAYS '/' separated do not use filepath here
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnmarshalFromCommit parses out a valid template from the commit
 | 
			
		||||
@@ -95,14 +96,27 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
 | 
			
		||||
	}{}
 | 
			
		||||
 | 
			
		||||
	if typ := it.Type(); typ == api.IssueTemplateTypeMarkdown {
 | 
			
		||||
		templateBody, err := markdown.ExtractMetadata(string(content), it)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		it.Content = templateBody
 | 
			
		||||
		if it.About == "" {
 | 
			
		||||
			if _, err := markdown.ExtractMetadata(string(content), compatibleTemplate); err == nil && compatibleTemplate.About != "" {
 | 
			
		||||
				it.About = compatibleTemplate.About
 | 
			
		||||
		if templateBody, err := markdown.ExtractMetadata(string(content), it); err != nil {
 | 
			
		||||
			// The only thing we know here is that we can't extract metadata from the content,
 | 
			
		||||
			// it's hard to tell if metadata doesn't exist or metadata isn't valid.
 | 
			
		||||
			// There's an example template:
 | 
			
		||||
			//
 | 
			
		||||
			//    ---
 | 
			
		||||
			//    # Title
 | 
			
		||||
			//    ---
 | 
			
		||||
			//    Content
 | 
			
		||||
			//
 | 
			
		||||
			// It could be a valid markdown with two horizontal lines, or an invalid markdown with wrong metadata.
 | 
			
		||||
 | 
			
		||||
			it.Content = string(content)
 | 
			
		||||
			it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath!
 | 
			
		||||
			it.About, _ = util.SplitStringAtByteN(it.Content, 80)
 | 
			
		||||
		} else {
 | 
			
		||||
			it.Content = templateBody
 | 
			
		||||
			if it.About == "" {
 | 
			
		||||
				if _, err := markdown.ExtractMetadata(string(content), compatibleTemplate); err == nil && compatibleTemplate.About != "" {
 | 
			
		||||
					it.About = compatibleTemplate.About
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if typ == api.IssueTemplateTypeYaml {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,82 +9,86 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func validateMetadata(it structs.IssueTemplate) bool {
 | 
			
		||||
	/*
 | 
			
		||||
		A legacy to keep the unit tests working.
 | 
			
		||||
		Copied from the method "func (it IssueTemplate) Valid() bool", the original method has been removed.
 | 
			
		||||
		Because it becomes quite complicated to validate an issue template which is support yaml form now.
 | 
			
		||||
		The new way to validate an issue template is to call the Validate in modules/issue/template,
 | 
			
		||||
	*/
 | 
			
		||||
/*
 | 
			
		||||
IssueTemplate is a legacy to keep the unit tests working.
 | 
			
		||||
Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
 | 
			
		||||
*/
 | 
			
		||||
type IssueTemplate struct {
 | 
			
		||||
	Name   string   `json:"name" yaml:"name"`
 | 
			
		||||
	Title  string   `json:"title" yaml:"title"`
 | 
			
		||||
	About  string   `json:"about" yaml:"about"`
 | 
			
		||||
	Labels []string `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref    string   `json:"ref" yaml:"ref"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (it *IssueTemplate) Valid() bool {
 | 
			
		||||
	return strings.TrimSpace(it.Name) != "" && strings.TrimSpace(it.About) != ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExtractMetadata(t *testing.T) {
 | 
			
		||||
	t.Run("ValidFrontAndBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, bodyTest, body)
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoFirstSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoLastSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "", body)
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExtractMetadataBytes(t *testing.T) {
 | 
			
		||||
	t.Run("ValidFrontAndBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, bodyTest, string(body))
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoFirstSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoLastSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "", string(body))
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +101,7 @@ labels:
 | 
			
		||||
  - bug
 | 
			
		||||
  - "test label"`
 | 
			
		||||
	bodyTest = "This is the body"
 | 
			
		||||
	metaTest = structs.IssueTemplate{
 | 
			
		||||
	metaTest = IssueTemplate{
 | 
			
		||||
		Name:   "Test",
 | 
			
		||||
		About:  "A Test",
 | 
			
		||||
		Title:  "Test Title",
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,13 @@ func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
 | 
			
		||||
	return s.store.Open(KeyToRelativePath(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME: Workaround to be removed in v1.20
 | 
			
		||||
// https://github.com/go-gitea/gitea/issues/19586
 | 
			
		||||
func (s *ContentStore) Has(key BlobHash256Key) error {
 | 
			
		||||
	_, err := s.store.Stat(KeyToRelativePath(key))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save stores a package blob
 | 
			
		||||
func (s *ContentStore) Save(key BlobHash256Key, r io.Reader, size int64) error {
 | 
			
		||||
	_, err := s.store.Save(KeyToRelativePath(key), r, size)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,10 @@ package nuget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/zip"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
@@ -183,7 +185,23 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
 | 
			
		||||
	return &Package{
 | 
			
		||||
		PackageType: packageType,
 | 
			
		||||
		ID:          p.Metadata.ID,
 | 
			
		||||
		Version:     v.String(),
 | 
			
		||||
		Version:     toNormalizedVersion(v),
 | 
			
		||||
		Metadata:    m,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers
 | 
			
		||||
// https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121
 | 
			
		||||
func toNormalizedVersion(v *version.Version) string {
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	segments := v.Segments64()
 | 
			
		||||
	fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
 | 
			
		||||
	if len(segments) > 3 && segments[3] > 0 {
 | 
			
		||||
		fmt.Fprintf(&buf, ".%d", segments[3])
 | 
			
		||||
	}
 | 
			
		||||
	pre := v.Prerelease()
 | 
			
		||||
	if pre != "" {
 | 
			
		||||
		fmt.Fprint(&buf, "-", pre)
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -147,6 +147,19 @@ func TestParseNuspecMetaData(t *testing.T) {
 | 
			
		||||
		assert.Len(t, deps, 1)
 | 
			
		||||
		assert.Equal(t, dependencyID, deps[0].ID)
 | 
			
		||||
		assert.Equal(t, dependencyVersion, deps[0].Version)
 | 
			
		||||
 | 
			
		||||
		t.Run("NormalizedVersion", func(t *testing.T) {
 | 
			
		||||
			np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 | 
			
		||||
  <metadata>
 | 
			
		||||
	<id>test</id>
 | 
			
		||||
	<version>1.04.5.2.5-rc.1+metadata</version>
 | 
			
		||||
  </metadata>
 | 
			
		||||
</package>`))
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.NotNil(t, np)
 | 
			
		||||
			assert.Equal(t, "1.4.5.2-rc.1", np.Version)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Symbols Package", func(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,32 +18,33 @@ import (
 | 
			
		||||
// Mailer represents mail service.
 | 
			
		||||
type Mailer struct {
 | 
			
		||||
	// Mailer
 | 
			
		||||
	Name                 string
 | 
			
		||||
	From                 string
 | 
			
		||||
	EnvelopeFrom         string
 | 
			
		||||
	OverrideEnvelopeFrom bool `ini:"-"`
 | 
			
		||||
	FromName             string
 | 
			
		||||
	FromEmail            string
 | 
			
		||||
	SendAsPlainText      bool
 | 
			
		||||
	SubjectPrefix        string
 | 
			
		||||
	Name                 string `ini:"NAME"`
 | 
			
		||||
	From                 string `ini:"FROM"`
 | 
			
		||||
	EnvelopeFrom         string `ini:"ENVELOPE_FROM"`
 | 
			
		||||
	OverrideEnvelopeFrom bool   `ini:"-"`
 | 
			
		||||
	FromName             string `ini:"-"`
 | 
			
		||||
	FromEmail            string `ini:"-"`
 | 
			
		||||
	SendAsPlainText      bool   `ini:"SEND_AS_PLAIN_TEXT"`
 | 
			
		||||
	SubjectPrefix        string `ini:"SUBJECT_PREFIX"`
 | 
			
		||||
 | 
			
		||||
	// SMTP sender
 | 
			
		||||
	Protocol             string
 | 
			
		||||
	SMTPAddr             string
 | 
			
		||||
	SMTPPort             string
 | 
			
		||||
	User, Passwd         string
 | 
			
		||||
	EnableHelo           bool
 | 
			
		||||
	HeloHostname         string
 | 
			
		||||
	ForceTrustServerCert bool
 | 
			
		||||
	UseClientCert        bool
 | 
			
		||||
	ClientCertFile       string
 | 
			
		||||
	ClientKeyFile        string
 | 
			
		||||
	Protocol             string `ini:"PROTOCOL"`
 | 
			
		||||
	SMTPAddr             string `ini:"SMTP_ADDR"`
 | 
			
		||||
	SMTPPort             string `ini:"SMTP_PORT"`
 | 
			
		||||
	User                 string `ini:"USER"`
 | 
			
		||||
	Passwd               string `ini:"PASSWD"`
 | 
			
		||||
	EnableHelo           bool   `ini:"ENABLE_HELO"`
 | 
			
		||||
	HeloHostname         string `ini:"HELO_HOSTNAME"`
 | 
			
		||||
	ForceTrustServerCert bool   `ini:"FORCE_TRUST_SERVER_CERT"`
 | 
			
		||||
	UseClientCert        bool   `ini:"USE_CLIENT_CERT"`
 | 
			
		||||
	ClientCertFile       string `ini:"CLIENT_CERT_FILE"`
 | 
			
		||||
	ClientKeyFile        string `ini:"CLIENT_KEY_FILE"`
 | 
			
		||||
 | 
			
		||||
	// Sendmail sender
 | 
			
		||||
	SendmailPath        string
 | 
			
		||||
	SendmailArgs        []string
 | 
			
		||||
	SendmailTimeout     time.Duration
 | 
			
		||||
	SendmailConvertCRLF bool
 | 
			
		||||
	SendmailPath        string        `ini:"SENDMAIL_PATH"`
 | 
			
		||||
	SendmailArgs        []string      `ini:"-"`
 | 
			
		||||
	SendmailTimeout     time.Duration `ini:"SENDMAIL_TIMEOUT"`
 | 
			
		||||
	SendmailConvertCRLF bool          `ini:"SENDMAIL_CONVERT_CRLF"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MailService the global mailer
 | 
			
		||||
@@ -56,35 +57,12 @@ func newMailService() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MailService = &Mailer{
 | 
			
		||||
		Name:            sec.Key("NAME").MustString(AppName),
 | 
			
		||||
		SendAsPlainText: sec.Key("SEND_AS_PLAIN_TEXT").MustBool(false),
 | 
			
		||||
 | 
			
		||||
		Protocol:             sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy"}),
 | 
			
		||||
		SMTPAddr:             sec.Key("SMTP_ADDR").String(),
 | 
			
		||||
		SMTPPort:             sec.Key("SMTP_PORT").String(),
 | 
			
		||||
		User:                 sec.Key("USER").String(),
 | 
			
		||||
		Passwd:               sec.Key("PASSWD").String(),
 | 
			
		||||
		EnableHelo:           sec.Key("ENABLE_HELO").MustBool(true),
 | 
			
		||||
		HeloHostname:         sec.Key("HELO_HOSTNAME").String(),
 | 
			
		||||
		ForceTrustServerCert: sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false),
 | 
			
		||||
		UseClientCert:        sec.Key("USE_CLIENT_CERT").MustBool(false),
 | 
			
		||||
		ClientCertFile:       sec.Key("CLIENT_CERT_FILE").String(),
 | 
			
		||||
		ClientKeyFile:        sec.Key("CLIENT_KEY_FILE").String(),
 | 
			
		||||
		SubjectPrefix:        sec.Key("SUBJECT_PREFIX").MustString(""),
 | 
			
		||||
 | 
			
		||||
		SendmailPath:        sec.Key("SENDMAIL_PATH").MustString("sendmail"),
 | 
			
		||||
		SendmailTimeout:     sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
 | 
			
		||||
		SendmailConvertCRLF: sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true),
 | 
			
		||||
	}
 | 
			
		||||
	MailService.From = sec.Key("FROM").MustString(MailService.User)
 | 
			
		||||
	MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("")
 | 
			
		||||
 | 
			
		||||
	// Handle Deprecations and map on to new configuration
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "MAILER_TYPE", "mailer", "PROTOCOL")
 | 
			
		||||
	if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
 | 
			
		||||
		if sec.Key("MAILER_TYPE").String() == "sendmail" {
 | 
			
		||||
			MailService.Protocol = "sendmail"
 | 
			
		||||
			sec.Key("PROTOCOL").MustString("sendmail")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -96,31 +74,91 @@ func newMailService() {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err)
 | 
			
		||||
		}
 | 
			
		||||
		MailService.SMTPAddr = addr
 | 
			
		||||
		MailService.SMTPPort = port
 | 
			
		||||
		sec.Key("SMTP_ADDR").MustString(addr)
 | 
			
		||||
		sec.Key("SMTP_PORT").MustString(port)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL")
 | 
			
		||||
	if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
 | 
			
		||||
		if sec.Key("IS_TLS_ENABLED").MustBool() {
 | 
			
		||||
			MailService.Protocol = "smtps"
 | 
			
		||||
			sec.Key("PROTOCOL").MustString("smtps")
 | 
			
		||||
		} else {
 | 
			
		||||
			MailService.Protocol = "smtp+startls"
 | 
			
		||||
			sec.Key("PROTOCOL").MustString("smtp+starttls")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO")
 | 
			
		||||
	if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
 | 
			
		||||
		sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT")
 | 
			
		||||
	if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
 | 
			
		||||
		sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT")
 | 
			
		||||
	if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
 | 
			
		||||
		sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE")
 | 
			
		||||
	if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
 | 
			
		||||
		sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE")
 | 
			
		||||
	if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
 | 
			
		||||
		sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT")
 | 
			
		||||
	if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
 | 
			
		||||
		sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
 | 
			
		||||
		log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0")
 | 
			
		||||
		sec.Key("PROTOCOL").SetValue("smtp+starttls")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set default values & validate
 | 
			
		||||
	sec.Key("NAME").MustString(AppName)
 | 
			
		||||
	sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"})
 | 
			
		||||
	sec.Key("ENABLE_HELO").MustBool(true)
 | 
			
		||||
	sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false)
 | 
			
		||||
	sec.Key("USE_CLIENT_CERT").MustBool(false)
 | 
			
		||||
	sec.Key("SENDMAIL_PATH").MustString("sendmail")
 | 
			
		||||
	sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
 | 
			
		||||
	sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
 | 
			
		||||
	sec.Key("FROM").MustString(sec.Key("USER").String())
 | 
			
		||||
 | 
			
		||||
	// Now map the values on to the MailService
 | 
			
		||||
	MailService = &Mailer{}
 | 
			
		||||
	if err := sec.MapTo(MailService); err != nil {
 | 
			
		||||
		log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Infer SMTPPort if not set
 | 
			
		||||
	if MailService.SMTPPort == "" {
 | 
			
		||||
		switch MailService.Protocol {
 | 
			
		||||
		case "smtp":
 | 
			
		||||
			MailService.SMTPPort = "25"
 | 
			
		||||
		case "smtps":
 | 
			
		||||
			MailService.SMTPPort = "465"
 | 
			
		||||
		case "smtp+startls":
 | 
			
		||||
		case "smtp+starttls":
 | 
			
		||||
			MailService.SMTPPort = "587"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Infer Protocol
 | 
			
		||||
	if MailService.Protocol == "" {
 | 
			
		||||
		if strings.ContainsAny(MailService.SMTPAddr, "/\\") {
 | 
			
		||||
			MailService.Protocol = "smtp+unix"
 | 
			
		||||
@@ -131,7 +169,7 @@ func newMailService() {
 | 
			
		||||
			case "465":
 | 
			
		||||
				MailService.Protocol = "smtps"
 | 
			
		||||
			case "587":
 | 
			
		||||
				MailService.Protocol = "smtp+startls"
 | 
			
		||||
				MailService.Protocol = "smtp+starttls"
 | 
			
		||||
			default:
 | 
			
		||||
				log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort)
 | 
			
		||||
				MailService.Protocol = "smtps"
 | 
			
		||||
@@ -151,42 +189,6 @@ func newMailService() {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO")
 | 
			
		||||
	if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
 | 
			
		||||
		MailService.EnableHelo = !sec.Key("DISABLE_HELO").MustBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT")
 | 
			
		||||
	if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
 | 
			
		||||
		MailService.ForceTrustServerCert = sec.Key("SKIP_VERIFY").MustBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT")
 | 
			
		||||
	if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
 | 
			
		||||
		MailService.UseClientCert = sec.Key("USE_CLIENT_CERT").MustBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE")
 | 
			
		||||
	if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
 | 
			
		||||
		MailService.ClientCertFile = sec.Key("CERT_FILE").String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE")
 | 
			
		||||
	if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
 | 
			
		||||
		MailService.ClientKeyFile = sec.Key("KEY_FILE").String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: DEPRECATED to be removed in v1.19.0
 | 
			
		||||
	deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT")
 | 
			
		||||
	if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
 | 
			
		||||
		MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if MailService.From != "" {
 | 
			
		||||
		parsed, err := mail.ParseAddress(MailService.From)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,7 @@ var (
 | 
			
		||||
			DefaultMergeMessageOfficialApproversOnly bool
 | 
			
		||||
			PopulateSquashCommentWithCommitMessages  bool
 | 
			
		||||
			AddCoCommitterTrailers                   bool
 | 
			
		||||
			TestConflictingPatchesWithGitApply       bool
 | 
			
		||||
		} `ini:"repository.pull-request"`
 | 
			
		||||
 | 
			
		||||
		// Issue Setting
 | 
			
		||||
@@ -205,6 +206,7 @@ var (
 | 
			
		||||
			DefaultMergeMessageOfficialApproversOnly bool
 | 
			
		||||
			PopulateSquashCommentWithCommitMessages  bool
 | 
			
		||||
			AddCoCommitterTrailers                   bool
 | 
			
		||||
			TestConflictingPatchesWithGitApply       bool
 | 
			
		||||
		}{
 | 
			
		||||
			WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
 | 
			
		||||
			// Same as GitHub. See
 | 
			
		||||
@@ -219,6 +221,7 @@ var (
 | 
			
		||||
			DefaultMergeMessageOfficialApproversOnly: true,
 | 
			
		||||
			PopulateSquashCommentWithCommitMessages:  false,
 | 
			
		||||
			AddCoCommitterTrailers:                   true,
 | 
			
		||||
			TestConflictingPatchesWithGitApply:       true,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// Issue settings
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/generate"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/user"
 | 
			
		||||
@@ -463,6 +464,13 @@ func getAppPath() (string, error) {
 | 
			
		||||
		appPath, err = exec.LookPath(os.Args[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// FIXME: Once we switch to go 1.19 use !errors.Is(err, exec.ErrDot)
 | 
			
		||||
		if !strings.Contains(err.Error(), "cannot run executable found relative to current directory") {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		appPath, err = filepath.Abs(os.Args[0])
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
@@ -602,7 +610,7 @@ func LoadForTest(extraConfigs ...string) {
 | 
			
		||||
 | 
			
		||||
func deprecatedSetting(oldSection, oldKey, newSection, newKey string) {
 | 
			
		||||
	if Cfg.Section(oldSection).HasKey(oldKey) {
 | 
			
		||||
		log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.18.0", oldSection, oldKey, newSection, newKey)
 | 
			
		||||
		log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.19.0", oldSection, oldKey, newSection, newKey)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -962,6 +970,11 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 | 
			
		||||
	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
 | 
			
		||||
 | 
			
		||||
	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
 | 
			
		||||
	if InstallLock && InternalToken == "" {
 | 
			
		||||
		// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
 | 
			
		||||
		// some users do cluster deployment, they still depend on this auto-generating behavior.
 | 
			
		||||
		generateSaveInternalToken()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
 | 
			
		||||
	if len(cfgdata) == 0 {
 | 
			
		||||
@@ -1026,7 +1039,10 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 | 
			
		||||
	// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
 | 
			
		||||
	// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
 | 
			
		||||
	unsafeAllowRunAsRoot := Cfg.Section("").Key("I_AM_BEING_UNSAFE_RUNNING_AS_ROOT").MustBool(false)
 | 
			
		||||
	RunMode = Cfg.Section("").Key("RUN_MODE").MustString("prod")
 | 
			
		||||
	RunMode = os.Getenv("GITEA_RUN_MODE")
 | 
			
		||||
	if RunMode == "" {
 | 
			
		||||
		RunMode = Cfg.Section("").Key("RUN_MODE").MustString("prod")
 | 
			
		||||
	}
 | 
			
		||||
	IsProd = strings.EqualFold(RunMode, "prod")
 | 
			
		||||
	// Does not check run user when the install lock is off.
 | 
			
		||||
	if InstallLock {
 | 
			
		||||
@@ -1150,6 +1166,8 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
 | 
			
		||||
	return authorizedPrincipalsAllow, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
 | 
			
		||||
// If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear.
 | 
			
		||||
func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string {
 | 
			
		||||
	// don't allow setting both URI and verbatim string
 | 
			
		||||
	uri := sec.Key(uriKey).String()
 | 
			
		||||
@@ -1173,7 +1191,15 @@ func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err)
 | 
			
		||||
		}
 | 
			
		||||
		return strings.TrimSpace(string(buf))
 | 
			
		||||
		val := strings.TrimSpace(string(buf))
 | 
			
		||||
		if val == "" {
 | 
			
		||||
			// The file shouldn't be empty, otherwise we can not know whether the user has ever set the KEY or KEY_URI
 | 
			
		||||
			// For example: if INTERNAL_TOKEN_URI=file:///empty-file,
 | 
			
		||||
			// Then if the token is re-generated during installation and saved to INTERNAL_TOKEN
 | 
			
		||||
			// Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't)
 | 
			
		||||
			log.Fatal("Failed to read %s (%s): the file is empty", uriKey, tempURI.RequestURI())
 | 
			
		||||
		}
 | 
			
		||||
		return val
 | 
			
		||||
 | 
			
		||||
	// only file URIs are allowed
 | 
			
		||||
	default:
 | 
			
		||||
@@ -1182,6 +1208,19 @@ func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// generateSaveInternalToken generates and saves the internal token to app.ini
 | 
			
		||||
func generateSaveInternalToken() {
 | 
			
		||||
	token, err := generate.NewInternalToken()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error generate internal token: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	InternalToken = token
 | 
			
		||||
	CreateOrAppendToCustomConf("security.INTERNAL_TOKEN", func(cfg *ini.File) {
 | 
			
		||||
		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
 | 
			
		||||
func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string {
 | 
			
		||||
	parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/"))
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,8 @@ func (l *LocalStorage) Save(path string, r io.Reader, size int64) (int64, error)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	// Golang's tmp file (os.CreateTemp) always have 0o600 mode, so we need to change the file to follow the umask (as what Create/MkDir does)
 | 
			
		||||
	if err := util.ApplyUmask(p, os.ModePerm); err != nil {
 | 
			
		||||
	// but we don't want to make these files executable - so ensure that we mask out the executable bits
 | 
			
		||||
	if err := util.ApplyUmask(p, os.ModePerm&0o666); err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,12 @@
 | 
			
		||||
package structs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StateType issue state type
 | 
			
		||||
@@ -143,14 +147,47 @@ type IssueFormField struct {
 | 
			
		||||
// IssueTemplate represents an issue template for a repository
 | 
			
		||||
// swagger:model
 | 
			
		||||
type IssueTemplate struct {
 | 
			
		||||
	Name     string            `json:"name" yaml:"name"`
 | 
			
		||||
	Title    string            `json:"title" yaml:"title"`
 | 
			
		||||
	About    string            `json:"about" yaml:"about"` // Using "description" in a template file is compatible
 | 
			
		||||
	Labels   []string          `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref      string            `json:"ref" yaml:"ref"`
 | 
			
		||||
	Content  string            `json:"content" yaml:"-"`
 | 
			
		||||
	Fields   []*IssueFormField `json:"body" yaml:"body"`
 | 
			
		||||
	FileName string            `json:"file_name" yaml:"-"`
 | 
			
		||||
	Name     string              `json:"name" yaml:"name"`
 | 
			
		||||
	Title    string              `json:"title" yaml:"title"`
 | 
			
		||||
	About    string              `json:"about" yaml:"about"` // Using "description" in a template file is compatible
 | 
			
		||||
	Labels   IssueTemplateLabels `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref      string              `json:"ref" yaml:"ref"`
 | 
			
		||||
	Content  string              `json:"content" yaml:"-"`
 | 
			
		||||
	Fields   []*IssueFormField   `json:"body" yaml:"body"`
 | 
			
		||||
	FileName string              `json:"file_name" yaml:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type IssueTemplateLabels []string
 | 
			
		||||
 | 
			
		||||
func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
 | 
			
		||||
	var labels []string
 | 
			
		||||
	if value.IsZero() {
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	switch value.Kind {
 | 
			
		||||
	case yaml.ScalarNode:
 | 
			
		||||
		str := ""
 | 
			
		||||
		err := value.Decode(&str)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		for _, v := range strings.Split(str, ",") {
 | 
			
		||||
			if v = strings.TrimSpace(v); v == "" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			labels = append(labels, v)
 | 
			
		||||
		}
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	case yaml.SequenceNode:
 | 
			
		||||
		if err := value.Decode(&labels); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplateType defines issue template type
 | 
			
		||||
@@ -163,14 +200,14 @@ const (
 | 
			
		||||
 | 
			
		||||
// Type returns the type of IssueTemplate, can be "md", "yaml" or empty for known
 | 
			
		||||
func (it IssueTemplate) Type() IssueTemplateType {
 | 
			
		||||
	if it.Name == "config.yaml" || it.Name == "config.yml" {
 | 
			
		||||
	if base := path.Base(it.FileName); base == "config.yaml" || base == "config.yml" {
 | 
			
		||||
		// ignore config.yaml which is a special configuration file
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	if ext := filepath.Ext(it.FileName); ext == ".md" {
 | 
			
		||||
	if ext := path.Ext(it.FileName); ext == ".md" {
 | 
			
		||||
		return IssueTemplateTypeMarkdown
 | 
			
		||||
	} else if ext == ".yaml" || ext == ".yml" {
 | 
			
		||||
		return "yaml"
 | 
			
		||||
		return IssueTemplateTypeYaml
 | 
			
		||||
	}
 | 
			
		||||
	return IssueTemplateTypeYaml
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										106
									
								
								modules/structs/issue_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								modules/structs/issue_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package structs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIssueTemplate_Type(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		fileName string
 | 
			
		||||
		want     IssueTemplateType
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.yaml",
 | 
			
		||||
			want:     IssueTemplateTypeYaml,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.md",
 | 
			
		||||
			want:     IssueTemplateTypeMarkdown,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.txt",
 | 
			
		||||
			want:     "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/config.yaml",
 | 
			
		||||
			want:     "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.fileName, func(t *testing.T) {
 | 
			
		||||
			it := IssueTemplate{
 | 
			
		||||
				FileName: tt.fileName,
 | 
			
		||||
			}
 | 
			
		||||
			assert.Equal(t, tt.want, it.Type())
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIssueTemplateLabels_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		content string
 | 
			
		||||
		tmpl    *IssueTemplate
 | 
			
		||||
		want    *IssueTemplate
 | 
			
		||||
		wantErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:    "array",
 | 
			
		||||
			content: `labels: ["a", "b", "c"]`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"a", "b", "c"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "string",
 | 
			
		||||
			content: `labels: "a,b,c"`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"a", "b", "c"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "empty",
 | 
			
		||||
			content: `labels:`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: nil,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "error",
 | 
			
		||||
			content: `
 | 
			
		||||
labels:
 | 
			
		||||
  a: aa
 | 
			
		||||
  b: bb
 | 
			
		||||
`,
 | 
			
		||||
			tmpl:    &IssueTemplate{},
 | 
			
		||||
			wantErr: "line 3: cannot unmarshal !!map into IssueTemplateLabels",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			err := yaml.Unmarshal([]byte(tt.content), tt.tmpl)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				assert.EqualError(t, err, tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.Equal(t, tt.want, tt.tmpl)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@ package system
 | 
			
		||||
 | 
			
		||||
// RuntimeState contains app state for runtime, and we can save remote version for update checker here in future
 | 
			
		||||
type RuntimeState struct {
 | 
			
		||||
	LastAppPath string `json:"last_app_path"`
 | 
			
		||||
	LastAppPath    string `json:"last_app_path"`
 | 
			
		||||
	LastCustomConf string `json:"last_custom_conf"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns the item name
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func genKey(key string) string {
 | 
			
		||||
	return "system.setting." + key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(key string) (string, error) {
 | 
			
		||||
	return cache.GetString(genKey(key), func() (string, error) {
 | 
			
		||||
		res, err := system.GetSetting(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return res.SettingValue, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingBool return bool value of setting,
 | 
			
		||||
// none existing keys and errors are ignored and result in false
 | 
			
		||||
func GetSettingBool(key string) bool {
 | 
			
		||||
	s, _ := GetSetting(key)
 | 
			
		||||
	b, _ := strconv.ParseBool(s)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSetting sets the setting value
 | 
			
		||||
func SetSetting(key, value string, version int) error {
 | 
			
		||||
	cache.Remove(genKey(key))
 | 
			
		||||
 | 
			
		||||
	return system.SetSetting(&system.Setting{
 | 
			
		||||
		SettingKey:   key,
 | 
			
		||||
		SettingValue: value,
 | 
			
		||||
		Version:      version,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func genUserKey(userID int64, key string) string {
 | 
			
		||||
	return fmt.Sprintf("user_%d.setting.%s", userID, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserSetting returns the user setting value via the key
 | 
			
		||||
func GetUserSetting(userID int64, key string) (string, error) {
 | 
			
		||||
	return cache.GetString(genUserKey(userID, key), func() (string, error) {
 | 
			
		||||
		res, err := user.GetSetting(userID, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return res.SettingValue, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUserSetting sets the user setting value
 | 
			
		||||
func SetUserSetting(userID int64, key, value string) error {
 | 
			
		||||
	cache.Remove(genUserKey(userID, key))
 | 
			
		||||
 | 
			
		||||
	return user.SetUserSetting(userID, key, value)
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	system_module "code.gitea.io/gitea/modules/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
			
		||||
@@ -87,7 +86,7 @@ func NewFuncMap() []template.FuncMap {
 | 
			
		||||
			return setting.AssetVersion
 | 
			
		||||
		},
 | 
			
		||||
		"DisableGravatar": func() bool {
 | 
			
		||||
			return system_module.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
			return system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
		},
 | 
			
		||||
		"DefaultShowFullName": func() bool {
 | 
			
		||||
			return setting.UI.DefaultShowFullName
 | 
			
		||||
@@ -647,7 +646,7 @@ func SVG(icon string, others ...interface{}) template.HTML {
 | 
			
		||||
 | 
			
		||||
// Avatar renders user avatars. args: user, size (int), class (string)
 | 
			
		||||
func Avatar(item interface{}, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar vm", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
 | 
			
		||||
	switch t := item.(type) {
 | 
			
		||||
	case *user_model.User:
 | 
			
		||||
@@ -678,7 +677,7 @@ func AvatarByAction(action *activities_model.Action, others ...interface{}) temp
 | 
			
		||||
 | 
			
		||||
// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
 | 
			
		||||
func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
 | 
			
		||||
	src := repo.RelAvatarLink()
 | 
			
		||||
	if src != "" {
 | 
			
		||||
@@ -689,7 +688,7 @@ func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTM
 | 
			
		||||
 | 
			
		||||
// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
 | 
			
		||||
func AvatarByEmail(email, name string, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
	src := avatars.GenerateEmailAvatarFastLink(email, size*setting.Avatar.RenderedSizeFactor)
 | 
			
		||||
 | 
			
		||||
	if src != "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,13 @@ import (
 | 
			
		||||
// TimeStamp defines a timestamp
 | 
			
		||||
type TimeStamp int64
 | 
			
		||||
 | 
			
		||||
// mock is NOT concurrency-safe!!
 | 
			
		||||
var mock time.Time
 | 
			
		||||
var (
 | 
			
		||||
	// mock is NOT concurrency-safe!!
 | 
			
		||||
	mock time.Time
 | 
			
		||||
 | 
			
		||||
	// Used for IsZero, to check if timestamp is the zero time instant.
 | 
			
		||||
	timeZeroUnix = time.Time{}.Unix()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Set sets the time to a mocked time.Time
 | 
			
		||||
func Set(now time.Time) {
 | 
			
		||||
@@ -103,5 +108,5 @@ func (ts TimeStamp) FormatDate() string {
 | 
			
		||||
 | 
			
		||||
// IsZero is zero time
 | 
			
		||||
func (ts TimeStamp) IsZero() bool {
 | 
			
		||||
	return ts.AsTimeInLocation(time.Local).IsZero()
 | 
			
		||||
	return int64(ts) == 0 || int64(ts) == timeZeroUnix
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/composer"
 | 
			
		||||
@@ -57,7 +58,13 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
 | 
			
		||||
	authGroup := auth.NewGroup(authMethods...)
 | 
			
		||||
	r.Use(func(ctx *context.Context) {
 | 
			
		||||
		ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Verify: %v", err)
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.IsSigned = ctx.Doer != nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@@ -179,6 +186,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
		r.Group("/maven", func() {
 | 
			
		||||
			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
 | 
			
		||||
			r.Get("/*", maven.DownloadPackageFile)
 | 
			
		||||
			r.Head("/*", maven.ProvidePackageFileHeader)
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
		r.Group("/nuget", func() {
 | 
			
		||||
			r.Group("", func() { // Needs to be unauthenticated for the NuGet client.
 | 
			
		||||
@@ -316,7 +324,13 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
 | 
			
		||||
	authGroup := auth.NewGroup(authMethods...)
 | 
			
		||||
	r.Use(func(ctx *context.Context) {
 | 
			
		||||
		ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Failed to verify user: %v", err)
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "Verify")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.IsSigned = ctx.Doer != nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -184,7 +184,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackage creates a new package
 | 
			
		||||
 
 | 
			
		||||
@@ -20,22 +20,22 @@ func (a *Auth) Name() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify extracts the user from the Bearer token
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
 | 
			
		||||
	uid, err := packages.ParseAuthorizationToken(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Trace("ParseAuthorizationToken: %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if uid == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	u, err := user_model.GetUserByID(uid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetUserByID:  %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return u
 | 
			
		||||
	return u, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -473,7 +473,10 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteRecipeV1 deletes the requested recipe(s)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,25 +21,25 @@ func (a *Auth) Name() string {
 | 
			
		||||
 | 
			
		||||
// Verify extracts the user from the Bearer token
 | 
			
		||||
// If it's an anonymous session a ghost user is returned
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
 | 
			
		||||
	uid, err := packages.ParseAuthorizationToken(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Trace("ParseAuthorizationToken: %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if uid == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	if uid == -1 {
 | 
			
		||||
		return user_model.NewGhostUser()
 | 
			
		||||
		return user_model.NewGhostUser(), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	u, err := user_model.GetUserByID(uid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetUserByID:  %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return u
 | 
			
		||||
	return u, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,11 @@ package container
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
@@ -16,9 +19,12 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	container_module "code.gitea.io/gitea/modules/packages/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var uploadVersionMutex sync.Mutex
 | 
			
		||||
 | 
			
		||||
// saveAsPackageBlob creates a package blob from an upload
 | 
			
		||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
 | 
			
		||||
func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_service.PackageInfo) (*packages_model.PackageBlob, error) {
 | 
			
		||||
@@ -28,6 +34,11 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
 | 
			
		||||
 | 
			
		||||
	contentStore := packages_module.NewContentStore()
 | 
			
		||||
 | 
			
		||||
	var uploadVersion *packages_model.PackageVersion
 | 
			
		||||
 | 
			
		||||
	// FIXME: Replace usage of mutex with database transaction
 | 
			
		||||
	// https://github.com/go-gitea/gitea/pull/21862
 | 
			
		||||
	uploadVersionMutex.Lock()
 | 
			
		||||
	err := db.WithTx(func(ctx context.Context) error {
 | 
			
		||||
		created := true
 | 
			
		||||
		p := &packages_model.Package{
 | 
			
		||||
@@ -68,11 +79,30 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		uploadVersion = pv
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	uploadVersionMutex.Unlock()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.WithTx(func(ctx context.Context) error {
 | 
			
		||||
		pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Error inserting package blob: %v", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		// FIXME: Workaround to be removed in v1.20
 | 
			
		||||
		// https://github.com/go-gitea/gitea/issues/19586
 | 
			
		||||
		if exists {
 | 
			
		||||
			err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256))
 | 
			
		||||
			if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
 | 
			
		||||
				log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
 | 
			
		||||
				exists = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !exists {
 | 
			
		||||
			if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil {
 | 
			
		||||
				log.Error("Error saving package blob in content store: %v", err)
 | 
			
		||||
@@ -83,7 +113,7 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
 | 
			
		||||
		filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
 | 
			
		||||
 | 
			
		||||
		pf := &packages_model.PackageFile{
 | 
			
		||||
			VersionID:    pv.ID,
 | 
			
		||||
			VersionID:    uploadVersion.ID,
 | 
			
		||||
			BlobID:       pb.ID,
 | 
			
		||||
			Name:         filename,
 | 
			
		||||
			LowerName:    filename,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -24,6 +25,7 @@ import (
 | 
			
		||||
	container_module "code.gitea.io/gitea/modules/packages/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/container/oci"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
			
		||||
@@ -193,7 +195,7 @@ func InitiateUploadBlob(ctx *context.Context) {
 | 
			
		||||
	mount := ctx.FormTrim("mount")
 | 
			
		||||
	from := ctx.FormTrim("from")
 | 
			
		||||
	if mount != "" {
 | 
			
		||||
		blob, _ := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
		blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
			Image:  from,
 | 
			
		||||
			Digest: mount,
 | 
			
		||||
		})
 | 
			
		||||
@@ -406,7 +408,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri
 | 
			
		||||
		return nil, container_model.ErrContainerBlobNotExist
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
	return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
		OwnerID: ctx.Package.Owner.ID,
 | 
			
		||||
		Image:   ctx.Params("image"),
 | 
			
		||||
		Digest:  digest,
 | 
			
		||||
@@ -548,7 +550,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe
 | 
			
		||||
		return nil, container_model.ErrContainerBlobNotExist
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return container_model.GetContainerBlob(ctx, opts)
 | 
			
		||||
	return workaroundGetContainerBlob(ctx, opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
 | 
			
		||||
@@ -688,3 +690,23 @@ func GetTagList(ctx *context.Context) {
 | 
			
		||||
		Tags: tags,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME: Workaround to be removed in v1.20
 | 
			
		||||
// https://github.com/go-gitea/gitea/issues/19586
 | 
			
		||||
func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
 | 
			
		||||
	blob, err := container_model.GetContainerBlob(ctx, opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
			log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
 | 
			
		||||
			return nil, container_model.ErrContainerBlobNotExist
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return blob, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,10 @@ package container
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -19,6 +21,7 @@ import (
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	container_module "code.gitea.io/gitea/modules/packages/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/packages/container/oci"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -403,6 +406,15 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack
 | 
			
		||||
		log.Error("Error inserting package blob: %v", err)
 | 
			
		||||
		return nil, false, "", err
 | 
			
		||||
	}
 | 
			
		||||
	// FIXME: Workaround to be removed in v1.20
 | 
			
		||||
	// https://github.com/go-gitea/gitea/issues/19586
 | 
			
		||||
	if exists {
 | 
			
		||||
		err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256))
 | 
			
		||||
		if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
 | 
			
		||||
			log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
 | 
			
		||||
			exists = false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !exists {
 | 
			
		||||
		contentStore := packages_module.NewContentStore()
 | 
			
		||||
		if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackage uploads the specific generic package.
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackage creates a new package
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ package maven
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
@@ -23,12 +22,8 @@ type MetadataResponse struct {
 | 
			
		||||
	Version    []string `xml:"versioning>versions>version"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pds is expected to be sorted ascending by CreatedUnix
 | 
			
		||||
func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse {
 | 
			
		||||
	sort.Slice(pds, func(i, j int) bool {
 | 
			
		||||
		// Maven and Gradle order packages by their creation timestamp and not by their version string
 | 
			
		||||
		return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	var release *packages_model.PackageDescriptor
 | 
			
		||||
 | 
			
		||||
	versions := make([]string, 0, len(pds))
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
@@ -34,6 +36,10 @@ const (
 | 
			
		||||
	extensionSHA1     = ".sha1"
 | 
			
		||||
	extensionSHA256   = ".sha256"
 | 
			
		||||
	extensionSHA512   = ".sha512"
 | 
			
		||||
	extensionPom      = ".pom"
 | 
			
		||||
	extensionJar      = ".jar"
 | 
			
		||||
	contentTypeJar    = "application/java-archive"
 | 
			
		||||
	contentTypeXML    = "text/xml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -49,6 +55,15 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
 | 
			
		||||
 | 
			
		||||
// DownloadPackageFile serves the content of a package
 | 
			
		||||
func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	handlePackageFile(ctx, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProvidePackageFileHeader provides only the headers describing a package
 | 
			
		||||
func ProvidePackageFileHeader(ctx *context.Context) {
 | 
			
		||||
	handlePackageFile(ctx, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handlePackageFile(ctx *context.Context, serveContent bool) {
 | 
			
		||||
	params, err := extractPathParameters(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, err)
 | 
			
		||||
@@ -58,7 +73,7 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	if params.IsMeta && params.Version == "" {
 | 
			
		||||
		serveMavenMetadata(ctx, params)
 | 
			
		||||
	} else {
 | 
			
		||||
		servePackageFile(ctx, params)
 | 
			
		||||
		servePackageFile(ctx, params, serveContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -82,6 +97,11 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sort.Slice(pds, func(i, j int) bool {
 | 
			
		||||
		// Maven and Gradle order packages by their creation timestamp and not by their version string
 | 
			
		||||
		return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	xmlMetadata, err := xml.Marshal(createMetadataResponse(pds))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
@@ -89,6 +109,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 | 
			
		||||
	}
 | 
			
		||||
	xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
 | 
			
		||||
 | 
			
		||||
	latest := pds[len(pds)-1]
 | 
			
		||||
	ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat))
 | 
			
		||||
 | 
			
		||||
	ext := strings.ToLower(filepath.Ext(params.Filename))
 | 
			
		||||
	if isChecksumExtension(ext) {
 | 
			
		||||
		var hash []byte
 | 
			
		||||
@@ -110,10 +133,15 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader)
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Type", contentTypeXML)
 | 
			
		||||
 | 
			
		||||
	if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil {
 | 
			
		||||
		log.Error("write bytes failed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func servePackageFile(ctx *context.Context, params parameters) {
 | 
			
		||||
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
 | 
			
		||||
	packageName := params.GroupID + "-" + params.ArtifactID
 | 
			
		||||
 | 
			
		||||
	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
 | 
			
		||||
@@ -165,6 +193,23 @@ func servePackageFile(ctx *context.Context, params parameters) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := &context.ServeHeaderOptions{
 | 
			
		||||
		ContentLength: &pb.Size,
 | 
			
		||||
		LastModified:  pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	}
 | 
			
		||||
	switch ext {
 | 
			
		||||
	case extensionJar:
 | 
			
		||||
		opts.ContentType = contentTypeJar
 | 
			
		||||
	case extensionPom:
 | 
			
		||||
		opts.ContentType = contentTypeXML
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !serveContent {
 | 
			
		||||
		ctx.SetServeHeaders(opts)
 | 
			
		||||
		ctx.Status(http.StatusOK)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
@@ -177,7 +222,9 @@ func servePackageFile(ctx *context.Context, params parameters) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	opts.Filename = pf.Name
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(s, opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
 | 
			
		||||
@@ -272,7 +319,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If it's the package pom file extract the metadata
 | 
			
		||||
	if ext == ".pom" {
 | 
			
		||||
	if ext == extensionPom {
 | 
			
		||||
		pfci.IsLead = true
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DownloadPackageFileByName finds the version and serves the contents of a package
 | 
			
		||||
@@ -146,7 +149,10 @@ func DownloadPackageFileByName(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackage creates a new package
 | 
			
		||||
@@ -396,8 +402,9 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
 | 
			
		||||
 | 
			
		||||
func PackageSearch(ctx *context.Context) {
 | 
			
		||||
	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 | 
			
		||||
		OwnerID: ctx.Package.Owner.ID,
 | 
			
		||||
		Type:    packages_model.TypeNpm,
 | 
			
		||||
		OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
		Type:       packages_model.TypeNpm,
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		Name: packages_model.SearchValue{
 | 
			
		||||
			ExactMatch: false,
 | 
			
		||||
			Value:      ctx.FormTrim("text"),
 | 
			
		||||
 
 | 
			
		||||
@@ -345,7 +345,7 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames
 | 
			
		||||
		Content: content,
 | 
			
		||||
		Properties: &FeedEntryProperties{
 | 
			
		||||
			Version:                  pd.Version.Version,
 | 
			
		||||
			NormalizedVersion:        normalizeVersion(pd.SemVer),
 | 
			
		||||
			NormalizedVersion:        pd.Version.Version,
 | 
			
		||||
			Authors:                  metadata.Authors,
 | 
			
		||||
			Dependencies:             buildDependencyString(metadata),
 | 
			
		||||
			Description:              metadata.Description,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,11 @@
 | 
			
		||||
package nuget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
 | 
			
		||||
 | 
			
		||||
	"github.com/hashicorp/go-version"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
 | 
			
		||||
@@ -96,8 +92,8 @@ func createRegistrationIndexResponse(l *linkBuilder, pds []*packages_model.Packa
 | 
			
		||||
			{
 | 
			
		||||
				RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
 | 
			
		||||
				Count:               len(pds),
 | 
			
		||||
				Lower:               normalizeVersion(pds[0].SemVer),
 | 
			
		||||
				Upper:               normalizeVersion(pds[len(pds)-1].SemVer),
 | 
			
		||||
				Lower:               pds[0].Version.Version,
 | 
			
		||||
				Upper:               pds[len(pds)-1].Version.Version,
 | 
			
		||||
				Items:               items,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@@ -174,7 +170,7 @@ type PackageVersionsResponse struct {
 | 
			
		||||
func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *PackageVersionsResponse {
 | 
			
		||||
	versions := make([]string, 0, len(pds))
 | 
			
		||||
	for _, pd := range pds {
 | 
			
		||||
		versions = append(versions, normalizeVersion(pd.SemVer))
 | 
			
		||||
		versions = append(versions, pd.Version.Version)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &PackageVersionsResponse{
 | 
			
		||||
@@ -249,15 +245,3 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor)
 | 
			
		||||
		RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// normalizeVersion removes the metadata
 | 
			
		||||
func normalizeVersion(v *version.Version) string {
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	segments := v.Segments64()
 | 
			
		||||
	fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
 | 
			
		||||
	pre := v.Prerelease()
 | 
			
		||||
	if pre != "" {
 | 
			
		||||
		fmt.Fprintf(&buf, "-%s", pre)
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,19 +21,20 @@ func (a *Auth) Name() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
 | 
			
		||||
	token, err := auth_model.GetAccessTokenBySHA(req.Header.Get("X-NuGet-ApiKey"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) {
 | 
			
		||||
			log.Error("GetAccessTokenBySHA: %v", err)
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	u, err := user_model.GetUserByID(token.UID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetUserByID:  %v", err)
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token.UpdatedUnix = timeutil.TimeStampNow()
 | 
			
		||||
@@ -41,5 +42,5 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
 | 
			
		||||
		log.Error("UpdateAccessToken:  %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return u
 | 
			
		||||
	return u, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -342,7 +342,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file
 | 
			
		||||
@@ -552,7 +555,10 @@ func DownloadSymbolFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePackage hard deletes the package
 | 
			
		||||
 
 | 
			
		||||
@@ -271,5 +271,8 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,19 @@ import (
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://www.python.org/dev/peps/pep-0503/#normalized-names
 | 
			
		||||
// https://peps.python.org/pep-0426/#name
 | 
			
		||||
var normalizer = strings.NewReplacer(".", "-", "_", "-")
 | 
			
		||||
var nameMatcher = regexp.MustCompile(`\A[a-zA-Z0-9\.\-_]+\z`)
 | 
			
		||||
var nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
 | 
			
		||||
 | 
			
		||||
// https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
var versionMatcher = regexp.MustCompile(`^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$`)
 | 
			
		||||
// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
var versionMatcher = regexp.MustCompile(`\Av?` +
 | 
			
		||||
	`(?:[0-9]+!)?` + // epoch
 | 
			
		||||
	`[0-9]+(?:\.[0-9]+)*` + // release segment
 | 
			
		||||
	`(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release
 | 
			
		||||
	`(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release
 | 
			
		||||
	`(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release
 | 
			
		||||
	`(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version
 | 
			
		||||
	`\z`)
 | 
			
		||||
 | 
			
		||||
func apiError(ctx *context.Context, status int, obj interface{}) {
 | 
			
		||||
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
 | 
			
		||||
@@ -88,7 +95,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
 | 
			
		||||
@@ -121,7 +131,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	packageName := normalizer.Replace(ctx.Req.FormValue("name"))
 | 
			
		||||
	packageVersion := ctx.Req.FormValue("version")
 | 
			
		||||
	if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) {
 | 
			
		||||
	if !isValidNameAndVersion(packageName, packageVersion) {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, "invalid name or version")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -139,7 +149,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
				Name:        packageName,
 | 
			
		||||
				Version:     packageVersion,
 | 
			
		||||
			},
 | 
			
		||||
			SemverCompatible: true,
 | 
			
		||||
			SemverCompatible: false,
 | 
			
		||||
			Creator:          ctx.Doer,
 | 
			
		||||
			Metadata: &pypi_module.Metadata{
 | 
			
		||||
				Author:          ctx.Req.FormValue("author"),
 | 
			
		||||
@@ -170,3 +180,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusCreated)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isValidNameAndVersion(packageName, packageVersion string) bool {
 | 
			
		||||
	return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								routers/api/packages/pypi/pypi_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								routers/api/packages/pypi/pypi_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package pypi
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIsValidNameAndVersion(t *testing.T) {
 | 
			
		||||
	// The test cases below were created from the following Python PEPs:
 | 
			
		||||
	// https://peps.python.org/pep-0426/#name
 | 
			
		||||
	// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
 | 
			
		||||
	// Valid Cases
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("A", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("Test.Name.1234", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test_name", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "v1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "2012.4"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1-alpha"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1a1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0b2.r345.dev456"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1!1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1+local.1"))
 | 
			
		||||
 | 
			
		||||
	// Invalid Cases
 | 
			
		||||
	assert.False(t, isValidNameAndVersion(".test-name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test!name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("-test-name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name-", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "a1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
 | 
			
		||||
}
 | 
			
		||||
@@ -77,7 +77,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetServeHeaders(filename + ".gz")
 | 
			
		||||
	ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
		Filename: filename + ".gz",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	zw := gzip.NewWriter(ctx.Resp)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
@@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetServeHeaders(filename)
 | 
			
		||||
	ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
		Filename: filename,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	zw := zlib.NewWriter(ctx.Resp)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
@@ -188,7 +192,10 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
 | 
			
		||||
 
 | 
			
		||||
@@ -235,5 +235,8 @@ func DownloadPackageFile(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime())
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -232,13 +232,10 @@ func reqExploreSignIn() func(ctx *context.APIContext) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
 | 
			
		||||
func reqBasicAuth() func(ctx *context.APIContext) {
 | 
			
		||||
	return func(ctx *context.APIContext) {
 | 
			
		||||
		if ctx.IsSigned && setting.Service.EnableReverseProxyAuth && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !ctx.Context.IsBasicAuth {
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "reqBasicOrRevProxyAuth", "auth required")
 | 
			
		||||
			ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.CheckForOTP()
 | 
			
		||||
@@ -597,9 +594,6 @@ func buildAuthGroup() *auth.Group {
 | 
			
		||||
		&auth.HTTPSign{},
 | 
			
		||||
		&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
 | 
			
		||||
	)
 | 
			
		||||
	if setting.Service.EnableReverseProxyAuth {
 | 
			
		||||
		group.Add(&auth.ReverseProxy{})
 | 
			
		||||
	}
 | 
			
		||||
	specialAdd(group)
 | 
			
		||||
 | 
			
		||||
	return group
 | 
			
		||||
@@ -689,7 +683,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
					m.Combo("").Get(user.ListAccessTokens).
 | 
			
		||||
						Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
 | 
			
		||||
					m.Combo("/{id}").Delete(user.DeleteAccessToken)
 | 
			
		||||
				}, reqBasicOrRevProxyAuth())
 | 
			
		||||
				}, reqBasicAuth())
 | 
			
		||||
			}, context_service.UserAssignmentAPI())
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
@@ -898,7 +892,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
					m.Group("/{index}", func() {
 | 
			
		||||
						m.Combo("").Get(repo.GetIssue).
 | 
			
		||||
							Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
 | 
			
		||||
							Delete(reqToken(), reqAdmin(), repo.DeleteIssue)
 | 
			
		||||
							Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
 | 
			
		||||
						m.Group("/comments", func() {
 | 
			
		||||
							m.Combo("").Get(repo.ListIssueComments).
 | 
			
		||||
								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
 | 
			
		||||
 
 | 
			
		||||
@@ -252,42 +252,50 @@ func ListBranches(ctx *context.APIContext) {
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/BranchList"
 | 
			
		||||
 | 
			
		||||
	listOptions := utils.GetListOptions(ctx)
 | 
			
		||||
	skip, _ := listOptions.GetStartEnd()
 | 
			
		||||
	branches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "GetBranches", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var totalNumOfBranches int
 | 
			
		||||
	var apiBranches []*api.Branch
 | 
			
		||||
 | 
			
		||||
	apiBranches := make([]*api.Branch, 0, len(branches))
 | 
			
		||||
	for i := range branches {
 | 
			
		||||
		c, err := branches[i].GetCommit()
 | 
			
		||||
	listOptions := utils.GetListOptions(ctx)
 | 
			
		||||
 | 
			
		||||
	if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil {
 | 
			
		||||
		skip, _ := listOptions.GetStartEnd()
 | 
			
		||||
		branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// Skip if this branch doesn't exist anymore.
 | 
			
		||||
			if git.IsErrNotExist(err) {
 | 
			
		||||
				totalNumOfBranches--
 | 
			
		||||
				continue
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetBranches", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		apiBranches = make([]*api.Branch, 0, len(branches))
 | 
			
		||||
		for i := range branches {
 | 
			
		||||
			c, err := branches[i].GetCommit()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// Skip if this branch doesn't exist anymore.
 | 
			
		||||
				if git.IsErrNotExist(err) {
 | 
			
		||||
					total--
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "GetCommit", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
 | 
			
		||||
			return
 | 
			
		||||
			branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			apiBranches = append(apiBranches, apiBranch)
 | 
			
		||||
		}
 | 
			
		||||
		branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiBranches = append(apiBranches, apiBranch)
 | 
			
		||||
 | 
			
		||||
		totalNumOfBranches = total
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize)
 | 
			
		||||
	ctx.SetTotalCountHeader(int64(totalNumOfBranches))
 | 
			
		||||
	ctx.JSON(http.StatusOK, &apiBranches)
 | 
			
		||||
	ctx.JSON(http.StatusOK, apiBranches)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetBranchProtection gets a branch protection
 | 
			
		||||
 
 | 
			
		||||
@@ -341,7 +341,11 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer fr.Close()
 | 
			
		||||
	ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime())
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(fr, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     downloadName,
 | 
			
		||||
		LastModified: archiver.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetEditorconfig get editor config of a repository
 | 
			
		||||
 
 | 
			
		||||
@@ -491,6 +491,11 @@ func EditPullRequest(ctx *context.APIContext) {
 | 
			
		||||
	issue := pr.Issue
 | 
			
		||||
	issue.Repo = ctx.Repo.Repository
 | 
			
		||||
 | 
			
		||||
	if err := issue.LoadAttributes(ctx); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
 | 
			
		||||
		ctx.Status(http.StatusForbidden)
 | 
			
		||||
		return
 | 
			
		||||
@@ -1443,7 +1448,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 | 
			
		||||
		end = totalNumberOfFiles
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, end-start)
 | 
			
		||||
	lenFiles := end - start
 | 
			
		||||
	if lenFiles < 0 {
 | 
			
		||||
		lenFiles = 0
 | 
			
		||||
	}
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, lenFiles)
 | 
			
		||||
	for i := start; i < end; i++ {
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -184,6 +184,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
 | 
			
		||||
		ctx.Error(http.StatusBadRequest, "ref/sha not given", nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	sha = utils.MustConvertToSHA1(ctx.Context, sha)
 | 
			
		||||
	repo := ctx.Repo.Repository
 | 
			
		||||
 | 
			
		||||
	listOptions := utils.GetListOptions(ctx)
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,8 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sha = MustConvertToSHA1(ctx.Context, sha)
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.GitRepo != nil {
 | 
			
		||||
		err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -66,3 +68,30 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
 | 
			
		||||
	}
 | 
			
		||||
	return "", "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConvertToSHA1 returns a full-length SHA1 from a potential ID string
 | 
			
		||||
func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
 | 
			
		||||
	if len(commitID) == git.SHAFullLength && git.IsValidSHAPattern(commitID) {
 | 
			
		||||
		sha1, err := git.NewIDFromString(commitID)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return sha1, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository.RepoPath())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return git.SHA1{}, fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer closer.Close()
 | 
			
		||||
 | 
			
		||||
	return gitRepo.ConvertToSHA1(commitID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1
 | 
			
		||||
func MustConvertToSHA1(ctx *context.Context, commitID string) string {
 | 
			
		||||
	sha, err := ConvertToSHA1(ctx, commitID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return commitID
 | 
			
		||||
	}
 | 
			
		||||
	return sha.String()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,7 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -53,50 +51,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
 | 
			
		||||
		buf = buf[:n]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
 | 
			
		||||
	opts := &context.ServeHeaderOptions{
 | 
			
		||||
		Filename: path.Base(filePath),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if size >= 0 {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
 | 
			
		||||
		opts.ContentLength = &size
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileName := path.Base(filePath)
 | 
			
		||||
	sniffedType := typesniffer.DetectContentType(buf)
 | 
			
		||||
	isPlain := sniffedType.IsText() || ctx.FormBool("render")
 | 
			
		||||
	mimeType := ""
 | 
			
		||||
	charset := ""
 | 
			
		||||
 | 
			
		||||
	if setting.MimeTypeMap.Enabled {
 | 
			
		||||
		fileExtension := strings.ToLower(filepath.Ext(fileName))
 | 
			
		||||
		mimeType = setting.MimeTypeMap.Map[fileExtension]
 | 
			
		||||
		fileExtension := strings.ToLower(filepath.Ext(filePath))
 | 
			
		||||
		opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mimeType == "" {
 | 
			
		||||
	if opts.ContentType == "" {
 | 
			
		||||
		if sniffedType.IsBrowsableBinaryType() {
 | 
			
		||||
			mimeType = sniffedType.GetMimeType()
 | 
			
		||||
			opts.ContentType = sniffedType.GetMimeType()
 | 
			
		||||
		} else if isPlain {
 | 
			
		||||
			mimeType = "text/plain"
 | 
			
		||||
			opts.ContentType = "text/plain"
 | 
			
		||||
		} else {
 | 
			
		||||
			mimeType = typesniffer.ApplicationOctetStream
 | 
			
		||||
			opts.ContentType = typesniffer.ApplicationOctetStream
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isPlain {
 | 
			
		||||
		var charset string
 | 
			
		||||
		charset, err = charsetModule.DetectEncoding(buf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
 | 
			
		||||
			charset = "utf-8"
 | 
			
		||||
		}
 | 
			
		||||
		opts.ContentTypeCharset = strings.ToLower(charset)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if charset != "" {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", mimeType)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
 | 
			
		||||
	isSVG := sniffedType.IsSvgImage()
 | 
			
		||||
 | 
			
		||||
	// serve types that can present a security risk with CSP
 | 
			
		||||
@@ -109,16 +101,12 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disposition := "inline"
 | 
			
		||||
	opts.Disposition = "inline"
 | 
			
		||||
	if isSVG && !setting.UI.SVG.Enabled {
 | 
			
		||||
		disposition = "attachment"
 | 
			
		||||
		opts.Disposition = "attachment"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
 | 
			
		||||
	encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName)
 | 
			
		||||
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName)
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
 | 
			
		||||
	_, err = ctx.Resp.Write(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,21 +76,31 @@ func InitGitServices() {
 | 
			
		||||
	mustInit(repo_service.Init)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func syncAppPathForGit(ctx context.Context) error {
 | 
			
		||||
func syncAppConfForGit(ctx context.Context) error {
 | 
			
		||||
	runtimeState := new(system.RuntimeState)
 | 
			
		||||
	if err := system.AppState.Get(runtimeState); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updated := false
 | 
			
		||||
	if runtimeState.LastAppPath != setting.AppPath {
 | 
			
		||||
		log.Info("AppPath changed from '%s' to '%s'", runtimeState.LastAppPath, setting.AppPath)
 | 
			
		||||
		runtimeState.LastAppPath = setting.AppPath
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
	if runtimeState.LastCustomConf != setting.CustomConf {
 | 
			
		||||
		log.Info("CustomConf changed from '%s' to '%s'", runtimeState.LastCustomConf, setting.CustomConf)
 | 
			
		||||
		runtimeState.LastCustomConf = setting.CustomConf
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if updated {
 | 
			
		||||
		log.Info("re-sync repository hooks ...")
 | 
			
		||||
		mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
 | 
			
		||||
 | 
			
		||||
		log.Info("re-write ssh public keys ...")
 | 
			
		||||
		mustInit(asymkey_model.RewriteAllPublicKeys)
 | 
			
		||||
 | 
			
		||||
		runtimeState.LastAppPath = setting.AppPath
 | 
			
		||||
		return system.AppState.Set(runtimeState)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -153,7 +163,7 @@ func GlobalInitInstalled(ctx context.Context) {
 | 
			
		||||
	mustInit(repo_migrations.Init)
 | 
			
		||||
	eventsource.GetManager().Init()
 | 
			
		||||
 | 
			
		||||
	mustInitCtx(ctx, syncAppPathForGit)
 | 
			
		||||
	mustInitCtx(ctx, syncAppConfForGit)
 | 
			
		||||
 | 
			
		||||
	mustInit(ssh.Init)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -473,12 +473,16 @@ func SubmitInstall(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
 | 
			
		||||
 | 
			
		||||
	var internalToken string
 | 
			
		||||
	if internalToken, err = generate.NewInternalToken(); err != nil {
 | 
			
		||||
		ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
 | 
			
		||||
		return
 | 
			
		||||
	// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
 | 
			
		||||
	// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
 | 
			
		||||
	if setting.InternalToken == "" {
 | 
			
		||||
		var internalToken string
 | 
			
		||||
		if internalToken, err = generate.NewInternalToken(); err != nil {
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
 | 
			
		||||
	}
 | 
			
		||||
	cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
 | 
			
		||||
 | 
			
		||||
	// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
 | 
			
		||||
	if setting.SecretKey == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 | 
			
		||||
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
 | 
			
		||||
	return &smtp.Source{
 | 
			
		||||
		Auth:           form.SMTPAuth,
 | 
			
		||||
		Addr:           form.SMTPAddr,
 | 
			
		||||
		Host:           form.SMTPHost,
 | 
			
		||||
		Port:           form.SMTPPort,
 | 
			
		||||
		AllowedDomains: form.AllowedDomains,
 | 
			
		||||
		ForceSMTPS:     form.ForceSMTPS,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	system_module "code.gitea.io/gitea/modules/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/mailer"
 | 
			
		||||
 | 
			
		||||
@@ -203,7 +202,11 @@ func ChangeConfig(ctx *context.Context) {
 | 
			
		||||
	value := ctx.FormString("value")
 | 
			
		||||
	version := ctx.FormInt("version")
 | 
			
		||||
 | 
			
		||||
	if err := system_module.SetSetting(key, value, version); err != nil {
 | 
			
		||||
	if err := system_model.SetSetting(&system_model.Setting{
 | 
			
		||||
		SettingKey:   key,
 | 
			
		||||
		SettingValue: value,
 | 
			
		||||
		Version:      version,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		log.Error("set setting failed: %v", err)
 | 
			
		||||
		ctx.JSON(http.StatusOK, map[string]string{
 | 
			
		||||
			"err": ctx.Tr("admin.config.set_setting_failed", key),
 | 
			
		||||
 
 | 
			
		||||
@@ -783,6 +783,13 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Register last login
 | 
			
		||||
	user.SetLastLogin()
 | 
			
		||||
	if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
 | 
			
		||||
		ctx.ServerError("UpdateUserCols", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Flash.Success(ctx.Tr("auth.account_activated"))
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,10 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
 | 
			
		||||
		orderBy = "`user`.updated_unix ASC"
 | 
			
		||||
	case "reversealphabetically":
 | 
			
		||||
		orderBy = "`user`.name DESC"
 | 
			
		||||
	case "lastlogin":
 | 
			
		||||
		orderBy = "`user`.last_login_unix ASC"
 | 
			
		||||
	case "reverselastlogin":
 | 
			
		||||
		orderBy = "`user`.last_login_unix DESC"
 | 
			
		||||
	case UserSearchDefaultSortType: // "alphabetically"
 | 
			
		||||
	default:
 | 
			
		||||
		orderBy = "`user`.name ASC"
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user