Compare commits
30 Commits
forgejo
...
release/v1
Author | SHA1 | Date |
---|---|---|
techknowlogick | 9bccc60cf5 | |
Xinyu Zhou | 16772ffde3 | |
Xinyu Zhou | c844c4ff88 | |
KN4CK3R | f4ec03a4e5 | |
KN4CK3R | b2369830bb | |
silverwind | ef08998bf6 | |
Jason Song | 7a004ad7eb | |
Gusted | af8b2250c4 | |
Jason Song | 8917af8701 | |
zeripath | 0d25292fbc | |
Jason Song | ac409fcfba | |
Gusted | df512f77b7 | |
silverwind | e4bf9cad1e | |
Jason Song | 169eeee101 | |
wxiaoguang | 3aacc9b4ac | |
wxiaoguang | 87d05d376d | |
Lunny Xiao | b9dcf991b9 | |
Xinyu Zhou | a2a42cd5de | |
Wayne Starr | 805a14cc91 | |
Xinyu Zhou | 69a54545a8 | |
Wayne Starr | e054f80fe0 | |
wxiaoguang | 89d52922d0 | |
zeripath | 3a0d000b94 | |
silverwind | fd4e7447e7 | |
Jason Song | 7a8e34b255 | |
Jason Song | e4a10f8c78 | |
silverwind | 6dba648e5d | |
KN4CK3R | 4d39fd8aae | |
wxiaoguang | 4869f9c3c8 | |
qwerty287 | 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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
if value, ok := cached.(string); ok {
|
||||
return value, nil
|
||||
}
|
||||
value := conn.Get(key)
|
||||
if v, ok := value.(string); ok {
|
||||
return v, nil
|
||||
|
||||
if stringer, ok := cached.(fmt.Stringer); ok {
|
||||
return stringer.String(), nil
|
||||
}
|
||||
if v, ok := value.(fmt.Stringer); ok {
|
||||
return v.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,17 +6,20 @@ 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
|
||||
filename string
|
||||
content string
|
||||
want *api.IssueTemplate
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
|
@ -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,8 +377,8 @@ body:
|
|||
required: false
|
||||
- label: Option 3 of checkboxes
|
||||
required: true
|
||||
`
|
||||
want := &api.IssueTemplate{
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
Title: "Title",
|
||||
About: "About",
|
||||
|
@ -454,28 +445,159 @@ body:
|
|||
"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},
|
||||
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",
|
||||
},
|
||||
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: "",
|
||||
},
|
||||
}
|
||||
got, err := unmarshal("test.yaml", []byte(content))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filename := "test.yaml"
|
||||
if tt.filename != "" {
|
||||
filename = tt.filename
|
||||
}
|
||||
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)
|
||||
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,16 +96,29 @@ 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
|
||||
}
|
||||
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 {
|
||||
if err := yaml.Unmarshal(content, it); err != nil {
|
||||
return nil, fmt.Errorf("yaml unmarshal: %w", err)
|
||||
|
|
|
@ -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
|
||||
|
@ -146,13 +150,46 @@ 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"`
|
||||
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
|
||||
type IssueTemplateType string
|
||||
|
||||
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ 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"`
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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,6 +148,7 @@ export function initRepoCodeView() {
|
|||
let $first;
|
||||
if (m) {
|
||||
$first = $list.filter(`[rel=${m[1]}]`);
|
||||
if ($first.length) {
|
||||
selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
|
||||
|
||||
// show code view menu marker (don't show in blame page)
|
||||
|
@ -155,9 +159,11 @@ export function initRepoCodeView() {
|
|||
$('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]}]`);
|
||||
if ($first.length) {
|
||||
selectRange($list, $first);
|
||||
|
||||
// show code view menu marker (don't show in blame page)
|
||||
|
@ -167,6 +173,7 @@ export function initRepoCodeView() {
|
|||
|
||||
$('html, body').scrollTop($first.offset().top - 200);
|
||||
}
|
||||
}
|
||||
}).trigger('hashchange');
|
||||
}
|
||||
$(document).on('click', '.fold-file', ({currentTarget}) => {
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue