mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			30 Commits
		
	
	
		
			v1.19.0-rc
			...
			v1.18.0-rc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | 
							
								
								
									
										176
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,182 @@ This changelog goes through all the changes that have been made in each release
 | 
			
		||||
without substantial changes to our git log; to see the highlights of what has
 | 
			
		||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
 | 
			
		||||
 | 
			
		||||
## [1.18.0-rc1](https://github.com/go-gitea/gitea/releases/tag/v1.18.0-rc1) - 2022-11-15
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Remove U2F support (#20141)
 | 
			
		||||
* 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
 | 
			
		||||
  * 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
 | 
			
		||||
  * 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
 | 
			
		||||
  * 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.3](https://github.com/go-gitea/gitea/releases/tag/v1.17.3) - 2022-10-15
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								go.sum
									
									
									
									
									
								
							@@ -1608,8 +1608,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 +1721,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 +1876,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 +1892,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=
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,12 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 | 
			
		||||
const DefaultAvatarPixelSize = 28
 | 
			
		||||
const (
 | 
			
		||||
	// DefaultAvatarClass is the default class of a rendered avatar
 | 
			
		||||
	DefaultAvatarClass = "ui avatar vm"
 | 
			
		||||
	// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 | 
			
		||||
	DefaultAvatarPixelSize = 28
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
 | 
			
		||||
type EmailHash struct {
 | 
			
		||||
@@ -150,10 +154,11 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return DefaultAvatarLink()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatarSetting, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatar := enableFederatedAvatarSetting.GetValueBool()
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil {
 | 
			
		||||
	if enableFederatedAvatar && system_model.LibravatarService != nil {
 | 
			
		||||
		emailHash := saveEmailHash(email)
 | 
			
		||||
		if final {
 | 
			
		||||
			// for final link, we can spend more time on slow external query
 | 
			
		||||
@@ -171,8 +176,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
 | 
			
		||||
		return urlStr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	if disableGravatar != nil && !disableGravatar.GetValueBool() {
 | 
			
		||||
	disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
 | 
			
		||||
	disableGravatar := disableGravatarSetting.GetValueBool()
 | 
			
		||||
	if !disableGravatar {
 | 
			
		||||
		// copy GravatarSourceURL, because we will modify its Path.
 | 
			
		||||
		avatarURLCopy := *system_model.GravatarSourceURL
 | 
			
		||||
		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,10 @@ func (s *Setting) TableName() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Setting) GetValueBool() bool {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b, _ := strconv.ParseBool(s.SettingValue)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
@@ -75,8 +80,8 @@ func IsErrDataExpired(err error) bool {
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns specific setting
 | 
			
		||||
func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
// GetSettingNoCache returns specific setting without using the cache
 | 
			
		||||
func GetSettingNoCache(key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetSettings([]string{key})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -87,6 +92,24 @@ func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
	return v[key], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(key string) (*Setting, error) {
 | 
			
		||||
	return cache.Get(genSettingCacheKey(key), func() (*Setting, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return res, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingBool return bool value of setting,
 | 
			
		||||
// none existing keys and errors are ignored and result in false
 | 
			
		||||
func GetSettingBool(key string) bool {
 | 
			
		||||
	s, _ := GetSetting(key)
 | 
			
		||||
	return s.GetValueBool()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettings returns specific settings
 | 
			
		||||
func GetSettings(keys []string) (map[string]*Setting, error) {
 | 
			
		||||
	for i := 0; i < len(keys); i++ {
 | 
			
		||||
@@ -139,12 +162,13 @@ func GetAllSettings() (AllSettings, error) {
 | 
			
		||||
 | 
			
		||||
// DeleteSetting deletes a specific setting for a user
 | 
			
		||||
func DeleteSetting(setting *Setting) error {
 | 
			
		||||
	cache.Remove(genSettingCacheKey(setting.SettingKey))
 | 
			
		||||
	_, err := db.GetEngine(db.DefaultContext).Delete(setting)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetSettingNoVersion(key, value string) error {
 | 
			
		||||
	s, err := GetSetting(key)
 | 
			
		||||
	s, err := GetSettingNoCache(key)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		return SetSetting(&Setting{
 | 
			
		||||
			SettingKey:   key,
 | 
			
		||||
@@ -160,9 +184,13 @@ func SetSettingNoVersion(key, value string) error {
 | 
			
		||||
 | 
			
		||||
// SetSetting updates a users' setting for a specific key
 | 
			
		||||
func SetSetting(setting *Setting) error {
 | 
			
		||||
	if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
 | 
			
		||||
	_, err := cache.Set(genSettingCacheKey(setting.SettingKey), func() (*Setting, error) {
 | 
			
		||||
		return setting, upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting.Version++
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -213,7 +241,7 @@ var (
 | 
			
		||||
 | 
			
		||||
func Init() error {
 | 
			
		||||
	var disableGravatar bool
 | 
			
		||||
	disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar)
 | 
			
		||||
	disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		disableGravatar = setting.GetDefaultDisableGravatar()
 | 
			
		||||
		disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
 | 
			
		||||
@@ -224,7 +252,7 @@ func Init() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var enableFederatedAvatar bool
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
 | 
			
		||||
	if IsErrSettingIsNotExist(err) {
 | 
			
		||||
		enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar)
 | 
			
		||||
		enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,3 +9,8 @@ const (
 | 
			
		||||
	KeyPictureDisableGravatar       = "picture.disable_gravatar"
 | 
			
		||||
	KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// genSettingCacheKey returns the cache key for some configuration
 | 
			
		||||
func genSettingCacheKey(key string) string {
 | 
			
		||||
	return "system.setting." + key
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 | 
			
		||||
		&user_model.UserBadge{UserID: u.ID},
 | 
			
		||||
		&pull_model.AutoMerge{DoerID: u.ID},
 | 
			
		||||
		&pull_model.ReviewState{UserID: u.ID},
 | 
			
		||||
		&user_model.Redirect{RedirectUserID: u.ID},
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		return fmt.Errorf("deleteBeans: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,11 +68,9 @@ func (u *User) AvatarLinkWithSize(size int) string {
 | 
			
		||||
	useLocalAvatar := false
 | 
			
		||||
	autoGenerateAvatar := false
 | 
			
		||||
 | 
			
		||||
	var disableGravatar bool
 | 
			
		||||
	disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
	if disableGravatarSetting != nil {
 | 
			
		||||
		disableGravatar = disableGravatarSetting.GetValueBool()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disableGravatar := disableGravatarSetting.GetValueBool()
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case u.UseCustomAvatar:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
@@ -47,9 +48,25 @@ func IsErrUserSettingIsNotExist(err error) bool {
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns specific setting
 | 
			
		||||
// genSettingCacheKey returns the cache key for some configuration
 | 
			
		||||
func genSettingCacheKey(userID int64, key string) string {
 | 
			
		||||
	return fmt.Sprintf("user_%d.setting.%s", userID, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetUserSettings(uid, []string{key})
 | 
			
		||||
	return cache.Get(genSettingCacheKey(uid, key), func() (*Setting, error) {
 | 
			
		||||
		res, err := GetSettingNoCache(uid, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return res, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingNoCache returns specific setting without using the cache
 | 
			
		||||
func GetSettingNoCache(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	v, err := GetSettings(uid, []string{key})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -59,8 +76,8 @@ func GetSetting(uid int64, key string) (*Setting, error) {
 | 
			
		||||
	return v[key], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserSettings returns specific settings from user
 | 
			
		||||
func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) {
 | 
			
		||||
// GetSettings returns specific settings from user
 | 
			
		||||
func GetSettings(uid int64, keys []string) (map[string]*Setting, error) {
 | 
			
		||||
	settings := make([]*Setting, 0, len(keys))
 | 
			
		||||
	if err := db.GetEngine(db.DefaultContext).
 | 
			
		||||
		Where("user_id=?", uid).
 | 
			
		||||
@@ -105,6 +122,7 @@ func GetUserSetting(userID int64, key string, def ...string) (string, error) {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting := &Setting{UserID: userID, SettingKey: key}
 | 
			
		||||
	has, err := db.GetEngine(db.DefaultContext).Get(setting)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -124,7 +142,10 @@ func DeleteUserSetting(userID int64, key string) error {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cache.Remove(genSettingCacheKey(userID, key))
 | 
			
		||||
	_, err := db.GetEngine(db.DefaultContext).Delete(&Setting{UserID: userID, SettingKey: key})
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -133,7 +154,12 @@ func SetUserSetting(userID int64, key, value string) error {
 | 
			
		||||
	if err := validateUserSettingKey(key); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return upsertUserSettingValue(userID, key, value)
 | 
			
		||||
 | 
			
		||||
	_, err := cache.Set(genSettingCacheKey(userID, key), func() (string, error) {
 | 
			
		||||
		return value, upsertUserSettingValue(userID, key, value)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func upsertUserSettingValue(userID int64, key, value string) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ func TestSettings(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// get specific setting
 | 
			
		||||
	settings, err := user_model.GetUserSettings(99, []string{keyName})
 | 
			
		||||
	settings, err := user_model.GetSettings(99, []string{keyName})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, settings, 1)
 | 
			
		||||
	assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import (
 | 
			
		||||
// GetKeyPair function returns a user's private and public keys
 | 
			
		||||
func GetKeyPair(user *user_model.User) (pub, priv string, err error) {
 | 
			
		||||
	var settings map[string]*user_model.Setting
 | 
			
		||||
	settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
 | 
			
		||||
	settings, err = user_model.GetSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	} else if len(settings) == 0 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										131
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										131
									
								
								modules/cache/cache.go
									
									
									
									
										vendored
									
									
								
							@@ -46,32 +46,64 @@ func GetCache() mc.Cache {
 | 
			
		||||
	return conn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get returns the key value from cache with callback when no key exists in cache
 | 
			
		||||
func Get[V interface{}](key string, getFunc func() (V, error)) (V, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
	if value, ok := cached.(V); ok {
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, err := getFunc()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return value, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set updates and returns the key value in the cache with callback. The old value is only removed if the updateFunc() is successful
 | 
			
		||||
func Set[V interface{}](key string, valueFunc func() (V, error)) (V, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return valueFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, err := valueFunc()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return value, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetString returns the key value from cache with callback when no key exists in cache
 | 
			
		||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value string
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	value := conn.Get(key)
 | 
			
		||||
	if v, ok := value.(string); ok {
 | 
			
		||||
		return v, nil
 | 
			
		||||
 | 
			
		||||
	if value, ok := cached.(string); ok {
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
	if v, ok := value.(fmt.Stringer); ok {
 | 
			
		||||
		return v.String(), nil
 | 
			
		||||
 | 
			
		||||
	if stringer, ok := cached.(fmt.Stringer); ok {
 | 
			
		||||
		return stringer.String(), nil
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s", conn.Get(key)), nil
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s", cached), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetInt returns key value from cache with callback when no key exists in cache
 | 
			
		||||
@@ -79,30 +111,33 @@ func GetInt(key string, getFunc func() (int, error)) (int, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value int
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	switch value := conn.Get(key).(type) {
 | 
			
		||||
 | 
			
		||||
	switch v := cached.(type) {
 | 
			
		||||
	case int:
 | 
			
		||||
		return value, nil
 | 
			
		||||
		return v, nil
 | 
			
		||||
	case string:
 | 
			
		||||
		v, err := strconv.Atoi(value)
 | 
			
		||||
		value, err := strconv.Atoi(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		return v, nil
 | 
			
		||||
		return value, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("Unsupported cached value type: %v", value)
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -111,30 +146,34 @@ func GetInt64(key string, getFunc func() (int64, error)) (int64, error) {
 | 
			
		||||
	if conn == nil || setting.CacheService.TTL == 0 {
 | 
			
		||||
		return getFunc()
 | 
			
		||||
	}
 | 
			
		||||
	if !conn.IsExist(key) {
 | 
			
		||||
		var (
 | 
			
		||||
			value int64
 | 
			
		||||
			err   error
 | 
			
		||||
		)
 | 
			
		||||
		if value, err = getFunc(); err != nil {
 | 
			
		||||
 | 
			
		||||
	cached := conn.Get(key)
 | 
			
		||||
 | 
			
		||||
	if cached == nil {
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
		err = conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
	switch value := conn.Get(key).(type) {
 | 
			
		||||
 | 
			
		||||
	switch v := conn.Get(key).(type) {
 | 
			
		||||
	case int64:
 | 
			
		||||
		return value, nil
 | 
			
		||||
		return v, nil
 | 
			
		||||
	case string:
 | 
			
		||||
		v, err := strconv.ParseInt(value, 10, 64)
 | 
			
		||||
		value, err := strconv.ParseInt(v, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		return v, nil
 | 
			
		||||
		return value, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("Unsupported cached value type: %v", value)
 | 
			
		||||
		value, err := getFunc()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return value, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,36 +346,55 @@ func (ctx *Context) RespHeader() http.Header {
 | 
			
		||||
	return ctx.Resp.Header()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ServeHeaderOptions struct {
 | 
			
		||||
	ContentType        string // defaults to "application/octet-stream"
 | 
			
		||||
	ContentTypeCharset string
 | 
			
		||||
	Disposition        string // defaults to "attachment"
 | 
			
		||||
	Filename           string
 | 
			
		||||
	CacheDuration      time.Duration // defaults to 5 minutes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeContent serves content to http request
 | 
			
		||||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
 | 
			
		||||
	ctx.SetServeHeaders(name)
 | 
			
		||||
	ctx.SetServeHeaders(&ServeHeaderOptions{
 | 
			
		||||
		Filename: 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadStream returns the request body or the first form file
 | 
			
		||||
// Only form files need to get closed.
 | 
			
		||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -205,6 +205,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 | 
			
		||||
		// find stopwatches without existing issue
 | 
			
		||||
		genericOrphanCheck("Orphaned Stopwatches without existing Issue",
 | 
			
		||||
			"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
 | 
			
		||||
		// find redirects without existing user.
 | 
			
		||||
		genericOrphanCheck("Orphaned Redirects without existing redirect user",
 | 
			
		||||
			"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for _, c := range consistencyChecks {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,9 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
	numReposUpdated := 0
 | 
			
		||||
	err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
 | 
			
		||||
		numRepos++
 | 
			
		||||
		runOpts := &git.RunOpts{Dir: repo.RepoPath()}
 | 
			
		||||
		_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
 | 
			
		||||
		_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(runOpts)
 | 
			
		||||
 | 
			
		||||
		head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts)
 | 
			
		||||
		head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
 | 
			
		||||
		// what we expect: default branch is valid, and HEAD points to it
 | 
			
		||||
		if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch {
 | 
			
		||||
@@ -49,7 +47,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// otherwise, let's try fixing HEAD
 | 
			
		||||
		err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", repo.DefaultBranch).Run(runOpts)
 | 
			
		||||
		err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", git.BranchPrefix+repo.DefaultBranch).Run(&git.RunOpts{Dir: repo.RepoPath()})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err)
 | 
			
		||||
			return nil
 | 
			
		||||
@@ -65,7 +63,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
 | 
			
		||||
		logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated)
 | 
			
		||||
	} else {
 | 
			
		||||
		if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 {
 | 
			
		||||
			logger.Info("All %d repos have their HEADs in the correct state")
 | 
			
		||||
			logger.Info("All %d repos have their HEADs in the correct state", numRepos)
 | 
			
		||||
		} else {
 | 
			
		||||
			if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 {
 | 
			
		||||
				logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos)
 | 
			
		||||
 
 | 
			
		||||
@@ -202,8 +202,11 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		||||
	if opts == nil {
 | 
			
		||||
		opts = &RunOpts{}
 | 
			
		||||
	}
 | 
			
		||||
	if opts.Timeout <= 0 {
 | 
			
		||||
		opts.Timeout = defaultCommandExecutionTimeout
 | 
			
		||||
 | 
			
		||||
	// We must not change the provided options
 | 
			
		||||
	timeout := opts.Timeout
 | 
			
		||||
	if timeout <= 0 {
 | 
			
		||||
		timeout = defaultCommandExecutionTimeout
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(opts.Dir) == 0 {
 | 
			
		||||
@@ -238,7 +241,7 @@ func (c *Command) Run(opts *RunOpts) error {
 | 
			
		||||
	if opts.UseContextTimeout {
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc)
 | 
			
		||||
		ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc)
 | 
			
		||||
	}
 | 
			
		||||
	defer finished()
 | 
			
		||||
 | 
			
		||||
@@ -339,9 +342,20 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
 | 
			
		||||
	}
 | 
			
		||||
	stdoutBuf := &bytes.Buffer{}
 | 
			
		||||
	stderrBuf := &bytes.Buffer{}
 | 
			
		||||
	opts.Stdout = stdoutBuf
 | 
			
		||||
	opts.Stderr = stderrBuf
 | 
			
		||||
	err := c.Run(opts)
 | 
			
		||||
 | 
			
		||||
	// We must not change the provided options as it could break future calls - therefore make a copy.
 | 
			
		||||
	newOpts := &RunOpts{
 | 
			
		||||
		Env:               opts.Env,
 | 
			
		||||
		Timeout:           opts.Timeout,
 | 
			
		||||
		UseContextTimeout: opts.UseContextTimeout,
 | 
			
		||||
		Dir:               opts.Dir,
 | 
			
		||||
		Stdout:            stdoutBuf,
 | 
			
		||||
		Stderr:            stderrBuf,
 | 
			
		||||
		Stdin:             opts.Stdin,
 | 
			
		||||
		PipelineFunc:      opts.PipelineFunc,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := c.Run(newOpts)
 | 
			
		||||
	stderr = stderrBuf.Bytes()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,9 @@ import (
 | 
			
		||||
	"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,
 | 
			
		||||
@@ -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 = filepath.Base(it.FileName)
 | 
			
		||||
			it.About, _ = util.SplitStringAtByteN(it.Content, 80)
 | 
			
		||||
		} else {
 | 
			
		||||
			it.Content = templateBody
 | 
			
		||||
			if it.About == "" {
 | 
			
		||||
				if _, err := markdown.ExtractMetadata(string(content), compatibleTemplate); err == nil && compatibleTemplate.About != "" {
 | 
			
		||||
					it.About = compatibleTemplate.About
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if typ == api.IssueTemplateTypeYaml {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,82 +9,86 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func validateMetadata(it structs.IssueTemplate) bool {
 | 
			
		||||
	/*
 | 
			
		||||
		A legacy to keep the unit tests working.
 | 
			
		||||
		Copied from the method "func (it IssueTemplate) Valid() bool", the original method has been removed.
 | 
			
		||||
		Because it becomes quite complicated to validate an issue template which is support yaml form now.
 | 
			
		||||
		The new way to validate an issue template is to call the Validate in modules/issue/template,
 | 
			
		||||
	*/
 | 
			
		||||
/*
 | 
			
		||||
IssueTemplate is a legacy to keep the unit tests working.
 | 
			
		||||
Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
 | 
			
		||||
*/
 | 
			
		||||
type IssueTemplate struct {
 | 
			
		||||
	Name   string   `json:"name" yaml:"name"`
 | 
			
		||||
	Title  string   `json:"title" yaml:"title"`
 | 
			
		||||
	About  string   `json:"about" yaml:"about"`
 | 
			
		||||
	Labels []string `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref    string   `json:"ref" yaml:"ref"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (it *IssueTemplate) Valid() bool {
 | 
			
		||||
	return strings.TrimSpace(it.Name) != "" && strings.TrimSpace(it.About) != ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExtractMetadata(t *testing.T) {
 | 
			
		||||
	t.Run("ValidFrontAndBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, bodyTest, body)
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoFirstSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoLastSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "", body)
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExtractMetadataBytes(t *testing.T) {
 | 
			
		||||
	t.Run("ValidFrontAndBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, bodyTest, string(body))
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoFirstSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoLastSeparator", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta)
 | 
			
		||||
		assert.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("NoBody", func(t *testing.T) {
 | 
			
		||||
		var meta structs.IssueTemplate
 | 
			
		||||
		var meta IssueTemplate
 | 
			
		||||
		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, "", string(body))
 | 
			
		||||
		assert.Equal(t, metaTest, meta)
 | 
			
		||||
		assert.True(t, validateMetadata(meta))
 | 
			
		||||
		assert.True(t, meta.Valid())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +101,7 @@ labels:
 | 
			
		||||
  - bug
 | 
			
		||||
  - "test label"`
 | 
			
		||||
	bodyTest = "This is the body"
 | 
			
		||||
	metaTest = structs.IssueTemplate{
 | 
			
		||||
	metaTest = IssueTemplate{
 | 
			
		||||
		Name:   "Test",
 | 
			
		||||
		About:  "A Test",
 | 
			
		||||
		Title:  "Test Title",
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -962,6 +963,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 {
 | 
			
		||||
@@ -1150,6 +1156,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 +1181,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 +1198,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, "/"))
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,12 @@
 | 
			
		||||
package structs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StateType issue state type
 | 
			
		||||
@@ -143,14 +147,47 @@ type IssueFormField struct {
 | 
			
		||||
// IssueTemplate represents an issue template for a repository
 | 
			
		||||
// swagger:model
 | 
			
		||||
type IssueTemplate struct {
 | 
			
		||||
	Name     string            `json:"name" yaml:"name"`
 | 
			
		||||
	Title    string            `json:"title" yaml:"title"`
 | 
			
		||||
	About    string            `json:"about" yaml:"about"` // Using "description" in a template file is compatible
 | 
			
		||||
	Labels   []string          `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref      string            `json:"ref" yaml:"ref"`
 | 
			
		||||
	Content  string            `json:"content" yaml:"-"`
 | 
			
		||||
	Fields   []*IssueFormField `json:"body" yaml:"body"`
 | 
			
		||||
	FileName string            `json:"file_name" yaml:"-"`
 | 
			
		||||
	Name     string              `json:"name" yaml:"name"`
 | 
			
		||||
	Title    string              `json:"title" yaml:"title"`
 | 
			
		||||
	About    string              `json:"about" yaml:"about"` // Using "description" in a template file is compatible
 | 
			
		||||
	Labels   IssueTemplateLabels `json:"labels" yaml:"labels"`
 | 
			
		||||
	Ref      string              `json:"ref" yaml:"ref"`
 | 
			
		||||
	Content  string              `json:"content" yaml:"-"`
 | 
			
		||||
	Fields   []*IssueFormField   `json:"body" yaml:"body"`
 | 
			
		||||
	FileName string              `json:"file_name" yaml:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type IssueTemplateLabels []string
 | 
			
		||||
 | 
			
		||||
func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
 | 
			
		||||
	var labels []string
 | 
			
		||||
	if value.IsZero() {
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	switch value.Kind {
 | 
			
		||||
	case yaml.ScalarNode:
 | 
			
		||||
		str := ""
 | 
			
		||||
		err := value.Decode(&str)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		for _, v := range strings.Split(str, ",") {
 | 
			
		||||
			if v = strings.TrimSpace(v); v == "" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			labels = append(labels, v)
 | 
			
		||||
		}
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	case yaml.SequenceNode:
 | 
			
		||||
		if err := value.Decode(&labels); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*l = labels
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplateType defines issue template type
 | 
			
		||||
@@ -163,14 +200,14 @@ const (
 | 
			
		||||
 | 
			
		||||
// Type returns the type of IssueTemplate, can be "md", "yaml" or empty for known
 | 
			
		||||
func (it IssueTemplate) Type() IssueTemplateType {
 | 
			
		||||
	if it.Name == "config.yaml" || it.Name == "config.yml" {
 | 
			
		||||
	if base := path.Base(it.FileName); base == "config.yaml" || base == "config.yml" {
 | 
			
		||||
		// ignore config.yaml which is a special configuration file
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	if ext := filepath.Ext(it.FileName); ext == ".md" {
 | 
			
		||||
	if ext := path.Ext(it.FileName); ext == ".md" {
 | 
			
		||||
		return IssueTemplateTypeMarkdown
 | 
			
		||||
	} else if ext == ".yaml" || ext == ".yml" {
 | 
			
		||||
		return "yaml"
 | 
			
		||||
		return IssueTemplateTypeYaml
 | 
			
		||||
	}
 | 
			
		||||
	return IssueTemplateTypeYaml
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										106
									
								
								modules/structs/issue_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								modules/structs/issue_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package structs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIssueTemplate_Type(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		fileName string
 | 
			
		||||
		want     IssueTemplateType
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.yaml",
 | 
			
		||||
			want:     IssueTemplateTypeYaml,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.md",
 | 
			
		||||
			want:     IssueTemplateTypeMarkdown,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/bug_report.txt",
 | 
			
		||||
			want:     "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			fileName: ".gitea/ISSUE_TEMPLATE/config.yaml",
 | 
			
		||||
			want:     "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.fileName, func(t *testing.T) {
 | 
			
		||||
			it := IssueTemplate{
 | 
			
		||||
				FileName: tt.fileName,
 | 
			
		||||
			}
 | 
			
		||||
			assert.Equal(t, tt.want, it.Type())
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIssueTemplateLabels_UnmarshalYAML(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		content string
 | 
			
		||||
		tmpl    *IssueTemplate
 | 
			
		||||
		want    *IssueTemplate
 | 
			
		||||
		wantErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:    "array",
 | 
			
		||||
			content: `labels: ["a", "b", "c"]`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"a", "b", "c"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "string",
 | 
			
		||||
			content: `labels: "a,b,c"`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"a", "b", "c"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "empty",
 | 
			
		||||
			content: `labels:`,
 | 
			
		||||
			tmpl: &IssueTemplate{
 | 
			
		||||
				Labels: []string{"should_be_overwrote"},
 | 
			
		||||
			},
 | 
			
		||||
			want: &IssueTemplate{
 | 
			
		||||
				Labels: nil,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "error",
 | 
			
		||||
			content: `
 | 
			
		||||
labels:
 | 
			
		||||
  a: aa
 | 
			
		||||
  b: bb
 | 
			
		||||
`,
 | 
			
		||||
			tmpl:    &IssueTemplate{},
 | 
			
		||||
			wantErr: "line 3: cannot unmarshal !!map into IssueTemplateLabels",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			err := yaml.Unmarshal([]byte(tt.content), tt.tmpl)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				assert.EqualError(t, err, tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.Equal(t, tt.want, tt.tmpl)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@ package system
 | 
			
		||||
 | 
			
		||||
// RuntimeState contains app state for runtime, and we can save remote version for update checker here in future
 | 
			
		||||
type RuntimeState struct {
 | 
			
		||||
	LastAppPath string `json:"last_app_path"`
 | 
			
		||||
	LastAppPath    string `json:"last_app_path"`
 | 
			
		||||
	LastCustomConf string `json:"last_custom_conf"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns the item name
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func genKey(key string) string {
 | 
			
		||||
	return "system.setting." + key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSetting returns the setting value via the key
 | 
			
		||||
func GetSetting(key string) (string, error) {
 | 
			
		||||
	return cache.GetString(genKey(key), func() (string, error) {
 | 
			
		||||
		res, err := system.GetSetting(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return res.SettingValue, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingBool return bool value of setting,
 | 
			
		||||
// none existing keys and errors are ignored and result in false
 | 
			
		||||
func GetSettingBool(key string) bool {
 | 
			
		||||
	s, _ := GetSetting(key)
 | 
			
		||||
	b, _ := strconv.ParseBool(s)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSetting sets the setting value
 | 
			
		||||
func SetSetting(key, value string, version int) error {
 | 
			
		||||
	cache.Remove(genKey(key))
 | 
			
		||||
 | 
			
		||||
	return system.SetSetting(&system.Setting{
 | 
			
		||||
		SettingKey:   key,
 | 
			
		||||
		SettingValue: value,
 | 
			
		||||
		Version:      version,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package system
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func genUserKey(userID int64, key string) string {
 | 
			
		||||
	return fmt.Sprintf("user_%d.setting.%s", userID, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserSetting returns the user setting value via the key
 | 
			
		||||
func GetUserSetting(userID int64, key string) (string, error) {
 | 
			
		||||
	return cache.GetString(genUserKey(userID, key), func() (string, error) {
 | 
			
		||||
		res, err := user.GetSetting(userID, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return res.SettingValue, nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUserSetting sets the user setting value
 | 
			
		||||
func SetUserSetting(userID int64, key, value string) error {
 | 
			
		||||
	cache.Remove(genUserKey(userID, key))
 | 
			
		||||
 | 
			
		||||
	return user.SetUserSetting(userID, key, value)
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	system_module "code.gitea.io/gitea/modules/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
			
		||||
@@ -87,7 +86,7 @@ func NewFuncMap() []template.FuncMap {
 | 
			
		||||
			return setting.AssetVersion
 | 
			
		||||
		},
 | 
			
		||||
		"DisableGravatar": func() bool {
 | 
			
		||||
			return system_module.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
			return system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
 | 
			
		||||
		},
 | 
			
		||||
		"DefaultShowFullName": func() bool {
 | 
			
		||||
			return setting.UI.DefaultShowFullName
 | 
			
		||||
@@ -647,7 +646,7 @@ func SVG(icon string, others ...interface{}) template.HTML {
 | 
			
		||||
 | 
			
		||||
// Avatar renders user avatars. args: user, size (int), class (string)
 | 
			
		||||
func Avatar(item interface{}, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar vm", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
 | 
			
		||||
	switch t := item.(type) {
 | 
			
		||||
	case *user_model.User:
 | 
			
		||||
@@ -678,7 +677,7 @@ func AvatarByAction(action *activities_model.Action, others ...interface{}) temp
 | 
			
		||||
 | 
			
		||||
// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
 | 
			
		||||
func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
 | 
			
		||||
	src := repo.RelAvatarLink()
 | 
			
		||||
	if src != "" {
 | 
			
		||||
@@ -689,7 +688,7 @@ func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTM
 | 
			
		||||
 | 
			
		||||
// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
 | 
			
		||||
func AvatarByEmail(email, name string, others ...interface{}) template.HTML {
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...)
 | 
			
		||||
	size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
 | 
			
		||||
	src := avatars.GenerateEmailAvatarFastLink(email, size*setting.Avatar.RenderedSizeFactor)
 | 
			
		||||
 | 
			
		||||
	if src != "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -103,5 +103,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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,19 @@ import (
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://www.python.org/dev/peps/pep-0503/#normalized-names
 | 
			
		||||
// https://peps.python.org/pep-0426/#name
 | 
			
		||||
var normalizer = strings.NewReplacer(".", "-", "_", "-")
 | 
			
		||||
var nameMatcher = regexp.MustCompile(`\A[a-zA-Z0-9\.\-_]+\z`)
 | 
			
		||||
var nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
 | 
			
		||||
 | 
			
		||||
// https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
var versionMatcher = regexp.MustCompile(`^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$`)
 | 
			
		||||
// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
var versionMatcher = regexp.MustCompile(`\Av?` +
 | 
			
		||||
	`(?:[0-9]+!)?` + // epoch
 | 
			
		||||
	`[0-9]+(?:\.[0-9]+)*` + // release segment
 | 
			
		||||
	`(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release
 | 
			
		||||
	`(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release
 | 
			
		||||
	`(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release
 | 
			
		||||
	`(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version
 | 
			
		||||
	`\z`)
 | 
			
		||||
 | 
			
		||||
func apiError(ctx *context.Context, status int, obj interface{}) {
 | 
			
		||||
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
 | 
			
		||||
@@ -121,7 +128,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	packageName := normalizer.Replace(ctx.Req.FormValue("name"))
 | 
			
		||||
	packageVersion := ctx.Req.FormValue("version")
 | 
			
		||||
	if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) {
 | 
			
		||||
	if !isValidNameAndVersion(packageName, packageVersion) {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, "invalid name or version")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -139,7 +146,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
				Name:        packageName,
 | 
			
		||||
				Version:     packageVersion,
 | 
			
		||||
			},
 | 
			
		||||
			SemverCompatible: true,
 | 
			
		||||
			SemverCompatible: false,
 | 
			
		||||
			Creator:          ctx.Doer,
 | 
			
		||||
			Metadata: &pypi_module.Metadata{
 | 
			
		||||
				Author:          ctx.Req.FormValue("author"),
 | 
			
		||||
@@ -170,3 +177,7 @@ func UploadPackageFile(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusCreated)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isValidNameAndVersion(packageName, packageVersion string) bool {
 | 
			
		||||
	return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								routers/api/packages/pypi/pypi_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								routers/api/packages/pypi/pypi_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package pypi
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIsValidNameAndVersion(t *testing.T) {
 | 
			
		||||
	// The test cases below were created from the following Python PEPs:
 | 
			
		||||
	// https://peps.python.org/pep-0426/#name
 | 
			
		||||
	// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
 | 
			
		||||
 | 
			
		||||
	// Valid Cases
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("A", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("Test.Name.1234", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test_name", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "v1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "2012.4"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1-alpha"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1a1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0b2.r345.dev456"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1!1.0.1"))
 | 
			
		||||
	assert.True(t, isValidNameAndVersion("test-name", "1.0.1+local.1"))
 | 
			
		||||
 | 
			
		||||
	// Invalid Cases
 | 
			
		||||
	assert.False(t, isValidNameAndVersion(".test-name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test!name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("-test-name", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name-", "1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "a1.0.1"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
 | 
			
		||||
	assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
 | 
			
		||||
}
 | 
			
		||||
@@ -77,7 +77,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetServeHeaders(filename + ".gz")
 | 
			
		||||
	ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
		Filename: filename + ".gz",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	zw := gzip.NewWriter(ctx.Resp)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
@@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetServeHeaders(filename)
 | 
			
		||||
	ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
		Filename: filename,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	zw := zlib.NewWriter(ctx.Resp)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
 
 | 
			
		||||
@@ -898,7 +898,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
					m.Group("/{index}", func() {
 | 
			
		||||
						m.Combo("").Get(repo.GetIssue).
 | 
			
		||||
							Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
 | 
			
		||||
							Delete(reqToken(), reqAdmin(), repo.DeleteIssue)
 | 
			
		||||
							Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
 | 
			
		||||
						m.Group("/comments", func() {
 | 
			
		||||
							m.Combo("").Get(repo.ListIssueComments).
 | 
			
		||||
								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
 | 
			
		||||
 
 | 
			
		||||
@@ -1443,7 +1443,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 | 
			
		||||
		end = totalNumberOfFiles
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, end-start)
 | 
			
		||||
	lenFiles := end - start
 | 
			
		||||
	if lenFiles < 0 {
 | 
			
		||||
		lenFiles = 0
 | 
			
		||||
	}
 | 
			
		||||
	apiFiles := make([]*api.ChangedFile, 0, lenFiles)
 | 
			
		||||
	for i := start; i < end; i++ {
 | 
			
		||||
		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package common
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -53,50 +52,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
 | 
			
		||||
		buf = buf[:n]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
 | 
			
		||||
 | 
			
		||||
	if size >= 0 {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileName := path.Base(filePath)
 | 
			
		||||
	sniffedType := typesniffer.DetectContentType(buf)
 | 
			
		||||
	isPlain := sniffedType.IsText() || ctx.FormBool("render")
 | 
			
		||||
	mimeType := ""
 | 
			
		||||
	charset := ""
 | 
			
		||||
 | 
			
		||||
	if setting.MimeTypeMap.Enabled {
 | 
			
		||||
		fileExtension := strings.ToLower(filepath.Ext(fileName))
 | 
			
		||||
		mimeType = setting.MimeTypeMap.Map[fileExtension]
 | 
			
		||||
	opts := &context.ServeHeaderOptions{
 | 
			
		||||
		Filename: path.Base(filePath),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mimeType == "" {
 | 
			
		||||
	sniffedType := typesniffer.DetectContentType(buf)
 | 
			
		||||
	isPlain := sniffedType.IsText() || ctx.FormBool("render")
 | 
			
		||||
 | 
			
		||||
	if setting.MimeTypeMap.Enabled {
 | 
			
		||||
		fileExtension := strings.ToLower(filepath.Ext(filePath))
 | 
			
		||||
		opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.ContentType == "" {
 | 
			
		||||
		if sniffedType.IsBrowsableBinaryType() {
 | 
			
		||||
			mimeType = sniffedType.GetMimeType()
 | 
			
		||||
			opts.ContentType = sniffedType.GetMimeType()
 | 
			
		||||
		} else if isPlain {
 | 
			
		||||
			mimeType = "text/plain"
 | 
			
		||||
			opts.ContentType = "text/plain"
 | 
			
		||||
		} else {
 | 
			
		||||
			mimeType = typesniffer.ApplicationOctetStream
 | 
			
		||||
			opts.ContentType = typesniffer.ApplicationOctetStream
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isPlain {
 | 
			
		||||
		var charset string
 | 
			
		||||
		charset, err = charsetModule.DetectEncoding(buf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
 | 
			
		||||
			charset = "utf-8"
 | 
			
		||||
		}
 | 
			
		||||
		opts.ContentTypeCharset = strings.ToLower(charset)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if charset != "" {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", mimeType)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
 | 
			
		||||
	isSVG := sniffedType.IsSvgImage()
 | 
			
		||||
 | 
			
		||||
	// serve types that can present a security risk with CSP
 | 
			
		||||
@@ -109,16 +102,12 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disposition := "inline"
 | 
			
		||||
	opts.Disposition = "inline"
 | 
			
		||||
	if isSVG && !setting.UI.SVG.Enabled {
 | 
			
		||||
		disposition = "attachment"
 | 
			
		||||
		opts.Disposition = "attachment"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
 | 
			
		||||
	encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName)
 | 
			
		||||
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName)
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
 | 
			
		||||
	_, err = ctx.Resp.Write(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,21 +76,31 @@ func InitGitServices() {
 | 
			
		||||
	mustInit(repo_service.Init)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func syncAppPathForGit(ctx context.Context) error {
 | 
			
		||||
func syncAppConfForGit(ctx context.Context) error {
 | 
			
		||||
	runtimeState := new(system.RuntimeState)
 | 
			
		||||
	if err := system.AppState.Get(runtimeState); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updated := false
 | 
			
		||||
	if runtimeState.LastAppPath != setting.AppPath {
 | 
			
		||||
		log.Info("AppPath changed from '%s' to '%s'", runtimeState.LastAppPath, setting.AppPath)
 | 
			
		||||
		runtimeState.LastAppPath = setting.AppPath
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
	if runtimeState.LastCustomConf != setting.CustomConf {
 | 
			
		||||
		log.Info("CustomConf changed from '%s' to '%s'", runtimeState.LastCustomConf, setting.CustomConf)
 | 
			
		||||
		runtimeState.LastCustomConf = setting.CustomConf
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if updated {
 | 
			
		||||
		log.Info("re-sync repository hooks ...")
 | 
			
		||||
		mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
 | 
			
		||||
 | 
			
		||||
		log.Info("re-write ssh public keys ...")
 | 
			
		||||
		mustInit(asymkey_model.RewriteAllPublicKeys)
 | 
			
		||||
 | 
			
		||||
		runtimeState.LastAppPath = setting.AppPath
 | 
			
		||||
		return system.AppState.Set(runtimeState)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -153,7 +163,7 @@ func GlobalInitInstalled(ctx context.Context) {
 | 
			
		||||
	mustInit(repo_migrations.Init)
 | 
			
		||||
	eventsource.GetManager().Init()
 | 
			
		||||
 | 
			
		||||
	mustInitCtx(ctx, syncAppPathForGit)
 | 
			
		||||
	mustInitCtx(ctx, syncAppConfForGit)
 | 
			
		||||
 | 
			
		||||
	mustInit(ssh.Init)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -473,12 +473,16 @@ func SubmitInstall(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
 | 
			
		||||
 | 
			
		||||
	var internalToken string
 | 
			
		||||
	if internalToken, err = generate.NewInternalToken(); err != nil {
 | 
			
		||||
		ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
 | 
			
		||||
		return
 | 
			
		||||
	// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
 | 
			
		||||
	// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
 | 
			
		||||
	if setting.InternalToken == "" {
 | 
			
		||||
		var internalToken string
 | 
			
		||||
		if internalToken, err = generate.NewInternalToken(); err != nil {
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
 | 
			
		||||
	}
 | 
			
		||||
	cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
 | 
			
		||||
 | 
			
		||||
	// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
 | 
			
		||||
	if setting.SecretKey == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 | 
			
		||||
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
 | 
			
		||||
	return &smtp.Source{
 | 
			
		||||
		Auth:           form.SMTPAuth,
 | 
			
		||||
		Addr:           form.SMTPAddr,
 | 
			
		||||
		Host:           form.SMTPHost,
 | 
			
		||||
		Port:           form.SMTPPort,
 | 
			
		||||
		AllowedDomains: form.AllowedDomains,
 | 
			
		||||
		ForceSMTPS:     form.ForceSMTPS,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	system_module "code.gitea.io/gitea/modules/system"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/mailer"
 | 
			
		||||
 | 
			
		||||
@@ -203,7 +202,11 @@ func ChangeConfig(ctx *context.Context) {
 | 
			
		||||
	value := ctx.FormString("value")
 | 
			
		||||
	version := ctx.FormInt("version")
 | 
			
		||||
 | 
			
		||||
	if err := system_module.SetSetting(key, value, version); err != nil {
 | 
			
		||||
	if err := system_model.SetSetting(&system_model.Setting{
 | 
			
		||||
		SettingKey:   key,
 | 
			
		||||
		SettingValue: value,
 | 
			
		||||
		Version:      version,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		log.Error("set setting failed: %v", err)
 | 
			
		||||
		ctx.JSON(http.StatusOK, map[string]string{
 | 
			
		||||
			"err": ctx.Tr("admin.config.set_setting_failed", key),
 | 
			
		||||
 
 | 
			
		||||
@@ -783,6 +783,13 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Register last login
 | 
			
		||||
	user.SetLastLogin()
 | 
			
		||||
	if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
 | 
			
		||||
		ctx.ServerError("UpdateUserCols", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Flash.Success(ctx.Tr("auth.account_activated"))
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
package feed
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	activities_model "code.gitea.io/gitea/models/activities"
 | 
			
		||||
@@ -59,7 +58,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
 | 
			
		||||
 | 
			
		||||
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
 | 
			
		||||
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
 | 
			
		||||
	ctx.Resp.WriteHeader(http.StatusOK)
 | 
			
		||||
	if formatType == "atom" {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
 | 
			
		||||
		if err := feed.WriteAtom(ctx.Resp); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -597,7 +597,10 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
 | 
			
		||||
	m.Group("", func() {
 | 
			
		||||
		m.Get("/favicon.ico", func(ctx *context.Context) {
 | 
			
		||||
			ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
 | 
			
		||||
			ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
				Filename: "favicon.png",
 | 
			
		||||
			})
 | 
			
		||||
			http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png"))
 | 
			
		||||
		})
 | 
			
		||||
		m.Group("/{username}", func() {
 | 
			
		||||
			m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })
 | 
			
		||||
 
 | 
			
		||||
@@ -58,10 +58,10 @@ var ErrUnsupportedLoginType = errors.New("Login source is unknown")
 | 
			
		||||
func Authenticate(a smtp.Auth, source *Source) error {
 | 
			
		||||
	tlsConfig := &tls.Config{
 | 
			
		||||
		InsecureSkipVerify: source.SkipVerify,
 | 
			
		||||
		ServerName:         source.Addr,
 | 
			
		||||
		ServerName:         source.Host,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conn, err := net.Dial("tcp", net.JoinHostPort(source.Addr, strconv.Itoa(source.Port)))
 | 
			
		||||
	conn, err := net.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -71,7 +71,7 @@ func Authenticate(a smtp.Auth, source *Source) error {
 | 
			
		||||
		conn = tls.Client(conn, tlsConfig)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := smtp.NewClient(conn, source.Addr)
 | 
			
		||||
	client, err := smtp.NewClient(conn, source.Host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create NewClient: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import (
 | 
			
		||||
// Source holds configuration for the SMTP login source.
 | 
			
		||||
type Source struct {
 | 
			
		||||
	Auth           string
 | 
			
		||||
	Addr           string
 | 
			
		||||
	Host           string
 | 
			
		||||
	Port           int
 | 
			
		||||
	AllowedDomains string `xorm:"TEXT"`
 | 
			
		||||
	ForceSMTPS     bool
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
 | 
			
		||||
	var auth smtp.Auth
 | 
			
		||||
	switch source.Auth {
 | 
			
		||||
	case PlainAuthentication:
 | 
			
		||||
		auth = smtp.PlainAuth("", userName, password, source.Addr)
 | 
			
		||||
		auth = smtp.PlainAuth("", userName, password, source.Host)
 | 
			
		||||
	case LoginAuthentication:
 | 
			
		||||
		auth = &loginAuthenticator{userName, password}
 | 
			
		||||
	case CRAMMD5Authentication:
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ type AuthenticationForm struct {
 | 
			
		||||
	IsActive                      bool
 | 
			
		||||
	IsSyncEnabled                 bool
 | 
			
		||||
	SMTPAuth                      string
 | 
			
		||||
	SMTPAddr                      string
 | 
			
		||||
	SMTPHost                      string
 | 
			
		||||
	SMTPPort                      int
 | 
			
		||||
	AllowedDomains                string
 | 
			
		||||
	SecurityProtocol              int `binding:"Range(0,2)"`
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@@ -218,21 +219,21 @@ func DeleteUnadoptedRepository(doer, u *user_model.User, repoName string) error
 | 
			
		||||
	return util.RemoveAll(repoPath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type unadoptedRrepositories struct {
 | 
			
		||||
type unadoptedRepositories struct {
 | 
			
		||||
	repositories []string
 | 
			
		||||
	index        int
 | 
			
		||||
	start        int
 | 
			
		||||
	end          int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (unadopted *unadoptedRrepositories) add(repository string) {
 | 
			
		||||
func (unadopted *unadoptedRepositories) add(repository string) {
 | 
			
		||||
	if unadopted.index >= unadopted.start && unadopted.index < unadopted.end {
 | 
			
		||||
		unadopted.repositories = append(unadopted.repositories, repository)
 | 
			
		||||
	}
 | 
			
		||||
	unadopted.index++
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRrepositories) error {
 | 
			
		||||
func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error {
 | 
			
		||||
	if len(repoNamesToCheck) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -264,7 +265,7 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad
 | 
			
		||||
	}
 | 
			
		||||
	for _, repoName := range repoNamesToCheck {
 | 
			
		||||
		if !repoNames.Contains(repoName) {
 | 
			
		||||
			unadopted.add(filepath.Join(userName, repoName))
 | 
			
		||||
			unadopted.add(path.Join(userName, repoName)) // These are not used as filepaths - but as reponames - therefore use path.Join not filepath.Join
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -292,7 +293,7 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in
 | 
			
		||||
	var repoNamesToCheck []string
 | 
			
		||||
 | 
			
		||||
	start := (opts.Page - 1) * opts.PageSize
 | 
			
		||||
	unadopted := &unadoptedRrepositories{
 | 
			
		||||
	unadopted := &unadoptedRepositories{
 | 
			
		||||
		repositories: make([]string, 0, opts.PageSize),
 | 
			
		||||
		start:        start,
 | 
			
		||||
		end:          start + opts.PageSize,
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import (
 | 
			
		||||
func TestCheckUnadoptedRepositories_Add(t *testing.T) {
 | 
			
		||||
	start := 10
 | 
			
		||||
	end := 20
 | 
			
		||||
	unadopted := &unadoptedRrepositories{
 | 
			
		||||
	unadopted := &unadoptedRepositories{
 | 
			
		||||
		start: start,
 | 
			
		||||
		end:   end,
 | 
			
		||||
		index: 0,
 | 
			
		||||
@@ -39,7 +39,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
 | 
			
		||||
	//
 | 
			
		||||
	// Non existent user
 | 
			
		||||
	//
 | 
			
		||||
	unadopted := &unadoptedRrepositories{start: 0, end: 100}
 | 
			
		||||
	unadopted := &unadoptedRepositories{start: 0, end: 100}
 | 
			
		||||
	err := checkUnadoptedRepositories("notauser", []string{"repo"}, unadopted)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, 0, len(unadopted.repositories))
 | 
			
		||||
@@ -50,14 +50,14 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
 | 
			
		||||
	userName := "user2"
 | 
			
		||||
	repoName := "repo2"
 | 
			
		||||
	unadoptedRepoName := "unadopted"
 | 
			
		||||
	unadopted = &unadoptedRrepositories{start: 0, end: 100}
 | 
			
		||||
	unadopted = &unadoptedRepositories{start: 0, end: 100}
 | 
			
		||||
	err = checkUnadoptedRepositories(userName, []string{repoName, unadoptedRepoName}, unadopted)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, []string{path.Join(userName, unadoptedRepoName)}, unadopted.repositories)
 | 
			
		||||
	//
 | 
			
		||||
	// Existing (adopted) repository is not returned
 | 
			
		||||
	//
 | 
			
		||||
	unadopted = &unadoptedRrepositories{start: 0, end: 100}
 | 
			
		||||
	unadopted = &unadoptedRepositories{start: 0, end: 100}
 | 
			
		||||
	err = checkUnadoptedRepositories(userName, []string{repoName}, unadopted)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, 0, len(unadopted.repositories))
 | 
			
		||||
 
 | 
			
		||||
@@ -32,16 +32,16 @@
 | 
			
		||||
							{{end}}
 | 
			
		||||
						{{end}}
 | 
			
		||||
						{{if .IsFork}}
 | 
			
		||||
							<span class="tooltip" data-content="{{$.locale.Tr "repo.fork"}}" data-position="bottom center">{{svg "octicon-repo-forked"}}</span>
 | 
			
		||||
							<span class="tooltip df" data-content="{{$.locale.Tr "repo.fork"}}" data-position="bottom center">{{svg "octicon-repo-forked"}}</span>
 | 
			
		||||
						{{else if .IsMirror}}
 | 
			
		||||
							<span class="tooltip" data-content="{{$.locale.Tr "mirror"}}" data-position="bottom center">{{svg "octicon-mirror"}}</span>
 | 
			
		||||
							<span class="tooltip df" data-content="{{$.locale.Tr "mirror"}}" data-position="bottom center">{{svg "octicon-mirror"}}</span>
 | 
			
		||||
						{{end}}
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="metas df ac">
 | 
			
		||||
				<div class="metas df ac text grey">
 | 
			
		||||
					{{if .PrimaryLanguage}}
 | 
			
		||||
						<a href="{{$.Link}}?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}">
 | 
			
		||||
							<span class="text grey df ac mr-3"><i class="color-icon mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
 | 
			
		||||
						<a class="muted" href="{{$.Link}}?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}">
 | 
			
		||||
							<span class="df ac mr-3"><i class="color-icon mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
 | 
			
		||||
						</a>
 | 
			
		||||
					{{end}}
 | 
			
		||||
					{{if not $.DisableStars}}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,8 +68,8 @@
 | 
			
		||||
							<div class="item">
 | 
			
		||||
								<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong class="team-name">{{.Name}}</strong></a>
 | 
			
		||||
								<p class="text grey">
 | 
			
		||||
									<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.NumMembers}}</strong> {{$.locale.Tr "org.lower_members"}}</a> ·
 | 
			
		||||
									<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/repositories"><strong>{{.NumRepos}}</strong> {{$.locale.Tr "org.lower_repositories"}}</a>
 | 
			
		||||
									<a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.NumMembers}}</strong> {{$.locale.Tr "org.lower_members"}}</a> ·
 | 
			
		||||
									<a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/repositories"><strong>{{.NumRepos}}</strong> {{$.locale.Tr "org.lower_repositories"}}</a>
 | 
			
		||||
								</p>
 | 
			
		||||
							</div>
 | 
			
		||||
						{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
		<div class="ui top attached header comment-header df ac sb">
 | 
			
		||||
			<div class="comment-header-left df ac">
 | 
			
		||||
				{{if .OriginalAuthor}}
 | 
			
		||||
					<span class="text black mr-2">
 | 
			
		||||
					<span class="text black bold mr-2">
 | 
			
		||||
						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
 | 
			
		||||
						{{.OriginalAuthor}}
 | 
			
		||||
					</span>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
					<div class="ui top attached header comment-header df ac sb">
 | 
			
		||||
						<div class="comment-header-left df ac">
 | 
			
		||||
							{{if .Issue.OriginalAuthor}}
 | 
			
		||||
								<span class="text black">
 | 
			
		||||
								<span class="text black bold">
 | 
			
		||||
									{{svg (MigrationIcon .Repository.GetOriginalURLHostname)}}
 | 
			
		||||
									{{.Issue.OriginalAuthor}}
 | 
			
		||||
								</span>
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
									{{avatar .Issue.Poster}}
 | 
			
		||||
								</a>
 | 
			
		||||
								<span class="text grey">
 | 
			
		||||
									<a class="author"{{if gt .Issue.Poster.ID 0}} href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
 | 
			
		||||
									{{template "shared/user/authorlink" .Issue.Poster}}
 | 
			
		||||
									{{.locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}}
 | 
			
		||||
								</span>
 | 
			
		||||
							{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
					<div class="ui top attached header comment-header df ac sb">
 | 
			
		||||
						<div class="comment-header-left df ac">
 | 
			
		||||
							{{if .OriginalAuthor}}
 | 
			
		||||
								<span class="text black mr-2">
 | 
			
		||||
								<span class="text black bold mr-2">
 | 
			
		||||
									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 | 
			
		||||
									{{.OriginalAuthor}}
 | 
			
		||||
								</span>
 | 
			
		||||
@@ -42,9 +42,7 @@
 | 
			
		||||
									</a>
 | 
			
		||||
								{{end}}
 | 
			
		||||
								<span class="text grey">
 | 
			
		||||
									<a class="author"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 | 
			
		||||
										{{.Poster.GetDisplayName}}
 | 
			
		||||
									</a>
 | 
			
		||||
									{{template "shared/user/authorlink" .Poster}}
 | 
			
		||||
									{{$.locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
 | 
			
		||||
								</span>
 | 
			
		||||
							{{end}}
 | 
			
		||||
@@ -151,14 +149,14 @@
 | 
			
		||||
				<span class="badge">{{svg "octicon-bookmark"}}</span>
 | 
			
		||||
				{{template "shared/user/avatarlink" .Poster}}
 | 
			
		||||
				{{if eq .RefAction 3}}<del>{{end}}
 | 
			
		||||
				<span class="text grey">
 | 
			
		||||
				<span class="text grey muted-links">
 | 
			
		||||
					{{template "shared/user/authorlink" .Poster}}
 | 
			
		||||
					{{$.locale.Tr $refTr (.EventTag|Escape) $createdStr (.RefCommentHTMLURL|Escape) $refFrom | Safe}}
 | 
			
		||||
				</span>
 | 
			
		||||
				{{if eq .RefAction 3}}</del>{{end}}
 | 
			
		||||
 | 
			
		||||
				<div class="detail">
 | 
			
		||||
					<span class="text grey"><a href="{{.RefIssueHTMLURL}}"><b>{{.RefIssueTitle}}</b> {{.RefIssueIdent}}</a></span>
 | 
			
		||||
					<span class="text grey"><a class="muted" href="{{.RefIssueHTMLURL}}"><b>{{.RefIssueTitle}}</b> {{.RefIssueIdent}}</a></span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		{{else if eq .Type 4}}
 | 
			
		||||
@@ -207,7 +205,7 @@
 | 
			
		||||
					{{if .RemovedAssignee}}
 | 
			
		||||
						{{template "shared/user/avatarlink" .Assignee}}
 | 
			
		||||
						<span class="text grey">
 | 
			
		||||
							<a class="author" {{if gt .Assignee.ID 0}}href="{{.Assignee.HomeLink}}"{{end}}>{{.Assignee.GetDisplayName}}</a>
 | 
			
		||||
							{{template "shared/user/authorlink" .Assignee}}
 | 
			
		||||
							{{if eq .Poster.ID .Assignee.ID}}
 | 
			
		||||
								{{$.locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}}
 | 
			
		||||
							{{else}}
 | 
			
		||||
@@ -331,7 +329,7 @@
 | 
			
		||||
					<div class="detail">
 | 
			
		||||
						{{svg "octicon-plus"}}
 | 
			
		||||
						<span class="text grey">
 | 
			
		||||
							<a href="{{.DependentIssue.HTMLURL}}">
 | 
			
		||||
							<a class="muted" href="{{.DependentIssue.HTMLURL}}">
 | 
			
		||||
								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
			
		||||
									#{{.DependentIssue.Index}} {{.DependentIssue.Title}}
 | 
			
		||||
								{{else}}
 | 
			
		||||
@@ -354,7 +352,7 @@
 | 
			
		||||
					<div class="detail">
 | 
			
		||||
						<span class="text grey">{{svg "octicon-trash"}}</span>
 | 
			
		||||
						<span class="text grey">
 | 
			
		||||
							<a href="{{.DependentIssue.HTMLURL}}">
 | 
			
		||||
							<a class="muted" href="{{.DependentIssue.HTMLURL}}">
 | 
			
		||||
								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
			
		||||
									#{{.DependentIssue.Index}} {{.DependentIssue.Title}}
 | 
			
		||||
								{{else}}
 | 
			
		||||
@@ -408,7 +406,7 @@
 | 
			
		||||
							<div class="comment-header-left df ac">
 | 
			
		||||
								<span class="text grey">
 | 
			
		||||
									{{if .OriginalAuthor}}
 | 
			
		||||
										<span class="text black">
 | 
			
		||||
										<span class="text black bold">
 | 
			
		||||
											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 | 
			
		||||
											{{.OriginalAuthor}}
 | 
			
		||||
										</span>
 | 
			
		||||
@@ -536,7 +534,7 @@
 | 
			
		||||
																{{end}}
 | 
			
		||||
																<span class="text grey">
 | 
			
		||||
																	{{if .OriginalAuthor}}
 | 
			
		||||
																		<span class="text black">
 | 
			
		||||
																		<span class="text black bold">
 | 
			
		||||
																			{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 | 
			
		||||
																			{{.OriginalAuthor}}
 | 
			
		||||
																		</span>
 | 
			
		||||
 
 | 
			
		||||
@@ -389,7 +389,7 @@
 | 
			
		||||
									{{avatar $user}}
 | 
			
		||||
								</a>
 | 
			
		||||
								<div class="content">
 | 
			
		||||
									<a class="author">{{$user.DisplayName}}</a>
 | 
			
		||||
									{{template "shared/user/authorlink" $user}}
 | 
			
		||||
									<div class="text">
 | 
			
		||||
										{{$trackedtime}}
 | 
			
		||||
									</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -160,7 +160,7 @@
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="issue-item-icon-right text grey">
 | 
			
		||||
					{{if .NumComments}}
 | 
			
		||||
						<a class="tdn" href="{{if .HTMLURL}}{{.HTMLURL}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
 | 
			
		||||
						<a class="tdn muted" href="{{if .HTMLURL}}{{.HTMLURL}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
 | 
			
		||||
							{{svg "octicon-comment" 16 "mr-2"}}{{.NumComments}}
 | 
			
		||||
						</a>
 | 
			
		||||
					{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1 @@
 | 
			
		||||
<a class="author"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>
 | 
			
		||||
	{{.GetDisplayName}}
 | 
			
		||||
</a>
 | 
			
		||||
<a class="author text black bold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1 @@
 | 
			
		||||
<a class="avatar"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>
 | 
			
		||||
	{{avatar .}}
 | 
			
		||||
</a>
 | 
			
		||||
<a class="avatar"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{avatar .}}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1 @@
 | 
			
		||||
<a{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>
 | 
			
		||||
	{{.GetDisplayName}}
 | 
			
		||||
</a>
 | 
			
		||||
<a{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -16806,11 +16806,7 @@
 | 
			
		||||
          "x-go-name": "FileName"
 | 
			
		||||
        },
 | 
			
		||||
        "labels": {
 | 
			
		||||
          "type": "array",
 | 
			
		||||
          "items": {
 | 
			
		||||
            "type": "string"
 | 
			
		||||
          },
 | 
			
		||||
          "x-go-name": "Labels"
 | 
			
		||||
          "$ref": "#/definitions/IssueTemplateLabels"
 | 
			
		||||
        },
 | 
			
		||||
        "name": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
@@ -16827,6 +16823,13 @@
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "IssueTemplateLabels": {
 | 
			
		||||
      "type": "array",
 | 
			
		||||
      "items": {
 | 
			
		||||
        "type": "string"
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "Label": {
 | 
			
		||||
      "description": "Label a label to an issue or a pr",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ func TestPackagePyPI(t *testing.T) {
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
 | 
			
		||||
	packageName := "test-package"
 | 
			
		||||
	packageVersion := "1.0.1"
 | 
			
		||||
	packageVersion := "1!1.0.1+r1234"
 | 
			
		||||
	packageAuthor := "KN4CK3R"
 | 
			
		||||
	packageDescription := "Test Description"
 | 
			
		||||
 | 
			
		||||
@@ -72,7 +72,7 @@ func TestPackagePyPI(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, pd.SemVer)
 | 
			
		||||
		assert.Nil(t, pd.SemVer)
 | 
			
		||||
		assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
 | 
			
		||||
		assert.Equal(t, packageName, pd.Package.Name)
 | 
			
		||||
		assert.Equal(t, packageVersion, pd.Version.Version)
 | 
			
		||||
@@ -100,7 +100,7 @@ func TestPackagePyPI(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, pd.SemVer)
 | 
			
		||||
		assert.Nil(t, pd.SemVer)
 | 
			
		||||
		assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
 | 
			
		||||
		assert.Equal(t, packageName, pd.Package.Name)
 | 
			
		||||
		assert.Equal(t, packageVersion, pd.Version.Version)
 | 
			
		||||
@@ -164,7 +164,7 @@ func TestPackagePyPI(t *testing.T) {
 | 
			
		||||
		nodes := htmlDoc.doc.Find("a").Nodes
 | 
			
		||||
		assert.Len(t, nodes, 2)
 | 
			
		||||
 | 
			
		||||
		hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, packageName, packageVersion, hashSHA256))
 | 
			
		||||
		hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, regexp.QuoteMeta(packageName), regexp.QuoteMeta(packageVersion), hashSHA256))
 | 
			
		||||
 | 
			
		||||
		for _, a := range nodes {
 | 
			
		||||
			for _, att := range a.Attr {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								web_src/js/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								web_src/js/bootstrap.js
									
									
									
									
										vendored
									
									
								
							@@ -26,9 +26,6 @@ function processWindowErrorEvent(e) {
 | 
			
		||||
    return; // ignore such nonsense error event
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for upstream fix: https://github.com/microsoft/monaco-editor/issues/2962
 | 
			
		||||
  if (e.message.includes('Language id "vs.editor.nullLanguage" is not configured nor known')) return;
 | 
			
		||||
 | 
			
		||||
  showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -99,6 +99,10 @@ export async function createMonaco(textarea, filename, editorOpts) {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962
 | 
			
		||||
  monaco.languages.register({id: 'vs.editor.nullLanguage'});
 | 
			
		||||
  monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {});
 | 
			
		||||
 | 
			
		||||
  const editor = monaco.editor.create(container, {
 | 
			
		||||
    value: textarea.value,
 | 
			
		||||
    theme: 'gitea',
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ export function initHeadNavbarContentToggle() {
 | 
			
		||||
export function initFootLanguageMenu() {
 | 
			
		||||
  function linkLanguageAction() {
 | 
			
		||||
    const $this = $(this);
 | 
			
		||||
    $.post($this.data('url')).always(() => {
 | 
			
		||||
    $.get($this.data('url')).always(() => {
 | 
			
		||||
      window.location.reload();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,9 @@ import {invertFileFolding} from './file-fold.js';
 | 
			
		||||
import {createTippy} from '../modules/tippy.js';
 | 
			
		||||
import {copyToClipboard} from './clipboard.js';
 | 
			
		||||
 | 
			
		||||
export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/;
 | 
			
		||||
export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/;
 | 
			
		||||
 | 
			
		||||
function changeHash(hash) {
 | 
			
		||||
  if (window.history.pushState) {
 | 
			
		||||
    window.history.pushState(null, null, hash);
 | 
			
		||||
@@ -135,7 +138,7 @@ export function initRepoCodeView() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(window).on('hashchange', () => {
 | 
			
		||||
      let m = window.location.hash.match(/^#(L\d+)-(L\d+)$/);
 | 
			
		||||
      let m = window.location.hash.match(rangeAnchorRegex);
 | 
			
		||||
      let $list;
 | 
			
		||||
      if ($('div.blame').length) {
 | 
			
		||||
        $list = $('.code-view td.lines-code.blame-code');
 | 
			
		||||
@@ -145,27 +148,31 @@ export function initRepoCodeView() {
 | 
			
		||||
      let $first;
 | 
			
		||||
      if (m) {
 | 
			
		||||
        $first = $list.filter(`[rel=${m[1]}]`);
 | 
			
		||||
        selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
 | 
			
		||||
        if ($first.length) {
 | 
			
		||||
          selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
 | 
			
		||||
 | 
			
		||||
        // show code view menu marker (don't show in blame page)
 | 
			
		||||
        if ($('div.blame').length === 0) {
 | 
			
		||||
          showLineButton();
 | 
			
		||||
          // show code view menu marker (don't show in blame page)
 | 
			
		||||
          if ($('div.blame').length === 0) {
 | 
			
		||||
            showLineButton();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $('html, body').scrollTop($first.offset().top - 200);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $('html, body').scrollTop($first.offset().top - 200);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      m = window.location.hash.match(/^#(L|n)(\d+)$/);
 | 
			
		||||
      m = window.location.hash.match(singleAnchorRegex);
 | 
			
		||||
      if (m) {
 | 
			
		||||
        $first = $list.filter(`[rel=L${m[2]}]`);
 | 
			
		||||
        selectRange($list, $first);
 | 
			
		||||
        if ($first.length) {
 | 
			
		||||
          selectRange($list, $first);
 | 
			
		||||
 | 
			
		||||
        // show code view menu marker (don't show in blame page)
 | 
			
		||||
        if ($('div.blame').length === 0) {
 | 
			
		||||
          showLineButton();
 | 
			
		||||
          // show code view menu marker (don't show in blame page)
 | 
			
		||||
          if ($('div.blame').length === 0) {
 | 
			
		||||
            showLineButton();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $('html, body').scrollTop($first.offset().top - 200);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $('html, body').scrollTop($first.offset().top - 200);
 | 
			
		||||
      }
 | 
			
		||||
    }).trigger('hashchange');
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								web_src/js/features/repo-code.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web_src/js/features/repo-code.test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import {test, expect} from 'vitest';
 | 
			
		||||
import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.js';
 | 
			
		||||
 | 
			
		||||
test('singleAnchorRegex', () => {
 | 
			
		||||
  expect(singleAnchorRegex.test('#L0')).toEqual(false);
 | 
			
		||||
  expect(singleAnchorRegex.test('#L1')).toEqual(true);
 | 
			
		||||
  expect(singleAnchorRegex.test('#L01')).toEqual(false);
 | 
			
		||||
  expect(singleAnchorRegex.test('#n0')).toEqual(false);
 | 
			
		||||
  expect(singleAnchorRegex.test('#n1')).toEqual(true);
 | 
			
		||||
  expect(singleAnchorRegex.test('#n01')).toEqual(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('rangeAnchorRegex', () => {
 | 
			
		||||
  expect(rangeAnchorRegex.test('#L0-L10')).toEqual(false);
 | 
			
		||||
  expect(rangeAnchorRegex.test('#L1-L10')).toEqual(true);
 | 
			
		||||
  expect(rangeAnchorRegex.test('#L01-L10')).toEqual(false);
 | 
			
		||||
  expect(rangeAnchorRegex.test('#L1-L01')).toEqual(false);
 | 
			
		||||
});
 | 
			
		||||
@@ -2,6 +2,7 @@ import {isDarkTheme} from '../utils.js';
 | 
			
		||||
const {mermaidMaxSourceCharacters} = window.config;
 | 
			
		||||
 | 
			
		||||
const iframeCss = `
 | 
			
		||||
  :root {color-scheme: normal}
 | 
			
		||||
  body {margin: 0; padding: 0}
 | 
			
		||||
  #mermaid {display: block; margin: 0 auto}
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,7 @@
 | 
			
		||||
  --color-pink: #e03997;
 | 
			
		||||
  --color-brown: #a5673f;
 | 
			
		||||
  --color-grey: #888888;
 | 
			
		||||
  --color-black: #1b1c1d;
 | 
			
		||||
  /* light variants - produced via Sass scale-color(color, $lightness: +25%) */
 | 
			
		||||
  --color-red-light: #e45e5e;
 | 
			
		||||
  --color-orange-light: #f59555;
 | 
			
		||||
@@ -92,9 +93,9 @@
 | 
			
		||||
  --color-pink-light: #e86bb1;
 | 
			
		||||
  --color-brown-light: #c58b66;
 | 
			
		||||
  --color-grey-light: #a6a6a6;
 | 
			
		||||
  --color-black-light: #525558;
 | 
			
		||||
  /* other colors */
 | 
			
		||||
  --color-gold: #a1882b;
 | 
			
		||||
  --color-black: #1b1c1d;
 | 
			
		||||
  --color-white: #ffffff;
 | 
			
		||||
  --color-diff-removed-word-bg: #fdb8c0;
 | 
			
		||||
  --color-diff-added-word-bg: #acf2bd;
 | 
			
		||||
@@ -168,6 +169,7 @@
 | 
			
		||||
  --color-active-line: #fffbdd;
 | 
			
		||||
 | 
			
		||||
  accent-color: var(--color-accent);
 | 
			
		||||
  color-scheme: light;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root * {
 | 
			
		||||
@@ -292,13 +294,15 @@ a,
 | 
			
		||||
  text-decoration-skip-ink: all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.muted {
 | 
			
		||||
a.muted,
 | 
			
		||||
.muted-links a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a:hover,
 | 
			
		||||
a.muted:hover,
 | 
			
		||||
a.muted:hover [class*="color-text"],
 | 
			
		||||
.muted-links a:hover,
 | 
			
		||||
.ui.breadcrumb a:hover {
 | 
			
		||||
  color: var(--color-primary);
 | 
			
		||||
}
 | 
			
		||||
@@ -1300,6 +1304,22 @@ a.ui.card:hover,
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text.red { color: var(--color-red) !important; }
 | 
			
		||||
.text.orange { color: var(--color-orange) !important; }
 | 
			
		||||
.text.yellow { color: var(--color-yellow) !important; }
 | 
			
		||||
.text.olive { color: var(--color-olive) !important; }
 | 
			
		||||
.text.green { color: var(--color-green) !important; }
 | 
			
		||||
.text.teal { color: var(--color-teal) !important; }
 | 
			
		||||
.text.blue { color: var(--color-blue) !important; }
 | 
			
		||||
.text.violet { color: var(--color-violet) !important; }
 | 
			
		||||
.text.purple { color: var(--color-purple) !important; }
 | 
			
		||||
.text.pink { color: var(--color-pink) !important; }
 | 
			
		||||
.text.brown { color: var(--color-brown) !important; }
 | 
			
		||||
.text.black { color: var(--color-text) !important; }
 | 
			
		||||
.text.grey { color: var(--color-text-light) !important; }
 | 
			
		||||
.text.light.grey { color: var(--color-grey-light) !important; }
 | 
			
		||||
.text.gold { color: var(--color-gold) !important; }
 | 
			
		||||
 | 
			
		||||
.ui {
 | 
			
		||||
  &.left:not(.action) {
 | 
			
		||||
    float: left;
 | 
			
		||||
@@ -1369,74 +1389,6 @@ a.ui.card:hover,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .text {
 | 
			
		||||
    &.red {
 | 
			
		||||
      color: var(--color-red) !important;
 | 
			
		||||
 | 
			
		||||
      a {
 | 
			
		||||
        color: inherit !important;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          color: var(--color-red-light) !important;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.blue {
 | 
			
		||||
      color: var(--color-blue) !important;
 | 
			
		||||
 | 
			
		||||
      a {
 | 
			
		||||
        color: inherit !important;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          color: var(--color-blue-light) !important;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.black {
 | 
			
		||||
      color: var(--color-text);
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        color: var(--color-text-dark);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.grey {
 | 
			
		||||
      color: var(--color-text-light) !important;
 | 
			
		||||
 | 
			
		||||
      a {
 | 
			
		||||
        color: var(--color-text) !important;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          color: var(--color-primary) !important;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.light.grey {
 | 
			
		||||
      color: var(--color-text-light-2) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.green {
 | 
			
		||||
      color: var(--color-green) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.purple {
 | 
			
		||||
      color: var(--color-purple) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.yellow {
 | 
			
		||||
      color: var(--color-yellow) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.orange {
 | 
			
		||||
      color: var(--color-orange) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.gold {
 | 
			
		||||
      color: var(--color-gold) !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.left {
 | 
			
		||||
      text-align: left !important;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -829,7 +829,7 @@
 | 
			
		||||
 | 
			
		||||
        .timeline-avatar {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: -72px;
 | 
			
		||||
          left: -68px;
 | 
			
		||||
 | 
			
		||||
          img {
 | 
			
		||||
            width: 40px !important;
 | 
			
		||||
@@ -846,7 +846,6 @@
 | 
			
		||||
        .avatar img {
 | 
			
		||||
          width: 20px;
 | 
			
		||||
          height: 20px;
 | 
			
		||||
          margin: 0 .25rem;
 | 
			
		||||
          vertical-align: middle;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -981,10 +980,6 @@
 | 
			
		||||
          margin-top: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .author {
 | 
			
		||||
          font-weight: 600;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .comment-form-reply .footer {
 | 
			
		||||
          padding-bottom: 1em;
 | 
			
		||||
        }
 | 
			
		||||
@@ -1165,9 +1160,12 @@
 | 
			
		||||
        padding-left: 15px;
 | 
			
		||||
 | 
			
		||||
        .detail {
 | 
			
		||||
          font-size: .9rem;
 | 
			
		||||
          margin-top: 5px;
 | 
			
		||||
          margin-left: 8px;
 | 
			
		||||
          margin-top: 4px;
 | 
			
		||||
          margin-left: 14px;
 | 
			
		||||
 | 
			
		||||
          .svg {
 | 
			
		||||
            margin-right: 2px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .segments {
 | 
			
		||||
@@ -2673,12 +2671,10 @@
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    color: var(--color-text);
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  a:hover {
 | 
			
		||||
    color: var(--color-primary);
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -537,6 +537,7 @@
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: var(--height-loading); // actual height is set in JS after loading
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  color-scheme: normal; // match the value inside the iframe to allow it to become transparent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.markup-block-error {
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,14 @@
 | 
			
		||||
          border-radius: 3px;
 | 
			
		||||
          vertical-align: 2px !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        progress::-webkit-progress-value {
 | 
			
		||||
          background-color: var(--color-secondary-dark-4);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        progress::-moz-progress-bar {
 | 
			
		||||
          background-color: var(--color-secondary-dark-4);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .conflicting {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,34 +56,35 @@
 | 
			
		||||
  --color-secondary-alpha-80: #454a57cc;
 | 
			
		||||
  --color-secondary-alpha-90: #454a57e1;
 | 
			
		||||
  /* colors */
 | 
			
		||||
  --color-red: #7d3434;
 | 
			
		||||
  --color-red: #cc4848;
 | 
			
		||||
  --color-orange: #cc580c;
 | 
			
		||||
  --color-yellow: #cc9903;
 | 
			
		||||
  --color-olive: #91a313;
 | 
			
		||||
  --color-green: #87ab63;
 | 
			
		||||
  --color-teal: #00918a;
 | 
			
		||||
  --color-blue: #1a6aa6;
 | 
			
		||||
  --color-violet: #502aa1;
 | 
			
		||||
  --color-purple: #8229a0;
 | 
			
		||||
  --color-pink: #c21e7b;
 | 
			
		||||
  --color-brown: #845232;
 | 
			
		||||
  --color-grey: #5e626a;
 | 
			
		||||
  /* light variants */
 | 
			
		||||
  --color-red-light: #984646;
 | 
			
		||||
  --color-orange-light: #e6630d;
 | 
			
		||||
  --color-yellow-light: #e5ac04;
 | 
			
		||||
  --color-olive-light: #a3b816;
 | 
			
		||||
  --color-green-light: #9fbc82;
 | 
			
		||||
  --color-teal-light: #00a39c;
 | 
			
		||||
  --color-blue-light: #1e78bb;
 | 
			
		||||
  --color-violet-light: #5a30b5;
 | 
			
		||||
  --color-purple-light: #932eb4;
 | 
			
		||||
  --color-pink-light: #db228a;
 | 
			
		||||
  --color-brown-light: #955d39;
 | 
			
		||||
  --color-grey-light: #6a6e78;
 | 
			
		||||
  /* other colors */
 | 
			
		||||
  --color-blue: #3a8ac6;
 | 
			
		||||
  --color-violet: #906ae1;
 | 
			
		||||
  --color-purple: #b259d0;
 | 
			
		||||
  --color-pink: #d22e8b;
 | 
			
		||||
  --color-brown: #a47252;
 | 
			
		||||
  --color-grey: #9ea2aa;
 | 
			
		||||
  --color-black: #1e222e;
 | 
			
		||||
  --color-gold: #a1882b;
 | 
			
		||||
  /* light variants - produced via Sass scale-color(color, $lightness: -10%) */
 | 
			
		||||
  --color-red-light: #c23636;
 | 
			
		||||
  --color-orange-light: #b84f0b;
 | 
			
		||||
  --color-yellow-light: #b88a03;
 | 
			
		||||
  --color-olive-light: #839311;
 | 
			
		||||
  --color-green-light: #7a9e55;
 | 
			
		||||
  --color-teal-light: #00837c;
 | 
			
		||||
  --color-blue-light: #347cb3;
 | 
			
		||||
  --color-violet-light: #7b4edb;
 | 
			
		||||
  --color-purple-light: #a742c9;
 | 
			
		||||
  --color-pink-light: #be297d;
 | 
			
		||||
  --color-brown-light: #94674a;
 | 
			
		||||
  --color-grey-light: #8d919b;
 | 
			
		||||
  --color-black-light: #1b1f29;
 | 
			
		||||
  /* other colors */
 | 
			
		||||
  --color-gold: #b1983b;
 | 
			
		||||
  --color-white: #ffffff;
 | 
			
		||||
  --color-diff-removed-word-bg: #6f3333;
 | 
			
		||||
  --color-diff-added-word-bg: #3c653c;
 | 
			
		||||
@@ -153,10 +154,9 @@
 | 
			
		||||
  --color-accent: var(--color-primary-light-1);
 | 
			
		||||
  --color-small-accent: var(--color-primary-light-5);
 | 
			
		||||
  --color-active-line: #534d1b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-calendar-picker-indicator {
 | 
			
		||||
  filter: invert(.8);
 | 
			
		||||
  accent-color: var(--color-accent);
 | 
			
		||||
  color-scheme: dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* invert emojis that are hard to read otherwise */
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user