mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			116 Commits
		
	
	
		
			v1.26.0-de
			...
			v1.18.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3c531d3957 | ||
| 
						 | 
					1ae2525922 | ||
| 
						 | 
					fd7ebaaa9c | ||
| 
						 | 
					fa33271157 | ||
| 
						 | 
					4b3e456afa | ||
| 
						 | 
					63e5db5d7a | ||
| 
						 | 
					e6e2c2f4a4 | ||
| 
						 | 
					e902b98cc2 | ||
| 
						 | 
					6992e72647 | ||
| 
						 | 
					1bbf490926 | ||
| 
						 | 
					45bdeac730 | ||
| 
						 | 
					a32700d0fd | ||
| 
						 | 
					a9400ba7a3 | ||
| 
						 | 
					9a6d78eaa8 | ||
| 
						 | 
					af8151cbb9 | ||
| 
						 | 
					ee37edc465 | ||
| 
						 | 
					29bbfcc118 | ||
| 
						 | 
					f430050d24 | ||
| 
						 | 
					510c811574 | ||
| 
						 | 
					f93522ddae | ||
| 
						 | 
					10c9f96a1e | ||
| 
						 | 
					7b60d47c3c | ||
| 
						 | 
					265d438a6e | ||
| 
						 | 
					93e907de41 | ||
| 
						 | 
					f3034b1fd9 | ||
| 
						 | 
					d0c74dd2d2 | ||
| 
						 | 
					2f91a12143 | ||
| 
						 | 
					3ad62127df | ||
| 
						 | 
					37e23c982f | ||
| 
						 | 
					421d87933b | ||
| 
						 | 
					426c0ad14c | ||
| 
						 | 
					41a06d2e82 | ||
| 
						 | 
					885082f7a7 | ||
| 
						 | 
					32999e2511 | ||
| 
						 | 
					16d7596635 | ||
| 
						 | 
					adc0bcaebb | ||
| 
						 | 
					0cca1e079b | ||
| 
						 | 
					55c6433fac | ||
| 
						 | 
					5b8763476a | ||
| 
						 | 
					09c667eb45 | ||
| 
						 | 
					791f290c26 | ||
| 
						 | 
					58e642c1d6 | ||
| 
						 | 
					72d1f9e63e | ||
| 
						 | 
					0697075547 | ||
| 
						 | 
					f1e07d8c87 | ||
| 
						 | 
					443fd27a90 | ||
| 
						 | 
					75f128ebf8 | ||
| 
						 | 
					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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										303
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,309 @@ 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.2](https://github.com/go-gitea/gitea/releases/tag/v1.18.2) - 2023-01-19
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * When updating by rebase we need to set the environment for head repo (#22535) (#22536)
 | 
			
		||||
  * Fix issue not auto-closing when it includes a reference to a branch (#22514) (#22521)
 | 
			
		||||
  * Fix invalid issue branch reference if not specified in template (#22513) (#22520)
 | 
			
		||||
  * Fix 500 error viewing pull request when fork has pull requests disabled (#22512) (#22515)
 | 
			
		||||
  * Reliable selection of admin user (#22509) (#22511)
 | 
			
		||||
  * Set disable_gravatar/enable_federated_avatar when offline mode is true (#22479) (#22496)
 | 
			
		||||
* BUILD
 | 
			
		||||
  * cgo cross-compile for freebsd (#22397) (#22519)
 | 
			
		||||
 | 
			
		||||
## [1.18.1](https://github.com/go-gitea/gitea/releases/tag/v1.18.1) - 2023-01-17
 | 
			
		||||
 | 
			
		||||
* API
 | 
			
		||||
  * Add `sync_on_commit` option for push mirrors api (#22271) (#22292)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Update `github.com/zeripath/zapx/v15` (#22485)
 | 
			
		||||
  * Fix pull request API field `closed_at` always being `null` (#22482) (#22483)
 | 
			
		||||
  * Fix container blob mount (#22226) (#22476)
 | 
			
		||||
  * Fix error when calculating repository size (#22392) (#22474)
 | 
			
		||||
  * Fix Operator does not exist bug on explore page with ONLY_SHOW_RELEVANT_REPOS (#22454) (#22472)
 | 
			
		||||
  * Fix environments for KaTeX and error reporting (#22453) (#22473)
 | 
			
		||||
  * Remove the netgo tag for Windows build (#22467) (#22468)
 | 
			
		||||
  * Fix migration from GitBucket (#22477) (#22465)
 | 
			
		||||
  * Prevent panic on looking at api "git" endpoints for empty repos (#22457) (#22458)
 | 
			
		||||
  * Fix PR status layout on mobile (#21547) (#22441)
 | 
			
		||||
  * Fix wechatwork webhook sends empty content in PR review (#21762) (#22440)
 | 
			
		||||
  * Remove duplicate "Actions" label in mobile view (#21974) (#22439)
 | 
			
		||||
  * Fix leaving organization bug on user settings -> orgs (#21983) (#22438)
 | 
			
		||||
  * Fixed colour transparency regex matching in project board sorting (#22092) (#22437)
 | 
			
		||||
  * Correctly handle select on multiple channels in Queues (#22146) (#22428)
 | 
			
		||||
  * Prepend refs/heads/ to issue template refs (#20461) (#22427)
 | 
			
		||||
  * Restore function to "Show more" buttons (#22399) (#22426)
 | 
			
		||||
  * Continue GCing other repos on error in one repo (#22422) (#22425)
 | 
			
		||||
  * Allow HOST has no port (#22280) (#22409)
 | 
			
		||||
  * Fix omit avatar_url in discord payload when empty (#22393) (#22394)
 | 
			
		||||
  * Don't display stop watch top bar icon when disabled and hidden when click other place (#22374) (#22387)
 | 
			
		||||
  * Don't lookup mail server when using sendmail (#22300) (#22383)
 | 
			
		||||
  * Fix gravatar disable bug (#22337)
 | 
			
		||||
  * Fix update settings table on install (#22326) (#22327)
 | 
			
		||||
  * Fix sitemap (#22272) (#22320)
 | 
			
		||||
  * Fix code search title translation (#22285) (#22316)
 | 
			
		||||
  * Fix due date rendering the wrong date in issue (#22302) (#22306)
 | 
			
		||||
  * Fix get system setting bug when enabled redis cache (#22298)
 | 
			
		||||
  * Fix bug of DisableGravatar default value (#22297)
 | 
			
		||||
  * Fix key signature error page (#22229) (#22230)
 | 
			
		||||
* TESTING
 | 
			
		||||
  * Remove test session cache to reduce possible concurrent problem (#22199) (#22429)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Restore previous official review when an official review is deleted (#22449) (#22460)
 | 
			
		||||
  * Log STDERR of external renderer when it fails (#22442) (#22444)
 | 
			
		||||
 | 
			
		||||
## [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 []
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								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
 | 
			
		||||
@@ -733,16 +733,16 @@ $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 | 
			
		||||
 | 
			
		||||
.PHONY: release
 | 
			
		||||
release: frontend generate release-windows release-linux release-darwin release-copy release-compress vendor release-sources release-docs release-check
 | 
			
		||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-docs release-check
 | 
			
		||||
 | 
			
		||||
$(DIST_DIRS):
 | 
			
		||||
	mkdir -p $(DIST_DIRS)
 | 
			
		||||
 | 
			
		||||
.PHONY: release-windows
 | 
			
		||||
release-windows: | $(DIST_DIRS)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
 | 
			
		||||
ifeq (,$(findstring gogit,$(TAGS)))
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(CI),true)
 | 
			
		||||
	cp /build/* $(DIST)/binaries
 | 
			
		||||
@@ -762,6 +762,13 @@ ifeq ($(CI),true)
 | 
			
		||||
	cp /build/* $(DIST)/binaries
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
.PHONY: release-freebsd
 | 
			
		||||
release-freebsd: | $(DIST_DIRS)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
 | 
			
		||||
ifeq ($(CI),true)
 | 
			
		||||
	cp /build/* $(DIST)/binaries
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
.PHONY: release-copy
 | 
			
		||||
release-copy: | $(DIST_DIRS)
 | 
			
		||||
	cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								assets/emoji.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								assets/emoji.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -26,7 +26,7 @@ import (
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	gemojiURL         = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
 | 
			
		||||
	maxUnicodeVersion = 12
 | 
			
		||||
	maxUnicodeVersion = 14
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -734,9 +735,9 @@ and
 | 
			
		||||
 | 
			
		||||
- `GRAVATAR_SOURCE`: **gravatar**: Can be `gravatar`, `duoshuo` or anything like
 | 
			
		||||
   `http://cn.gravatar.com/avatar/`.
 | 
			
		||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
 | 
			
		||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
 | 
			
		||||
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
 | 
			
		||||
   [http://www.libravatar.org](http://www.libravatar.org)).
 | 
			
		||||
   [http://www.libravatar.org](http://www.libravatar.org)). **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
 | 
			
		||||
 | 
			
		||||
- `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`.
 | 
			
		||||
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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-2
 | 
			
		||||
 | 
			
		||||
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-2 h1:IRB+69BV7fTT5ccw35ca7TCBe2b7dm5Q5y5tUMQmCvU=
 | 
			
		||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2/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.
 | 
			
		||||
 
 | 
			
		||||
@@ -68,8 +68,16 @@ func (key *GPGKey) PaddedKeyID() string {
 | 
			
		||||
	if len(key.KeyID) > 15 {
 | 
			
		||||
		return key.KeyID
 | 
			
		||||
	}
 | 
			
		||||
	return PaddedKeyID(key.KeyID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PaddedKeyID show KeyID padded to 16 characters
 | 
			
		||||
func PaddedKeyID(keyID string) string {
 | 
			
		||||
	if len(keyID) > 15 {
 | 
			
		||||
		return keyID
 | 
			
		||||
	}
 | 
			
		||||
	zeros := "0000000000000000"
 | 
			
		||||
	return zeros[0:16-len(key.KeyID)] + key.KeyID
 | 
			
		||||
	return zeros[0:16-len(keyID)] + keyID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListGPGKeys returns a list of public keys belongs to given user.
 | 
			
		||||
 
 | 
			
		||||
@@ -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,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return DefaultAvatarLink()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
 | 
			
		||||
	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 +175,8 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return urlStr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	if disableGravatar != nil && !disableGravatar.GetValueBool() {
 | 
			
		||||
	disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	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
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
  fork_id: 0
 | 
			
		||||
  is_template: false
 | 
			
		||||
  template_id: 0
 | 
			
		||||
  size: 0
 | 
			
		||||
  size: 6708
 | 
			
		||||
  is_fsck_enabled: true
 | 
			
		||||
  close_issues_via_commit_in_any_branch: false
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -742,17 +742,9 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if official {
 | 
			
		||||
		// recalculate the latest official review for reviewer
 | 
			
		||||
		review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
 | 
			
		||||
		if err != nil && !IsErrReviewNotExist(err) {
 | 
			
		||||
		if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if review != nil {
 | 
			
		||||
			if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{
 | 
			
		||||
@@ -770,6 +762,22 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen
 | 
			
		||||
	return comment, committer.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Recalculate the latest official review for reviewer
 | 
			
		||||
func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
 | 
			
		||||
	review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
 | 
			
		||||
	if err != nil && !IsErrReviewNotExist(err) {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if review != nil {
 | 
			
		||||
		if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddTeamReviewRequest add a review request from one team
 | 
			
		||||
func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
 | 
			
		||||
	ctx, committer, err := db.TxContext()
 | 
			
		||||
@@ -988,6 +996,12 @@ func DeleteReview(r *Review) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.Official {
 | 
			
		||||
		if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return committer.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -201,3 +201,38 @@ func TestDismissReview(t *testing.T) {
 | 
			
		||||
	assert.False(t, requestReviewExample.Dismissed)
 | 
			
		||||
	assert.True(t, approveReviewExample.Dismissed)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteReview(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
			
		||||
 | 
			
		||||
	review1, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
 | 
			
		||||
		Content:  "Official rejection",
 | 
			
		||||
		Type:     issues_model.ReviewTypeReject,
 | 
			
		||||
		Official: false,
 | 
			
		||||
		Issue:    issue,
 | 
			
		||||
		Reviewer: user,
 | 
			
		||||
	})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	review2, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
 | 
			
		||||
		Content:  "Official approval",
 | 
			
		||||
		Type:     issues_model.ReviewTypeApprove,
 | 
			
		||||
		Official: true,
 | 
			
		||||
		Issue:    issue,
 | 
			
		||||
		Reviewer: user,
 | 
			
		||||
	})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, issues_model.DeleteReview(review2))
 | 
			
		||||
 | 
			
		||||
	_, err = issues_model.GetReviewByID(db.DefaultContext, review2.ID)
 | 
			
		||||
	assert.Error(t, err)
 | 
			
		||||
	assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
 | 
			
		||||
 | 
			
		||||
	review1, err = issues_model.GetReviewByID(db.DefaultContext, review1.ID)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, review1.Official)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ type BlobSearchOptions struct {
 | 
			
		||||
	Digest     string
 | 
			
		||||
	Tag        string
 | 
			
		||||
	IsManifest bool
 | 
			
		||||
	Repository string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts *BlobSearchOptions) toConds() builder.Cond {
 | 
			
		||||
@@ -54,6 +55,15 @@ func (opts *BlobSearchOptions) toConds() builder.Cond {
 | 
			
		||||
 | 
			
		||||
		cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property")))
 | 
			
		||||
	}
 | 
			
		||||
	if opts.Repository != "" {
 | 
			
		||||
		var propsCond builder.Cond = builder.Eq{
 | 
			
		||||
			"package_property.ref_type": packages.PropertyTypePackage,
 | 
			
		||||
			"package_property.name":     container_module.PropertyRepository,
 | 
			
		||||
			"package_property.value":    opts.Repository,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property")))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return cond
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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'",
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
@@ -498,8 +499,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 | 
			
		||||
		// Only show a repo that either has a topic or description.
 | 
			
		||||
		subQueryCond := builder.NewCond()
 | 
			
		||||
 | 
			
		||||
		// Topic checking. Topics is non-null.
 | 
			
		||||
		subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
 | 
			
		||||
		// Topic checking. Topics are present.
 | 
			
		||||
		if setting.Database.UsePostgreSQL { // postgres stores the topics as json and not as text
 | 
			
		||||
			subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"}))
 | 
			
		||||
		} else {
 | 
			
		||||
			subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Description checking. Description not empty.
 | 
			
		||||
		subQueryCond = subQueryCond.Or(builder.Neq{"description": ""})
 | 
			
		||||
 
 | 
			
		||||
@@ -185,7 +185,7 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s
 | 
			
		||||
	return committer.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize
 | 
			
		||||
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
 | 
			
		||||
func UpdateRepoSize(ctx context.Context, repoID, size int64) error {
 | 
			
		||||
	_, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{
 | 
			
		||||
		Size: size,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,8 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
	setting_module "code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"strk.kbt.io/projects/go/libravatar"
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -84,7 +89,26 @@ func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
	if len(v) == 0 {
 | 
			
		||||
		return nil, ErrSettingIsNotExist{key}
 | 
			
		||||
	}
 | 
			
		||||
	return v[key], nil
 | 
			
		||||
	return v[strings.ToLower(key)], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(key string) (string, error) {
 | 
			
		||||
	return cache.GetString(genSettingCacheKey(key), func() (string, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(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)
 | 
			
		||||
	v, _ := strconv.ParseBool(s)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettings returns specific settings
 | 
			
		||||
@@ -108,7 +132,7 @@ func GetSettings(keys []string) (map[string]*Setting, error) {
 | 
			
		||||
type AllSettings map[string]*Setting
 | 
			
		||||
 | 
			
		||||
func (settings AllSettings) Get(key string) Setting {
 | 
			
		||||
	if v, ok := settings[key]; ok {
 | 
			
		||||
	if v, ok := settings[strings.ToLower(key)]; ok {
 | 
			
		||||
		return *v
 | 
			
		||||
	}
 | 
			
		||||
	return Setting{}
 | 
			
		||||
@@ -139,12 +163,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,
 | 
			
		||||
@@ -163,7 +188,14 @@ func SetSetting(setting *Setting) error {
 | 
			
		||||
	if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting.Version++
 | 
			
		||||
 | 
			
		||||
	cc := cache.GetCache()
 | 
			
		||||
	if cc != nil {
 | 
			
		||||
		return cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -213,9 +245,9 @@ var (
 | 
			
		||||
 | 
			
		||||
func Init() error {
 | 
			
		||||
	var disableGravatar bool
 | 
			
		||||
	disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar)
 | 
			
		||||
	disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		disableGravatar = setting.GetDefaultDisableGravatar()
 | 
			
		||||
		disableGravatar = setting_module.GetDefaultDisableGravatar()
 | 
			
		||||
		disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -224,9 +256,9 @@ func Init() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var enableFederatedAvatar bool
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar)
 | 
			
		||||
		enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
 | 
			
		||||
		enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -234,20 +266,30 @@ func Init() error {
 | 
			
		||||
		enableFederatedAvatar = disableGravatarSetting.GetValueBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if setting.OfflineMode {
 | 
			
		||||
	if setting_module.OfflineMode {
 | 
			
		||||
		disableGravatar = true
 | 
			
		||||
		enableFederatedAvatar = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if disableGravatar || !enableFederatedAvatar {
 | 
			
		||||
		var err error
 | 
			
		||||
		GravatarSourceURL, err = url.Parse(setting.GravatarSource)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting.GravatarSource, err)
 | 
			
		||||
		if !GetSettingBool(KeyPictureDisableGravatar) {
 | 
			
		||||
			if err := SetSettingNoVersion(KeyPictureDisableGravatar, "true"); err != nil {
 | 
			
		||||
				return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if GetSettingBool(KeyPictureEnableFederatedAvatar) {
 | 
			
		||||
			if err := SetSettingNoVersion(KeyPictureEnableFederatedAvatar, "false"); err != nil {
 | 
			
		||||
				return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if enableFederatedAvatarSetting.GetValueBool() {
 | 
			
		||||
	if enableFederatedAvatar || !disableGravatar {
 | 
			
		||||
		var err error
 | 
			
		||||
		GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() {
 | 
			
		||||
		LibravatarService = libravatar.New()
 | 
			
		||||
		if GravatarSourceURL.Scheme == "https" {
 | 
			
		||||
			LibravatarService.SetUseHTTPS(true)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,10 +34,14 @@ func TestSettings(t *testing.T) {
 | 
			
		||||
	assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue)
 | 
			
		||||
 | 
			
		||||
	// updated setting
 | 
			
		||||
	updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version}
 | 
			
		||||
	updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version}
 | 
			
		||||
	err = system.SetSetting(updatedSetting)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	value, err := system.GetSetting(keyName)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, updatedSetting.SettingValue, value)
 | 
			
		||||
 | 
			
		||||
	// get all settings
 | 
			
		||||
	settings, err = system.GetAllSettings()
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 @@ 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 := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
func GetSetting(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetUserSettings(uid, []string{key})
 | 
			
		||||
// 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) (string, error) {
 | 
			
		||||
	return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(uid, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return res.SettingValue, 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.GetString(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)
 | 
			
		||||
 
 | 
			
		||||
@@ -1227,7 +1227,10 @@ func GetUserByOpenID(uri string) (*User, error) {
 | 
			
		||||
// GetAdminUser returns the first administrator
 | 
			
		||||
func GetAdminUser() (*User, error) {
 | 
			
		||||
	var admin User
 | 
			
		||||
	has, err := db.GetEngine(db.DefaultContext).Where("is_admin=?", true).Get(&admin)
 | 
			
		||||
	has, err := db.GetEngine(db.DefaultContext).
 | 
			
		||||
		Where("is_admin=?", true).
 | 
			
		||||
		Asc("id"). // Reliably get the admin with the lowest ID.
 | 
			
		||||
		Get(&admin)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if !has {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										98
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										98
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							@@ -51,27 +51,26 @@ 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 +78,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 +113,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))))
 | 
			
		||||
				}
 | 
			
		||||
@@ -1087,6 +1087,9 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat
 | 
			
		||||
			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
 | 
			
		||||
				invalidFiles[fullName] = err
 | 
			
		||||
			} else {
 | 
			
		||||
				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
 | 
			
		||||
					it.Ref = git.BranchPrefix + it.Ref
 | 
			
		||||
				}
 | 
			
		||||
				issueTemplates = append(issueTemplates, it)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pr.Issue.ClosedUnix != 0 {
 | 
			
		||||
		apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,9 @@ func RefURL(repoURL, ref string) string {
 | 
			
		||||
		return repoURL + "/src/branch/" + refName
 | 
			
		||||
	case strings.HasPrefix(ref, TagPrefix):
 | 
			
		||||
		return repoURL + "/src/tag/" + refName
 | 
			
		||||
	case !IsValidSHAPattern(ref):
 | 
			
		||||
		// assume they mean a branch
 | 
			
		||||
		return repoURL + "/src/branch/" + refName
 | 
			
		||||
	default:
 | 
			
		||||
		return repoURL + "/src/commit/" + refName
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							@@ -5,6 +5,7 @@
 | 
			
		||||
package external
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -133,11 +134,13 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
 | 
			
		||||
	if !p.IsInputFile {
 | 
			
		||||
		cmd.Stdin = input
 | 
			
		||||
	}
 | 
			
		||||
	var stderr bytes.Buffer
 | 
			
		||||
	cmd.Stdout = output
 | 
			
		||||
	cmd.Stderr = &stderr
 | 
			
		||||
	process.SetSysProcAttribute(cmd)
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		return fmt.Errorf("%s render run command %s %v failed: %w", p.Name(), commands[0], args, err)
 | 
			
		||||
		return fmt.Errorf("%s render run command %s %v failed: %w\nStderr: %s", p.Name(), commands[0], args, err, stderr.String())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -110,32 +110,6 @@ func (q *ChannelQueue) Flush(timeout time.Duration) error {
 | 
			
		||||
	return q.FlushWithContext(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty
 | 
			
		||||
func (q *ChannelQueue) FlushWithContext(ctx context.Context) error {
 | 
			
		||||
	log.Trace("ChannelQueue: %d Flush", q.qid)
 | 
			
		||||
	paused, _ := q.IsPausedIsResumed()
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			return nil
 | 
			
		||||
		case data, ok := <-q.dataChan:
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			if unhandled := q.handle(data); unhandled != nil {
 | 
			
		||||
				log.Error("Unhandled Data whilst flushing queue %d", q.qid)
 | 
			
		||||
			}
 | 
			
		||||
			atomic.AddInt64(&q.numInQueue, -1)
 | 
			
		||||
		case <-q.baseCtx.Done():
 | 
			
		||||
			return q.baseCtx.Err()
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return ctx.Err()
 | 
			
		||||
		default:
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Shutdown processing from this queue
 | 
			
		||||
func (q *ChannelQueue) Shutdown() {
 | 
			
		||||
	q.lock.Lock()
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"runtime/pprof"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
@@ -168,35 +167,6 @@ func (q *ChannelUniqueQueue) Flush(timeout time.Duration) error {
 | 
			
		||||
	return q.FlushWithContext(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty
 | 
			
		||||
func (q *ChannelUniqueQueue) FlushWithContext(ctx context.Context) error {
 | 
			
		||||
	log.Trace("ChannelUniqueQueue: %d Flush", q.qid)
 | 
			
		||||
	paused, _ := q.IsPausedIsResumed()
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			return nil
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
		select {
 | 
			
		||||
		case data, ok := <-q.dataChan:
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			if unhandled := q.handle(data); unhandled != nil {
 | 
			
		||||
				log.Error("Unhandled Data whilst flushing queue %d", q.qid)
 | 
			
		||||
			}
 | 
			
		||||
			atomic.AddInt64(&q.numInQueue, -1)
 | 
			
		||||
		case <-q.baseCtx.Done():
 | 
			
		||||
			return q.baseCtx.Err()
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return ctx.Err()
 | 
			
		||||
		default:
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Shutdown processing from this queue
 | 
			
		||||
func (q *ChannelUniqueQueue) Shutdown() {
 | 
			
		||||
	log.Trace("ChannelUniqueQueue: %s Shutting down", q.name)
 | 
			
		||||
 
 | 
			
		||||
@@ -464,13 +464,43 @@ func (p *WorkerPool) IsEmpty() bool {
 | 
			
		||||
	return atomic.LoadInt64(&p.numInQueue) == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// contextError returns either ctx.Done(), the base context's error or nil
 | 
			
		||||
func (p *WorkerPool) contextError(ctx context.Context) error {
 | 
			
		||||
	select {
 | 
			
		||||
	case <-p.baseCtx.Done():
 | 
			
		||||
		return p.baseCtx.Err()
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		return ctx.Err()
 | 
			
		||||
	default:
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty
 | 
			
		||||
// NB: The worker will not be registered with the manager.
 | 
			
		||||
func (p *WorkerPool) FlushWithContext(ctx context.Context) error {
 | 
			
		||||
	log.Trace("WorkerPool: %d Flush", p.qid)
 | 
			
		||||
	paused, _ := p.IsPausedIsResumed()
 | 
			
		||||
	for {
 | 
			
		||||
		// Because select will return any case that is satisified at random we precheck here before looking at dataChan.
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-p.dataChan:
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			// Ensure that even if paused that the cancelled error is still sent
 | 
			
		||||
			return p.contextError(ctx)
 | 
			
		||||
		case <-p.baseCtx.Done():
 | 
			
		||||
			return p.baseCtx.Err()
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return ctx.Err()
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		select {
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			return p.contextError(ctx)
 | 
			
		||||
		case data, ok := <-p.dataChan:
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			if unhandled := p.handle(data); unhandled != nil {
 | 
			
		||||
				log.Error("Unhandled Data whilst flushing queue %d", p.qid)
 | 
			
		||||
			}
 | 
			
		||||
@@ -496,6 +526,7 @@ func (p *WorkerPool) doWork(ctx context.Context) {
 | 
			
		||||
	paused, _ := p.IsPausedIsResumed()
 | 
			
		||||
	data := make([]Data, 0, p.batchLength)
 | 
			
		||||
	for {
 | 
			
		||||
		// Because select will return any case that is satisified at random we precheck here before looking at dataChan.
 | 
			
		||||
		select {
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			log.Trace("Worker for Queue %d Pausing", p.qid)
 | 
			
		||||
@@ -516,8 +547,19 @@ func (p *WorkerPool) doWork(ctx context.Context) {
 | 
			
		||||
				log.Trace("Worker shutting down")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			if len(data) > 0 {
 | 
			
		||||
				log.Trace("Handling: %d data, %v", len(data), data)
 | 
			
		||||
				if unhandled := p.handle(data...); unhandled != nil {
 | 
			
		||||
					log.Error("Unhandled Data in queue %d", p.qid)
 | 
			
		||||
				}
 | 
			
		||||
				atomic.AddInt64(&p.numInQueue, -1*int64(len(data)))
 | 
			
		||||
			}
 | 
			
		||||
			log.Trace("Worker shutting down")
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		select {
 | 
			
		||||
		case <-paused:
 | 
			
		||||
			// go back around
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
@@ -286,9 +287,36 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m
 | 
			
		||||
	return repo, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize
 | 
			
		||||
const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
 | 
			
		||||
 | 
			
		||||
// getDirectorySize returns the disk consumption for a given path
 | 
			
		||||
func getDirectorySize(path string) (int64, error) {
 | 
			
		||||
	var size int64
 | 
			
		||||
	err := filepath.WalkDir(path, func(_ string, info os.DirEntry, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if os.IsNotExist(err) { // ignore the error because the file maybe deleted during traversing.
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if info.IsDir() {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		f, err := info.Info()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if (f.Mode() & notRegularFileMode) == 0 {
 | 
			
		||||
			size += f.Size()
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
	return size, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
 | 
			
		||||
func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
 | 
			
		||||
	size, err := util.GetDirectorySize(repo.RepoPath())
 | 
			
		||||
	size, err := getDirectorySize(repo.RepoPath())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("updateSize: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -169,3 +169,13 @@ func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, act.IsPrivate)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetDirectorySize(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	repo, err := repo_model.GetRepositoryByID(1)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	size, err := getDirectorySize(repo.RepoPath())
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, size, repo.Size)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,78 +13,57 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
 | 
			
		||||
	shellquote "github.com/kballard/go-shellquote"
 | 
			
		||||
	ini "gopkg.in/ini.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
var MailService *Mailer
 | 
			
		||||
 | 
			
		||||
func newMailService() {
 | 
			
		||||
	sec := Cfg.Section("mailer")
 | 
			
		||||
func parseMailerConfig(rootCfg *ini.File) {
 | 
			
		||||
	sec := rootCfg.Section("mailer")
 | 
			
		||||
	// Check mailer setting.
 | 
			
		||||
	if !sec.Key("ENABLED").MustBool() {
 | 
			
		||||
		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")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -93,34 +72,99 @@ func newMailService() {
 | 
			
		||||
	if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
 | 
			
		||||
		givenHost := sec.Key("HOST").String()
 | 
			
		||||
		addr, port, err := net.SplitHostPort(givenHost)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		if err != nil && strings.Contains(err.Error(), "missing port in address") {
 | 
			
		||||
			addr = givenHost
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err)
 | 
			
		||||
		}
 | 
			
		||||
		MailService.SMTPAddr = addr
 | 
			
		||||
		MailService.SMTPPort = port
 | 
			
		||||
		if addr == "" {
 | 
			
		||||
			addr = "127.0.0.1"
 | 
			
		||||
		}
 | 
			
		||||
		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,60 +175,38 @@ 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"
 | 
			
		||||
				if MailService.SMTPPort == "" {
 | 
			
		||||
					MailService.SMTPPort = "465"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we want to warn if users use SMTP on a non-local IP;
 | 
			
		||||
	// we might as well take the opportunity to check that it has an IP at all
 | 
			
		||||
	ips := tryResolveAddr(MailService.SMTPAddr)
 | 
			
		||||
	if MailService.Protocol == "smtp" {
 | 
			
		||||
		for _, ip := range ips {
 | 
			
		||||
			if !ip.IsLoopback() {
 | 
			
		||||
				log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
 | 
			
		||||
				break
 | 
			
		||||
	// This check is not needed for sendmail
 | 
			
		||||
	switch MailService.Protocol {
 | 
			
		||||
	case "sendmail":
 | 
			
		||||
		var err error
 | 
			
		||||
		MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err)
 | 
			
		||||
		}
 | 
			
		||||
	case "smtp", "smtps", "smtp+starttls", "smtp+unix":
 | 
			
		||||
		ips := tryResolveAddr(MailService.SMTPAddr)
 | 
			
		||||
		if MailService.Protocol == "smtp" {
 | 
			
		||||
			for _, ip := range ips {
 | 
			
		||||
				if !ip.IsLoopback() {
 | 
			
		||||
					log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
	case "dummy": // just mention and do nothing
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if MailService.From != "" {
 | 
			
		||||
@@ -213,14 +235,6 @@ func newMailService() {
 | 
			
		||||
		MailService.EnvelopeFrom = parsed.Address
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if MailService.Protocol == "sendmail" {
 | 
			
		||||
		var err error
 | 
			
		||||
		MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Failed to parse Sendmail args: %s with error %v", CustomConf, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Mail Service Enabled")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								modules/setting/mailer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								modules/setting/mailer_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package setting
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	ini "gopkg.in/ini.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParseMailerConfig(t *testing.T) {
 | 
			
		||||
	iniFile := ini.Empty()
 | 
			
		||||
	kases := map[string]*Mailer{
 | 
			
		||||
		"smtp.mydomain.com": {
 | 
			
		||||
			SMTPAddr: "smtp.mydomain.com",
 | 
			
		||||
			SMTPPort: "465",
 | 
			
		||||
		},
 | 
			
		||||
		"smtp.mydomain.com:123": {
 | 
			
		||||
			SMTPAddr: "smtp.mydomain.com",
 | 
			
		||||
			SMTPPort: "123",
 | 
			
		||||
		},
 | 
			
		||||
		":123": {
 | 
			
		||||
			SMTPAddr: "127.0.0.1",
 | 
			
		||||
			SMTPPort: "123",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for host, kase := range kases {
 | 
			
		||||
		t.Run(host, func(t *testing.T) {
 | 
			
		||||
			iniFile.DeleteSection("mailer")
 | 
			
		||||
			sec := iniFile.Section("mailer")
 | 
			
		||||
			sec.NewKey("ENABLED", "true")
 | 
			
		||||
			sec.NewKey("HOST", host)
 | 
			
		||||
 | 
			
		||||
			// Check mailer setting
 | 
			
		||||
			parseMailerConfig(iniFile)
 | 
			
		||||
 | 
			
		||||
			assert.EqualValues(t, kase.SMTPAddr, MailService.SMTPAddr)
 | 
			
		||||
			assert.EqualValues(t, kase.SMTPPort, MailService.SMTPPort)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -69,7 +69,7 @@ func newPictureService() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDefaultDisableGravatar() bool {
 | 
			
		||||
	return !OfflineMode
 | 
			
		||||
	return OfflineMode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool {
 | 
			
		||||
 
 | 
			
		||||
@@ -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, "/"))
 | 
			
		||||
@@ -1295,7 +1334,7 @@ func NewServices() {
 | 
			
		||||
	newCacheService()
 | 
			
		||||
	newSessionService()
 | 
			
		||||
	newCORSService()
 | 
			
		||||
	newMailService()
 | 
			
		||||
	parseMailerConfig(Cfg)
 | 
			
		||||
	newRegisterMailService()
 | 
			
		||||
	newNotifyMailService()
 | 
			
		||||
	newProxyService()
 | 
			
		||||
@@ -1312,5 +1351,5 @@ func NewServices() {
 | 
			
		||||
// NewServicesForInstall initializes the services for install
 | 
			
		||||
func NewServicesForInstall() {
 | 
			
		||||
	newService()
 | 
			
		||||
	newMailService()
 | 
			
		||||
	parseMailerConfig(Cfg)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,48 +12,62 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// sitemapFileLimit contains the maximum size of a sitemap file
 | 
			
		||||
const sitemapFileLimit = 50 * 1024 * 1024
 | 
			
		||||
const (
 | 
			
		||||
	sitemapFileLimit = 50 * 1024 * 1024 // the maximum size of a sitemap file
 | 
			
		||||
	urlsLimit        = 50000
 | 
			
		||||
 | 
			
		||||
// Url represents a single sitemap entry
 | 
			
		||||
	schemaURL        = "http://www.sitemaps.org/schemas/sitemap/0.9"
 | 
			
		||||
	urlsetName       = "urlset"
 | 
			
		||||
	sitemapindexName = "sitemapindex"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// URL represents a single sitemap entry
 | 
			
		||||
type URL struct {
 | 
			
		||||
	URL     string     `xml:"loc"`
 | 
			
		||||
	LastMod *time.Time `xml:"lastmod,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SitemapUrl represents a sitemap
 | 
			
		||||
// Sitemap represents a sitemap
 | 
			
		||||
type Sitemap struct {
 | 
			
		||||
	XMLName   xml.Name
 | 
			
		||||
	Namespace string `xml:"xmlns,attr"`
 | 
			
		||||
 | 
			
		||||
	URLs []URL `xml:"url"`
 | 
			
		||||
	URLs     []URL `xml:"url"`
 | 
			
		||||
	Sitemaps []URL `xml:"sitemap"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSitemap creates a sitemap
 | 
			
		||||
func NewSitemap() *Sitemap {
 | 
			
		||||
	return &Sitemap{
 | 
			
		||||
		XMLName:   xml.Name{Local: "urlset"},
 | 
			
		||||
		Namespace: "http://www.sitemaps.org/schemas/sitemap/0.9",
 | 
			
		||||
		XMLName:   xml.Name{Local: urlsetName},
 | 
			
		||||
		Namespace: schemaURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSitemap creates a sitemap index.
 | 
			
		||||
// NewSitemapIndex creates a sitemap index.
 | 
			
		||||
func NewSitemapIndex() *Sitemap {
 | 
			
		||||
	return &Sitemap{
 | 
			
		||||
		XMLName:   xml.Name{Local: "sitemapindex"},
 | 
			
		||||
		Namespace: "http://www.sitemaps.org/schemas/sitemap/0.9",
 | 
			
		||||
		XMLName:   xml.Name{Local: sitemapindexName},
 | 
			
		||||
		Namespace: schemaURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add adds a URL to the sitemap
 | 
			
		||||
func (s *Sitemap) Add(u URL) {
 | 
			
		||||
	s.URLs = append(s.URLs, u)
 | 
			
		||||
	if s.XMLName.Local == sitemapindexName {
 | 
			
		||||
		s.Sitemaps = append(s.Sitemaps, u)
 | 
			
		||||
	} else {
 | 
			
		||||
		s.URLs = append(s.URLs, u)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write writes the sitemap to a response
 | 
			
		||||
// WriteTo writes the sitemap to a response
 | 
			
		||||
func (s *Sitemap) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	if len(s.URLs) > 50000 {
 | 
			
		||||
		return 0, fmt.Errorf("The sitemap contains too many URLs: %d", len(s.URLs))
 | 
			
		||||
	if l := len(s.URLs); l > urlsLimit {
 | 
			
		||||
		return 0, fmt.Errorf("The sitemap contains %d URLs, but only %d are allowed", l, urlsLimit)
 | 
			
		||||
	}
 | 
			
		||||
	if l := len(s.Sitemaps); l > urlsLimit {
 | 
			
		||||
		return 0, fmt.Errorf("The sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit)
 | 
			
		||||
	}
 | 
			
		||||
	buf := bytes.NewBufferString(xml.Header)
 | 
			
		||||
	if err := xml.NewEncoder(buf).Encode(s); err != nil {
 | 
			
		||||
@@ -63,7 +77,7 @@ func (s *Sitemap) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	if buf.Len() > sitemapFileLimit {
 | 
			
		||||
		return 0, fmt.Errorf("The sitemap is too big: %d", buf.Len())
 | 
			
		||||
		return 0, fmt.Errorf("The sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit)
 | 
			
		||||
	}
 | 
			
		||||
	return buf.WriteTo(w)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package sitemap
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -15,63 +14,154 @@ import (
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestOk(t *testing.T) {
 | 
			
		||||
	testReal := func(s *Sitemap, name string, urls []URL, expected string) {
 | 
			
		||||
		for _, url := range urls {
 | 
			
		||||
			s.Add(url)
 | 
			
		||||
		}
 | 
			
		||||
		buf := &bytes.Buffer{}
 | 
			
		||||
		_, err := s.WriteTo(buf)
 | 
			
		||||
		assert.NoError(t, nil, err)
 | 
			
		||||
		assert.Equal(t, xml.Header+"<"+name+" xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"+expected+"</"+name+">\n", buf.String())
 | 
			
		||||
	}
 | 
			
		||||
	test := func(urls []URL, expected string) {
 | 
			
		||||
		testReal(NewSitemap(), "urlset", urls, expected)
 | 
			
		||||
		testReal(NewSitemapIndex(), "sitemapindex", urls, expected)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func TestNewSitemap(t *testing.T) {
 | 
			
		||||
	ts := time.Unix(1651322008, 0).UTC()
 | 
			
		||||
 | 
			
		||||
	test(
 | 
			
		||||
		[]URL{},
 | 
			
		||||
		"",
 | 
			
		||||
	)
 | 
			
		||||
	test(
 | 
			
		||||
		[]URL{
 | 
			
		||||
			{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		urls    []URL
 | 
			
		||||
		want    string
 | 
			
		||||
		wantErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty",
 | 
			
		||||
			urls: []URL{},
 | 
			
		||||
			want: xml.Header + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"" +
 | 
			
		||||
				"</urlset>\n",
 | 
			
		||||
		},
 | 
			
		||||
		"<url><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></url>",
 | 
			
		||||
	)
 | 
			
		||||
	test(
 | 
			
		||||
		[]URL{
 | 
			
		||||
			{URL: "https://gitea.io/test2", LastMod: nil},
 | 
			
		||||
		{
 | 
			
		||||
			name: "regular",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<url><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></url>" +
 | 
			
		||||
				"</urlset>\n",
 | 
			
		||||
		},
 | 
			
		||||
		"<url><loc>https://gitea.io/test2</loc></url>",
 | 
			
		||||
	)
 | 
			
		||||
	test(
 | 
			
		||||
		[]URL{
 | 
			
		||||
			{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
			{URL: "https://gitea.io/test2", LastMod: nil},
 | 
			
		||||
		{
 | 
			
		||||
			name: "without lastmod",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1"},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<url><loc>https://gitea.io/test1</loc></url>" +
 | 
			
		||||
				"</urlset>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
				{URL: "https://gitea.io/test2", LastMod: nil},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<url><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></url>" +
 | 
			
		||||
				"<url><loc>https://gitea.io/test2</loc></url>" +
 | 
			
		||||
				"</urlset>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "too many urls",
 | 
			
		||||
			urls:    make([]URL, 50001),
 | 
			
		||||
			wantErr: "The sitemap contains 50001 URLs, but only 50000 are allowed",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "too big file",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: strings.Repeat("b", 50*1024*1024+1)},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "The sitemap has 52428932 bytes, but only 52428800 are allowed",
 | 
			
		||||
		},
 | 
			
		||||
		"<url><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></url>"+
 | 
			
		||||
			"<url><loc>https://gitea.io/test2</loc></url>",
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTooManyURLs(t *testing.T) {
 | 
			
		||||
	s := NewSitemap()
 | 
			
		||||
	for i := 0; i < 50001; i++ {
 | 
			
		||||
		s.Add(URL{URL: fmt.Sprintf("https://gitea.io/test%d", i)})
 | 
			
		||||
	}
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	_, err := s.WriteTo(buf)
 | 
			
		||||
	assert.EqualError(t, err, "The sitemap contains too many URLs: 50001")
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			s := NewSitemap()
 | 
			
		||||
			for _, url := range tt.urls {
 | 
			
		||||
				s.Add(url)
 | 
			
		||||
			}
 | 
			
		||||
			buf := &bytes.Buffer{}
 | 
			
		||||
			_, err := s.WriteTo(buf)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				assert.EqualError(t, err, tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.Equalf(t, tt.want, buf.String(), "NewSitemap()")
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSitemapTooBig(t *testing.T) {
 | 
			
		||||
	s := NewSitemap()
 | 
			
		||||
	s.Add(URL{URL: strings.Repeat("b", sitemapFileLimit)})
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	_, err := s.WriteTo(buf)
 | 
			
		||||
	assert.EqualError(t, err, "The sitemap is too big: 52428931")
 | 
			
		||||
func TestNewSitemapIndex(t *testing.T) {
 | 
			
		||||
	ts := time.Unix(1651322008, 0).UTC()
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		urls    []URL
 | 
			
		||||
		want    string
 | 
			
		||||
		wantErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty",
 | 
			
		||||
			urls: []URL{},
 | 
			
		||||
			want: xml.Header + `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"" +
 | 
			
		||||
				"</sitemapindex>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "regular",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<sitemap><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></sitemap>" +
 | 
			
		||||
				"</sitemapindex>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "without lastmod",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1"},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<sitemap><loc>https://gitea.io/test1</loc></sitemap>" +
 | 
			
		||||
				"</sitemapindex>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: "https://gitea.io/test1", LastMod: &ts},
 | 
			
		||||
				{URL: "https://gitea.io/test2", LastMod: nil},
 | 
			
		||||
			},
 | 
			
		||||
			want: xml.Header + `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
 | 
			
		||||
				"<sitemap><loc>https://gitea.io/test1</loc><lastmod>2022-04-30T12:33:28Z</lastmod></sitemap>" +
 | 
			
		||||
				"<sitemap><loc>https://gitea.io/test2</loc></sitemap>" +
 | 
			
		||||
				"</sitemapindex>\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "too many sitemaps",
 | 
			
		||||
			urls:    make([]URL, 50001),
 | 
			
		||||
			wantErr: "The sitemap contains 50001 sub-sitemaps, but only 50000 are allowed",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "too big file",
 | 
			
		||||
			urls: []URL{
 | 
			
		||||
				{URL: strings.Repeat("b", 50*1024*1024+1)},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "The sitemap has 52428952 bytes, but only 52428800 are allowed",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			s := NewSitemapIndex()
 | 
			
		||||
			for _, url := range tt.urls {
 | 
			
		||||
				s.Add(url)
 | 
			
		||||
			}
 | 
			
		||||
			buf := &bytes.Buffer{}
 | 
			
		||||
			_, err := s.WriteTo(buf)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				assert.EqualError(t, err, tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.Equalf(t, tt.want, buf.String(), "NewSitemapIndex()")
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,7 @@ type CreatePushMirrorOption struct {
 | 
			
		||||
	RemoteUsername string `json:"remote_username"`
 | 
			
		||||
	RemotePassword string `json:"remote_password"`
 | 
			
		||||
	Interval       string `json:"interval"`
 | 
			
		||||
	SyncOnCommit   bool   `json:"sync_on_commit"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushMirror represents information of a push mirror
 | 
			
		||||
@@ -22,4 +23,5 @@ type PushMirror struct {
 | 
			
		||||
	LastUpdateUnix string `json:"last_update"`
 | 
			
		||||
	LastError      string `json:"last_error"`
 | 
			
		||||
	Interval       string `json:"interval"`
 | 
			
		||||
	SyncOnCommit   bool   `json:"sync_on_commit"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 != "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,8 +76,15 @@ func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
 | 
			
		||||
	compilingTemplates = false
 | 
			
		||||
	if !setting.IsProd {
 | 
			
		||||
		watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
 | 
			
		||||
			PathsCallback:   walkTemplateFiles,
 | 
			
		||||
			BetweenCallback: renderer.CompileTemplates,
 | 
			
		||||
			PathsCallback: walkTemplateFiles,
 | 
			
		||||
			BetweenCallback: func() {
 | 
			
		||||
				defer func() {
 | 
			
		||||
					if err := recover(); err != nil {
 | 
			
		||||
						log.Error("PANIC: %v\n%s", err, log.Stack(2))
 | 
			
		||||
					}
 | 
			
		||||
				}()
 | 
			
		||||
				renderer.CompileTemplates()
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	return context.WithValue(ctx, rendererKey, renderer), renderer
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,20 +23,6 @@ func EnsureAbsolutePath(path, absoluteBase string) string {
 | 
			
		||||
	return filepath.Join(absoluteBase, path)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const notRegularFileMode os.FileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
 | 
			
		||||
 | 
			
		||||
// GetDirectorySize returns the disk consumption for a given path
 | 
			
		||||
func GetDirectorySize(path string) (int64, error) {
 | 
			
		||||
	var size int64
 | 
			
		||||
	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
 | 
			
		||||
		if info != nil && (info.Mode()¬RegularFileMode) == 0 {
 | 
			
		||||
			size += info.Size()
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
	return size, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDir returns true if given path is a directory,
 | 
			
		||||
// or returns false when it's a file or does not exist.
 | 
			
		||||
func IsDir(dir string) (bool, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -497,6 +497,7 @@ team_not_exist = The team does not exist.
 | 
			
		||||
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
 | 
			
		||||
cannot_add_org_to_team = An organization cannot be added as a team member.
 | 
			
		||||
duplicate_invite_to_team = The user was already invited as a team member.
 | 
			
		||||
organization_leave_success = You have successfully left the organization %s.
 | 
			
		||||
 | 
			
		||||
invalid_ssh_key = Can not verify your SSH key: %s
 | 
			
		||||
invalid_gpg_key = Can not verify your GPG key: %s
 | 
			
		||||
 
 | 
			
		||||
@@ -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,65 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
 | 
			
		||||
 | 
			
		||||
	contentStore := packages_module.NewContentStore()
 | 
			
		||||
 | 
			
		||||
	uploadVersion, err := getOrCreateUploadVersion(pi)
 | 
			
		||||
	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)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return createFileForBlob(ctx, uploadVersion, pb)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !exists {
 | 
			
		||||
			if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
 | 
			
		||||
				log.Error("Error deleting package blob from content store: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return pb, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mountBlob mounts the specific blob to a different package
 | 
			
		||||
func mountBlob(pi *packages_service.PackageInfo, pb *packages_model.PackageBlob) error {
 | 
			
		||||
	uploadVersion, err := getOrCreateUploadVersion(pi)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return db.WithTx(func(ctx context.Context) error {
 | 
			
		||||
		return createFileForBlob(ctx, uploadVersion, pb)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getOrCreateUploadVersion(pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
 | 
			
		||||
	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,52 +133,40 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Error inserting package blob: %v", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		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)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
 | 
			
		||||
 | 
			
		||||
		pf := &packages_model.PackageFile{
 | 
			
		||||
			VersionID:    pv.ID,
 | 
			
		||||
			BlobID:       pb.ID,
 | 
			
		||||
			Name:         filename,
 | 
			
		||||
			LowerName:    filename,
 | 
			
		||||
			CompositeKey: packages_model.EmptyFileKey,
 | 
			
		||||
		}
 | 
			
		||||
		if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
 | 
			
		||||
			if err == packages_model.ErrDuplicatePackageFile {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			log.Error("Error inserting package file: %v", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
 | 
			
		||||
			log.Error("Error setting package file property: %v", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		uploadVersion = pv
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !exists {
 | 
			
		||||
			if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
 | 
			
		||||
				log.Error("Error deleting package blob from content store: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
	uploadVersionMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	return uploadVersion, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error {
 | 
			
		||||
	filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
 | 
			
		||||
 | 
			
		||||
	pf := &packages_model.PackageFile{
 | 
			
		||||
		VersionID:    pv.ID,
 | 
			
		||||
		BlobID:       pb.ID,
 | 
			
		||||
		Name:         filename,
 | 
			
		||||
		LowerName:    filename,
 | 
			
		||||
		CompositeKey: packages_model.EmptyFileKey,
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
	if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
 | 
			
		||||
		if err == packages_model.ErrDuplicatePackageFile {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
		log.Error("Error inserting package file: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return pb, nil
 | 
			
		||||
	if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
 | 
			
		||||
		log.Error("Error setting package file property: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteBlob(ownerID int64, image, digest string) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,11 +195,16 @@ func InitiateUploadBlob(ctx *context.Context) {
 | 
			
		||||
	mount := ctx.FormTrim("mount")
 | 
			
		||||
	from := ctx.FormTrim("from")
 | 
			
		||||
	if mount != "" {
 | 
			
		||||
		blob, _ := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
			Image:  from,
 | 
			
		||||
			Digest: mount,
 | 
			
		||||
		blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
			
		||||
			Repository: from,
 | 
			
		||||
			Digest:     mount,
 | 
			
		||||
		})
 | 
			
		||||
		if blob != nil {
 | 
			
		||||
			if err := mountBlob(&packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
 | 
			
		||||
				apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
				Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
 | 
			
		||||
				ContentDigest: mount,
 | 
			
		||||
@@ -406,7 +413,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 +555,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 +695,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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user