Compare commits

...

122 Commits

Author SHA1 Message Date
Thomas Miceli
c14380f4de v1.9.0 2025-02-02 20:48:40 +01:00
Thomas Miceli
da36e9eb55 Add Docker hub in release images registry CI 2025-02-02 20:11:58 +01:00
Thomas Miceli
7aa8f84eff Search gists on user profile with title, visibility, language & topics (#422) 2025-02-02 18:14:03 +01:00
Thomas Miceli
76fc129c09 Remove memdb for gist init (#421) 2025-01-30 10:46:35 +01:00
Thomas Miceli
62d56cd1c7 Save content form on gist create error (#420) 2025-01-29 16:00:58 +01:00
Thomas Miceli
d363743203 Fix empty password error when trying to change the username (#418) 2025-01-27 00:57:46 +01:00
Thomas Miceli
28c7e75657 Use jdenticon for default avatars (#416) 2025-01-27 00:08:50 +01:00
soup
0609b64cff feat: add MIME type support for raw file serving (#417) 2025-01-26 23:40:59 +01:00
Thomas Miceli
f5b8881d35 Add topics for Gists (#413) 2025-01-24 14:39:42 +01:00
gofastasf
8369cbf2f0 fix: replace path.Join with filepath.Join for file system paths (#414) 2025-01-21 07:46:59 +01:00
千橙
2ab9cf556f Add git push option for description (#412) 2025-01-20 18:16:31 +01:00
Thomas Miceli
662f553d37 Remove CSRF check for Git HTTP packs (#408) 2025-01-20 03:18:28 +01:00
Andreas Jaggi
a752e0561d Skip CSRF for embeds (#402)
* Skip CSRF for embeds

The CSRF middleware sets a _csrf cookie also for loading the embed
javascript on third-party sites. With this change no _csrf cookie is set
when loading the embed javascript (regardless if third-party site or
first-party).
2025-01-20 02:18:45 +01:00
Thomas Miceli
f935ee1a7e Refactor server code (#407) 2025-01-20 01:57:39 +01:00
Thomas Miceli
4c5a7bda63 v1.8.4 2024-12-16 01:46:26 +01:00
Thomas Miceli
f6bf09d5c2 Translations update from Opengist (#398)
* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (281 of 281 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Polish)

Currently translated at 100.0% (281 of 281 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/pl/

---------

Co-authored-by: GabrielxD <gabrielxduo@outlook.com>
Co-authored-by: GGORG <GGORG0@protonmail.com>
2024-12-15 17:52:52 +01:00
Phani Rithvij
86dd59c695 fixup! esbuild for all other platforms (#395) 2024-12-15 17:52:33 +01:00
Sangelo
20aef5e694 feat: Add custom instance names (#399)
* Add custom name variable

* Add custom name variable usage to docs

* Remove leftover testing config options (oops)
2024-12-15 17:39:51 +01:00
soup
00951bf63b feat(web): prevent password manager autofill on filename inputs (#357)
* feat(web): add data-1p-ignore attribute to ignore fields

* feat(web): extend password manager ignore attributes

- Add autocomplete="off" to prevent browser autofill
- Add data-lpignore for LastPass compatibility
- Add data-bwignore for Bitwarden compatibility
2024-12-15 17:35:08 +01:00
Thomas Miceli
526da6ccbb v1.8.3 2024-11-26 22:46:25 +01:00
Phani Rithvij
3a4080176c esbuild for all other platforms (#393)
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-11-26 22:38:51 +01:00
Phani Rithvij
64306be2d6 init git config failure -> warn (#392)
* init git config failure -> warn

Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-11-26 22:28:17 +01:00
Thomas Miceli
8543f3adfa v1.8.2 2024-11-25 22:29:31 +01:00
Thomas Miceli
391ffde12e Update Go deps 2024-11-25 22:20:43 +01:00
Thomas Miceli
3193a9e888 Translations update from Opengist (#373)
* Translated using Weblate (French)

Currently translated at 87.5% (245 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (280 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/es/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 100.0% (280 of 280 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/pl/

---------

Co-authored-by: Lucas Colombo <lucasncolombo@gmail.com>
Co-authored-by: GabrielxD <gabrielxduo@outlook.com>
Co-authored-by: GGORG <GGORG0@protonmail.com>
2024-11-25 22:08:45 +01:00
Santhosh Raju
58c5ac11c7 Respect file scheme URIs for SQLite. (#387) 2024-11-25 22:07:13 +01:00
Thomas Miceli
6a8e827d61 Fix nits typos and translation (#388) 2024-11-23 17:41:15 +01:00
Thomas Miceli
8f482bce33 Improve Git config 2024-11-23 17:25:58 +01:00
Thomas Miceli
5994cd6ccd Enforce git config on startup (#383) 2024-11-21 11:23:57 +01:00
Thomas Miceli
00e3d09cc5 Fix escaping for embed gists (#381) 2024-11-18 02:29:05 +01:00
Thomas Miceli
40ff4c7b3f Fix git clone on SSH with MySQL (#382) 2024-11-17 21:25:59 +01:00
Thomas Miceli
c1e046f428 Convert octal notation file names in Git (#380) 2024-11-17 18:09:44 +01:00
Thomas Miceli
92bac3bf8c v1.8.1 2024-11-02 02:00:48 +01:00
Thomas Miceli
73c2fb55bc Fix confirm() popup messages (#370) 2024-11-02 01:40:10 +01:00
Thomas Miceli
75162b3ef9 Hide passkey login when login form is disabled (#369) 2024-11-02 01:06:14 +01:00
Thomas Miceli
d537153785 Fix Markdown preview (#368) 2024-11-02 01:05:43 +01:00
Thomas Miceli
97b9fa1100 Fix typos 2024-11-01 00:00:09 +01:00
Thomas Miceli
393c9756d4 v1.8.0 2024-10-31 20:48:54 +01:00
Aloys
63d4b46a41 Fix typos (#363) 2024-10-31 20:19:02 +01:00
Thomas Miceli
91c412d97e Translations update from Opengist (#339)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (262 of 262 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (262 of 262 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

---------

Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: lkw123 <2020393267@qq.com>
Co-authored-by: Doracoin <doracoin@foxmail.com>
2024-10-31 18:34:04 +01:00
Thomas Miceli
7cc2b497ca Use mail handle if oauth nickname is empty (#362) 2024-10-31 18:24:15 +01:00
zdebel
d5e66d3994 Fix oauth endpoint to support detecting https in 'Forwarded' header, enabling google support (#359) 2024-10-31 15:03:35 +01:00
Thomas Miceli
4fd0832df9 Allow to define secret key & move the secret key file to parent directory (#358) 2024-10-31 14:50:13 +01:00
Thomas Miceli
20372f44e4 Change json response detection (#361) 2024-10-31 14:41:42 +01:00
Thomas Miceli
d0b4815798 Update Go deps and use Go 1.23 (#354) 2024-10-25 01:04:16 +02:00
Phani Rithvij
3cc3fb4572 package-lock.json add integrity, resolved fields (#350)
used https://github.com/jeslie0/npm-lockfile-fix

Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-10-25 00:25:10 +02:00
Thomas Miceli
ca44abfc43 Fix build Postcss error (#353) 2024-10-24 23:37:04 +02:00
Thomas Miceli
2bf434f00e Add TOTP MFA (#342) 2024-10-24 23:23:00 +02:00
Thomas Miceli
df226cbd99 Add SVG parser (#346) 2024-10-14 21:20:56 +02:00
Thomas Miceli
3068588111 Send Markdown preview data as form params (#347) 2024-10-14 14:43:12 +02:00
Emmanuel Ferdman
12696d23b0 Update config file (#343)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-10-13 23:47:06 +02:00
Patrick MARIE
798a0bfc28 Allow adding multiple empty lines in editor. (#345) 2024-10-13 23:45:50 +02:00
Thomas Miceli
6959929094 Add passkeys support + MFA (#341) 2024-10-07 23:56:32 +02:00
Thomas Miceli
41dc2e451b Use Docker secrets (#340) 2024-09-28 01:31:18 +02:00
Thomas Miceli
56b4fd45fd Add queriable shorter uuids (#338) 2024-09-23 18:14:56 +02:00
Thomas Miceli
605c8b892a Add/Remove admins (#337) 2024-09-23 16:55:57 +02:00
Thomas Miceli
fa8217e27f Separate OAuth unlink URL (#336) 2024-09-22 23:21:43 +02:00
Thomas Miceli
9ac7a76f4a Fix CI trigger 2024-09-22 23:21:30 +02:00
Thomas Miceli
17237713a1 Add Postgres and MySQL databases support (#335) 2024-09-20 16:01:09 +02:00
Thomas Miceli
4b039b0703 v1.7.5 2024-09-12 02:00:37 +02:00
Thomas Miceli
6d31ef9732 Add Vitepress docs (#326)
* Add vitepress for docs

* some fix

* Use vitepress and update docs

* Use vitepress and update docs

* Update README.md

* Add favicon

* Add docs by @jiriks74

Co-authored-by: jiriks74 <jiri@stefka.eu>

---------

Co-authored-by: jiriks74 <jiri@stefka.eu>
2024-09-12 01:47:15 +02:00
Thomas Miceli
678fb9938c Add dummy /metrics endpoint (#327) 2024-09-12 01:45:30 +02:00
Artem D.
df73b29fb1 Add ukrainian localization (#325) 2024-09-12 00:55:53 +02:00
Thomas Miceli
690a6d55f9 v1.7.4 2024-09-09 12:33:55 +02:00
Thomas Miceli
0ef35fdb36 Improve logger (#322)
* Improve logger

* Update docs
2024-09-09 11:50:05 +02:00
Thomas Miceli
cf4e0e303c Translations update from Weblate (#304)
* Translated using Weblate (Russian)

Currently translated at 69.2% (169 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/ru/

* Translated using Weblate (French)

Currently translated at 100.0% (244 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/

---------

Co-authored-by: lotigara <lotigara@yandex.ru>
Co-authored-by: Mathéo Galuba <matheo.galu56@gmail.com>
2024-09-09 11:44:51 +02:00
Thomas Miceli
ab4bfcbcfb Add atomic pointer for indexer (#321) 2024-09-09 11:44:22 +02:00
Thomas Miceli
6499e3cc63 Hide secret values in admin config page 2024-09-08 03:45:28 +02:00
Thomas Miceli
d4e4ae0b43 Cache assets 2024-09-08 03:41:41 +02:00
Thomas Miceli
de6578d9e8 Add file delete button on create editor (#320) 2024-09-07 15:17:56 +02:00
Thomas Miceli
0950c9ce38 Fix search unlisted gists (#319) 2024-09-07 14:36:16 +02:00
Thomas Miceli
f881e1c13c Hide change password form when login via password disabled (#314) 2024-09-03 17:48:45 +02:00
Thomas Miceli
069a999297 Fix package cases crash (#313) 2024-09-03 17:15:08 +02:00
Florian Gareis
a97f54d92f Finish german translation (#294)
* Finish german translation

* More fixes
2024-06-03 21:16:48 +02:00
Thomas Miceli
22dbc32f23 v1.7.3 2024-06-03 17:32:39 +02:00
Thomas Miceli
9043cbcefe Update deps 2024-06-03 17:24:20 +02:00
Thomas Miceli
e969f04084 Translations update from Weblate (#281)
* Translated using Weblate (Turkish)

Currently translated at 98.3% (237 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/tr/

* Added translation using Weblate (Italian)

* Translated using Weblate (Italian)

Currently translated at 100.0% (244 of 244 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/it/

---------

Co-authored-by: Ramazan Sancar <ramazansancar4545@gmail.com>
Co-authored-by: Cecchellone <cecchellone@gmail.com>
2024-06-03 17:22:06 +02:00
Thomas Miceli
f490f36e56 Update deps 2024-06-03 17:20:12 +02:00
Thomas Miceli
d40eb65086 Fix translation string (#293) 2024-06-03 17:14:23 +02:00
Thomas Miceli
7d113e026e Fix ssh error login (#292) 2024-06-03 17:14:06 +02:00
Thomas Miceli
38892d8a4a Fix perms for http/ssh clone (#288) 2024-05-28 01:30:08 +02:00
Thomas Miceli
77d87aeecd Fix CI check for additional translations only (#289) 2024-05-28 00:00:05 +02:00
Jade Lovelace
22052bd38f Add a setting to allow anonymous access to individual gists while still RequireLogin everywhere else (#229)
* Add a setting to allow accessing individual gists without auth

This is a middle ground between the existing setting "Require Login",
which requires login to do anything at all, and having it off, which
shows a public list of gists and more generally allows discovering info
about the users/gists of the instance without login.

The idea of this setting is that it is "require login" for everything
except individual gists.

Fixes #228.


Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
2024-05-12 23:40:11 +02:00
John Olheiser
2fd053a077 feat: make edit visibility a toggle (#277)
* feat: make edit visibility a toggle

Signed-off-by: jolheiser <john.olheiser@gmail.com>

* Tweak SVG dropdown icon size & color

---------

Signed-off-by: jolheiser <john.olheiser@gmail.com>
Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
2024-05-11 21:03:25 +02:00
Thomas Miceli
97636b23f5 Check translations keys in CI (#279) 2024-05-11 21:02:57 +02:00
思无邪
f705e879a1 Translated using Weblate (Chinese (Simplified))
Currently translated at 67.2% (162 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/
2024-05-10 15:37:43 +02:00
John Olheiser
6836dedda4 feat: add String method to visibility (#276)
This allows templates that directly use `Private`, for example, to show a string rather than an int.

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2024-05-10 14:11:40 +02:00
Ramazan Sancar
88f0f6e4c0 add: Turkish language support added (#274) 2024-05-10 14:10:59 +02:00
Thomas Miceli
9b0c06d98b v1.7.2 2024-05-05 00:56:56 +02:00
Thomas Miceli
0757c4e7fb Use go 1.22 and update deps (#244) 2024-05-05 00:38:06 +02:00
Thomas Miceli
1ec77590e9 Translations update from Weblate (#271)
* Translated using Weblate (Czech)

Currently translated at 67.2% (162 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/cs/

* Translated using Weblate (Czech)

Currently translated at 67.2% (162 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/cs/

* Translated using Weblate (Spanish)

Currently translated at 64.7% (156 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/es/

* Translated using Weblate (French)

Currently translated at 73.8% (178 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/

* Translated using Weblate (Hungarian)

Currently translated at 73.0% (176 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/hu/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.7% (156 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/pt_BR/

* Translated using Weblate (Russian)

Currently translated at 65.1% (157 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/ru/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 65.1% (157 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 73.8% (178 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hant/

* Translated using Weblate (German)

Currently translated at 73.4% (177 of 241 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Jiří Štefka <jiri@stefka.eu>
2024-05-05 00:37:10 +02:00
Thomas Miceli
e439d96e43 Add translation strings (#269) 2024-05-05 00:24:25 +02:00
Thomas Miceli
1aa94292db Frontend fixes (#267)
* Fix mermaid display

* Move Login/Register buttons on mobile

* Min width on avatar
2024-04-28 02:54:18 +02:00
Thomas Miceli
3551fd745a Set Opengist version from git tags (#261)
* Set Opengist version from git tags

* Add volume for docker dev container
2024-04-27 02:53:48 +02:00
Thomas Miceli
785d89d6ab Rework git log parsing and truncating (#260) 2024-04-27 01:49:53 +02:00
Dennis
6a8759e21e fix missing preview button when editing .md gist (#259)
Co-authored-by: Dennis Sumser <dennis.sumser@schmolck.de>
2024-04-24 21:02:21 +02:00
Guilhem Lettron
a3a3d367ea feat: add kubernetes deployment with kustomize (#258)
Signed-off-by: Guilhem Lettron <guilhem@barpilot.io>
2024-04-24 21:01:17 +02:00
TehPeGaSuS
e4bbd756f0 Update run-with-systemd.md (#254)
Add documention how to use systemd without root access
2024-04-23 23:43:26 +02:00
Thomas Miceli
2782ced03d v1.7.1 2024-04-05 17:41:35 +02:00
Marcel Herrguth
45a84df5b4 Add a more detailed variant for custom pages (#248) 2024-04-05 15:13:55 +02:00
Thomas Miceli
57273946c3 Fix empty invitation on user creation (#247) 2024-04-04 17:36:18 +02:00
dependabot[bot]
572e834999 Bump vite from 4.5.2 to 4.5.3 (#246)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 17:35:43 +02:00
hitian
f1541368e5 Fix auth page GitlabName Error (#242)
`FTL error="template: auth_form.html:71:65: executing \"auth_form.html\" at <.c.GitLabName>: can't evaluate field GitLabName in type interface {}"`
2024-04-03 10:22:52 +02:00
Thomas Miceli
9936c6bf1e v1.7.0 2024-04-03 02:06:05 +02:00
Thomas Miceli
a97d9cdbf4 Use filesystem session store (#240) 2024-04-03 01:56:55 +02:00
Thomas Miceli
ef004675a5 Create invitations for closed registrations (#233) 2024-04-03 01:56:55 +02:00
Thomas Miceli
3f5f4e01f1 Add custom static links (#234) 2024-04-03 01:56:55 +02:00
Thomas Miceli
c185cb8933 Fix new line literal in embed (#237) 2024-04-03 01:56:55 +02:00
Thomas Miceli
1c1e3a8919 Reset a user password using CLI (#226) 2024-04-03 01:56:55 +02:00
Thomas Miceli
fc9a75ce8f Markdown preview (#224) 2024-04-03 01:56:55 +02:00
Thomas Miceli
2bf0e9b7ce Show theme change button on responsive devices (#225) 2024-04-03 01:56:55 +02:00
Thomas Miceli
e1303c95d0 Increase login for 1 year (#222) 2024-04-03 01:56:55 +02:00
crapStone
915287dc10 Add ability to specify custom names in the OAuth login buttons (#214) 2024-04-03 01:56:55 +02:00
Thomas Miceli
86590d2990 Translations update from Weblate (#210)
Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/

* Added translation using Weblate (German)

* Translated using Weblate (German)

Currently translated at 26.1% (47 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hant/

* Translated using Weblate (German)

Currently translated at 60.0% (108 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (German)

Currently translated at 98.3% (177 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (German)

Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

---------

Co-authored-by: crapStone <github@crapstone.dev>
Co-authored-by: Chiawei Chen <qaz855175b@gmail.com>
Co-authored-by: Marcel Herrguth <github@thehomeofanime.de>
Co-authored-by: DerEingerostete <timo.accounts@nachrichtenlager.de>
2024-04-03 01:56:55 +02:00
Thomas Miceli
3179762fd3 Create docker dev env (#220) 2024-04-03 01:56:55 +02:00
Thomas Miceli
86ad88fb09 Set gist URL and title via push options (#216) 2024-04-03 01:56:55 +02:00
Thomas Miceli
db6d6a5eba Set gist visibility via Git push options (#215) 2024-04-03 01:56:55 +02:00
Thomas Miceli
7a75c5ecfa Move Git hook logic to Opengist (#213) 2024-04-03 01:56:55 +02:00
Thomas Miceli
dfe70dc4cf GitHub security updates 2024-04-03 01:56:55 +02:00
Thomas Miceli
afbecd9a1e Add custom logo configuration (#209) 2024-04-03 01:56:55 +02:00
Thomas Miceli
7f4be43bb4 dev-1.7 2024-04-03 01:56:55 +02:00
WilliamNT
05eccfa8e7 Added missing hungarian translations (#207) 2024-02-19 01:59:05 +01:00
203 changed files with 14351 additions and 4958 deletions

47
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Build / Deploy docs
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install JS dependencies
run: |
npm install vitepress@1.3.4 tailwindcss@3.4.10
- name: Build docs
run: |
cd docs
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
npm run docs:build
- name: Deploy to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: "docs/.vitepress/dist/*"
target: ${{ secrets.SERVER_PATH }}
- name: Update remote docs
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
${{ secrets.UPDATE_DOCS }}

View File

@@ -4,52 +4,105 @@ on:
branches: branches:
- master - master
- 'dev-*' - 'dev-*'
workflow_dispatch:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.yml' - '**.yml'
- '**.md'
jobs: jobs:
lint: lint:
name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.21 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.23"
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.54 version: v1.60
skip-pkg-cache: true args: --out-format=colored-line-number --timeout=20m
args: --out-format=colored-line-number --timeout=20m
- name: Format - name: Format
run: make fmt check_changes run: make fmt check_changes
check: check:
name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.21 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.23"
- name: Check - name: Check Go modules
run: make go_mod check_changes run: make go_mod check_changes
- name: Check translations
run: make check-tr
test-db:
name: Test
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
go: ["1.23"]
database: [postgres, mysql]
include:
- database: postgres
image: postgres:16
port: 5432:5432
- database: mysql
image: mysql:8
port: 3306:3306
runs-on: ${{ matrix.os }}
services:
database:
image: ${{ matrix.image }}
ports:
- ${{ matrix.port }}
env:
POSTGRES_PASSWORD: opengist
POSTGRES_DB: opengist_test
MYSQL_ROOT_PASSWORD: opengist
MYSQL_DATABASE: opengist_test
options: >-
--health-cmd ${{ matrix.database == 'postgres' && 'pg_isready' || '"mysqladmin ping"' }}
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Run tests
run: make test TEST_DB_TYPE=${{ matrix.database }}
test: test:
name: Test
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"] os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.21"] go: ["1.23"]
database: ["sqlite"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
@@ -61,5 +114,25 @@ jobs:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run tests - name: Run tests
run: make test run: make test TEST_DB_TYPE=${{ matrix.database }}
build:
name: Build
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.23"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Build
shell: bash
run: make

View File

@@ -13,10 +13,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.21 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.23"
- name: Cross compile build - name: Cross compile build
run: make all_crosscompile run: make all_crosscompile
@@ -46,6 +46,7 @@ jobs:
with: with:
images: | images: |
ghcr.io/thomiceli/opengist ghcr.io/thomiceli/opengist
docker.io/thomiceli/opengist
tags: | tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@@ -65,6 +66,12 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
@@ -74,4 +81,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ public/assets/*
public/manifest.json public/manifest.json
opengist opengist
build/ build/
docs/.vitepress/dist/
docs/.vitepress/cache/

View File

@@ -1,5 +1,211 @@
# Changelog # Changelog
## [1.9.0](https://github.com/thomiceli/opengist/compare/v1.8.4...v1.9.0) - 2025-02-02
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- Topics (tags) for Gists (#413)
- Gist languages saved in database (#422)
- Search gists on user profile with title, visibility, language & topics (#422)
- Jdenticon for default avatars (#416)
- Git push option for description (#412)
- MIME type support for raw file serving (#417)
### Fixed
- Skip CSRF for embed gists (#402)
- Remove CSRF check for Git HTTP packs (#408)
- Replace path.Join with filepath.Join for file system paths (#414)
- Empty password error when trying to change the username (#418)
- Save content form on gist create error (#420)
### Other
- Refactor server code (#407)
- Remove memdb for gist init (#421)
- Added Opengist Docker images to Docker Hub
## [1.8.4](https://github.com/thomiceli/opengist/compare/v1.8.3...v1.8.4) - 2024-12-15
See here how to [update](/docs/update.md) Opengist.
### Added
- More translation strings (#398)
- Custom instance names (#399)
### Fixed
- Prevent passwords managers autofill on filename inputs (#357)
## [1.8.3](https://github.com/thomiceli/opengist/compare/v1.8.2...v1.8.3) - 2024-11-26
See here how to [update](/docs/update.md) Opengist.
### Changed
- Throw `warn` instead of `fatal` on Git global config init failure (#392)
- Define esbuild as a Javascript dependency for all other platforms (#393)
## [1.8.2](https://github.com/thomiceli/opengist/compare/v1.8.1...v1.8.2) - 2024-11-25
See here how to [update](/docs/update.md) Opengist.
### Added
- More translation strings (#373) (#388)
### Changed
- Enforce git config on startup (#383)
- Respect file scheme URIs for SQLite. (#387)
### Fixed
- Convert octal notation file names in Git (#380)
- Git clone on SSH with MySQL (#382)
- Escaping for embed gists (#381)
### Other
- Update deps Golang & JS deps
## [1.8.1](https://github.com/thomiceli/opengist/compare/v1.8.0...v1.8.1) - 2024-11-02
See here how to [update](/docs/update.md) Opengist.
### Changed
- Hide passkey login when login form is disabled (#369)
### Fixed
- Markdown preview (#368)
- confirm() popup messages (#370)
## [1.8.0](https://github.com/thomiceli/opengist/compare/v1.7.5...v1.8.0) - 2024-10-31
See here how to [update](https://opengist.io/docs/update) Opengist.
### 🔴 Deprecations
_Removed in the next SemVer MAJOR version of Opengist._
* Use the configuration option `db-uri`/`OG_DB_URI` **instead of** `db-filename`/`OG_DB_FILENAME`.\
More info [here](https://opengist.io/docs/configuration/databases/sqlite) if you plan to keep SQLite as a DBMS for Opengist.
### Added
- Postgres and MySQL databases support (#335)
- Passkeys & TOTP support + MFA (#341) (#342)
- Add/Remove admins (#337)
- Queriable shorter uuids (#338)
- Use Docker secrets (#340)
- SVG preview in Markdown (#346)
- Secret key definition & move the secret key file to its parent directory (#358)
- More translation strings (#339)
### Changed
- Separate OAuth unlink URL (#336)
### Fixed
- Adding multiple empty lines in editor. (#345)
- Config URL (#343)
- Send Markdown preview data as form params (#347)
- Fix oauth endpoint to support detecting https in 'Forwarded' header, enabling google support (#359)
- Use mail handle if OAuth nickname is empty (#362)
### Other
- Use go 1.23 and update deps (#354)
- Typos in README (#363)
## [1.7.5](https://github.com/thomiceli/opengist/compare/v1.7.4...v1.7.5) - 2024-09-12
See here how to [update](/docs/update.md) Opengist.
### Added
- New website for documentation using Vitepress [https://opengist.io](https://opengist.io) (#326)
- Ukrainian localization (#325)
- Dummy /metrics endpoint (#327)
## [1.7.4](https://github.com/thomiceli/opengist/compare/v1.7.3...v1.7.4) - 2024-09-09
See here how to [update](/docs/update.md) Opengist.
### Added
- More translations strings (#294) (#304)
- Hide change password form when login via password disabled (#314)
- File delete button on create editor (#320)
- Assets cache header
- Hide secret values in admin config page
- Atomic pointer for indexer (#321)
### Fixed
- Fatal error using `cases.Title()` (#313)
- Search unlisted gist (#319)
### Other
- Removed logger `trace` and `fatal` levels (#322)
## [1.7.3](https://github.com/thomiceli/opengist/compare/v1.7.2...v1.7.3) - 2024-06-03
See here how to [update](/docs/update.md) Opengist.
### Added
- Setting to allow anonymous access to individual gists while still RequireLogin everywhere else (#229)
- Make edit visibility a toggle (#277)
- More translation strings (#274) (#281)
- String method to visibility (#276)
### Fixed
- Perms for http/ssh clone (#288)
- Fix translation string (#293)
### Other
- Update deps Golang & JS deps
- Check translations keys in CI (#279)
- Fix CI check for additional translations only (#289)
## [1.7.2](https://github.com/thomiceli/opengist/compare/v1.7.1...v1.7.2) - 2024-05-05
See here how to [update](/docs/update.md) Opengist.
### Added
- Docs:
- Run with systemd as a normal user (#254)
- Kubernetes deployment (#258)
- More translation strings (#269) (#271)
### Changed
- Rework git log parsing and truncating (#260)
- Set Opengist version from git tags (#261)
### Fixed
- Missing preview button when editing .md gist (#259)
- Frontend (#267)
- Fix mermaid display
- Move Login/Register buttons on mobile
- Set minimum width on avatar
### Other
- Use go 1.22 and update deps (#244)
## [1.7.1](https://github.com/thomiceli/opengist/compare/v1.7.0...v1.7.1) - 2024-04-05
See here how to [update](/docs/update.md) Opengist.
### Added
- Docs: More detailed variant for custom pages (#248)
### Fixed
- Auth page GitlabName Error (#242)
- Empty invitation on user creation (#247)
## [1.7.0](https://github.com/thomiceli/opengist/compare/v1.6.1...v1.7.0) - 2024-04-03
See here how to [update](/docs/update.md) Opengist.
Note: all sessions will be invalidated after this update.
### Added
- Custom logo configuration (#209)
- Custom static links (#234)
- Invitations for closed registrations (#233)
- Set gist visibility via Git push options (#215)
- Set gist URL and title via push options (#216)
- Specify custom names in the OAuth login buttons (#214)
- Markdown preview (#224)
- Reset a user password using CLI (#226)
- Translations (#207, #210)
### Changed
- Use filesystem session store (#240)
- Move Git hook logic to Opengist (#213)
- Increase login for 1 year (#222)
### Fixed
- Show theme change button on responsive devices (#225)
- New line literal in embed gists (#237)
### Other
- GitHub security updates
- New docker dev env (#220)
## [1.6.1](https://github.com/thomiceli/opengist/compare/v1.6.0...v1.6.1) - 2024-01-06 ## [1.6.1](https://github.com/thomiceli/opengist/compare/v1.6.0...v1.6.1) - 2024-01-06
See here how to [update](/docs/update.md) Opengist. See here how to [update](/docs/update.md) Opengist.

View File

@@ -1,14 +1,23 @@
FROM alpine:3.19 AS build FROM alpine:3.19 AS base
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
make \ make \
gcc \ shadow \
musl-dev \ openssl \
libstdc++ openssh \
curl \
wget \
git \
gnupg \
xz \
gcc \
musl-dev \
libstdc++
COPY --from=golang:1.21-alpine /usr/local/go/ /usr/local/go/ COPY --from=golang:1.23-alpine /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0
COPY --from=node:20-alpine /usr/local/ /usr/local/ COPY --from=node:20-alpine /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules" ENV NODE_PATH="/usr/local/lib/node_modules"
@@ -18,10 +27,21 @@ WORKDIR /opengist
COPY . . COPY . .
FROM base AS dev
EXPOSE 6157 2222 16157
VOLUME /opengist
CMD ["make", "watch"]
FROM base AS build
RUN make RUN make
FROM alpine:3.19 as run FROM alpine:3.19 as prod
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
@@ -38,7 +58,7 @@ RUN apk update && \
libstdc++ libstdc++
RUN addgroup -S opengist && \ RUN addgroup -S opengist && \
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist adduser -S -G opengist -s /bin/ash -g 'Opengist User' opengist
COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml

View File

@@ -1,7 +1,10 @@
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test .PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
# Specify the name of your Go binary output # Specify the name of your Go binary output
BINARY_NAME := opengist BINARY_NAME := opengist
GIT_TAG := $(shell git describe --tags)
VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion
TEST_DB_TYPE ?= sqlite
all: clean install build all: clean install build
@@ -20,7 +23,7 @@ build_frontend:
build_backend: build_backend:
@echo "Building Opengist binary..." @echo "Building Opengist binary..."
go build -tags fs_embed -o $(BINARY_NAME) . go build -tags fs_embed -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" -o $(BINARY_NAME) .
build: build_frontend build_backend build: build_frontend build_backend
@@ -31,16 +34,23 @@ build_docker:
@echo "Building Docker image..." @echo "Building Docker image..."
docker build -t $(BINARY_NAME):latest . docker build -t $(BINARY_NAME):latest .
build_dev_docker:
@echo "Building Docker image..."
docker build -t $(BINARY_NAME)-dev:latest --target dev .
run_dev_docker:
docker run -v .:/opengist -p 6157:6157 -p 16157:16157 -p 2222:2222 -v $(HOME)/.opengist-dev:/root/.opengist --rm $(BINARY_NAME)-dev:latest
watch_frontend: watch_frontend:
@echo "Building frontend assets..." @echo "Building frontend assets..."
npx vite -c public/vite.config.js dev --port 16157 npx vite -c public/vite.config.js dev --port 16157 --host
watch_backend: watch_backend:
@echo "Building Opengist binary..." @echo "Building Opengist binary..."
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml' OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
watch: watch:
@bash ./scripts/watch.sh @sh ./scripts/watch.sh
clean: clean:
@echo "Cleaning up build artifacts..." @echo "Cleaning up build artifacts..."
@@ -63,4 +73,7 @@ fmt:
@go fmt ./... @go fmt ./...
test: test:
@go test ./... -p 1 @OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
check-tr:
@bash ./scripts/check-translations.sh

View File

@@ -1,26 +1,27 @@
# Opengist # Opengist
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" /> <img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be Opengist is a **self-hosted** Pastebin **powered by Git**. All snippets are stored in a Git repository and can be
read and/or modified using standard Git commands, or with the web interface. read and/or modified using standard Git commands, or with the web interface.
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted. It is similar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
[Documentation](/docs) • [Demo](https://opengist.thomice.li) [Home Page](https://opengist.io) • [Documentation](https://opengist.io/docs) • [Discord](https://discord.gg/9Pm3X5scZT) • [Demo](https://demo.opengist.io)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/thomiceli/opengist?sort=semver) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/thomiceli/opengist?sort=semver)
![License](https://img.shields.io/github/license/thomiceli/opengist?color=blue) ![License](https://img.shields.io/github/license/thomiceli/opengist?color=blue)
[![Go CI](https://github.com/thomiceli/opengist/actions/workflows/go.yml/badge.svg)](https://github.com/thomiceli/opengist/actions/workflows/go.yml) [![Go CI](https://github.com/thomiceli/opengist/actions/workflows/go.yml/badge.svg)](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/thomiceli/opengist)](https://goreportcard.com/report/github.com/thomiceli/opengist) [![Go Report Card](https://goreportcard.com/badge/github.com/thomiceli/opengist)](https://goreportcard.com/report/github.com/thomiceli/opengist)
[![Translate](https://tr.opengist.io/widget/_/svg-badge.svg)](https://tr.opengist.io/projects/_/opengist/)
## Features ## Features
* Create public, unlisted or private snippets * Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH * [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support * Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks * Search code in snippets; browse users snippets, likes and forks
* Add topics to snippets
* Embed snippets in other websites * Embed snippets in other websites
* Revisions history * Revisions history
* Like / Fork snippets * Like / Fork snippets
@@ -28,7 +29,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect * OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users * Restrict or unrestrict snippets visibility to anonymous users
* Docker support * Docker support
* [More...](/docs/index.md#features) * [More...](/docs/introduction.md#features)
## Quick start ## Quick start
@@ -37,7 +38,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release : Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell ```shell
docker pull ghcr.io/thomiceli/opengist:1 docker pull ghcr.io/thomiceli/opengist:1.9
``` ```
It can be used in a `docker-compose.yml` file : It can be used in a `docker-compose.yml` file :
@@ -47,11 +48,9 @@ It can be used in a `docker-compose.yml` file :
3. Opengist is now running on port 6157, you can browse http://localhost:6157 3. Opengist is now running on port 6157, you can browse http://localhost:6157
```yml ```yml
version: "3"
services: services:
opengist: opengist:
image: ghcr.io/thomiceli/opengist:1 image: ghcr.io/thomiceli/opengist:1.9
container_name: opengist container_name: opengist
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -78,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.6.1/opengist1.6.1-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.9.0/opengist1.9.0-linux-amd64.tar.gz
tar xzvf opengist1.6.1-linux-amd64.tar.gz tar xzvf opengist1.9.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`
@@ -90,7 +89,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
### From source ### From source
Requirements : [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.21+), [Node.js](https://nodejs.org/en/download/) (16+) Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.23+), [Node.js](https://nodejs.org/en/download/) (16+), [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell ```shell
git clone https://github.com/thomiceli/opengist git clone https://github.com/thomiceli/opengist
@@ -101,10 +100,13 @@ make
Opengist is now running on port 6157, you can browse http://localhost:6157 Opengist is now running on port 6157, you can browse http://localhost:6157
---
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
## Documentation ## Documentation
The documentation is available in [/docs](/docs) directory. The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
## License ## License

View File

@@ -1,8 +1,8 @@
# Learn more about Opengist configuration here: # Learn more about Opengist configuration here:
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md # https://github.com/thomiceli/opengist/blob/master/docs/configuration/configure.md
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md # https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn # Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
log-level: warn log-level: warn
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file # Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
@@ -14,8 +14,14 @@ external-url:
# Directory where Opengist will store its data. Default: ~/.opengist/ # Directory where Opengist will store its data. Default: ~/.opengist/
opengist-home: opengist-home:
# Name of the SQLite database file. Default: opengist.db # Secret key used for session store & encrypt MFA data on database. Default: <randomized 32 bytes>
db-filename: opengist.db secret-key:
# URI of the database. Default: opengist.db (SQLite) is placed in opengist-home
# SQLite: file:/path/to/database
# PostgreSQL: postgres://user:password@host:port/database
# MySQL/MariaDB: mysql://user:password@host:port/database
db-uri: opengist.db
# Enable or disable the code search index (either `true` or `false`). Default: true # Enable or disable the code search index (either `true` or `false`). Default: true
index.enabled: true index.enabled: true
@@ -29,9 +35,9 @@ git.default-branch:
# Set the journal mode for SQLite. Default: WAL # Set the journal mode for SQLite. Default: WAL
# See https://www.sqlite.org/pragma.html#pragma_journal_mode # See https://www.sqlite.org/pragma.html#pragma_journal_mode
# For SQLite databases only.
sqlite.journal-mode: WAL sqlite.journal-mode: WAL
# HTTP server configuration # HTTP server configuration
# Host to bind to. Default: 0.0.0.0 # Host to bind to. Default: 0.0.0.0
http.host: 0.0.0.0 http.host: 0.0.0.0
@@ -65,7 +71,6 @@ ssh.external-domain:
# Path or alias to ssh-keygen executable. Default: ssh-keygen # Path or alias to ssh-keygen executable. Default: ssh-keygen
ssh.keygen-executable: ssh-keygen ssh.keygen-executable: ssh-keygen
# OAuth2 configuration # OAuth2 configuration
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback # The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
@@ -78,15 +83,36 @@ gitlab.client-key:
gitlab.secret: gitlab.secret:
# URL of the Gitlab instance. Default: https://gitlab.com/ # URL of the Gitlab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/ gitlab.url: https://gitlab.com/
# The name of the GitLab instance. It is displayed in the OAuth login button. Default: GitLab
gitlab.name: GitLab
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications # To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
gitea.client-key: gitea.client-key:
gitea.secret: gitea.secret:
# URL of the Gitea instance. Default: https://gitea.com/ # URL of the Gitea instance. Default: https://gitea.com/
gitea.url: https://gitea.com/ gitea.url: https://gitea.com/
# The name of the Gitea instance. It is displayed in the OAuth login button. Default: Gitea
gitea.name: Gitea
# To create a new OAuth2 application using OpenID Connect: # To create a new OAuth2 application using OpenID Connect:
oidc.client-key: oidc.client-key:
oidc.secret: oidc.secret:
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration # Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
oidc.discovery-url: oidc.discovery-url:
# Instance name
# Set your own custom name to be displayed instead of 'Opengist'
custom.name:
# Custom assets
# Add your own custom assets, that are files relatives to $opengist-home/custom/
custom.logo:
custom.favicon:
# Static pages in footer (like legal notices, privacy policy, etc.)
# The path can be a URL or a relative path to a file in the $opengist-home/custom/ directory
custom.static-links:
# - name: Gitea
# path: https://gitea.com
# - name: Legal notices
# path: legal.html

75
deploy/README.md Normal file
View File

@@ -0,0 +1,75 @@
# kustomize
## Simple
`kustomization.yaml`:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
resources:
- https://github.com/thomiceli/opengist/deploy/
```
## Full example
`kustomization.yaml`:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
namespace: opengist
resources:
- namespace.yaml
- https://github.com/thomiceli/opengist/deploy/?ref:v1.9.0
images:
- name: ghcr.io/thomiceli/opengist
newTag: 1.9.0
patches:
# Add your ingress
- path: ingress.yaml
- patch: |-
- op: add
path: /spec/rules/0/host
value: opengist.mydomain.com
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: opengist
```
`namespace.yaml`:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: opengist
```
`ingress.yaml`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: nginx
tls:
- hosts:
- opengist.mydomain.com
secretName: opengist-tls
```

29
deploy/deployment.yaml Normal file
View File

@@ -0,0 +1,29 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opengist
spec:
selector:
matchLabels:
app.kubernetes.io/name: opengist
template:
metadata:
labels:
app.kubernetes.io/name: opengist
spec:
containers:
- name: opengist
image: ghcr.io/thomiceli/opengist
ports:
- name: http
containerPort: 6157
- name: ssh
containerPort: 2222
volumeMounts:
- mountPath: /opengist
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: opengist-data

20
deploy/ingress.yaml Normal file
View File

@@ -0,0 +1,20 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
labels:
app.kubernetes.io/name: opengist
app.kubernetes.io/component: ingress
spec:
rules:
- host: opengist.local
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: opengist
port:
name: http

11
deploy/kustomization.yaml Normal file
View File

@@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
resources:
- deployment.yaml
- pvc.yaml
- ingress.yaml
- service.yaml

15
deploy/pvc.yaml Normal file
View File

@@ -0,0 +1,15 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: opengist-data
labels:
app.kubernetes.io/name: opengist
app.kubernetes.io/component: data
spec:
resources:
requests:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce

14
deploy/service.yaml Normal file
View File

@@ -0,0 +1,14 @@
---
apiVersion: v1
kind: Service
metadata:
name: opengist
labels:
app.kubernetes.io/name: opengist
spec:
selector:
app.kubernetes.io/name: opengist
ports:
- port: 80
targetPort: http
name: http

View File

@@ -9,4 +9,10 @@ usermod -o -u "$UID" $USER
chown -R "$USER:$USER" /opengist chown -R "$USER:$USER" /opengist
chown -R "$USER:$USER" /config.yml chown -R "$USER:$USER" /config.yml
if [ -f "/run/secrets/opengist_secrets" ]; then
set -a
. /run/secrets/opengist_secrets
set +a
fi
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml" exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"

View File

@@ -0,0 +1,95 @@
import {defineConfig} from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Opengist",
description: "Documention for Opengist",
rewrites: {
'index.md': 'index.md',
'introduction.md': 'docs/index.md',
':path(.*)': 'docs/:path'
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg',
logoLink: '/',
nav: [
{ text: 'Demo', link: 'https://demo.opengist.io' },
{ text: 'Translate', link: 'https://tr.opengist.io' }
],
sidebar: {
'/docs/': [
{
text: '', items: [
{text: 'Introduction', link: '/docs'},
{text: 'Installation', link: '/docs/installation', items: [
{text: 'Docker', link: '/docs/installation/docker'},
{text: 'Binary', link: '/docs/installation/binary'},
{text: 'Source', link: '/docs/installation/source'},
],
collapsed: true
},
{text: 'Update', link: '/docs/update'},
], collapsed: false
},
{
text: 'Configuration', base: '/docs/configuration', items: [
{text: 'Configure Opengist', link: '/configure'},
{text: 'Databases', items: [
{text: 'SQLite', link: '/databases/sqlite'},
{text: 'PostgreSQL', link: '/databases/postgresql'},
{text: 'MySQL', link: '/databases/mysql'},
], collapsed: true
},
{text: 'OAuth Providers', link: '/oauth-providers'},
{text: 'Custom assets', link: '/custom-assets'},
{text: 'Custom links', link: '/custom-links'},
{text: 'Cheat Sheet', link: '/cheat-sheet'},
{text: 'Admin panel', link: '/admin-panel'},
], collapsed: false
},
{
text: 'Usage', base: '/docs/usage', items: [
{text: 'Init via Git', link: '/init-via-git'},
{text: 'Embed Gist', link: '/embed'},
{text: 'Gist as JSON', link: '/gist-json'},
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
{text: 'Git push options', link: '/git-push-options'},
], collapsed: false
},
{
text: 'Administration', base: '/docs/administration', items: [
{text: 'Run with systemd', link: '/run-with-systemd'},
{text: 'Reverse proxy', items: [
{text: 'Nginx', link: '/nginx-reverse-proxy'},
{text: 'Traefik', link: '/traefik-reverse-proxy'},
], collapsed: true},
{text: 'Fail2ban', link: '/fail2ban-setup'},
{text: 'Healthcheck', link: '/healthcheck'},
], collapsed: false
},
{
text: 'Contributing', base: '/docs/contributing', items: [
{text: 'Community', link: '/community'},
{text: 'Development', link: '/development'},
], collapsed: false
},
]},
socialLinks: [
{icon: 'github', link: 'https://github.com/thomiceli/opengist'}
],
editLink: {
pattern: 'https://github.com/thomiceli/opengist/edit/stable/docs/:path'
},
// @ts-ignore
lastUpdated: true,
},
head: [
['link', {rel: 'icon', href: '/favicon.svg'}],
],
ignoreDeadLinks: true
})

37
docs/.vitepress/tailwind.config.js vendored Normal file
View File

@@ -0,0 +1,37 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./.vitepress/theme/*.vue",
],
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
white: colors.white,
black: colors.black,
gray: {
50: "#EEEFF1",
100: "#DEDFE3",
200: "#BABCC5",
300: "#999CA8",
400: "#75798A",
500: "#585B68",
600: "#464853",
700: "#363840",
800: "#232429",
900: "#131316"
},
indigo: colors.indigo,
},
extend: {
borderWidth: {
'1': '1px',
}
},
},
plugins: [],
darkMode: 'class',
}

View File

@@ -0,0 +1,101 @@
<script>
import { withBase } from 'vitepress';
import './theme.css'
export default {
setup() {
return { withBase };
},
};
</script>
<template>
<main class="home">
<header class="hero">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto lg:text-center">
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="" >
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
<span class="pr-1">Released 1.9</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
<h1 class="mt-5 text-4xl font-bold tracking-tight sm:text-5xl">Opengist</h1>
<h2 class="mt-4 text-xl">Self-hosted pastebin powered by Git, open-source alternative to Github Gist.</h2>
</div>
<div class="space-x-2 my-12">
<a href="/docs" class="rounded-md bg-indigo-600 mt-6 px-5 py-3 text-xl font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Docs</a>
<a target="_blank" href="https://demo.opengist.io" class="rounded-md bg-indigo-400 mt-6 px-5 py-3 text-xl border-white font-semibold text-white shadow-sm hover:bg-indigo-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Live demo</a>
<a target="_blank" href="https://github.com/thomiceli/opengist" class="rounded-md bg-gray-800 mt-6 px-3 py-3 text-xl dark:border dark:border-1 dark:border-gray-400 font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" class="w-7 h-auto inline" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"></path></svg>
</a>
</div>
<div class="border border-1 mt-6 px-5 py-3 rounded-md shadow-sm ">
<code class="select-all ">docker run --name <span class="text-indigo-700 dark:text-indigo-300 font-bold">opengist</span> -p <span class="text-indigo-700 dark:text-indigo-300 font-bold">6157</span>:6157 -v "<span class="text-indigo-700 dark:text-indigo-300 font-bold">$HOME/.opengist</span>:/opengist" ghcr.io/thomiceli/opengist:1</code>
</div>
</div>
</header>
<div class="relative w-full sm:max-w-7xl mx-auto overflow-auto">
<img class="block w-[200vw] max-w-none sm:w-full h-auto" :src="withBase('/opengist-demo.png')" alt="demo-opengist-screenshot" />
</div>
</main>
</template>
<style>
@-webkit-keyframes rotating /* Safari and Chrome */ {
from {
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotating {
from {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.home {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.rotating {
-webkit-animation: rotating 8s linear infinite;
-moz-animation: rotating 4s linear infinite;
-ms-animation: rotating 4s linear infinite;
-o-animation: rotating 4s linear infinite;
animation: rotating 12s linear infinite;
}
</style>

View File

@@ -0,0 +1,16 @@
<script setup>
import { useData } from 'vitepress'
import Home from './Home.vue'
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>
<template>
<Layout>
<template v-if="frontmatter.layout === 'home'" #home-hero-after>
<Home />
</template>
</Layout>
</template>

View File

@@ -0,0 +1,12 @@
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import Layout from "./Layout.vue";
export default {
...DefaultTheme,
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme

View File

@@ -0,0 +1,147 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attched to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#0f0513 30%,
#7e8b90
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#bd34fe 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
.content img {
padding-left: 20px;
height: 108px;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,11 @@
# Manage admins
You can add and remove Opengist admins from the CLI.
```bash
./opengist admin toggle-admin <username>
```
```bash
$ ./opengist admin toggle-admin thomas
User thomas admin set to true
```

View File

@@ -0,0 +1,7 @@
# Reset a user password
To reset a user password, run the following command using the Opengist binary:
```bash
./opengist admin reset-password <username> <new-password>
```

View File

@@ -2,6 +2,7 @@
For non-Docker users, you could run Opengist as a systemd service. For non-Docker users, you could run Opengist as a systemd service.
## As root
On Unix distributions with systemd, place the Opengist binary like: On Unix distributions with systemd, place the Opengist binary like:
```shell ```shell
@@ -45,3 +46,47 @@ systemctl daemon-reload
systemctl enable --now opengist systemctl enable --now opengist
systemctl status opengist systemctl status opengist
``` ```
----
## As a normal user
**NOTE: This was tested on Ubuntu 20.04 and newer. For other distros, please check the respective documentation**
#### For the purpose of this documentation, we will assume that:
- You've followed the instructions on how to run opengist [from source](https://github.com/thomiceli/opengist?tab=readme-ov-file#from-source)
- Your shell user is named `pastebin`
- All commands are being executed as the `pastebin` user
_If none of the above is true, then adapt the commands and paths to fit your needs._
Enable lingering for the user:
```shell
loginctl enable-linger
```
Create the user systemd folder:
```
mkdir -p /home/pastebin/.config/systemd/user
```
Then create a service file at `/home/pastebin/.config/systemd/user/opengist.service`:
```ini
[Unit]
Description=opengist Server
After=network.target
[Service]
Type=simple
ExecStart=/home/pastebin/opengist/opengist --config /home/pastebin/opengist/config.yml
Restart=on-failure
[Install]
WantedBy=default.target
```
Finally, start the service:
```shell
systemctl --user daemon-reload
systemctl --user enable --now opengist
systemctl --user status opengist
```

View File

@@ -0,0 +1,48 @@
# Use Traefik as a reverse proxy
You can set up Traefik in two ways:
<details>
<summary>Using Docker labels</summary>
Add these labels to your `docker-compose.yml` file:
```yml
labels:
- traefik.http.routers.opengist.rule=Host(`opengist.example.com`) # Change to your subdomain
# Uncomment the line below if you run Opengist in a subdirectory
# - traefik.http.routers.app1.rule=PathPrefix(`/opengist{regex:$$|/.*}`) # Change opentist in the regex to yuor subdirectory name
- traefik.http.routers.opengist.entrypoints=websecure # Change to the name of your 443 port entrypoint
- traefik.http.routers.opengist.tls.certresolver=lets-encrypt # Change to certresolver's name
- traefik.http.routers.opengist.service=opengist
- traefik.http.services.opengist.loadBalancer.server.port=6157
```
</details>
<details>
<summary>Using a <code>yml</code> file</summary>
> [!Note]
> Don't forget to change the `<server-address>` to your server's IP
`traefik_dynamic.yml`
```yml
http:
routers:
opengist:
entrypoints: websecure
rule: Host(`opengist.example.com`) # Comment this line and uncomment the line below if using a subpath
# rule: PathPrefix(`/opengist{regex:$$|/.*}`) # Change opentist in the regex to yuor subdirectory name
# middlewares:
# - opengist-fail2ban
service: opengist
tls:
certresolver: lets-encrypt
services:
opengist:
loadbalancer:
servers:
- url: "http://<server-address>:6157"
```
</details>

View File

@@ -0,0 +1,53 @@
# Admin panel
The first user created on your Opengist instance has access to the Admin panel.
To access the Admin panel:
1. Log in
2. Click your username in the upper right corner
3. Select `Admin`
## Usage
### General
Here you can see some basic information, like Opengist version, alongside some stats.
You can also start some actions like forcing synchronization of gists,
starting garbage collection, etc.
### Users
Here you can see your users and delete them.
### Gists
Here you can see all the gists and some basic information about them. You also have an option
to delete them.
### Invitations
Here you can create invitation links with some options like limiting the number of signed up
users or setting an expiration date.
> [!Note]
> Invitation links override the `Disable signup` option but not the `Disable login form` option.
>
> Users will see only the OAuth providers when `Disable login form` is enabled.
### Configuration
Here you can change a limited number of settings without restarting the instance.
- Disable signup
- Forbid the creation of new accounts.
- Require login
- Enforce users to be logged in to see gists.
- Allow individual gists without login
- Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
- Disable login form
- Forbid logging in via the login form to force using OAuth providers instead.
- Disable Gravatar
- Disable the usage of Gravatar as an avatar provider.

View File

@@ -1,32 +1,43 @@
---
aside: false
---
# Configuration Cheat Sheet # Configuration Cheat Sheet
| YAML Config Key | Environment Variable | Default value | Description | | YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|--------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | | log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. |
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | | log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | | external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | | opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | | secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. |
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) | | db-uri | OG_DB_URI | `opengist.db` | URI of the database. |
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. | | index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) |
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) | | index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. |
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | | git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | | sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | | http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | | http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | | http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | | ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | | ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | | ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | | ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | | ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | | github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | | github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | | gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | | gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | | gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | | gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | | gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | | gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | | gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | | gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
| custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title |
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). |

View File

@@ -27,7 +27,7 @@ Usage via command line :
./opengist --config /path/to/config.yml ./opengist --config /path/to/config.yml
``` ```
You can start by copying and/or modifying the provided [config.yml](/config.yml) file. You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
## Configuration via Environment Variables ## Configuration via Environment Variables
@@ -46,3 +46,27 @@ Usage via command line :
```shell ```shell
OG_LOG_LEVEL=info ./opengist OG_LOG_LEVEL=info ./opengist
``` ```
### Using Docker Compose secrets
You can use Docker Compose secrets to not expose sensitive information in your compose file, using a `.env` file.
```dotenv
# file secrets.env
OG_GITLAB_CLIENT_KEY=your_gitlab_client_key
OG_GITLAB_SECRET=your_gitlab_secret_key
```
And then use it in your compose file :
```yml
services:
opengist:
# ...
secrets:
- opengist_secrets
secrets:
opengist_secrets:
file: ./secrets.env
```

View File

@@ -0,0 +1,45 @@
# Custom assets
To add custom assets to your Opengist instance, you can use the `$opengist-home/custom` directory (where `$opengist-home` is the directory where Opengist stores its data).
### Logo / Favicon
To add a custom logo or favicon, you can add your own image file to the `$opengist-home/custom` directory, then define the relative path in the config.
For example, if you have a logo file `logo.png` in the `$opengist-home/custom` directory, you can set the logo path in the config as follows:
#### YAML
```yaml
custom.logo: logo.png
```
#### Environment variable
```sh
export OG_CUSTOM_LOGO=logo.png
```
Same as the favicon:
#### YAML
```yaml
custom.favicon: favicon.png
```
#### Environment variable
```sh
export OG_CUSTOM_FAVICON=favicon.png
```
### Instance Name
It is also possible to set a name for your instance, that would be displayed in the title bar instead of 'Opengist'.
#### YAML
```yaml
custom.name: My Gists
```
#### Environment variable
```sh
export OG_CUSTOM_NAME="My Gists"
```

View File

@@ -0,0 +1,62 @@
# Custom links
On the footer of your Opengist instance, you can add links to custom static templates or any other website you want to link to.
This can be useful for legal information, privacy policy, or any other information you want to provide to your users.
To add one or more links, you can add your own file to the `$opengist-home/custom` directory or set a URL, then define the relative path and its name in the config.
For example, if you have a legal information file `legal.html` in the `$opengist-home/custom` directory, and also wish to add a link to a Gitea instance, you can set the link in the config as follows:
#### YAML
```yaml
custom.static-links:
- name: Legal notices
path: legal.html
- name: Gitea
path: https://gitea.com
```
#### Environment variable
```sh
OG_CUSTOM_STATIC_LINK_0_NAME="Legal Notices" \
OG_CUSTOM_STATIC_LINK_0_PATH=legal.html \
OG_CUSTOM_STATIC_LINK_1_NAME=Gitea \
OG_CUSTOM_STATIC_LINK_1_PATH=https://gitea.com \
./opengist
```
## Templating custom HTML pages
In the start and end of the custom HTML files, you can use the syntax to include the header and footer of the Opengist instance:
```html
{{ template "header" . }}
<!-- my content -->
{{ template "footer" . }}
```
If you want your custom page to integrate well into the existing theme, you can use the following:
```html
{{ template "header" . }}
<div class="py-10">
<header class="pb-4 ">
<div class="flex">
<div class="flex-auto">
<h2 class="text-2xl font-bold leading-tight">Heading</h2>
</div>
</div>
</header>
<main>
<h3 class="text-xl font-bold leading-tight mt-4">Sub-Heading</h3>
<p class="mt-4 ml-1"><!-- my content --></p>
</main>
</div>
{{ template "footer" . }}
```
You can adjust above as needed. Opengist uses TailwindCSS classes.

View File

@@ -0,0 +1,47 @@
# Using MySQL/MariaDB
To use MySQL/MariaDB as the database backend, you need to set the database URI configuration to the connection string of your MySQL/MariaDB database with this format :
`mysql://<user>:<password>@<host>:<port>/<database>`
#### YAML
```yaml
# Example
db-uri: mysql://root:passwd@localhost:3306/opengist_db
```
#### Environment variable
```sh
# Example
OG_DB_URI=mysql://root:passwd@localhost:3306/opengist_db
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
depends_on:
- mysql
ports:
- "6157:6157"
- "2222:2222"
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_DB_URI: mysql://opengist:secret@mysql:3306/opengist_db
# other configuration options
mysql:
image: mysql:8.4
restart: unless-stopped
volumes:
- "./opengist-database:/var/lib/mysql"
environment:
MYSQL_USER: opengist
MYSQL_PASSWORD: secret
MYSQL_DATABASE: opengist_db
MYSQL_ROOT_PASSWORD: rootsecret
```

View File

@@ -0,0 +1,46 @@
# Using PostgreSQL
To use PostgreSQL as the database backend, you need to set the database URI configuration to the connection string of your PostgreSQL database with this format :
`postgres://<user>:<password>@<host>:<port>/<database>`
#### YAML
```yaml
# Example
db-uri: postgres://postgres:passwd@localhost:5432/opengist_db
```
#### Environment variable
```sh
# Example
OG_DB_URI=postgres://postgres:passwd@localhost:5432/opengist_db
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
depends_on:
- postgres
ports:
- "6157:6157"
- "2222:2222"
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_DB_URI: postgres://opengist:secret@postgres:5432/opengist_db
# other configuration options
postgres:
image: postgres:16.4
restart: unless-stopped
volumes:
- "./opengist-database:/var/lib/postgresql/data"
environment:
POSTGRES_USER: opengist
POSTGRES_PASSWORD: secret
POSTGRES_DB: opengist_db
```

View File

@@ -0,0 +1,44 @@
# Using SQLite
By default, Opengist uses SQLite as the database backend.
Because SQLite is a file-based database, there is not much configuration to tweak.
The configuration `db-uri`/`OG_DB_URI` refers to the path of the SQLite database file relative in the `$opengist-home/` directory (default `opengist.db`),
although it can be left untouched. You can also use an absolute path outside the `$opengist-home/` directory.
The SQLite journal mode is set to [`WAL` (Write-Ahead Logging)](https://www.sqlite.org/pragma.html#pragma_journal_mode) by default and can be changed.
#### YAML
```yaml
# default
db-uri: opengist.db
sqlite.journal-mode: WAL
# absolute path outside the $opengist-home/ directory
db-uri: file:/home/user/opengist.db
```
#### Environment variable
```sh
# default
OG_DB_URI=opengist.db
OG_SQLITE_JOURNAL_MODE=WAL
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
- "6157:6157" # HTTP port
- "2222:2222" # SSH port, can be removed if you don't use SSH
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_SQLITE_JOURNAL_MODE: WAL
# other configuration options
```

View File

@@ -2,51 +2,76 @@
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect. Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
## Github ## GitHub
* Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new) * Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
* Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback` * Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml ```yaml
github.client-key: <key> github.client-key: <key>
github.secret: <secret> github.secret: <secret>
``` ```
```shell
OG_GITHUB_CLIENT_KEY=<key>
OG_GITHUB_SECRET=<secret>
```
## GitLab ## GitLab
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications) * Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
* Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml ```yaml
gitlab.client-key: <key> gitlab.client-key: <key>
gitlab.secret: <secret> gitlab.secret: <secret>
# URL of the GitLab instance. Default: https://gitlab.com/ # URL of the GitLab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/ gitlab.url: https://gitlab.com/
``` ```
```shell
OG_GITLAB_CLIENT_KEY=<key>
OG_GITLAB_SECRET=<secret>
# URL of the GitLab instance. Default: https://gitlab.com/
OG_GITLAB_URL=https://gitlab.com/
```
## Gitea ## Gitea
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications) * Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
* Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml ```yaml
gitea.client-key: <key> gitea.client-key: <key>
gitea.secret: <secret> gitea.secret: <secret>
# URL of the Gitea instance. Default: https://gitea.com/ # URL of the Gitea instance. Default: https://gitea.com/
gitea.url: http://localhost:3000 gitea.url: http://localhost:3000
``` ```
```shell
OG_GITEA_CLIENT_KEY=<key>
OG_GITEA_SECRET=<secret>
# URL of the Gitea instance. Default: https://gitea.com/
OG_GITEA_URL=http://localhost:3000
```
## OpenID Connect ## OpenID Connect
* Add a new OAuth app in Application settings of your OIDC provider * Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback`
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](cheat-sheet.md) :
```yaml ```yaml
oidc.client-key: <key> oidc.client-key: <key>
oidc.secret: <secret> oidc.secret: <secret>
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration # Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
``` ```
```shell
OG_OIDC_CLIENT_KEY=<key>
OG_OIDC_SECRET=<secret>
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
OG_OIDC_DISCOVERY_URL=http://auth.example.com/.well-known/openid-configuration
```

View File

@@ -0,0 +1,6 @@
# Community
The following is a list of resources made by happy users of Opengist. Feel free to make a PR add your own!
- [Aetherinox/opengist-debian](https://github.com/Aetherinox/opengist-debian) - A Debian package for Opengist
- [How to Install Opengist on Your Synology NAS](https://mariushosting.com/how-to-install-opengist-on-your-synology-nas/) - A guide to install Opengist on a Synology NAS

View File

@@ -0,0 +1,38 @@
# Run Opengist in development mode
## With Docker
Assuming you have [Make](https://linux.die.net/man/1/make) installed,
```shell
# Clone the repository
git clone git@github.com:thomiceli/opengist.git
cd opengist
# Build the development image
make build_dev_docker
```
Now you can run the development image with the following command:
```shell
make run_dev_docker
```
Opengist is now running on port 6157, you can browse http://localhost:6157
## As a binary
Requirements:
* [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.23+)
* [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone git@github.com:thomiceli/opengist.git
cd opengist
make watch
```
Opengist is now running on port 6157, you can browse http://localhost:6157

View File

@@ -1,54 +1,4 @@
# Opengist ---
layout: home
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be navbar: false
read and/or modified using standard Git commands, or with the web interface. ---
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
## Features
* Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks
* Embed snippets in other websites
* Revisions history
* Like / Fork snippets
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* Retrieve snippet data/metadata via a JSON API
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Avatars via Gravatar or OAuth2 providers
* Light/Dark mode
* Responsive UI
* Enable or disable signups
* Restrict or unrestrict snippets visibility to anonymous users
* Admin panel :
* delete users/gists;
* clean database/filesystem by syncing gists
* run `git gc` for all repositories
* SQLite database
* Logging
* Docker support
## System requirements
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
Version **2.28** or later is recommended as the app has not been tested with older Git versions and some features would not work.
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
## Components
* Backend Web Framework: [Echo](https://echo.labstack.com/)
* ORM: [GORM](https://gorm.io/)
* Frontend libraries:
* [Tailwind CSS](https://tailwindcss.com/)
* [CodeMirror](https://codemirror.net/)
* [Day.js](https://day.js.org/)
* [highlight.js](https://highlightjs.org/)
* and [others](/package.json)

View File

@@ -1,73 +1,7 @@
# Installation # Install Opengist
## With Docker There are several ways to install Opengist, depending on your preferences and your environment.
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release : - [Docker](installation/docker.md)
- [Source](installation/source.md)
```shell - [Binary](installation/binary.md)
docker pull ghcr.io/thomiceli/opengist:1
```
It can be used in a `docker-compose.yml` file :
1. Create a `docker-compose.yml` file with the following content
2. Run `docker compose up -d`
3. Opengist is now running on port 6157, you can browse http://localhost:6157
```yml
version: "3"
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
- "6157:6157" # HTTP port
- "2222:2222" # SSH port, can be removed if you don't use SSH
volumes:
- "$HOME/.opengist:/opengist"
```
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
variables :
```yml
services:
opengist:
# ...
environment:
UID: 1001
GID: 1001
```
## Via binary
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.6.1/opengist1.6.1-linux-amd64.tar.gz
tar xzvf opengist1.6.1-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```
## From source
Requirements :
* [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.21+)
* [Node.js](https://nodejs.org/en/download/) (16+)
```shell
git clone https://github.com/thomiceli/opengist
cd opengist
make
./opengist
```
Opengist is now running on port 6157, you can browse http://localhost:6157

View File

@@ -0,0 +1,14 @@
# Install from binary
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.9.0/opengist1.9.0-linux-amd64.tar.gz
tar xzvf opengist1.9.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```

View File

@@ -0,0 +1,41 @@
# Install with Docker
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell
docker pull ghcr.io/thomiceli/opengist:1
```
It can be used in a `docker-compose.yml` file :
1. Create a `docker-compose.yml` file with the following content
2. Run `docker compose up -d`
3. Opengist is now running on port 6157, you can browse http://localhost:6157
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
- "6157:6157" # HTTP port
- "2222:2222" # SSH port, can be removed if you don't use SSH
volumes:
- "$HOME/.opengist:/opengist"
environment:
# OG_LOG_LEVEL: info
# other configuration options
```
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
variables :
```yml
services:
opengist:
# ...
environment:
UID: 1001
GID: 1001
```

View File

@@ -0,0 +1,19 @@
# Installation from source
Requirements:
* [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.23+)
* [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone https://github.com/thomiceli/opengist
cd opengist
git checkout v1.9.0 # optional, to checkout the latest release
make
./opengist
```
Opengist is now running on port 6157, you can browse http://localhost:6157

56
docs/introduction.md Normal file
View File

@@ -0,0 +1,56 @@
# Opengist
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
read and/or modified using standard Git commands, or with the web interface.
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
## Features
* Create public, unlisted or private snippets
* [Init](usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks
* Add topics to snippets
* Embed snippets in other websites
* Revisions history
* Like / Fork snippets
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* Retrieve snippet data/metadata via a JSON API
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Avatars via Gravatar or OAuth2 providers
* Light/Dark mode
* Responsive UI
* Enable or disable signups
* Restrict or unrestrict snippets visibility to anonymous users
* Admin panel :
* delete users/gists;
* clean database/filesystem by syncing gists
* run `git gc` for all repositories
* SQLite/PostgreSQL/MySQL database
* Logging
* Docker support
## System requirements
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
Version **2.28** or later is recommended as the app has not been tested with older Git versions and some features would not work.
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
## Components
* Backend Web Framework: [Echo](https://echo.labstack.com/)
* ORM: [GORM](https://gorm.io/)
* Frontend libraries:
* [TailwindCSS](https://tailwindcss.com/)
* [CodeMirror](https://codemirror.net/)
* [Day.js](https://day.js.org/)
* and [others](/package.json)

17
docs/public/favicon.svg Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="document" transform="scale(1.6666666666666667 1.6666666666666667) translate(150.0 150.0)">
<path class="st0" d="M131.3,24.3c13.7-71-33.9-139.5-106.4-152.9C-47.7-142-117.6-95.3-131.3-24.3s33.9,139.5,106.4,152.9 C47.7,142,117.6,95.3,131.3,24.3z"/>
<path class="st0" d="M128.9,0c0,55.7-36.8,103-88,119.8c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8 c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5 c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8 c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5 c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0z"/>
<path d="M0-145c-81.8,0-148.1,64.9-148.1,145S-81.8,145,0,145S148.1,80.1,148.1,0S81.8-145,0-145z M40.9,119.8 c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4 c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3 C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4 c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0 C128.9,55.7,92.1,103,40.9,119.8z"/>
<path class="st0" d="M-102.8-7.2l91.2-9.4l-0.3-7l-91.2,9.4L-102.8-7.2z"/>
<path class="st0" d="M12-17.3c0.8-9.6-6.5-18-16.3-18.8s-18.4,6.4-19.2,16S-17-2.1-7.2-1.3S11.2-7.7,12-17.3z"/>
<path class="st0" d="M62.9-24.6c0.8-9.6-6.5-18-16.3-18.8c-9.8-0.8-18.4,6.4-19.2,16c-0.8,9.6,6.5,18,16.3,18.8S62.1-15,62.9-24.6z "/>
<path class="st0" d="M-11.8-16.8l67.6-7.3l-0.5-6.3l-67.5,7.3L-11.8-16.8z"/>
<path class="st0" d="M53.1-23.6l49.5-12.2l-0.6-6.3L52.5-29.9L53.1-23.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,4 +1,4 @@
# Update # Update Opengist
## Make a backup ## Make a backup
@@ -27,9 +27,9 @@ Stop the running instance; then like your first installation of Opengist, downlo
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.6.1/opengist1.6.1-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.9.0/opengist1.9.0-linux-amd64.tar.gz
tar xzvf opengist1.6.1-linux-amd64.tar.gz tar xzvf opengist1.9.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`
@@ -40,6 +40,7 @@ chmod +x opengist
Stop the running instance; then pull the last changes from the master branch, and build the new version. Stop the running instance; then pull the last changes from the master branch, and build the new version.
```shell ```shell
git switch master
git pull git pull
make make
./opengist ./opengist

View File

@@ -0,0 +1,32 @@
# Push Options
Opengist has support for a few [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt).
These options are passed to `git push` command and can be used to change the metadata of a gist.
## Set URL
```shell
git push -o url=mygist # Will set the URL to https://opengist.example.com/user/mygist
```
## Change title
```shell
git push -o title=Gist123
git push -o title="My Gist 123"
```
## Change description
```shell
git push -o description="This is my gist description"
```
## Change visibility
```shell
git push -o visibility=public
git push -o visibility=unlisted
git push -o visibility=private
```

View File

@@ -39,4 +39,4 @@ To http://localhost:6157/init
* [new branch] master -> master * [new branch] master -> master
``` ```
https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066 <video controls="controls" src="https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066" />

112
go.mod
View File

@@ -1,88 +1,112 @@
module github.com/thomiceli/opengist module github.com/thomiceli/opengist
go 1.21 go 1.23
require ( require (
github.com/Kunde21/markdownfmt/v3 v3.1.0 github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.12.0 github.com/alecthomas/chroma/v2 v2.14.0
github.com/blevesearch/bleve/v2 v2.3.10 github.com/blevesearch/bleve/v2 v2.4.3
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/sqlite v1.11.0
github.com/glebarez/sqlite v1.10.0 github.com/go-playground/validator/v10 v10.23.0
github.com/go-playground/validator/v10 v10.16.0 github.com/go-webauthn/webauthn v0.11.2
github.com/google/uuid v1.5.0 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/schema v1.4.1
github.com/hashicorp/go-memdb v1.3.4 github.com/gorilla/securecookie v1.1.2
github.com/labstack/echo/v4 v4.11.4 github.com/gorilla/sessions v1.4.0
github.com/markbates/goth v1.78.0 github.com/labstack/echo/v4 v4.12.0
github.com/rs/zerolog v1.31.0 github.com/markbates/goth v1.80.0
github.com/stretchr/testify v1.8.4 github.com/pquerna/otp v1.4.0
github.com/yuin/goldmark v1.6.0 github.com/rs/zerolog v1.33.0
github.com/yuin/goldmark-emoji v1.0.2 github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.5
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-emoji v1.0.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.5.0 go.abhg.dev/goldmark/mermaid v0.5.0
golang.org/x/crypto v0.17.0 golang.org/x/crypto v0.29.0
golang.org/x/text v0.14.0 golang.org/x/text v0.20.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.5 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.10
gorm.io/gorm v1.25.12
) )
require ( require (
github.com/RoaringBitmap/roaring v1.7.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/blevesearch/bleve_index_api v1.1.4 // indirect github.com/bits-and-blooms/bitset v1.17.0 // indirect
github.com/blevesearch/geo v0.1.18 // indirect github.com/blevesearch/bleve_index_api v1.1.13 // indirect
github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.23 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.5 // indirect github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect
github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.0.10 // indirect github.com/blevesearch/vellum v1.0.11 // indirect
github.com/blevesearch/zapx/v11 v11.3.10 // indirect github.com/blevesearch/zapx/v11 v11.3.10 // indirect
github.com/blevesearch/zapx/v12 v12.3.10 // indirect github.com/blevesearch/zapx/v12 v12.3.10 // indirect
github.com/blevesearch/zapx/v13 v13.3.10 // indirect github.com/blevesearch/zapx/v13 v13.3.10 // indirect
github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v14 v14.3.10 // indirect
github.com/blevesearch/zapx/v15 v15.3.13 // indirect github.com/blevesearch/zapx/v15 v15.3.16 // indirect
github.com/blevesearch/zapx/v16 v16.1.8 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-webauthn/x v0.1.15 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
go.etcd.io/bbolt v1.3.8 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.19.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/oauth2 v0.15.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/net v0.31.0 // indirect
google.golang.org/appengine v1.6.8 // indirect golang.org/x/oauth2 v0.24.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect golang.org/x/sync v0.9.0 // indirect
modernc.org/libc v1.38.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
modernc.org/libc v1.61.2 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.28.0 // indirect modernc.org/sqlite v1.34.1 // indirect
) )

683
go.sum
View File

@@ -1,76 +1,44 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68= github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg= github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4=
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA= github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY=
github.com/blevesearch/bleve_index_api v1.1.4 h1:n9Ilxlb80g9DAhchR95IcVrzohamDSri0wPnkKnva50= github.com/blevesearch/bleve_index_api v1.1.13 h1:+nrA6oRJr85aCPyqaeZtsruObwKojutfonHJin/BP48=
github.com/blevesearch/bleve_index_api v1.1.4/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= github.com/blevesearch/bleve_index_api v1.1.13/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw= github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM= github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc=
github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.2.5 h1:5SsNQmR8v1bojtGQ1zFhZravcMg5rdiX8AVu6LwlVtc= github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY=
github.com/blevesearch/scorch_segment_api/v2 v2.2.5/go.mod h1:8N2ytOlBCdurlxDgbqsfeR1oTKRN0ZVIKdUUP1VFZNc= github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= github.com/blevesearch/vellum v1.0.11 h1:SJI97toEFTtA9WsDZxkyGTaBWFdWl1n2LEDCXLCq/AU=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= github.com/blevesearch/vellum v1.0.11/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
@@ -79,156 +47,101 @@ github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIq
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ= github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE=
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/blevesearch/zapx/v16 v16.1.8 h1:Bxzpw6YQpFs7UjoCV1+RvDw6fmAT2GZxldwX8b3wVBM=
github.com/blevesearch/zapx/v16 v16.1.8/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY= github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA= github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ= github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU=
github.com/go-webauthn/x v0.1.15/go.mod h1:pf7VI23raFLHPO9VVIs9/u1etqwAOP0S2KoHGL6WbZ8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -237,399 +150,145 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY= github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90=
github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38= go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs= go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= gorm.io/driver/postgres v1.5.10 h1:7Lggqempgy496c0WfHXsYWxk3Th+ZcW66/21QhVFdeE=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= gorm.io/driver/postgres v1.5.10/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo= modernc.org/ccgo/v4 v4.22.3 h1:C7AW89Zw3kygesTQWBzApwIn9ldM+cb/plrTIKq41Os=
modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/ccgo/v4 v4.22.3/go.mod h1:Dz7n0/UkBbH3pnYaxgi1mFSfF4REqUOZNziphZASx6k=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.61.2 h1:dkO4DlowfClcJYsvf/RiK6fUwvzCQTmB34bJLt0CAGQ=
modernc.org/libc v1.61.2/go.mod h1:4QGjNyX3h+rn7V5oHpJY2yH0QN6frt1X+5BkXzwLPCo=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -17,12 +17,13 @@ type ActionStatus struct {
} }
const ( const (
SyncReposFromFS = iota SyncReposFromFS = iota
SyncReposFromDB = iota SyncReposFromDB
GitGcRepos = iota GitGcRepos
SyncGistPreviews = iota SyncGistPreviews
ResetHooks = iota ResetHooks
IndexGists = iota IndexGists
SyncGistLanguages
) )
var ( var (
@@ -73,8 +74,10 @@ func Run(actionType int) {
functionToRun = resetHooks functionToRun = resetHooks
case IndexGists: case IndexGists:
functionToRun = indexGists functionToRun = indexGists
case SyncGistLanguages:
functionToRun = syncGistLanguages
default: default:
panic("unhandled default case") log.Error().Msg("Unknown action type")
} }
functionToRun() functionToRun()
@@ -141,17 +144,8 @@ func syncGistPreviews() {
func resetHooks() { func resetHooks() {
log.Info().Msg("Resetting Git server hooks for all repositories...") log.Info().Msg("Resetting Git server hooks for all repositories...")
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*")) if err := git.ResetHooks(); err != nil {
if err != nil { log.Error().Err(err).Msg("Error resetting hooks for repositories")
log.Error().Err(err).Msg("Cannot read repos directories")
return
}
for _, e := range entries {
path := strings.Split(e, string(os.PathSeparator))
if err := git.CreateDotGitFiles(path[len(path)-2], path[len(path)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", path[len(path)-2], path[len(path)-1])
}
} }
} }
@@ -175,3 +169,17 @@ func indexGists() {
} }
} }
} }
func syncGistLanguages() {
log.Info().Msg("Syncing all Gist languages...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
log.Info().Msgf("Syncing languages for gist %d", gist.ID)
gist.UpdateLanguages()
}
}

46
internal/auth/aes.go Normal file
View File

@@ -0,0 +1,46 @@
package auth
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
func AESEncrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(text))
iv := ciphertext[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], text)
return ciphertext, nil
}
func AESDecrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return ciphertext, nil
}

76
internal/auth/argon2id.go Normal file
View File

@@ -0,0 +1,76 @@
package auth
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
"strings"
)
type argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
}
var Argon2id = argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}
func (a argon2ID) Hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
func (a argon2ID) Verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}
hashParts := strings.Split(hash, "$")
if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
}

18
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,18 @@
package auth
type AuthInfoProvider interface {
RequireLogin() (bool, error)
AllowGistsWithoutLogin() (bool, error)
}
func ShouldAllowUnauthenticatedGistAccess(prov AuthInfoProvider, isSingleGistAccess bool) (bool, error) {
require, err := prov.RequireLogin()
if err != nil {
return false, err
}
allow, err := prov.AllowGistsWithoutLogin()
if err != nil {
return false, err
}
return !require || (isSingleGistAccess && allow), nil
}

View File

@@ -0,0 +1,117 @@
package oauth
import (
gocontext "context"
gojson "encoding/json"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"io"
"net/http"
)
type GiteaProvider struct {
Provider
URL string
}
func (p *GiteaProvider) RegisterProvider() error {
goth.UseProviders(
gitea.NewCustomisedURL(
config.C.GiteaClientKey,
config.C.GiteaSecret,
urlJoin(p.URL, "/oauth/gitea/callback"),
urlJoin(config.C.GiteaUrl, "/login/oauth/authorize"),
urlJoin(config.C.GiteaUrl, "/login/oauth/access_token"),
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
return nil
}
func (p *GiteaProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GiteaProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GiteaProvider) UserHasProvider(user *db.User) bool {
return user.GiteaID != ""
}
func NewGiteaProvider(url string) *GiteaProvider {
return &GiteaProvider{
URL: url,
}
}
type GiteaCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GiteaCallbackProvider) GetProvider() string {
return GiteaProviderString
}
func (p *GiteaCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GiteaCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GiteaID != ""
}
func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get(urlJoin(config.C.GiteaUrl, p.User.NickName+".keys"))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
user.GiteaID = p.User.UserID
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitea response body")
return
}
var result map[string]interface{}
err = gojson.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
return
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
return
}
user.AvatarURL = field.(string)
}
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
return &GiteaCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,84 @@
package oauth
import (
gocontext "context"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/github"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"net/http"
)
type GitHubProvider struct {
Provider
URL string
}
func (p *GitHubProvider) RegisterProvider() error {
goth.UseProviders(
github.New(
config.C.GithubClientKey,
config.C.GithubSecret,
urlJoin(p.URL, "/oauth/github/callback"),
),
)
return nil
}
func (p *GitHubProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GitHubProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GitHubProvider) UserHasProvider(user *db.User) bool {
return user.GithubID != ""
}
func NewGitHubProvider(url string) *GitHubProvider {
return &GitHubProvider{
URL: url,
}
}
type GitHubCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GitHubCallbackProvider) GetProvider() string {
return GitHubProviderString
}
func (p *GitHubCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GitHubCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GithubID != ""
}
func (p *GitHubCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get("https://github.com/" + p.User.NickName + ".keys")
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GitHubCallbackProvider) UpdateUserDB(user *db.User) {
user.GithubID = p.User.UserID
user.AvatarURL = "https://avatars.githubusercontent.com/u/" + p.User.UserID + "?v=4"
}
func NewGitHubCallbackProvider(user *goth.User) CallbackProvider {
return &GitHubCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,87 @@
package oauth
import (
gocontext "context"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitlab"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"net/http"
)
type GitLabProvider struct {
Provider
URL string
}
func (p *GitLabProvider) RegisterProvider() error {
goth.UseProviders(
gitlab.NewCustomisedURL(
config.C.GitlabClientKey,
config.C.GitlabSecret,
urlJoin(p.URL, "/oauth/gitlab/callback"),
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
urlJoin(config.C.GitlabUrl, "/oauth/token"),
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
),
)
return nil
}
func (p *GitLabProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GitLabProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GitLabProvider) UserHasProvider(user *db.User) bool {
return user.GitlabID != ""
}
func NewGitLabProvider(url string) *GitLabProvider {
return &GitLabProvider{
URL: url,
}
}
type GitLabCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GitLabCallbackProvider) GetProvider() string {
return GitLabProviderString
}
func (p *GitLabCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GitLabCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GitlabID != ""
}
func (p *GitLabCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get(urlJoin(config.C.GitlabUrl, p.User.NickName+".keys"))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GitLabCallbackProvider) UpdateUserDB(user *db.User) {
user.GitlabID = p.User.UserID
user.AvatarURL = urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", p.User.UserID, "/avatar.png") + "?width=400"
}
func NewGitLabCallbackProvider(user *goth.User) CallbackProvider {
return &GitLabCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,85 @@
package oauth
import (
gocontext "context"
"errors"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/openidConnect"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
)
type OIDCProvider struct {
Provider
URL string
}
func (p *OIDCProvider) RegisterProvider() error {
oidcProvider, err := openidConnect.New(
config.C.OIDCClientKey,
config.C.OIDCSecret,
urlJoin(p.URL, "/oauth/openid-connect/callback"),
config.C.OIDCDiscoveryUrl,
"openid",
"email",
"profile",
)
if err != nil {
return errors.New("Cannot create OIDC provider: " + err.Error())
}
goth.UseProviders(oidcProvider)
return nil
}
func (p *OIDCProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, OpenIDConnectString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *OIDCProvider) UserHasProvider(user *db.User) bool {
return user.OIDCID != ""
}
func NewOIDCProvider(url string) *OIDCProvider {
return &OIDCProvider{
URL: url,
}
}
type OIDCCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *OIDCCallbackProvider) GetProvider() string {
return OpenIDConnectString
}
func (p *OIDCCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *OIDCCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.OIDCID != ""
}
func (p *OIDCCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
return nil, nil
}
func (p *OIDCCallbackProvider) UpdateUserDB(user *db.User) {
user.OIDCID = p.User.UserID
user.AvatarURL = p.User.AvatarURL
}
func NewOIDCCallbackProvider(user *goth.User) CallbackProvider {
return &OIDCCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,93 @@
package oauth
import (
"fmt"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"io"
"net/http"
"net/url"
"strings"
)
const (
GitHubProviderString = "github"
GitLabProviderString = "gitlab"
GiteaProviderString = "gitea"
OpenIDConnectString = "openid-connect"
)
type Provider interface {
RegisterProvider() error
BeginAuthHandler(ctx *context.Context)
UserHasProvider(user *db.User) bool
}
type CallbackProvider interface {
GetProvider() string
GetProviderUser() *goth.User
GetProviderUserID(user *db.User) bool
GetProviderUserSSHKeys() ([]string, error)
UpdateUserDB(user *db.User)
}
func DefineProvider(provider string, url string) (Provider, error) {
switch provider {
case GitHubProviderString:
return NewGitHubProvider(url), nil
case GitLabProviderString:
return NewGitLabProvider(url), nil
case GiteaProviderString:
return NewGiteaProvider(url), nil
case OpenIDConnectString:
return NewOIDCProvider(url), nil
}
return nil, fmt.Errorf("unsupported provider %s", provider)
}
func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return nil, err
}
switch user.Provider {
case GitHubProviderString:
return NewGitHubCallbackProvider(&user), nil
case GitLabProviderString:
return NewGitLabCallbackProvider(&user), nil
case GiteaProviderString:
return NewGiteaCallbackProvider(&user), nil
case OpenIDConnectString:
return NewOIDCCallbackProvider(&user), nil
}
return nil, fmt.Errorf("unsupported provider %s", user.Provider)
}
func urlJoin(base string, elem ...string) string {
joined, err := url.JoinPath(base, elem...)
if err != nil {
log.Error().Err(err).Msg("Cannot join url")
}
return joined
}
func readKeys(response *http.Response) ([]string, error) {
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("could not get user keys %v", err)
}
keys := strings.Split(string(body), "\n")
if len(keys[len(keys)-1]) == 0 {
keys = keys[:len(keys)-1]
}
return keys, nil
}

View File

@@ -0,0 +1,11 @@
package password
import "github.com/thomiceli/opengist/internal/auth"
func HashPassword(code string) (string, error) {
return auth.Argon2id.Hash(code)
}
func VerifyPassword(code, hashedCode string) (bool, error) {
return auth.Argon2id.Verify(code, hashedCode)
}

View File

@@ -0,0 +1,61 @@
package totp
import (
"bytes"
"crypto/rand"
"encoding/base64"
"github.com/pquerna/otp/totp"
"html/template"
"image/png"
"strings"
)
const secretSize = 16
func GenerateQRCode(username, siteUrl string, secret []byte) (string, template.URL, error, []byte) {
var err error
if secret == nil {
secret, err = generateSecret()
if err != nil {
return "", "", err, nil
}
}
otpKey, err := totp.Generate(totp.GenerateOpts{
SecretSize: secretSize,
Issuer: "Opengist (" + strings.ReplaceAll(siteUrl, ":", "") + ")",
AccountName: username,
Secret: secret,
})
if err != nil {
return "", "", err, nil
}
qrcode, err := otpKey.Image(320, 240)
if err != nil {
return "", "", err, nil
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, qrcode); err != nil {
return "", "", err, nil
}
qrcodeImage := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
return otpKey.Secret(), qrcodeImage, nil, secret
}
func Validate(passcode, secret string) bool {
return totp.Validate(passcode, secret)
}
func generateSecret() ([]byte, error) {
secret := make([]byte, secretSize)
_, err := rand.Reader.Read(secret)
if err != nil {
return nil, err
}
return secret, nil
}

View File

@@ -0,0 +1,58 @@
package webauthn
import (
"encoding/binary"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/thomiceli/opengist/internal/db"
)
type user struct {
*db.User
}
func (u *user) WebAuthnID() []byte {
return uintToBytes(u.ID)
}
func (u *user) WebAuthnName() string {
return u.Username
}
func (u *user) WebAuthnDisplayName() string {
return u.Username
}
func (u *user) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := db.GetAllWACredentialsForUser(u.ID)
if err != nil {
return nil
}
return dbCreds
}
func (u *user) Exclusions() []protocol.CredentialDescriptor {
creds := u.WebAuthnCredentials()
exclusions := make([]protocol.CredentialDescriptor, len(creds))
for i, cred := range creds {
exclusions[i] = cred.Descriptor()
}
return exclusions
}
func discoverUser(rawID []byte, _ []byte) (webauthn.User, error) {
ogUser, err := db.GetUserByCredentialID(rawID)
if err != nil {
return nil, err
}
return &user{User: ogUser}, nil
}
func uintToBytes(n uint) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(n))
return b
}

View File

@@ -0,0 +1,138 @@
package webauthn
import (
"encoding/json"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"net/http"
"net/url"
)
var webAuthn *webauthn.WebAuthn
func Init(urlStr string) error {
var rpid, rporigin string
var err error
if urlStr == "" {
log.Info().Msg("External URL is not set, passkeys RP ID and Origins will be set to localhost")
rpid = "localhost"
rporigin = "http://localhost" + ":" + config.C.HttpPort
} else {
urlStruct, err := url.Parse(urlStr)
if err != nil {
return err
}
rpid = urlStruct.Hostname()
rporigin, err = protocol.FullyQualifiedOrigin(urlStr)
if err != nil {
log.Error().Err(err).Msg("Failed to get fully qualified origin from external URL")
}
}
webAuthn, err = webauthn.New(&webauthn.Config{
RPDisplayName: "Opengist",
RPID: rpid,
RPOrigins: []string{rporigin},
})
return err
}
func BeginBinding(dbUser *db.User) (credCreation *protocol.CredentialCreation, jsonSession []byte, err error) {
waUser := &user{User: dbUser}
credCreation, session, err := webAuthn.BeginRegistration(waUser, webauthn.WithAuthenticatorSelection(
protocol.AuthenticatorSelection{
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.VerificationRequired,
},
), webauthn.WithAppIdExcludeExtension("Opengist"), webauthn.WithExclusions(waUser.Exclusions()))
if err != nil {
return nil, nil, err
}
jsonSession, _ = json.Marshal(session)
return
}
func FinishBinding(dbUser *db.User, jsonSession []byte, response *http.Request) (*webauthn.Credential, error) {
waUser := &user{User: dbUser}
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
return webAuthn.FinishRegistration(waUser, session, response)
}
func BeginDiscoverableLogin() (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
credCreation, session, err := webAuthn.BeginDiscoverableLogin(
webauthn.WithUserVerification(protocol.VerificationPreferred),
)
jsonSession, _ = json.Marshal(session)
return
}
func FinishDiscoverableLogin(jsonSession []byte, response *http.Request) (uint, error) {
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
parsedResponse, err := protocol.ParseCredentialRequestResponse(response)
if err != nil {
return 0, err
}
waUser, cred, err := webAuthn.ValidatePasskeyLogin(discoverUser, session, parsedResponse)
if err != nil {
return 0, err
}
dbCredential, err := db.GetCredentialByID(cred.ID)
if err != nil {
return 0, err
}
if err = dbCredential.UpdateSignCount(); err != nil {
return 0, err
}
if err = dbCredential.UpdateLastUsedAt(); err != nil {
return 0, err
}
return waUser.(*user).User.ID, nil
}
func BeginLogin(dbUser *db.User) (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
waUser := &user{User: dbUser}
credCreation, session, err := webAuthn.BeginLogin(waUser)
jsonSession, _ = json.Marshal(session)
return
}
func FinishLogin(dbUser *db.User, jsonSession []byte, response *http.Request) error {
waUser := &user{User: dbUser}
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
cred, err := webAuthn.FinishLogin(waUser, session, response)
if err != nil {
return err
}
dbCredential, err := db.GetCredentialByID(cred.ID)
if err != nil {
return err
}
if err = dbCredential.UpdateSignCount(); err != nil {
return err
}
if err = dbCredential.UpdateLastUsedAt(); err != nil {
return err
}
return err
}

78
internal/cli/admin.go Normal file
View File

@@ -0,0 +1,78 @@
package cli
import (
"fmt"
"github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"github.com/urfave/cli/v2"
)
var CmdAdmin = cli.Command{
Name: "admin",
Usage: "Admin commands",
Subcommands: []*cli.Command{
&CmdAdminResetPassword,
&CmdAdminToggleAdmin,
},
}
var CmdAdminResetPassword = cli.Command{
Name: "reset-password",
Usage: "Reset the password for a given user",
ArgsUsage: "[username] [password]",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if ctx.NArg() < 2 {
return fmt.Errorf("username and password are required")
}
username := ctx.Args().Get(0)
plainPassword := ctx.Args().Get(1)
user, err := db.GetUserByUsername(username)
if err != nil {
fmt.Printf("Cannot get user %s: %s\n", username, err)
return err
}
password, err := password.HashPassword(plainPassword)
if err != nil {
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
return err
}
user.Password = password
if err = user.Update(); err != nil {
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
return err
}
fmt.Printf("Password for user %s has been reset.\n", username)
return nil
},
}
var CmdAdminToggleAdmin = cli.Command{
Name: "toggle-admin",
Usage: "Toggle the admin status for a given user",
ArgsUsage: "[username]",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if ctx.NArg() < 1 {
return fmt.Errorf("username is required")
}
username := ctx.Args().Get(0)
user, err := db.GetUserByUsername(username)
if err != nil {
fmt.Printf("Cannot get user %s: %s\n", username, err)
return err
}
user.IsAdmin = !user.IsAdmin
if err = user.Update(); err != nil {
fmt.Printf("Cannot update user %s: %s\n", username, err)
}
fmt.Printf("User %s admin set to %t\n", username, user.IsAdmin)
return nil
},
}

56
internal/cli/hook.go Normal file
View File

@@ -0,0 +1,56 @@
package cli
import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/hooks"
"github.com/urfave/cli/v2"
"io"
"os"
)
var CmdHook = cli.Command{
Name: "hook",
Usage: "Run Git server hooks, used and should only be called by Opengist itself",
Subcommands: []*cli.Command{
&CmdHookPreReceive,
&CmdHookPostReceive,
},
}
var CmdHookPreReceive = cli.Command{
Name: "pre-receive",
Usage: "Run Git server pre-receive hook for a repository",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if err := hooks.PreReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
os.Exit(1)
}
return nil
},
}
var CmdHookPostReceive = cli.Command{
Name: "post-receive",
Usage: "Run Git server post-receive hook for a repository",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if err := hooks.PostReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
os.Exit(1)
}
return nil
},
}
func initialize(ctx *cli.Context) {
if err := config.InitConfig(ctx.String("config"), io.Discard); err != nil {
panic(err)
}
config.InitLog()
db.DeprecationDBFilename()
if err := db.Setup(config.C.DBUri); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
}
}

184
internal/cli/main.go Normal file
View File

@@ -0,0 +1,184 @@
package cli
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/webauthn"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web/server"
"github.com/urfave/cli/v2"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
)
var CmdVersion = cli.Command{
Name: "version",
Usage: "Print the version of Opengist",
Action: func(c *cli.Context) error {
fmt.Println("Opengist " + config.OpengistVersion)
return nil
},
}
var CmdStart = cli.Command{
Name: "start",
Usage: "Start Opengist server",
Action: func(ctx *cli.Context) error {
stopCtx, stop := signal.NotifyContext(ctx.Context, syscall.SIGINT, syscall.SIGTERM)
defer stop()
Initialize(ctx)
go server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start()
go ssh.Start()
<-stopCtx.Done()
shutdown()
return nil
},
}
var ConfigFlag = cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to a config file in YAML format",
}
func App() error {
app := cli.NewApp()
app.Name = "Opengist"
app.Usage = "A self-hosted pastebin powered by Git."
app.HelpName = "opengist"
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
app.DefaultCommand = CmdStart.Name
app.Flags = []cli.Flag{
&ConfigFlag,
}
return app.Run(os.Args)
}
func Initialize(ctx *cli.Context) {
fmt.Println("Opengist " + config.OpengistVersion)
if err := config.InitConfig(ctx.String("config"), os.Stdout); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
panic(err)
}
config.SetupSecretKey()
config.InitLog()
gitVersion, err := git.GetGitVersion()
if err != nil {
log.Fatal().Err(err).Send()
}
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
log.Fatal().Err(err).Send()
} else if !ok {
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " +
"Current git version: " + gitVersion)
}
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)
if err := git.InitGitConfig(); err != nil {
log.Warn().Err(err).Msgf("Failed to change the host's git global config, ensure to add to `safe.directory` the path %s, and `receive.advertisePushOptions` is set to true.", homePath)
}
if err := createSymlink(homePath, ctx.String("config")); err != nil {
log.Fatal().Err(err).Msg("Failed to create symlinks")
}
if err := os.MkdirAll(filepath.Join(homePath, "sessions"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
db.DeprecationDBFilename()
if err := db.Setup(config.C.DBUri); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database")
}
if err := webauthn.Init(config.C.ExternalUrl); err != nil {
log.Error().Err(err).Msg("Failed to initialize WebAuthn")
}
if config.C.IndexEnabled {
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname))
index.Init(filepath.Join(homePath, config.C.IndexDirname))
}
}
func shutdown() {
log.Info().Msg("Shutting down database...")
if err := db.Close(); err != nil {
log.Error().Err(err).Msg("Failed to close database")
}
if config.C.IndexEnabled {
log.Info().Msg("Shutting down index...")
index.Close()
}
log.Info().Msg("Shutdown complete")
}
func createSymlink(homePath string, configPath string) error {
if err := os.MkdirAll(filepath.Join(homePath, "symlinks"), 0755); err != nil {
return err
}
exePath, err := os.Executable()
if err != nil {
return err
}
symlinkExePath := path.Join(config.GetHomeDir(), "symlinks", "opengist")
if _, err := os.Lstat(symlinkExePath); err == nil {
if err := os.Remove(symlinkExePath); err != nil {
return err
}
}
if err = os.Symlink(exePath, symlinkExePath); err != nil {
return err
}
if configPath == "" {
return nil
}
configPath, _ = filepath.Abs(configPath)
configPath = filepath.Clean(configPath)
symlinkConfigPath := path.Join(config.GetHomeDir(), "symlinks", "config.yml")
if _, err := os.Lstat(symlinkConfigPath); err == nil {
if err := os.Remove(symlinkConfigPath); err != nil {
return err
}
}
if err = os.Symlink(configPath, symlinkConfigPath); err != nil {
return err
}
return nil
}

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/session"
"io" "io"
"net/url" "net/url"
"os" "os"
@@ -10,25 +11,32 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
var OpengistVersion = "1.6.1" var OpengistVersion = ""
var C *config var C *config
var SecretKey []byte
// Not using nested structs because the library // Not using nested structs because the library
// doesn't support dot notation in this case sadly // doesn't support dot notation in this case sadly
type config struct { type config struct {
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"` LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"` LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"` ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"` OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` // deprecated
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"`
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"`
@@ -52,23 +60,37 @@ type config struct {
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"` GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"` GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"` GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
GitlabName string `yaml:"gitlab.name" env:"OG_GITLAB_NAME"`
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"` GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"` GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"` GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"` OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"` OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"` OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"`
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
}
type StaticLink struct {
Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"`
Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"`
} }
func configWithDefaults() (*config, error) { func configWithDefaults() (*config, error) {
c := &config{} c := &config{}
c.SecretKey = ""
c.LogLevel = "warn" c.LogLevel = "warn"
c.LogOutput = "stdout,file" c.LogOutput = "stdout,file"
c.OpengistHome = "" c.OpengistHome = ""
c.DBFilename = "opengist.db" c.DBUri = "opengist.db"
c.IndexEnabled = true c.IndexEnabled = true
c.IndexDirname = "opengist.index" c.IndexDirname = "opengist.index"
@@ -83,23 +105,26 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222" c.SshPort = "2222"
c.SshKeygen = "ssh-keygen" c.SshKeygen = "ssh-keygen"
c.GitlabName = "GitLab"
c.GiteaUrl = "https://gitea.com" c.GiteaUrl = "https://gitea.com"
c.GiteaName = "Gitea"
return c, nil return c, nil
} }
func InitConfig(configPath string) error { func InitConfig(configPath string, out io.Writer) error {
// Default values // Default values
c, err := configWithDefaults() c, err := configWithDefaults()
if err != nil { if err != nil {
return err return err
} }
if err = loadConfigFromYaml(c, configPath); err != nil { if err = loadConfigFromYaml(c, configPath, out); err != nil {
return err return err
} }
if err = loadConfigFromEnv(c); err != nil { if err = loadConfigFromEnv(c, out); err != nil {
return err return err
} }
@@ -118,6 +143,13 @@ func InitConfig(configPath string) error {
C = c C = c
if err = migrateConfig(); err != nil {
return err
}
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err
}
return nil return nil
} }
@@ -133,9 +165,24 @@ func InitLog() {
} }
var logWriters []io.Writer var logWriters []io.Writer
logOutputTypes := utils.RemoveDuplicates[string]( logOutputTypes := strings.Split(strings.ToLower(C.LogOutput), ",")
strings.Split(strings.ToLower(C.LogOutput), ","), slices.Sort(logOutputTypes)
logOutputTypes = slices.Compact(logOutputTypes)
consoleWriter := zerolog.NewConsoleWriter(
func(w *zerolog.ConsoleWriter) {
w.TimeFormat = time.TimeOnly
w.FormatCaller = func(i interface{}) string {
file := i.(string)
index := strings.Index(file, "internal")
if index == -1 {
return file
}
return file[index:]
}
},
) )
for _, logOutputType := range logOutputTypes { for _, logOutputType := range logOutputTypes {
logOutputType = strings.TrimSpace(logOutputType) logOutputType = strings.TrimSpace(logOutputType)
if !slices.Contains([]string{"stdout", "file"}, logOutputType) { if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
@@ -145,7 +192,7 @@ func InitLog() {
switch logOutputType { switch logOutputType {
case "stdout": case "stdout":
logWriters = append(logWriters, zerolog.NewConsoleWriter()) logWriters = append(logWriters, consoleWriter)
defer func() { log.Debug().Msg("Logging to stdout") }() defer func() { log.Debug().Msg("Logging to stdout") }()
case "file": case "file":
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
@@ -157,14 +204,14 @@ func InitLog() {
} }
} }
if len(logWriters) == 0 { if len(logWriters) == 0 {
logWriters = append(logWriters, zerolog.NewConsoleWriter()) logWriters = append(logWriters, consoleWriter)
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }() defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
} }
multi := zerolog.MultiLevelWriter(logWriters...) multi := zerolog.MultiLevelWriter(logWriters...)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger() log.Logger = zerolog.New(multi).Level(level).With().Caller().Timestamp().Logger()
if !slices.Contains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) { if !slices.Contains([]string{"debug", "info", "warn", "error", "fatal"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel) log.Warn().Msg("Invalid log level: " + C.LogLevel)
} }
} }
@@ -195,7 +242,16 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath) return filepath.Clean(absolutePath)
} }
func loadConfigFromYaml(c *config, configPath string) error { func SetupSecretKey() {
if C.SecretKey == "" {
path := filepath.Join(GetHomeDir(), "opengist-secret.key")
SecretKey, _ = session.GenerateSecretKey(path)
} else {
SecretKey = []byte(C.SecretKey)
}
}
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if configPath != "" { if configPath != "" {
absolutePath, _ := filepath.Abs(configPath) absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath) absolutePath = filepath.Clean(absolutePath)
@@ -204,9 +260,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return err return err
} }
fmt.Println("No YAML config file found at " + absolutePath) _, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
} else { } else {
fmt.Println("Using YAML config file: " + absolutePath) _, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
// Override default values with values from config.yml // Override default values with values from config.yml
d := yaml.NewDecoder(file) d := yaml.NewDecoder(file)
@@ -216,13 +272,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
defer file.Close() defer file.Close()
} }
} else { } else {
fmt.Println("No YAML config file specified.") _, _ = fmt.Fprintln(out, "No YAML config file specified.")
} }
return nil return nil
} }
func loadConfigFromEnv(c *config) error { func loadConfigFromEnv(c *config, out io.Writer) error {
v := reflect.ValueOf(c).Elem() v := reflect.ValueOf(c).Elem()
var envVars []string var envVars []string
@@ -234,28 +290,69 @@ func loadConfigFromEnv(c *config) error {
} }
envValue := os.Getenv(strings.ToUpper(tag)) envValue := os.Getenv(strings.ToUpper(tag))
if envValue == "" { if envValue == "" && v.Field(i).Kind() != reflect.Slice {
continue continue
} }
switch v.Field(i).Kind() { switch v.Field(i).Kind() {
case reflect.String: case reflect.String:
v.Field(i).SetString(envValue) v.Field(i).SetString(envValue)
envVars = append(envVars, tag)
case reflect.Bool: case reflect.Bool:
boolVal, err := strconv.ParseBool(envValue) boolVal, err := strconv.ParseBool(envValue)
if err != nil { if err != nil {
return err return err
} }
v.Field(i).SetBool(boolVal) v.Field(i).SetBool(boolVal)
envVars = append(envVars, tag)
case reflect.Slice:
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
prefix := strings.ToUpper(tag) + "_"
var sliceValue reflect.Value
elemType := v.Type().Field(i).Type.Elem()
for index := 0; ; index++ {
allFieldsPresent := true
elemValue := reflect.New(elemType).Elem()
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
elemField := elemValue.Type().Field(j)
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
envValue, present := os.LookupEnv(envName)
if !present {
allFieldsPresent = false
break
}
envVars = append(envVars, envName)
elemValue.Field(j).SetString(envValue)
}
if !allFieldsPresent {
break
}
if sliceValue.Kind() != reflect.Slice {
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
}
sliceValue = reflect.Append(sliceValue, elemValue)
}
if sliceValue.IsValid() {
v.Field(i).Set(sliceValue)
}
}
default:
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
} }
envVars = append(envVars, tag)
} }
if len(envVars) > 0 { if len(envVars) > 0 {
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", ")) _, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
} else { } else {
fmt.Println("No environment variables config specified.") _, _ = fmt.Fprintln(out, "No environment variables config specified.")
} }
return nil return nil

View File

@@ -0,0 +1,42 @@
package config
import (
"fmt"
"os"
"path/filepath"
)
// auto migration for newer versions of Opengist
func migrateConfig() error {
configMigrations := []struct {
Version string
Func func() error
}{
{"1.8.0", v1_8_0},
}
for _, fn := range configMigrations {
err := fn.Func()
if err != nil {
return err
}
}
return nil
}
func v1_8_0() error {
homeDir := GetHomeDir()
moveFile(filepath.Join(filepath.Join(homeDir, "sessions"), "session-auth.key"), filepath.Join(homeDir, "opengist-secret.key"))
return nil
}
func moveFile(oldPath, newPath string) {
if _, err := os.Stat(oldPath); err != nil {
return
}
if err := os.Rename(oldPath, newPath); err == nil {
fmt.Printf("Automatically moved %s to %s\n", oldPath, newPath)
}
}

View File

@@ -5,20 +5,27 @@ import (
) )
type AdminSetting struct { type AdminSetting struct {
Key string `gorm:"uniqueIndex"` Key string `gorm:"index:,unique"`
Value string Value string
} }
const ( const (
SettingDisableSignup = "disable-signup" SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login" SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form" SettingAllowGistsWithoutLogin = "allow-gists-without-login"
SettingDisableGravatar = "disable-gravatar" SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
) )
func GetSetting(key string) (string, error) { func GetSetting(key string) (string, error) {
var setting AdminSetting var setting AdminSetting
err := db.Where("key = ?", key).First(&setting).Error var err error
switch db.Dialector.Name() {
case "mysql", "sqlite":
err = db.Where("`key` = ?", key).First(&setting).Error
case "postgres":
err = db.Where("key = ?", key).First(&setting).Error
}
return setting.Value, err return setting.Value, err
} }
@@ -48,7 +55,7 @@ func UpdateSetting(key string, value string) error {
} }
func setSetting(key string, value string) error { func setSetting(key string, value string) error {
return db.Create(&AdminSetting{Key: key, Value: value}).Error return db.FirstOrCreate(&AdminSetting{Key: key, Value: value}, &AdminSetting{Key: key}).Error
} }
func initAdminSettings(settings map[string]string) error { func initAdminSettings(settings map[string]string) error {
@@ -62,3 +69,21 @@ func initAdminSettings(settings map[string]string) error {
return nil return nil
} }
type AuthInfo struct{}
func (auth AuthInfo) RequireLogin() (bool, error) {
s, err := GetSetting(SettingRequireLogin)
if err != nil {
return true, err
}
return s == "1", nil
}
func (auth AuthInfo) AllowGistsWithoutLogin() (bool, error) {
s, err := GetSetting(SettingAllowGistsWithoutLogin)
if err != nil {
return false, err
}
return s == "1", nil
}

View File

@@ -2,38 +2,140 @@ package db
import ( import (
"errors" "errors"
"fmt"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm/logger"
"net/url"
"path/filepath"
"slices" "slices"
"strings" "strings"
"time"
msqlite "github.com/glebarez/go-sqlite"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var db *gorm.DB var db *gorm.DB
func Setup(dbPath string, sharedCache bool) error { const (
var err error SQLite databaseType = iota
journalMode := strings.ToUpper(config.C.SqliteJournalMode) PostgreSQL
MySQL
)
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) { type databaseType int
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
func (d databaseType) String() string {
return [...]string{"SQLite", "PostgreSQL", "MySQL"}[d]
}
type databaseInfo struct {
Type databaseType
Host string
Port string
User string
Password string
Database string
}
var DatabaseInfo *databaseInfo
func parseDBURI(uri string) (*databaseInfo, error) {
info := &databaseInfo{}
if uri == ":memory:" {
info.Type = SQLite
info.Database = uri
return info, nil
} }
sharedCacheStr := "" u, err := url.Parse(uri)
if sharedCache { if err != nil {
sharedCacheStr = "&cache=shared" return nil, fmt.Errorf("invalid URI: %v", err)
} }
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{ if u.Scheme == "" {
Logger: logger.Default.LogMode(logger.Silent), info.Type = SQLite
}); err != nil { info.Database = filepath.Join(config.GetHomeDir(), uri)
return info, nil
}
switch u.Scheme {
case "postgres", "postgresql":
info.Type = PostgreSQL
case "mysql", "mariadb":
info.Type = MySQL
case "file":
info.Type = SQLite
default:
return nil, fmt.Errorf("unknown database: %v", err)
}
if u.Host != "" {
host, port, _ := strings.Cut(u.Host, ":")
info.Host = host
info.Port = port
}
if u.User != nil {
info.User = u.User.Username()
info.Password, _ = u.User.Password()
}
switch info.Type {
case PostgreSQL, MySQL:
info.Database = strings.TrimPrefix(u.Path, "/")
case SQLite:
info.Database = u.String()
default:
return nil, fmt.Errorf("unknown database: %v", err)
}
return info, nil
}
func Setup(dbUri string) error {
dbInfo, err := parseDBURI(dbUri)
if err != nil {
return err return err
} }
log.Info().Msgf("Setting up a %s database connection", dbInfo.Type)
var setupFunc func(databaseInfo) error
switch dbInfo.Type {
case SQLite:
setupFunc = setupSQLite
case PostgreSQL:
setupFunc = setupPostgres
case MySQL:
setupFunc = setupMySQL
default:
return fmt.Errorf("unknown database type: %v", dbInfo.Type)
}
maxAttempts := 60
retryInterval := 1 * time.Second
for attempt := 1; attempt <= maxAttempts; attempt++ {
err = setupFunc(*dbInfo)
if err == nil {
log.Info().Msg("Database connection established")
break
}
if attempt < maxAttempts {
log.Warn().Err(err).Msgf("Failed to connect to database (attempt %d), retrying in %v...", attempt, retryInterval)
time.Sleep(retryInterval)
} else {
return err
}
}
DatabaseInfo = dbInfo
if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil { if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil {
return err return err
} }
@@ -42,20 +144,21 @@ func Setup(dbPath string, sharedCache bool) error {
return err return err
} }
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil { if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}); err != nil {
return err return err
} }
if err = ApplyMigrations(db); err != nil { if err = applyMigrations(dbInfo); err != nil {
return err return err
} }
// Default admin setting values // Default admin setting values
return initAdminSettings(map[string]string{ return initAdminSettings(map[string]string{
SettingDisableSignup: "0", SettingDisableSignup: "0",
SettingRequireLogin: "0", SettingRequireLogin: "0",
SettingDisableLoginForm: "0", SettingAllowGistsWithoutLogin: "0",
SettingDisableGravatar: "0", SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
}) })
} }
@@ -74,11 +177,7 @@ func CountAll(table interface{}) (int64, error) {
} }
func IsUniqueConstraintViolation(err error) bool { func IsUniqueConstraintViolation(err error) bool {
var sqliteErr *msqlite.Error return errors.Is(err, gorm.ErrDuplicatedKey)
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
return true
}
return false
} }
func Ping() error { func Ping() error {
@@ -89,3 +188,75 @@ func Ping() error {
return sql.Ping() return sql.Ping()
} }
func setupSQLite(dbInfo databaseInfo) error {
var err error
var dsn string
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
}
if dbInfo.Database == ":memory:" {
dsn = ":memory:?_fk=true&cache=shared"
} else {
u, err := url.Parse(dbInfo.Database)
if err != nil {
return err
}
u.Scheme = "file"
q := u.Query()
q.Set("_pragma", "foreign_keys(1)")
q.Set("_journal_mode", journalMode)
u.RawQuery = q.Encode()
dsn = u.String()
}
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func setupPostgres(dbInfo databaseInfo) error {
var err error
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", dbInfo.Host, dbInfo.Port, dbInfo.User, dbInfo.Password, dbInfo.Database)
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func setupMySQL(dbInfo databaseInfo) error {
var err error
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbInfo.User, dbInfo.Password, dbInfo.Host, dbInfo.Port, dbInfo.Database)
db, err = gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
DontSupportRenameIndex: true,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func DeprecationDBFilename() {
if config.C.DBFilename != "" {
log.Warn().Msg("The 'db-filename'/'OG_DB_FILENAME' configuration option is deprecated and will be removed in a future version. Please use 'db-uri'/'OG_DB_URI' instead.")
}
if config.C.DBUri == "" {
config.C.DBUri = config.C.DBFilename
}
}
func TruncateDatabase() error {
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{})
}

View File

@@ -1,19 +1,21 @@
package db package db
import ( import (
"bytes"
"encoding/gob"
"fmt" "fmt"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/index"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"time" "time"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -25,6 +27,19 @@ const (
PrivateVisibility PrivateVisibility
) )
func (v Visibility) String() string {
switch v {
case PublicVisibility:
return "public"
case UnlistedVisibility:
return "unlisted"
case PrivateVisibility:
return "private"
default:
return "???"
}
}
func (v Visibility) Next() Visibility { func (v Visibility) Next() Visibility {
switch v { switch v {
case PublicVisibility: case PublicVisibility:
@@ -36,16 +51,16 @@ func (v Visibility) Next() Visibility {
} }
} }
func ParseVisibility[T string | int](v T) (Visibility, error) { func ParseVisibility[T string | int](v T) Visibility {
switch s := fmt.Sprint(v); s { switch s := fmt.Sprint(v); s {
case "0": case "0", "public":
return PublicVisibility, nil return PublicVisibility
case "1": case "1", "unlisted":
return UnlistedVisibility, nil return UnlistedVisibility
case "2": case "2", "private":
return PrivateVisibility, nil return PrivateVisibility
default: default:
return -1, fmt.Errorf("unknown visibility %q", s) return PublicVisibility
} }
} }
@@ -69,6 +84,9 @@ type Gist struct {
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint ForkedID uint
Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Languages []GistLanguage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
} }
type Like struct { type Like struct {
@@ -88,8 +106,8 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) { func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist) gist := new(Gist)
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user). Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
Joins("join users on gists.user_id = users.id"). Joins("join users on gists.user_id = users.id").
First(&gist).Error First(&gist).Error
@@ -98,7 +116,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) {
func GetGistByID(gistId string) (*Gist, error) { func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist) gist := new(Gist)
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.id = ?", gistId). Where("gists.id = ?", gistId).
First(&gist).Error First(&gist).Error
@@ -107,7 +125,9 @@ func GetGistByID(gistId string) (*Gist, error) {
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.private = 0 or gists.user_id = ?", currentUserId). Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11). Limit(11).
Offset(offset * 10). Offset(offset * 10).
@@ -128,12 +148,18 @@ func GetAllGists(offset int) ([]*Gist, error) {
return gists, err return gists, err
} }
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) { func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). tx := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%"). Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%")
Limit(11).
if topic != "" {
tx = tx.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic = ?", topic)
}
err := tx.Limit(11).
Offset(offset * 10). Offset(offset * 10).
Order("gists." + sort + "_at " + order). Order("gists." + sort + "_at " + order).
Find(&gists).Error Find(&gists).Error
@@ -142,30 +168,64 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
} }
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB { func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId). Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id") Joins("join users on gists.user_id = users.id")
} }
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { func gistsFromUserStatementWithPreloads(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, title string, language string, visibility string, topics []string, offset int, sort string, order string) ([]*Gist, int64, error) {
var gists []*Gist var gists []*Gist
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11). var count int64
baseQuery := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{})
if title != "" {
baseQuery = baseQuery.Where("gists.title like ?", "%"+title+"%")
}
if language != "" {
baseQuery = baseQuery.Joins("join gist_languages on gists.id = gist_languages.gist_id").
Where("gist_languages.language = ?", language)
}
if visibility != "" {
baseQuery = baseQuery.Where("gists.private = ?", ParseVisibility(visibility))
}
if len(topics) > 0 {
baseQuery = baseQuery.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic in ?", topics)
}
err := baseQuery.Count(&count).Error
if err != nil {
return nil, 0, err
}
err = baseQuery.Limit(11).
Offset(offset * 10). Offset(offset * 10).
Order("gists." + sort + "_at " + order). Order("gists." + sort + "_at " + order).
Find(&gists).Error Find(&gists).Error
return gists, err return gists, count, err
} }
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) { func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64 var count int64
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error err := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err return count, err
} }
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB { func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId). Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id"). Joins("join likes on gists.id = likes.gist_id").
@@ -188,7 +248,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
} }
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB { func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId). Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id") Joins("join users on gists.user_id = users.id")
@@ -230,11 +290,22 @@ func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
func GetAllGistsByIds(ids []uint) ([]*Gist, error) { func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("id in ?", ids). Where("id in ?", ids).
Find(&gists).Error Find(&gists).Error
return gists, err // keep order
ordered := make([]*Gist, 0, len(ids))
for _, wantedId := range ids {
for _, gist := range gists {
if gist.ID == wantedId {
ordered = append(ordered, gist)
break
}
}
}
return ordered, err
} }
func (gist *Gist) Create() error { func (gist *Gist) Create() error {
@@ -247,6 +318,12 @@ func (gist *Gist) CreateForked() error {
} }
func (gist *Gist) Update() error { func (gist *Gist) Update() error {
// reset the topics
err := db.Model(&GistTopic{}).Where("gist_id = ?", gist.ID).Delete(&GistTopic{}).Error
if err != nil {
return err
}
return db.Omit("forked_id").Save(&gist).Error return db.Omit("forked_id").Save(&gist).Error
} }
@@ -330,10 +407,6 @@ func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid) return git.InitRepository(gist.User.Username, gist.Uuid)
} }
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
}
func (gist *Gist) DeleteRepository() error { func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid) return git.DeleteRepository(gist.User.Username, gist.Uuid)
} }
@@ -527,16 +600,132 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
return languages, nil return languages, nil
} }
func (gist *Gist) GetTopics() ([]string, error) {
var topics []string
err := db.Model(&GistTopic{}).
Where("gist_id = ?", gist.ID).
Pluck("topic", &topics).Error
return topics, err
}
func (gist *Gist) TopicsSlice() []string {
topics := make([]string, 0, len(gist.Topics))
for _, topic := range gist.Topics {
topics = append(topics, topic.Topic)
}
return topics
}
func (gist *Gist) SerialiseInitRepository() error {
var gobBuffer bytes.Buffer
encoder := gob.NewEncoder(&gobBuffer)
if err := encoder.Encode(gist); err != nil {
return fmt.Errorf("gob encoding error: %v", err)
}
return git.SerialiseInitRepository(gist.User.Username, gobBuffer.Bytes())
}
func DeserialiseInitRepository(user string) (*Gist, error) {
data, err := git.DeserialiseInitRepository(user)
if err != nil {
return nil, err
}
var gist Gist
decoder := gob.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&gist); err != nil {
return nil, fmt.Errorf("gob decoding error: %v", err)
}
return &gist, nil
}
func (gist *Gist) UpdateLanguages() {
languages, err := gist.GetLanguagesFromFiles()
if err != nil {
log.Error().Err(err).Msgf("Cannot get languages for gist %d", gist.ID)
return
}
slices.Sort(languages)
languages = slices.Compact(languages)
tx := db.Begin()
if tx.Error != nil {
log.Error().Err(tx.Error).Msgf("Cannot start transaction for gist %d", gist.ID)
return
}
if err := tx.Where("gist_id = ?", gist.ID).Delete(&GistLanguage{}).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot delete languages for gist %d", gist.ID)
return
}
for _, language := range languages {
gistLanguage := &GistLanguage{
GistID: gist.ID,
Language: language,
}
if err := tx.Create(gistLanguage).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot create gist language %s for gist %d", language, gist.ID)
return
}
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot commit transaction for gist %d", gist.ID)
return
}
}
func (gist *Gist) ToDTO() (*GistDTO, error) {
files, err := gist.Files("HEAD", false)
if err != nil {
return nil, err
}
fileDTOs := make([]FileDTO, 0, len(files))
for _, file := range files {
fileDTOs = append(fileDTOs, FileDTO{
Filename: file.Filename,
Content: file.Content,
})
}
return &GistDTO{
Title: gist.Title,
Description: gist.Description,
URL: gist.URL,
Files: fileDTOs,
VisibilityDTO: VisibilityDTO{
Private: gist.Private,
},
Topics: strings.Join(gist.TopicsSlice(), " "),
}, nil
}
// -- DTO -- // // -- DTO -- //
type GistDTO struct { type GistDTO struct {
Title string `validate:"max=250" form:"title"` Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"` Description string `validate:"max=1000" form:"description"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"` URL string `validate:"max=32,alphanumdashorempty" form:"url"`
Private Visibility `validate:"number,min=0,max=2" form:"private"` Files []FileDTO `validate:"min=1,dive"`
Files []FileDTO `validate:"min=1,dive"` Name []string `form:"name"`
Name []string `form:"name"` Content []string `form:"content"`
Content []string `form:"content"` Topics string `validate:"gisttopics" form:"topics"`
VisibilityDTO
}
func (dto *GistDTO) HasMetadata() bool {
return dto.Title != "" || dto.Description != "" || dto.URL != "" || dto.Topics != ""
}
type VisibilityDTO struct {
Private Visibility `validate:"number,min=0,max=2" form:"private"`
} }
type FileDTO struct { type FileDTO struct {
@@ -550,6 +739,7 @@ func (dto *GistDTO) ToGist() *Gist {
Description: dto.Description, Description: dto.Description,
Private: dto.Private, Private: dto.Private,
URL: dto.URL, URL: dto.URL,
Topics: dto.TopicStrToSlice(),
} }
} }
@@ -557,9 +747,19 @@ func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title gist.Title = dto.Title
gist.Description = dto.Description gist.Description = dto.Description
gist.URL = dto.URL gist.URL = dto.URL
gist.Topics = dto.TopicStrToSlice()
return gist return gist
} }
func (dto *GistDTO) TopicStrToSlice() []GistTopic {
topics := strings.Fields(dto.Topics)
gistTopics := make([]GistTopic, 0, len(topics))
for _, topic := range topics {
gistTopics = append(gistTopics, GistTopic{Topic: topic})
}
return gistTopics
}
// -- Index -- // // -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) { func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
@@ -572,6 +772,9 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
wholeContent := "" wholeContent := ""
for _, file := range files { for _, file := range files {
wholeContent += file.Content wholeContent += file.Content
if !strings.HasSuffix(wholeContent, "\n") {
wholeContent += "\n"
}
exts = append(exts, filepath.Ext(file.Filename)) exts = append(exts, filepath.Ext(file.Filename))
} }
@@ -585,6 +788,11 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
return nil, err return nil, err
} }
topics, err := gist.GetTopics()
if err != nil {
return nil, err
}
indexedGist := &index.Gist{ indexedGist := &index.Gist{
GistID: gist.ID, GistID: gist.ID,
Username: gist.User.Username, Username: gist.User.Username,
@@ -593,6 +801,7 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
Filenames: fileNames, Filenames: fileNames,
Extensions: exts, Extensions: exts,
Languages: langs, Languages: langs,
Topics: topics,
CreatedAt: gist.CreatedAt, CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt, UpdatedAt: gist.UpdatedAt,
} }

View File

@@ -0,0 +1,27 @@
package db
type GistLanguage struct {
GistID uint `gorm:"primaryKey"`
Language string `gorm:"primaryKey;size:100"`
}
func GetGistLanguagesForUser(fromUserId, currentUserId uint) ([]struct {
Language string
Count int64
}, error) {
var results []struct {
Language string
Count int64
}
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&GistLanguage{}).
Select("language, count(*) as count").
Joins("JOIN gists ON gists.id = gist_languages.gist_id").
Where("gists.user_id = ?", fromUserId).
Group("language").
Order("count DESC").
Limit(15). // Added limit of 15
Find(&results).Error
return results, err
}

View File

@@ -0,0 +1,6 @@
package db
type GistTopic struct {
GistID uint `gorm:"primaryKey"`
Topic string `gorm:"primaryKey;size:50"`
}

99
internal/db/invitation.go Normal file
View File

@@ -0,0 +1,99 @@
package db
import (
"fmt"
"math/rand"
"time"
)
type Invitation struct {
ID uint `gorm:"primaryKey"`
Code string
ExpiresAt int64
NbUsed uint
NbMax uint
}
func GetAllInvitations() ([]*Invitation, error) {
var invitations []*Invitation
dialect := db.Dialector.Name()
query := db.Model(&Invitation{})
switch dialect {
case "sqlite":
query = query.Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
case "postgres":
query = query.Order("(((expires_at >= EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
case "mysql":
query = query.Order("(((expires_at >= UNIX_TIMESTAMP()) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
default:
return nil, fmt.Errorf("unsupported database dialect: %s", dialect)
}
err := query.Order("id ASC").Find(&invitations).Error
return invitations, err
}
func GetInvitationByID(id uint) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("id = ?", id).
First(&invitation).Error
return invitation, err
}
func GetInvitationByCode(code string) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("code = ?", code).
First(&invitation).Error
return invitation, err
}
func InvitationCodeExists(code string) (bool, error) {
var count int64
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
return count > 0, err
}
func (i *Invitation) Create() error {
i.Code = generateRandomCode()
return db.Create(&i).Error
}
func (i *Invitation) Update() error {
return db.Save(&i).Error
}
func (i *Invitation) Delete() error {
return db.Delete(&i).Error
}
func (i *Invitation) IsExpired() bool {
return i.ExpiresAt < time.Now().Unix()
}
func (i *Invitation) IsMaxedOut() bool {
return i.NbMax > 0 && i.NbUsed >= i.NbMax
}
func (i *Invitation) IsUsable() bool {
return !i.IsExpired() && !i.IsMaxedOut()
}
func (i *Invitation) Use() error {
i.NbUsed++
return i.Update()
}
func generateRandomCode() string {
const charset = "0123456789ABCDEF"
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
result := make([]byte, 16)
for i := range result {
result[i] = charset[seededRand.Intn(len(charset))]
}
return string(result)
}

View File

@@ -3,7 +3,6 @@ package db
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm"
) )
type MigrationVersion struct { type MigrationVersion struct {
@@ -11,7 +10,19 @@ type MigrationVersion struct {
Version uint Version uint
} }
func ApplyMigrations(db *gorm.DB) error { func applyMigrations(dbInfo *databaseInfo) error {
switch dbInfo.Type {
case SQLite:
return applySqliteMigrations()
case PostgreSQL, MySQL:
return nil
default:
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
}
}
func applySqliteMigrations() error {
// Create migration table if it doesn't exist // Create migration table if it doesn't exist
if err := db.AutoMigrate(&MigrationVersion{}); err != nil { if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
log.Fatal().Err(err).Msg("Error creating migration version table") log.Fatal().Err(err).Msg("Error creating migration version table")
@@ -25,7 +36,7 @@ func ApplyMigrations(db *gorm.DB) error {
// Define migrations // Define migrations
migrations := []struct { migrations := []struct {
Version uint Version uint
Func func(*gorm.DB) error Func func() error
}{ }{
{1, v1_modifyConstraintToSSHKeys}, {1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails}, {2, v2_lowercaseEmails},
@@ -41,7 +52,7 @@ func ApplyMigrations(db *gorm.DB) error {
return err return err
} }
if err := m.Func(db); err != nil { if err := m.Func(); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version)) log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback() tx.Rollback()
return err return err
@@ -61,7 +72,7 @@ func ApplyMigrations(db *gorm.DB) error {
} }
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE // Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error { func v1_modifyConstraintToSSHKeys() error {
createSQL := ` createSQL := `
CREATE TABLE ssh_keys_temp ( CREATE TABLE ssh_keys_temp (
id integer primary key, id integer primary key,
@@ -96,7 +107,7 @@ func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
return db.Exec(renameSQL).Error return db.Exec(renameSQL).Error
} }
func v2_lowercaseEmails(db *gorm.DB) error { func v2_lowercaseEmails() error {
// Copy the lowercase emails into the new column // Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);` copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error return db.Exec(copySQL).Error

View File

@@ -19,7 +19,7 @@ type SSHKey struct {
User User `validate:"-" ` User User `validate:"-" `
} }
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error { func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content)) pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil { if err != nil {
return err return err
@@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
return sshKey, err return sshKey, err
} }
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) { func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
sshKey := new(SSHKey) var count int64
err := db. err := db.Model(&SSHKey{}).
Where("content like ?", sshKeyContent+"%"). Where("content = ?", sshKeyContent).
First(&sshKey).Error Count(&count).Error
return count > 0, err
return sshKey, err
} }
func (sshKey *SSHKey) Create() error { func (sshKey *SSHKey) Create() error {

123
internal/db/totp.go Normal file
View File

@@ -0,0 +1,123 @@
package db
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/auth/password"
ogtotp "github.com/thomiceli/opengist/internal/auth/totp"
"github.com/thomiceli/opengist/internal/config"
"slices"
)
type TOTP struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"`
User User
Secret string
RecoveryCodes jsonData `gorm:"type:json"`
CreatedAt int64
LastUsedAt int64
}
func GetTOTPByUserID(userID uint) (*TOTP, error) {
var totp TOTP
err := db.Where("user_id = ?", userID).First(&totp).Error
return &totp, err
}
func (totp *TOTP) StoreSecret(secret string) error {
secretBytes := []byte(secret)
encrypted, err := auth.AESEncrypt(config.SecretKey, secretBytes)
if err != nil {
return err
}
totp.Secret = base64.URLEncoding.EncodeToString(encrypted)
return nil
}
func (totp *TOTP) ValidateCode(code string) (bool, error) {
ciphertext, err := base64.URLEncoding.DecodeString(totp.Secret)
if err != nil {
return false, err
}
secretBytes, err := auth.AESDecrypt(config.SecretKey, ciphertext)
if err != nil {
return false, err
}
return ogtotp.Validate(code, string(secretBytes)), nil
}
func (totp *TOTP) ValidateRecoveryCode(code string) (bool, error) {
var hashedCodes []string
if err := json.Unmarshal(totp.RecoveryCodes, &hashedCodes); err != nil {
return false, err
}
for i, hashedCode := range hashedCodes {
ok, err := password.VerifyPassword(code, hashedCode)
if err != nil {
return false, err
}
if ok {
codesJson, _ := json.Marshal(slices.Delete(hashedCodes, i, i+1))
totp.RecoveryCodes = codesJson
return true, db.Model(&totp).Updates(TOTP{RecoveryCodes: codesJson}).Error
}
}
return false, nil
}
func (totp *TOTP) GenerateRecoveryCodes() ([]string, error) {
codes, plainCodes, err := generateRandomCodes()
if err != nil {
return nil, err
}
codesJson, _ := json.Marshal(codes)
totp.RecoveryCodes = codesJson
return plainCodes, db.Model(&totp).Updates(TOTP{RecoveryCodes: codesJson}).Error
}
func (totp *TOTP) Create() error {
return db.Create(&totp).Error
}
func (totp *TOTP) Delete() error {
return db.Delete(&totp).Error
}
func generateRandomCodes() ([]string, []string, error) {
const count = 5
const length = 10
codes := make([]string, count)
plainCodes := make([]string, count)
for i := 0; i < count; i++ {
bytes := make([]byte, (length+1)/2)
if _, err := rand.Read(bytes); err != nil {
return nil, nil, err
}
hexCode := hex.EncodeToString(bytes)
code := fmt.Sprintf("%s-%s", hexCode[:length/2], hexCode[length/2:])
plainCodes[i] = code
hashed, err := password.HashPassword(code)
if err != nil {
return nil, nil, err
}
codes[i] = hashed
}
return codes, plainCodes, nil
}
// -- DTO -- //
type TOTPDTO struct {
Code string `form:"code" validate:"max=50"`
}

77
internal/db/types.go Normal file
View File

@@ -0,0 +1,77 @@
package db
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type binaryData []byte
func (b *binaryData) Value() (driver.Value, error) {
return []byte(*b), nil
}
func (b *binaryData) Scan(value interface{}) error {
valBytes, ok := value.([]byte)
if !ok {
return fmt.Errorf("failed to unmarshal BinaryData: %v", value)
}
*b = valBytes
return nil
}
func (*binaryData) GormDataType() string {
return "binary_data"
}
func (*binaryData) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
switch db.Dialector.Name() {
case "sqlite":
return "BLOB"
case "mysql":
return "VARBINARY(1024)"
case "postgres":
return "BYTEA"
default:
return "BLOB"
}
}
type jsonData json.RawMessage
func (j *jsonData) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
result := json.RawMessage{}
err := json.Unmarshal(bytes, &result)
*j = jsonData(result)
return err
}
func (j *jsonData) Value() (driver.Value, error) {
if len(*j) == 0 {
return nil, nil
}
return json.RawMessage(*j).MarshalJSON()
}
func (*jsonData) GormDataType() string {
return "json"
}
func (*jsonData) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
switch db.Dialector.Name() {
case "mysql", "sqlite":
return "JSON"
case "postgres":
return "JSONB"
}
return ""
}

View File

@@ -1,12 +1,13 @@
package db package db
import ( import (
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm" "gorm.io/gorm"
) )
type User struct { type User struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex"` Username string `gorm:"uniqueIndex,size:191"`
Password string Password string
IsAdmin bool IsAdmin bool
CreatedAt int64 CreatedAt int64
@@ -18,37 +19,43 @@ type User struct {
GiteaID string GiteaID string
OIDCID string `gorm:"column:oidc_id"` OIDCID string `gorm:"column:oidc_id"`
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
} }
func (user *User) BeforeDelete(tx *gorm.DB) error { func (user *User) BeforeDelete(tx *gorm.DB) error {
// Decrement likes counter for all gists liked by this user // Decrement likes counter using derived table
// The likes will be automatically deleted by the foreign key constraint err := tx.Exec(`
err := tx.Model(&Gist{}). UPDATE gists
Omit("updated_at"). SET nb_likes = nb_likes - 1
Where("id IN (?)", tx. WHERE id IN (
Select("gist_id"). SELECT gist_id
Table("likes"). FROM (
Where("user_id = ?", user.ID), SELECT gist_id
). FROM likes
UpdateColumn("nb_likes", gorm.Expr("nb_likes - 1")). WHERE user_id = ?
Error ) AS derived_likes
)
`, user.ID).Error
if err != nil { if err != nil {
return err return err
} }
// Decrement forks counter for all gists forked by this user // Decrement forks counter using derived table
err = tx.Model(&Gist{}). err = tx.Exec(`
Omit("updated_at"). UPDATE gists
Where("id IN (?)", tx. SET nb_forks = nb_forks - 1
Select("forked_id"). WHERE id IN (
Table("gists"). SELECT forked_id
Where("user_id = ?", user.ID), FROM (
). SELECT forked_id
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")). FROM gists
Error WHERE user_id = ? AND forked_id IS NOT NULL
) AS derived_forks
)
`, user.ID).Error
if err != nil { if err != nil {
return err return err
} }
@@ -58,8 +65,22 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
return err return err
} }
// Delete all gists created by this user err = tx.Where("user_id = ?", user.ID).Delete(&WebAuthnCredential{}).Error
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error if err != nil {
return err
}
err = tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
if err != nil {
return err
}
// Delete user directory
if err = git.DeleteUserDirectory(user.Username); err != nil {
return err
}
return nil
} }
func UserExists(username string) (bool, error) { func UserExists(username string) (bool, error) {
@@ -118,6 +139,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
return userMap, nil return userMap, nil
} }
func GetUserFromSSHKey(sshKey string) (*User, error) {
user := new(User)
err := db.
Joins("JOIN ssh_keys ON users.id = ssh_keys.user_id").
Where("ssh_keys.content = ?", sshKey).
First(&user).Error
return user, err
}
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) { func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
key := new(SSHKey) key := new(SSHKey)
err := db. err := db.
@@ -191,6 +221,19 @@ func (user *User) DeleteProviderID(provider string) error {
return nil return nil
} }
func (user *User) HasMFA() (bool, bool, error) {
var webauthn bool
var totp bool
err := db.Model(&WebAuthnCredential{}).Select("count(*) > 0").Where("user_id = ?", user.ID).Find(&webauthn).Error
if err != nil {
return false, false, err
}
err = db.Model(&TOTP{}).Select("count(*) > 0").Where("user_id = ?", user.ID).Find(&totp).Error
return webauthn, totp, err
}
// -- DTO -- // // -- DTO -- //
type UserDTO struct { type UserDTO struct {
@@ -204,3 +247,7 @@ func (dto *UserDTO) ToUser() *User {
Password: dto.Password, Password: dto.Password,
} }
} }
type UserUsernameDTO struct {
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
}

View File

@@ -0,0 +1,147 @@
package db
import (
"encoding/hex"
"github.com/go-webauthn/webauthn/webauthn"
"time"
)
type WebAuthnCredential struct {
ID uint `gorm:"primaryKey"`
Name string
UserID uint
User User
CredentialID binaryData `gorm:"type:binary_data"`
PublicKey binaryData `gorm:"type:binary_data"`
AttestationType string
AAGUID binaryData `gorm:"type:binary_data"`
SignCount uint32
CloneWarning bool
FlagUserPresent bool
FlagUserVerified bool
FlagBackupEligible bool
FlagBackupState bool
CreatedAt int64
LastUsedAt int64
}
func (*WebAuthnCredential) TableName() string {
return "webauthn"
}
func GetAllWACredentialsForUser(userID uint) ([]webauthn.Credential, error) {
var creds []WebAuthnCredential
err := db.Where("user_id = ?", userID).Find(&creds).Error
if err != nil {
return nil, err
}
webCreds := make([]webauthn.Credential, len(creds))
for i, cred := range creds {
webCreds[i] = webauthn.Credential{
ID: cred.CredentialID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,
CloneWarning: cred.CloneWarning,
},
Flags: webauthn.CredentialFlags{
UserPresent: cred.FlagUserPresent,
UserVerified: cred.FlagUserVerified,
BackupEligible: cred.FlagBackupEligible,
BackupState: cred.FlagBackupState,
},
}
}
return webCreds, nil
}
func GetAllCredentialsForUser(userID uint) ([]WebAuthnCredential, error) {
var creds []WebAuthnCredential
err := db.Where("user_id = ?", userID).Find(&creds).Error
return creds, err
}
func GetUserByCredentialID(credID binaryData) (*User, error) {
var credential WebAuthnCredential
var err error
switch db.Dialector.Name() {
case "postgres":
hexCredID := hex.EncodeToString(credID)
if err = db.Preload("User").Where("credential_id = decode(?, 'hex')", hexCredID).First(&credential).Error; err != nil {
return nil, err
}
case "mysql", "sqlite":
hexCredID := hex.EncodeToString(credID)
if err = db.Preload("User").Where("credential_id = unhex(?)", hexCredID).First(&credential).Error; err != nil {
return nil, err
}
}
return &credential.User, err
}
func GetCredentialByIDDB(id uint) (*WebAuthnCredential, error) {
var cred WebAuthnCredential
err := db.Where("id = ?", id).First(&cred).Error
return &cred, err
}
func GetCredentialByID(id binaryData) (*WebAuthnCredential, error) {
var cred WebAuthnCredential
var err error
switch db.Dialector.Name() {
case "postgres":
hexCredID := hex.EncodeToString(id)
if err = db.Where("credential_id = decode(?, 'hex')", hexCredID).First(&cred).Error; err != nil {
return nil, err
}
case "mysql", "sqlite":
hexCredID := hex.EncodeToString(id)
if err = db.Where("credential_id = unhex(?)", hexCredID).First(&cred).Error; err != nil {
return nil, err
}
}
return &cred, err
}
func CreateFromCrendential(userID uint, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
credDb := &WebAuthnCredential{
UserID: userID,
Name: name,
CredentialID: cred.ID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
AAGUID: cred.Authenticator.AAGUID,
SignCount: cred.Authenticator.SignCount,
CloneWarning: cred.Authenticator.CloneWarning,
FlagUserPresent: cred.Flags.UserPresent,
FlagUserVerified: cred.Flags.UserVerified,
FlagBackupEligible: cred.Flags.BackupEligible,
FlagBackupState: cred.Flags.BackupState,
}
err := db.Create(credDb).Error
return credDb, err
}
func (w *WebAuthnCredential) UpdateSignCount() error {
return db.Model(w).Update("sign_count", w.SignCount).Error
}
func (w *WebAuthnCredential) UpdateLastUsedAt() error {
return db.Model(w).Update("last_used_at", time.Now().Unix()).Error
}
func (w *WebAuthnCredential) Delete() error {
return db.Delete(w).Error
}
// -- DTO -- //
type CrendentialDTO struct {
PasskeyName string `json:"passkeyname" validate:"max=50"`
}

View File

@@ -4,8 +4,10 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -24,6 +26,8 @@ var (
) )
const truncateLimit = 2 << 18 const truncateLimit = 2 << 18
const diffSize = 2 << 12
const maxFilesPerDiffCommit = 10
type RevisionNotFoundError struct{} type RevisionNotFoundError struct{}
@@ -35,6 +39,10 @@ func RepositoryPath(user string, gist string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist) return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
} }
func UserRepositoriesPath(user string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user))
}
func RepositoryUrl(ctx echo.Context, user string, gist string) string { func RepositoryUrl(ctx echo.Context, user string, gist string) string {
httpProtocol := "http" httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" { if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
@@ -80,16 +88,6 @@ func InitRepository(user string, gist string) error {
return CreateDotGitFiles(user, gist) return CreateDotGitFiles(user, gist)
} }
func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
repositoryPath := RepositoryPath(user, gist)
if err := InitRepository(user, gist); err != nil {
return err
}
repositoryUrl := RepositoryUrl(ctx, user, gist)
return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl))
}
func CountCommits(user string, gist string) (string, error) { func CountCommits(user string, gist string) (string, error) {
repositoryPath := RepositoryPath(user, gist) repositoryPath := RepositoryPath(user, gist)
@@ -123,6 +121,9 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
} }
slice := strings.Split(string(stdout), "\n") slice := strings.Split(string(stdout), "\n")
for i, s := range slice {
slice[i] = convertOctalToUTF8(s)
}
return slice[:len(slice)-1], nil return slice[:len(slice)-1], nil
} }
@@ -161,7 +162,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
fileMap = append(fileMap, &catFileBatch{ fileMap = append(fileMap, &catFileBatch{
Hash: hash, Hash: hash,
Size: size, Size: size,
Name: name, Name: convertOctalToUTF8(name),
}) })
} }
@@ -257,7 +258,7 @@ func GetFileContent(user string, gist string, revision string, filename string,
"git", "git",
"--no-pager", "--no-pager",
"show", "show",
revision+":"+filename, revision+":"+convertURLToOctal(filename),
) )
cmd.Dir = repositoryPath cmd.Dir = repositoryPath
@@ -281,7 +282,7 @@ func GetFileSize(user string, gist string, revision string, filename string) (ui
"git", "git",
"cat-file", "cat-file",
"-s", "-s",
revision+":"+filename, revision+":"+convertURLToOctal(filename),
) )
cmd.Dir = repositoryPath cmd.Dir = repositoryPath
@@ -323,7 +324,7 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
} }
}(cmd) }(cmd)
return parseLog(stdout, truncateLimit), err return parseLog(stdout, maxFilesPerDiffCommit, diffSize)
} }
func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error { func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error {
@@ -424,7 +425,6 @@ func Push(gistTmpId string) error {
if err != nil { if err != nil {
return err return err
} }
return os.RemoveAll(tmpRepositoryPath) return os.RemoveAll(tmpRepositoryPath)
} }
@@ -490,6 +490,22 @@ func GcRepos() error {
return err return err
} }
func ResetHooks() error {
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), ReposDirectory, "*", "*"))
if err != nil {
return err
}
for _, e := range entries {
repoPath := strings.Split(e, string(os.PathSeparator))
if err := CreateDotGitFiles(repoPath[len(repoPath)-2], repoPath[len(repoPath)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", repoPath[len(repoPath)-2], repoPath[len(repoPath)-1])
}
}
return nil
}
func HasNoCommits(user string, gist string) (bool, error) { func HasNoCommits(user string, gist string) (bool, error) {
repositoryPath := RepositoryPath(user, gist) repositoryPath := RepositoryPath(user, gist)
@@ -534,13 +550,65 @@ func CreateDotGitFiles(user string, gist string) error {
} }
defer f1.Close() defer f1.Close()
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil { if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
return err for _, hook := range []string{"pre-receive", "post-receive"} {
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
return err
}
}
} }
return nil return nil
} }
func DeleteUserDirectory(user string) error {
return os.RemoveAll(filepath.Join(config.GetHomeDir(), ReposDirectory, user))
}
func SerialiseInitRepository(user string, serialized []byte) error {
userRepositoryPath := UserRepositoriesPath(user)
initPath := filepath.Join(userRepositoryPath, "_init")
f, err := os.OpenFile(initPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
encodedData := base64.StdEncoding.EncodeToString(serialized)
_, err = f.Write(append([]byte(encodedData), '\n'))
return err
}
func DeserialiseInitRepository(user string) ([]byte, error) {
initPath := filepath.Join(UserRepositoriesPath(user), "_init")
content, err := os.ReadFile(initPath)
if err != nil {
return nil, err
}
idx := bytes.Index(content, []byte{'\n'})
if idx == -1 {
return base64.StdEncoding.DecodeString(string(content))
}
firstLine := content[:idx]
remaining := content[idx+1:]
if len(remaining) == 0 {
if err := os.Remove(initPath); err != nil {
return nil, fmt.Errorf("failed to remove file: %v", err)
}
} else {
if err := os.WriteFile(initPath, remaining, 0644); err != nil {
return nil, fmt.Errorf("failed to write remaining content: %v", err)
}
}
return base64.StdEncoding.DecodeString(string(firstLine))
}
func createDotGitHookFile(repositoryPath string, hook string, content string) error { func createDotGitHookFile(repositoryPath string, hook string, content string) error {
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744) preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
if err != nil { if err != nil {
@@ -570,57 +638,48 @@ func removeFilesExceptGit(dir string) error {
}) })
} }
const preReceive = `#!/bin/sh func convertOctalToUTF8(name string) string {
name = strings.Trim(name, `"`)
utf8Name, err := strconv.Unquote(name)
if err != nil {
utf8Name, err = strconv.Unquote(`"` + name + `"`)
if err != nil {
return name
}
}
return utf8Name
}
disallowed_files="" func convertUTF8ToOctal(name string) string {
if strings.Contains(name, "\\") {
return name
}
while read -r old_rev new_rev ref needsQuoting := false
do for _, r := range name {
if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then if r > 127 {
# This is the first commit, so we check all the files in that commit needsQuoting = true
changed_files=$(git ls-tree -r --name-only "$new_rev") break
else }
# This is not the first commit, so we compare it with its predecessor }
changed_files=$(git diff --name-only "$old_rev" "$new_rev")
fi
while IFS= read -r file if !needsQuoting {
do return name
case $file in }
*/*)
disallowed_files="${disallowed_files}${file} "
;;
esac
done <<EOF
$changed_files
EOF
done
if [ -n "$disallowed_files" ]; then quoted := fmt.Sprintf("%q", name)
echo "" return strings.Trim(quoted, `"`)
echo "Pushing files in folders is not allowed:" }
for file in $disallowed_files; do
echo " $file" func convertURLToOctal(name string) string {
done decoded, err := url.QueryUnescape(name)
echo "" if err != nil {
exit 1 return name
fi }
`
return convertUTF8ToOctal(decoded)
const postReceive = `#!/bin/sh }
while read oldrev newrev refname; do const hookTemplate = `#!/bin/sh
if ! git rev-parse --verify --quiet HEAD &>/dev/null; then "$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
git symbolic-ref HEAD "$refname"
fi
done
echo ""
echo "Your new repository has been created here: %s"
echo ""
echo "If you want to keep working with your gist, you could set the remote URL via:"
echo "git remote set-url origin %s"
echo ""
rm -f $0
` `

View File

@@ -1,42 +1,18 @@
package git package git
import ( import (
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"net/http"
"net/http/httptest"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"strings" "strings"
"testing" "testing"
) )
func setup(t *testing.T) {
err := config.InitConfig("")
require.NoError(t, err, "Could not init config")
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
ReposDirectory = path.Join("tests")
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
require.NoError(t, err)
err = InitRepository("thomas", "gist1")
require.NoError(t, err)
}
func teardown(t *testing.T) {
err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
require.NoError(t, err, "Could not remove repos directory")
}
func TestInitDeleteRepository(t *testing.T) { func TestInitDeleteRepository(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
cmd := exec.Command("git", "rev-parse", "--is-bare-repository") cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
cmd.Dir = RepositoryPath("thomas", "gist1") cmd.Dir = RepositoryPath("thomas", "gist1")
@@ -44,9 +20,6 @@ func TestInitDeleteRepository(t *testing.T) {
require.NoError(t, err, "Could not run git command") require.NoError(t, err, "Could not run git command")
require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare") require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare")
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "hooks", "pre-receive"))
require.NoError(t, err, "pre-receive hook not found")
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok")) _, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok"))
require.NoError(t, err, "git-daemon-export-ok file not found") require.NoError(t, err, "git-daemon-export-ok file not found")
@@ -56,14 +29,14 @@ func TestInitDeleteRepository(t *testing.T) {
} }
func TestCommits(t *testing.T) { func TestCommits(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
hasNoCommits, err := HasNoCommits("thomas", "gist1") hasNoCommits, err := HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has no commits") require.NoError(t, err, "Could not check if repository has no commits")
require.True(t, hasNoCommits, "Repository should have no commits") require.True(t, hasNoCommits, "Repository should have no commits")
commitToBare(t, "thomas", "gist1", nil) CommitToBare(t, "thomas", "gist1", nil)
hasNoCommits, err = HasNoCommits("thomas", "gist1") hasNoCommits, err = HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has no commits") require.NoError(t, err, "Could not check if repository has no commits")
@@ -73,26 +46,27 @@ func TestCommits(t *testing.T) {
require.NoError(t, err, "Could not count commits") require.NoError(t, err, "Could not count commits")
require.Equal(t, "1", nbCommits, "Repository should have 1 commit") require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
commitToBare(t, "thomas", "gist1", nil) CommitToBare(t, "thomas", "gist1", nil)
nbCommits, err = CountCommits("thomas", "gist1") nbCommits, err = CountCommits("thomas", "gist1")
require.NoError(t, err, "Could not count commits") require.NoError(t, err, "Could not count commits")
require.Equal(t, "2", nbCommits, "Repository should have 2 commits") require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
} }
func TestContent(t *testing.T) { func TestContent(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "I love Opengist\n", "my_file.txt": "I love Opengist\n",
"my_other_file.txt": `I really "my_other_file.txt": `I really
hate Opengist`, hate Opengist`,
"rip.txt": "byebye", "rip.txt": "byebye",
"中文名.txt": "中文内容",
}) })
files, err := GetFilesOfRepository("thomas", "gist1", "HEAD") files, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
require.NoError(t, err, "Could not get files of repository") require.NoError(t, err, "Could not get files of repository")
require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt"}, files, "Files are not correct") require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt", "中文名.txt"}, files, "Files are not correct")
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false) content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false)
require.NoError(t, err, "Could not get content") require.NoError(t, err, "Could not get content")
@@ -104,16 +78,22 @@ hate Opengist`,
require.False(t, truncated, "Content should not be truncated") require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct") require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
commitToBare(t, "thomas", "gist1", map[string]string{ content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "中文名.txt", false)
require.NoError(t, err, "Could not get content")
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "中文内容", content, "Content is not correct")
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_renamed_file.txt": "I love Opengist\n", "my_renamed_file.txt": "I love Opengist\n",
"my_other_file.txt": `I really "my_other_file.txt": `I really
like Opengist actually`, like Opengist actually`,
"new_file.txt": "Wait now there is a new file", "new_file.txt": "Wait now there is a new file",
"中文名.txt": "中文内容",
}) })
files, err = GetFilesOfRepository("thomas", "gist1", "HEAD") files, err = GetFilesOfRepository("thomas", "gist1", "HEAD")
require.NoError(t, err, "Could not get files of repository") require.NoError(t, err, "Could not get files of repository")
require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt"}, files, "Files are not correct") require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt", "中文名.txt"}, files, "Files are not correct")
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false) content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
require.NoError(t, err, "Could not get content") require.NoError(t, err, "Could not get content")
@@ -152,7 +132,7 @@ like Opengist actually`,
require.Contains(t, commits[0].Files, File{ require.Contains(t, commits[0].Files, File{
Filename: "my_other_file.txt", Filename: "my_other_file.txt",
OldFilename: "", OldFilename: "my_other_file.txt",
Content: `@@ -1,2 +1,2 @@ Content: `@@ -1,2 +1,2 @@
I really I really
-hate Opengist -hate Opengist
@@ -183,18 +163,18 @@ like Opengist actually`,
} }
func TestGitGc(t *testing.T) { func TestGitGc(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
err := GcRepos() err := GcRepos()
require.NoError(t, err, "Could not run git gc") require.NoError(t, err, "Could not run git gc")
} }
func TestFork(t *testing.T) { func TestFork(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "I love Opengist\n", "my_file.txt": "I love Opengist\n",
}) })
@@ -211,10 +191,10 @@ func TestFork(t *testing.T) {
} }
func TestTruncate(t *testing.T) { func TestTruncate(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "A", "my_file.txt": "A",
}) })
@@ -228,7 +208,7 @@ func TestTruncate(t *testing.T) {
builder.WriteString("A") builder.WriteString("A")
} }
str := builder.String() str := builder.String()
commitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": str, "my_file.txt": str,
}) })
@@ -237,7 +217,7 @@ func TestTruncate(t *testing.T) {
require.True(t, truncated, "Content should be truncated") require.True(t, truncated, "Content should be truncated")
require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit") require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit")
commitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "AA\n" + str, "my_file.txt": "AA\n" + str,
}) })
@@ -247,33 +227,9 @@ func TestTruncate(t *testing.T) {
require.Equal(t, 2, len(content), "Content size is not correct") require.Equal(t, 2, len(content), "Content size is not correct")
} }
func TestInitViaGitInit(t *testing.T) {
setup(t)
defer teardown(t)
e := echo.New()
// Create a mock HTTP request
req := httptest.NewRequest(http.MethodPost, "/", nil)
// Create a mock HTTP response recorder
rec := httptest.NewRecorder()
// Create a new Echo context
c := e.NewContext(req, rec)
// Define your user and gist
user := "testUser"
gist := "testGist"
err := InitRepositoryViaInit(user, gist, c)
require.NoError(t, err)
}
func TestGitInitBranchNames(t *testing.T) { func TestGitInitBranchNames(t *testing.T) {
setup(t) SetupTest(t)
defer teardown(t) defer TeardownTest(t)
cmd := exec.Command("git", "symbolic-ref", "HEAD") cmd := exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = RepositoryPath("thomas", "gist1") cmd.Dir = RepositoryPath("thomas", "gist1")
@@ -291,29 +247,3 @@ func TestGitInitBranchNames(t *testing.T) {
require.NoError(t, err, "Could not run git command") require.NoError(t, err, "Could not run git command")
require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default") require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default")
} }
func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com", true)
require.NoError(t, err, "Could not commit to repository")
if len(files) > 0 {
for filename, content := range files {
if err := SetFileContent(gist, filename, content); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
if err := AddAll(gist); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
}
}
if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
if err := Push(gist); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
}

63
internal/git/config.go Normal file
View File

@@ -0,0 +1,63 @@
package git
import (
"errors"
"os/exec"
"regexp"
)
type configEntry struct {
value string
fn func(string, string) error
}
func InitGitConfig() error {
configs := map[string]configEntry{
"receive.advertisePushOptions": {value: "true", fn: setGitConfig},
"safe.directory": {value: "*", fn: addGitConfig},
}
for key, entry := range configs {
if err := entry.fn(key, entry.value); err != nil {
return err
}
}
return nil
}
func setGitConfig(key, value string) error {
_, err := getGitConfig(key, value)
if err != nil && !checkErrorCode(err, 1) {
return err
}
cmd := exec.Command("git", "config", "--global", key, value)
return cmd.Run()
}
func addGitConfig(key, value string) error {
_, err := getGitConfig(key, regexp.QuoteMeta(value))
if err == nil {
return nil
}
if checkErrorCode(err, 1) {
cmd := exec.Command("git", "config", "--global", "--add", key, value)
return cmd.Run()
}
return err
}
func getGitConfig(key, value string) (string, error) {
cmd := exec.Command("git", "config", "--global", "--get", key, value)
out, err := cmd.Output()
return string(out), err
}
func checkErrorCode(err error, code int) bool {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return exitError.ExitCode() == code
}
return false
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io" "io"
"regexp"
"strings" "strings"
) )
@@ -63,129 +62,287 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
return string(buf), truncated, nil return string(buf), truncated, nil
} }
func parseLog(out io.Reader, maxBytes int) []*Commit { // inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
scanner := bufio.NewScanner(out) func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
var commits []*Commit var commits []*Commit
var currentCommit *Commit var currentCommit *Commit
var currentFile *File var currentFile *File
var isContent bool var headerParsed = false
var bytesRead = 0 var skipped = false
scanNext := true var line string
var err error
input := bufio.NewReaderSize(out, maxBytes)
// Loop Commits
loopLog:
for {
// If a commit was skipped, do not read a new line
if !skipped {
line, err = input.ReadString('\n')
if err != nil {
if err == io.EOF {
break loopLog
}
return commits, err
}
}
// Remove trailing newline characters
if len(line) > 0 && (line[len(line)-1] == '\n' || line[len(line)-1] == '\r') {
line = line[:len(line)-1]
}
// Attempt to parse commit header (hash, author, mail, timestamp) or a diff
switch line[0] {
// Commit hash
case 'c':
if headerParsed {
commits = append(commits, currentCommit)
}
skipped = false
currentCommit = &Commit{Hash: line[2:], Files: []File{}}
continue
// Author name
case 'a':
headerParsed = true
currentCommit.AuthorName = line[2:]
continue
// Author email
case 'm':
currentCommit.AuthorEmail = line[2:]
continue
// Commit timestamp
case 't':
currentCommit.Timestamp = line[2:]
continue
// Commit shortstat
case ' ':
changed := []byte(line)[1:]
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
currentCommit.Changed = string(changed)
// shortstat is followed by an empty line
line, err = input.ReadString('\n')
if err != nil {
if err == io.EOF {
break loopLog
}
return commits, err
}
continue
// Commit diff
default:
// Loop files in diff
loopCommit:
for {
// If we have reached the maximum number of files to show for a single commit, skip to the next commit
if len(currentCommit.Files) >= maxFiles {
line, err = skipToNextCommit(input)
if err != nil {
if err == io.EOF {
break loopLog
}
return commits, err
}
// Skip to the next commit
headerParsed = false
skipped = true
break loopCommit
}
// Else create a new file and parse it
currentFile = &File{}
parseRename := true
loopFileDiff:
for {
line, err = input.ReadString('\n')
if err != nil {
if err != io.EOF {
return commits, err
}
headerParsed = false
break loopCommit
}
// If the line is a newline character, the commit is finished
if line == "\n" {
currentCommit.Files = append(currentCommit.Files, *currentFile)
headerParsed = false
break loopCommit
}
// Attempt to parse the file header
switch {
case strings.HasPrefix(line, "diff --git"):
currentCommit.Files = append(currentCommit.Files, *currentFile)
headerParsed = false
break loopFileDiff
case strings.HasPrefix(line, "old mode"):
case strings.HasPrefix(line, "new mode"):
case strings.HasPrefix(line, "index"):
case strings.HasPrefix(line, "similarity index"):
case strings.HasPrefix(line, "dissimilarity index"):
continue
case strings.HasPrefix(line, "rename from "):
currentFile.OldFilename = convertOctalToUTF8(line[12 : len(line)-1])
case strings.HasPrefix(line, "rename to "):
currentFile.Filename = convertOctalToUTF8(line[10 : len(line)-1])
parseRename = false
case strings.HasPrefix(line, "copy from "):
currentFile.OldFilename = convertOctalToUTF8(line[10 : len(line)-1])
case strings.HasPrefix(line, "copy to "):
currentFile.Filename = convertOctalToUTF8(line[8 : len(line)-1])
parseRename = false
case strings.HasPrefix(line, "new file"):
currentFile.IsCreated = true
case strings.HasPrefix(line, "deleted file"):
currentFile.IsDeleted = true
case strings.HasPrefix(line, "--- "):
name := convertOctalToUTF8(line[4 : len(line)-1])
if parseRename && currentFile.IsDeleted {
currentFile.Filename = name[2:]
} else if parseRename && strings.HasPrefix(name, "a/") {
currentFile.OldFilename = name[2:]
}
case strings.HasPrefix(line, "+++ "):
name := convertOctalToUTF8(line[4 : len(line)-1])
if parseRename && strings.HasPrefix(name, "b/") {
currentFile.Filename = name[2:]
}
// Header is finally parsed, now we can parse the file diff content
lineBytes, isFragment, err := parseDiffContent(currentFile, maxBytes, input)
if err != nil {
if err != io.EOF {
return commits, err
}
// EOF reached, commit is finished
currentCommit.Files = append(currentCommit.Files, *currentFile)
headerParsed = false
break loopCommit
}
currentCommit.Files = append(currentCommit.Files, *currentFile)
if string(lineBytes) == "" {
headerParsed = false
break loopCommit
}
for isFragment {
_, isFragment, err = input.ReadLine()
if err != nil {
return commits, fmt.Errorf("unable to ReadLine: %w", err)
}
}
break loopFileDiff
}
}
}
}
commits = append(commits, currentCommit)
}
return commits, nil
}
func parseDiffContent(currentFile *File, maxBytes int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
sb := &strings.Builder{}
var currFileLineCount int
for { for {
if scanNext && !scanner.Scan() { for isFragment {
break currentFile.Truncated = true
}
scanNext = true
// new commit found // Read the next line
currentFile = nil _, isFragment, err = input.ReadLine()
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}} if err != nil {
return nil, false, err
scanner.Scan() }
currentCommit.AuthorName = string(scanner.Bytes()[2:])
scanner.Scan()
currentCommit.AuthorEmail = string(scanner.Bytes()[2:])
scanner.Scan()
currentCommit.Timestamp = string(scanner.Bytes()[2:])
scanner.Scan()
if len(scanner.Bytes()) == 0 {
commits = append(commits, currentCommit)
break
} }
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one sb.Reset()
if scanner.Bytes()[0] != ' ' {
commits = append(commits, currentCommit)
// avoid scanning the next line, as we already did it // Read the next line
scanNext = false lineBytes, isFragment, err = input.ReadLine()
if err != nil {
if err == io.EOF {
return lineBytes, isFragment, err
}
return nil, false, err
}
// End of file
if len(lineBytes) == 0 {
return lineBytes, false, err
}
if lineBytes[0] == 'd' {
return lineBytes, false, err
}
if currFileLineCount >= maxBytes {
currentFile.Truncated = true
continue continue
} }
changed := scanner.Bytes()[1:] line := string(lineBytes)
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte("")) if isFragment {
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte("")) currentFile.Truncated = true
currentCommit.Changed = string(changed) for isFragment {
lineBytes, isFragment, err = input.ReadLine()
// twice because --shortstat adds a new line if err != nil {
scanner.Scan() return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err)
scanner.Scan()
// commit header parsed
// files changes inside the commit
for {
line := scanner.Bytes()
// end of content of file
if len(line) == 0 {
isContent = false
if currentFile != nil {
currentCommit.Files = append(currentCommit.Files, *currentFile)
}
break
}
// new file found
if bytes.HasPrefix(line, []byte("diff --git")) {
// current file is finished, we can add it to the commit
if currentFile != nil {
currentCommit.Files = append(currentCommit.Files, *currentFile)
}
// create a new file
isContent = false
bytesRead = 0
currentFile = &File{}
filenameRegex := regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
matches := filenameRegex.FindStringSubmatch(string(line))
if len(matches) == 3 {
currentFile.Filename = matches[2]
if matches[1] != matches[2] {
currentFile.OldFilename = matches[1]
}
}
scanner.Scan()
continue
}
if bytes.HasPrefix(line, []byte("new")) {
currentFile.IsCreated = true
}
if bytes.HasPrefix(line, []byte("deleted")) {
currentFile.IsDeleted = true
}
// file content found
if line[0] == '@' {
isContent = true
}
if isContent {
currentFile.Content += string(line) + "\n"
bytesRead += len(line)
if bytesRead > maxBytes {
currentFile.Truncated = true
currentFile.Content = ""
isContent = false
} }
} }
scanner.Scan()
} }
commits = append(commits, currentCommit) if len(line) > maxBytes {
currentFile.Truncated = true
line = line[:maxBytes]
}
currentFile.Content += line + "\n"
} }
}
return commits func skipToNextCommit(input *bufio.Reader) (line string, err error) {
// need to skip until the next cmdDiffHead
var isFragment, wasFragment bool
var lineBytes []byte
for {
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
return "", err
}
if wasFragment {
wasFragment = isFragment
continue
}
if bytes.HasPrefix(lineBytes, []byte("c")) {
break
}
wasFragment = isFragment
}
line = string(lineBytes)
if isFragment {
var tail string
tail, err = input.ReadString('\n')
if err != nil {
return "", err
}
line += tail
}
return line, err
} }
func ParseCsv(file *File) (*CsvFile, error) { func ParseCsv(file *File) (*CsvFile, error) {

View File

@@ -0,0 +1,71 @@
package git
import (
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
)
func SetupTest(t *testing.T) {
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
err := config.InitConfig("", io.Discard)
require.NoError(t, err, "Could not init config")
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
ReposDirectory = path.Join("tests")
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
require.NoError(t, err)
err = InitRepository("thomas", "gist1")
require.NoError(t, err)
}
func TeardownTest(t *testing.T) {
err := os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
require.NoError(t, err, "Could not remove repos directory")
}
func CommitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com", true)
require.NoError(t, err, "Could not clone repository")
if len(files) > 0 {
for filename, content := range files {
if strings.Contains(filename, "/") {
dir := filepath.Dir(filename)
err := os.MkdirAll(filepath.Join(TmpRepositoryPath(gist), dir), os.ModePerm)
require.NoError(t, err, "Could not create directory")
}
_ = os.WriteFile(filepath.Join(TmpRepositoryPath(gist), filename), []byte(content), 0644)
if err := AddAll(gist); err != nil {
require.NoError(t, err, "Could not add all to repository")
}
}
}
if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
if err := Push(gist); err != nil {
require.NoError(t, err, "Could not push to repository")
}
}
func LastHashOfCommit(t *testing.T, user string, gist string) string {
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Dir = RepositoryPath(user, gist)
out, err := cmd.Output()
require.NoError(t, err, "Could not run git command")
return strings.TrimSpace(string(out))
}

24
internal/hooks/hook.go Normal file
View File

@@ -0,0 +1,24 @@
package hooks
import (
"fmt"
"os"
"strconv"
"strings"
)
const BaseHash = "0000000000000000000000000000000000000000"
func pushOptions() map[string]string {
opts := make(map[string]string)
if pushCount, err := strconv.Atoi(os.Getenv("GIT_PUSH_OPTION_COUNT")); err == nil {
for i := 0; i < pushCount; i++ {
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", i))
kv := strings.SplitN(opt, "=", 2)
if len(kv) == 2 {
opts[kv[0]] = kv[1]
}
}
}
return opts
}

View File

@@ -0,0 +1,112 @@
package hooks
import (
"bufio"
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
validatorpkg "github.com/thomiceli/opengist/internal/validator"
"io"
"os"
"os/exec"
"slices"
"strings"
)
func PostReceive(in io.Reader, out, er io.Writer) error {
var outputSb strings.Builder
newGist := false
opts := pushOptions()
gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")
validator := validatorpkg.NewValidator()
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) != 3 {
_, _ = fmt.Fprintln(er, "Invalid input")
return fmt.Errorf("invalid input")
}
oldrev, _, refname := parts[0], parts[1], parts[2]
if err := verifyHEAD(); err != nil {
setSymbolicRef(refname)
}
if oldrev == BaseHash {
newGist = true
}
}
gist, err := db.GetGistByID(os.Getenv("OPENGIST_REPOSITORY_ID"))
if err != nil {
_, _ = fmt.Fprintln(er, "Failed to get gist")
return fmt.Errorf("failed to get gist: %w", err)
}
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
gist.Private = db.ParseVisibility(opts["visibility"])
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
}
if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil {
gist.URL = opts["url"]
lastIndex := strings.LastIndex(gistUrl, "/")
gistUrl = gistUrl[:lastIndex+1] + gist.URL
if !newGist {
outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl))
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
}
}
if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil {
gist.Title = opts["title"]
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"]))
}
if opts["description"] != "" && validator.Var(opts["description"], "max=1000") == nil {
gist.Description = opts["description"]
outputSb.WriteString(fmt.Sprintf("Gist description set to \"%s\"\n\n", opts["description"]))
}
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
_, _ = fmt.Fprintln(er, "Failed to check if gist has no commits")
return fmt.Errorf("failed to check if gist has no commits: %w", err)
} else if hasNoCommits {
if err = gist.Delete(); err != nil {
_, _ = fmt.Fprintln(er, "Failed to delete gist")
return fmt.Errorf("failed to delete gist: %w", err)
}
}
_ = gist.SetLastActiveNow()
err = gist.UpdatePreviewAndCount(true)
if err != nil {
_, _ = fmt.Fprintln(er, "Failed to update gist")
return fmt.Errorf("failed to update gist: %w", err)
}
gist.AddInIndex()
if newGist {
outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl))
outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n")
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
}
outputStr := outputSb.String()
if outputStr != "" {
_, _ = fmt.Fprint(out, "\n"+outputStr)
}
return nil
}
func verifyHEAD() error {
return exec.Command("git", "rev-parse", "--verify", "--quiet", "HEAD").Run()
}
func setSymbolicRef(refname string) {
_ = exec.Command("git", "symbolic-ref", "HEAD", refname).Run()
}

View File

@@ -0,0 +1,78 @@
package hooks
import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
"strings"
)
func PreReceive(in io.Reader, out, er io.Writer) error {
var err error
var disallowedFiles []string
var disallowedCommits []string
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, " ")
if len(parts) < 3 {
_, _ = fmt.Fprintln(er, "Invalid input")
return fmt.Errorf("invalid input")
}
oldRev, newRev := parts[0], parts[1]
var changedFiles string
if oldRev == BaseHash {
// First commit
if changedFiles, err = getChangedFiles(newRev); err != nil {
_, _ = fmt.Fprintln(er, "Failed to get changed files")
return err
}
} else {
if changedFiles, err = getChangedFiles(fmt.Sprintf("%s..%s", oldRev, newRev)); err != nil {
_, _ = fmt.Fprintln(er, "Failed to get changed files")
return err
}
}
var currentCommit string
for _, file := range strings.Fields(changedFiles) {
if strings.HasPrefix(file, "/") {
currentCommit = file[1:]
}
if strings.Contains(file[1:], "/") {
disallowedFiles = append(disallowedFiles, file)
disallowedCommits = append(disallowedCommits, currentCommit[0:7])
}
}
}
if len(disallowedFiles) > 0 {
_, _ = fmt.Fprintln(out, "\nPushing files in directories is not allowed:")
for i := range disallowedFiles {
_, _ = fmt.Fprintf(out, " %s (%s)\n", disallowedFiles[i], disallowedCommits[i])
}
_, _ = fmt.Fprintln(out)
return fmt.Errorf("pushing files in directories is not allowed: %s", disallowedFiles)
}
return nil
}
func getChangedFiles(rev string) (string, error) {
cmd := exec.Command("git", "log", "--name-only", "--format=/%H", "--diff-filter=AM", rev)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
return out.String(), nil
}

View File

@@ -0,0 +1,54 @@
package hooks
import (
"bytes"
"fmt"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/git"
"os"
"testing"
)
func TestPreReceiveHook(t *testing.T) {
git.SetupTest(t)
defer git.TeardownTest(t)
var lastCommitHash string
err := os.Chdir(git.RepositoryPath("thomas", "gist1"))
require.NoError(t, err, "Could not change directory")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"my_file2.txt": "some allowed file\nagain",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.NoError(t, err, "Should not have an error on pre-receive hook for commit+push 1")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"dir/my_file.txt": "some disallowed file suddenly",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 2")
require.Equal(t, "pushing files in directories is not allowed: [dir/my_file.txt]", err.Error(), "Error message is not correct")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"dir/ok/afileagain.txt": "some disallowed file\nagain",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 3")
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"allowedfile.txt": "some allowed file only",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 4")
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
_ = os.Chdir(os.TempDir()) // Leave the current dir to avoid errors on teardown
}

View File

@@ -14,7 +14,6 @@ import (
"strings" "strings"
) )
var title = cases.Title(language.English)
var Locales = NewLocaleStore() var Locales = NewLocaleStore()
type LocaleStore struct { type LocaleStore struct {
@@ -59,7 +58,7 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
locale := &Locale{ locale := &Locale{
Code: localeCode, Code: localeCode,
Name: title.String(name), Name: cases.Title(language.English).String(name),
Messages: make(map[string]string), Messages: make(map[string]string),
} }
@@ -112,6 +111,20 @@ func (store *LocaleStore) MatchTag(langs []language.Tag) string {
return "en-US" return "en-US"
} }
func (l *Locale) String(key string, args ...any) string {
message := l.Messages[key]
if message == "" {
return Locales.Locales["en-US"].String(key, args...)
}
if len(args) == 0 {
return message
}
return fmt.Sprintf(message, args...)
}
func (l *Locale) Tr(key string, args ...any) template.HTML { func (l *Locale) Tr(key string, args ...any) template.HTML {
message := l.Messages[key] message := l.Messages[key]

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Klonovat pomocí %s
gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP. gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP.
gist.header.clone-ssh: Klonovat pomocí SSH gist.header.clone-ssh: Klonovat pomocí SSH
gist.header.clone-ssh-help: Klonovat s pomocí Git pomocí klíče SSH. gist.header.clone-ssh-help: Klonovat s pomocí Git pomocí klíče SSH.
gist.header.embed: gist.header.embed: ''
gist.header.embed-help: gist.header.embed-help: ''
gist.header.download-zip: Stáhnout ZIP gist.header.download-zip: Stáhnout ZIP
gist.raw: Raw gist.raw: Raw
@@ -122,8 +122,7 @@ auth.username: Uživatelské jméno
auth.password: Heslo auth.password: Heslo
auth.register-instead: Raději se zaregistrovat auth.register-instead: Raději se zaregistrovat
auth.login-instead: Raději se přihlásit auth.login-instead: Raději se přihlásit
auth.github-oauth: Pokračovat s účtem na GitHubu auth.oauth: Pokračovat s účtem na %s
auth.gitea-oauth: Pokračovat s účtem na Gitea
error: Chyba error: Chyba
@@ -174,7 +173,8 @@ admin.disable-login: Zakázat přihlášení
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele. admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
admin.disable-gravatar: Zakázat Gravatar admin.disable-gravatar: Zakázat Gravatar
admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara. admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Opravdu chcete smazat tohoto uživatele? admin.users.delete_confirm: Opravdu chcete smazat tohoto uživatele?
admin.gists.title: Titulek admin.gists.title: Titulek
@@ -182,3 +182,80 @@ admin.gists.private: Soukromé?
admin.gists.nb-files: Počet souborů admin.gists.nb-files: Počet souborů
admin.gists.nb-likes: Počet lajků admin.gists.nb-likes: Počet lajků
admin.gists.delete_confirm: Opravdu chcete smazat tento gist? admin.gists.delete_confirm: Opravdu chcete smazat tento gist?
gist.forks.for: ''
gist.likes.for: ''
gist.revision-of: ''
error.page-not-found: ''
error.bad-request: ''
error.signup-disabled: ''
error.signup-disabled-form: ''
error.login-disabled-form: ''
error.complete-oauth-login: ''
error.oauth-unsupported: ''
error.cannot-bind-data: ''
error.invalid-number: ''
error.invalid-character-unescaped: ''
admin.actions.reset-hooks: ''
admin.invitations.expired: ''
flash.admin.user-deleted: ''
flash.admin.gist-deleted: ''
flash.admin.invitation-created: ''
flash.admin.invitation-deleted: ''
flash.admin.sync-fs: ''
flash.admin.sync-db: ''
flash.admin.git-gc: ''
flash.admin.sync-previews: ''
gist.new.create-a-new-gist: ''
gist.edit.edit-gist: ''
flash.admin.reset-hooks: ''
flash.admin.index-gists: ''
flash.auth.username-exists: ''
flash.auth.invalid-credentials: ''
flash.auth.account-linked-oauth: ''
flash.auth.account-unlinked-oauth: ''
flash.auth.user-sshkeys-not-retrievable: ''
flash.auth.user-sshkeys-not-created: ''
flash.auth.must-be-logged-in: ''
flash.gist.visibility-changed: ''
flash.gist.fork-own-gist: ''
flash.gist.forked: ''
flash.user.email-updated: ''
flash.user.invalid-ssh-key: ''
flash.user.ssh-key-added: ''
flash.user.ssh-key-deleted: ''
flash.user.password-updated: ''
flash.user.username-updated: ''
validation.is-too-long: ''
validation.should-not-be-empty: ''
validation.should-not-include-sub-directory: ''
validation.should-only-contain-alphanumeric-characters: ''
gist.list.all-liked-by: ''
gist.list.all-forked-by: ''
gist.list.all-from: ''
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
validation.not-enough: ''
validation.invalid: ''
html.title.admin-panel: ''
flash.gist.deleted: ''
gist.new.url: ''
gist.search.found: ''
gist.search.no-results: ''
gist.search.help.user: ''
gist.search.help.title: ''
gist.search.help.filename: ''
gist.search.help.extension: ''
gist.search.help.language: ''
settings.change-username: ''
admin.invitations: ''
admin.invitations.create: ''
admin.actions.sync-previews: ''
admin.actions.index-gists: ''
admin.invitations.code: ''
admin.invitations.copy_link: ''
admin.invitations.uses: ''
gist.new.preview: ''
settings.link-gitlab-account: ''
settings.unlink-gitlab-account: ''
admin.invitations.help: ''
admin.invitations.max_uses: ''
admin.invitations.expires_at: ''

View File

@@ -0,0 +1,269 @@
gist.public: 'Öffentlich'
gist.unlisted: 'Nicht Gelistet'
gist.private: 'Privat'
gist.header.like: 'Favorisieren'
gist.header.unlike: 'Favorit entfernen'
gist.header.fork: 'Fork'
gist.header.edit: 'Bearbeiten'
gist.header.delete: 'Löschen'
gist.header.forked-from: 'Geforkt von'
gist.header.last-active: 'Zuletzt aktiv'
gist.header.select-tab: 'Tab auswählen'
gist.header.code: 'Code'
gist.header.revisions: 'Änderungen'
gist.header.revision: 'Änderung'
gist.header.clone-http: 'Mit %s Klonen'
gist.header.clone-http-help: 'Mit Git und HTTP Basic Authentication Klonen.'
gist.header.clone-ssh: 'Mit SSH Klonen'
gist.header.clone-ssh-help: 'Mit Git und SSH Schlüssel Klonen.'
gist.header.embed: 'Einbetten'
gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.'
gist.header.download-zip: 'ZIP Herunterladen'
gist.raw: 'Orginalformat'
gist.file-truncated: 'Diese Datei wurde abgeschnitten.'
gist.watch-full-file: 'Die gesamte Datei anzeigen.'
gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.'
gist.no-content: 'Keine Dateien gefunden'
gist.new.new_gist: 'Neue Gist'
gist.new.title: 'Titel'
gist.new.description: 'Beschreibung'
gist.new.url: 'URL'
gist.new.filename-with-extension: 'Dateiname mit Erweiterung'
gist.new.indent-mode: 'Einrückungs Modus'
gist.new.indent-mode-space: 'Leerzeichen'
gist.new.indent-mode-tab: 'Tab'
gist.new.indent-size: 'Einrückungs Größe'
gist.new.wrap-mode: 'Textumbruch Modus'
gist.new.wrap-mode-no: 'kein Textumruch'
gist.new.wrap-mode-soft: 'weicher Zeilenumbruch'
gist.new.add-file: 'Datei hinzufügen'
gist.new.create-public-button: 'Öffentliche Gist erstellen'
gist.new.create-unlisted-button: 'Nicht gelistete Gist erstellen'
gist.new.create-private-button: 'Private Gist erstellen'
gist.new.preview: 'Vorschau'
gist.new.create-a-new-gist: 'Neue Gist erstellen'
gist.edit.editing: 'Bearbeiten'
gist.edit.edit-gist: '%s bearbeiten'
gist.edit.change-visibility: 'Sichtbarkeit ändern'
gist.edit.delete: 'Löschen'
gist.edit.cancel: 'Abbrechen'
gist.edit.save: 'Speichern'
gist.list.joined: 'Gemeinsam'
gist.list.all: 'Alle Gists'
gist.list.search-results: 'Suchergebnisse'
gist.list.sort: 'Sortieren'
gist.list.sort-by-created: 'erstellt'
gist.list.sort-by-updated: 'bearbeitet'
gist.list.order-by-asc: 'Älteste'
gist.list.order-by-desc: 'Neueste'
gist.list.select-tab: 'Tab Auswählen'
gist.list.liked: 'Favorisiert'
gist.list.likes: 'Favoriten'
gist.list.forked: 'Forked'
gist.list.forked-from: 'Forked von'
gist.list.forks: 'Forks'
gist.list.files: 'Dateien'
gist.list.last-active: 'Zuletzt aktiv'
gist.list.no-gists: 'Keine Gists'
gist.list.all-liked-by: 'Alle Gists favorisiert von %s'
gist.list.all-forked-by: 'Alle Gists geforked von %s'
gist.list.all-from: 'Alle Gists von %s'
gist.search.found: 'Gists gefunden'
gist.search.no-results: 'Keine Gists gefunden'
gist.search.help.user: 'Gists erstellt von Nutzer'
gist.search.help.title: 'Gists mit Titel'
gist.search.help.filename: 'Gists mit Dateinamen'
gist.search.help.extension: 'Gists mit Dateiendung'
gist.search.help.language: 'Gists in Sprache'
gist.forks: 'Forks'
gist.forks.view: 'Fork ansehen'
gist.forks.no: 'Keine öffentlichen Forks'
gist.forks.for: 'Fork für %s'
gist.likes: 'Favoriten'
gist.likes.no: 'Keine Favorisierungen'
gist.likes.for: 'Favortitisiert für %s'
gist.revisions: 'Revisionen'
gist.revision.revised: 'hat die Gist bearbeitet'
gist.revision.go-to-revision: 'Zu Änderung gehen'
gist.revision.file-created: 'Datei erstellt'
gist.revision.file-deleted: 'Datei gelöscht'
gist.revision.file-renamed: 'umbenannt zu'
gist.revision.diff-truncated: 'Diff zu groß um angezeigt zu werden'
gist.revision.file-renamed-no-changes: 'Datei ohne Änderung umbenannt'
gist.revision.empty-file: 'Leere Datei'
gist.revision.no-changes: 'Keine Änderungen'
gist.revision.no-revisions: 'Keine Änderungen zum Anzeigen'
gist.revision-of: 'Änderungen von %s'
settings: 'Einstellungen'
settings.email: 'Email'
settings.email-help: 'Für Commits und Gravatar genutzt'
settings.email-set: 'Email setzen'
settings.link-accounts: 'Accounts verlinken'
settings.link-github-account: 'GitHub-Account verlinken'
settings.link-gitlab-account: 'GitLab-Account verlinken'
settings.link-gitea-account: 'Gitea-Account verlinken'
settings.unlink-github-account: 'Github-Account Verlinkung aufheben'
settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben'
settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben'
settings.delete-account: 'Account löschen'
settings.delete-account-confirm: 'Bist du dir sicher, dass du den Account löschen willst?'
settings.add-ssh-key: 'SSH-Schlüssel hinzufügen'
settings.add-ssh-key-help: 'Wird nur zum Pullen/Pushen von Gists mit Git über SSH genutzt'
settings.add-ssh-key-title: 'Titel'
settings.add-ssh-key-content: 'Schlüssel'
settings.delete-ssh-key: 'Löschen'
settings.delete-ssh-key-confirm: 'Entfernen von SSH-Schlüssel bestätigen'
settings.ssh-key-added-at: 'Hinzugefügt'
settings.ssh-key-never-used: 'Nie benutzt'
settings.ssh-key-last-used: 'Zuletzt benutzt'
settings.ssh-key-exists: 'SSH Schlüssel existiert bereits'
settings.change-username: 'Benutzername ändern'
settings.create-password: 'Password erstellen'
settings.create-password-help: 'Passwort erstellen'
settings.change-password: 'Passwort ändern'
settings.change-password-help: 'Passwort ändern'
settings.password-label-title: 'Passwort'
auth.signup-disabled: 'Das Registrieren wurde vom Administrator deaktiviert'
auth.login: 'Anmeldung'
auth.signup: 'Registration'
auth.new-account: 'Neuer Account'
auth.username: 'Benutzername'
auth.password: 'Passwort'
auth.register-instead: 'Stattdessen registrieren'
auth.login-instead: 'Stattdessen anmelden'
auth.oauth: 'Weiter mit %s Account'
error: 'Fehler'
error.page-not-found: 'Seite nicht gefunden'
error.bad-request: 'Ungültige Anfrage'
error.signup-disabled: 'Registrierung ist deaktivert'
error.signup-disabled-form: 'Registrierung über das Formular ist deaktiviert'
error.login-disabled-form: 'Anmeldung über das Formular ist deaktiviert'
error.complete-oauth-login: 'Anmeldung kann nicht abgeschlossen werden: %s'
error.oauth-unsupported: 'Nicht unterstützer Anbieter'
error.cannot-bind-data: 'Daten können nicht gebunden werden'
error.invalid-number: 'Ungültige Nummer'
error.invalid-character-unescaped: 'Ungültiges Zeichen unescapped'
header.menu.all: 'Alle'
header.menu.new: 'Neu'
header.menu.search: 'Suchen'
header.menu.my-gists: 'Meine Gists'
header.menu.liked: 'Favorisiert'
header.menu.admin: 'Admin'
header.menu.settings: 'Einstellungen'
header.menu.logout: 'Abmelden'
header.menu.register: 'Registrieren'
header.menu.login: 'Anmelden'
header.menu.light: 'Hell'
header.menu.dark: 'Dunkel'
header.menu.system: 'System'
footer.powered-by: 'Powered by %s'
pagination.older: 'Älter'
pagination.newer: 'Neuer'
pagination.previous: 'Vorherige'
pagination.next: 'Nachfolgende'
admin.admin_panel: 'Admin Panel'
admin.general: 'Allgemein'
admin.users: 'Benutzer'
admin.gists: 'Gists'
admin.configuration: 'Konfiguration'
admin.invitations: 'Einladungen'
admin.invitations.create: 'Einladung erstellen'
admin.versions: 'Versionen'
admin.ssh_keys: 'SSH Schlüssel'
admin.stats: 'Statistiken'
admin.actions: 'Aktionen'
admin.actions.sync-fs: 'Gists auf dem Dateisystem sychronisieren'
admin.actions.sync-db: 'Gists von der Datenbank synchronisieren'
admin.actions.git-gc: '„garbage collection“ bei allen git Repositories ausführen'
admin.actions.sync-previews: 'Alle Gist Vorschauen synchronisieren'
admin.actions.reset-hooks: 'Alle Git server Hooks für alle Repositories synchronisieren'
admin.actions.index-gists: 'Alle Gists Indexieren'
admin.id: 'ID'
admin.user: 'Benutzer'
admin.delete: 'Löschen'
admin.created_at: 'Erstellt'
admin.config-link: 'Diese Konfiguration kann mithilfe der YAML Konfigurationsdatei und/oder Umgebungsvariablen %s werden.'
admin.config-link-overriden: 'überschrieben'
admin.disable-signup: 'Registrierung deaktivieren'
admin.disable-signup_help: 'Die Erstellung neuer Accounts verbieten.'
admin.require-login: 'Anmeldung nötig'
admin.require-login_help: 'Benutzer müssen sich anmelden, bevor sie Gists ansehen können.'
admin.allow-gists-without-login: 'Erlaube einzelne Gists ohne login'
admin.allow-gists-without-login_help: 'Einzelne Gists können ohne Anmeldung angesehen und heruntergeladen werden, während für das Auffinden von Gists eine Anmeldung erforderlich ist.'
admin.disable-login: 'Login-Maske deaktivieren'
admin.disable-login_help: 'Login über Login-Maske verbieten und Benutzung von OAuth Providern erzwingen.'
admin.disable-gravatar: 'Gravatar deaktivieren'
admin.disable-gravatar_help: 'Gravatar als Avatar-Anbieter deaktivieren.'
admin.users.delete_confirm: 'Willst du diesen Benutzer löschen?'
admin.gists.title: 'Titel'
admin.gists.private: 'Privat?'
admin.gists.nb-files: 'Anz. Dateien'
admin.gists.nb-likes: 'Anz. Favoriten'
admin.gists.delete_confirm: 'Willst du diese Gist löschen?'
admin.invitations.help: 'Einladungen können zur Erstellung eines Kontos verwendet werden, auch wenn die Anmeldung deaktiviert ist.'
admin.invitations.max_uses: 'Maximale Verwendungen'
admin.invitations.expires_at: 'Läuft ab am'
admin.invitations.code: 'Code'
admin.invitations.copy_link: 'Link kopieren'
admin.invitations.uses: 'Verwendungen'
admin.invitations.expired: 'Abgelaufen'
flash.admin.user-deleted: 'Benutzer wurde gelöscht'
flash.admin.gist-deleted: 'Gist wurde gelöscht'
flash.admin.invitation-created: 'Einladung wurde erstellt'
flash.admin.invitation-deleted: 'Einladung wurde gelöscht'
flash.admin.sync-fs: 'Synchronisiere Repositories vom Dateisystem...'
flash.admin.sync-db: 'Synchronisiere Repositories aus der Datenbank...'
flash.admin.git-gc: 'Sammle Repositories...'
flash.admin.sync-previews: 'Synchronisiere Gist-Vorschauen...'
flash.admin.reset-hooks: 'Setze Git-Server-Hooks für alle Repositories zurück...'
flash.admin.index-gists: 'Indiziere alle Gists...'
flash.auth.username-exists: 'Benutzername existiert bereits'
flash.auth.invalid-credentials: 'Ungültige Anmeldeinformationen'
flash.auth.account-linked-oauth: 'Konto verknüpft mit %s'
flash.auth.account-unlinked-oauth: 'Konto getrennt von %s'
flash.auth.user-sshkeys-not-retrievable: 'Benutzerschlüssel konnten nicht abgerufen werden'
flash.auth.user-sshkeys-not-created: 'SSH-Schlüssel konnte nicht erstellt werden'
flash.auth.must-be-logged-in: 'Sie müssen eingeloggt sein, um auf Gists zuzugreifen'
flash.gist.visibility-changed: 'Gist-Sichtbarkeit wurde geändert'
flash.gist.deleted: 'Gist wurde gelöscht'
flash.gist.fork-own-gist: 'Eigene Gists können nicht geforkt werden'
flash.gist.forked: 'Gist wurde geforkt'
flash.user.email-updated: 'E-Mail wurde aktualisiert'
flash.user.invalid-ssh-key: 'Ungültiger SSH-Schlüssel'
flash.user.ssh-key-added: 'SSH-Schlüssel hinzugefügt'
flash.user.ssh-key-deleted: 'SSH-Schlüssel gelöscht'
flash.user.password-updated: 'Passwort wurde aktualisiert'
flash.user.username-updated: 'Benutzername wurde aktualisiert'
validation.is-too-long: 'Feld %s ist zu lang'
validation.should-not-be-empty: 'Feld %s darf nicht leer sein'
validation.should-not-include-sub-directory: 'Feld %s darf kein Unterverzeichnis enthalten'
validation.should-only-contain-alphanumeric-characters: 'Feld %s darf nur alphanumerische Zeichen enthalten'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Feld %s darf nur alphanumerische Zeichen und Bindestriche enthalten'
validation.not-enough: 'Nicht genug %s'
validation.invalid: 'Ungültiges %s'
html.title.admin-panel: 'Admin-Panel'

View File

@@ -43,12 +43,17 @@ gist.new.add-file: Add file
gist.new.create-public-button: Create public gist gist.new.create-public-button: Create public gist
gist.new.create-unlisted-button: Create unlisted gist gist.new.create-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist gist.new.create-private-button: Create private gist
gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist
gist.new.topics: Topics (separate with spaces)
gist.edit.editing: Editing gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
gist.edit.change-visibility: Make gist.edit.change-visibility: Make
gist.edit.delete: Delete gist.edit.delete: Delete
gist.edit.cancel: Cancel gist.edit.cancel: Cancel
gist.edit.save: Save gist.edit.save: Save
gist.delete.confirm: Are you sure you want to delete this gist ?
gist.list.joined: Joined gist.list.joined: Joined
gist.list.all: All gists gist.list.all: All gists
@@ -67,6 +72,12 @@ gist.list.forks: forks
gist.list.files: files gist.list.files: files
gist.list.last-active: Last active gist.list.last-active: Last active
gist.list.no-gists: No gists gist.list.no-gists: No gists
gist.list.all-liked-by: All gists liked by %s
gist.list.all-forked-by: All gists forked by %s
gist.list.all-from: All gists from %s
gist.list.topic-results-topic: All gists matching topic %s
gist.list.topic-results: All gists matching topic
gist.search.found: gists found gist.search.found: gists found
gist.search.no-results: No gists found gist.search.no-results: No gists found
@@ -75,13 +86,25 @@ gist.search.help.title: gists with given title
gist.search.help.filename: gists having files with given name gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language gist.search.help.language: gists having files with given language
gist.search.help.topic: gists with given topic
gist.search.placeholder.title: Title
gist.search.placeholder.visibility: Visibility
gist.search.placeholder.public: Public
gist.search.placeholder.unlisted: Unlisted
gist.search.placeholder.private: Private
gist.search.placeholder.language: Language
gist.search.placeholder.all: All
gist.search.placeholder.topics: Topics
gist.search.placeholder.search: Search
gist.forks: Forks gist.forks: Forks
gist.forks.view: View fork gist.forks.view: View fork
gist.forks.no: No public forks gist.forks.no: No public forks
gist.forks.for: Forks for %s
gist.likes: Likes gist.likes: Likes
gist.likes.no: No likes yet gist.likes.no: No likes yet
gist.likes.for: Likes for %s
gist.revisions: Revisions gist.revisions: Revisions
gist.revision.revised: revised this gist gist.revision.revised: revised this gist
@@ -94,6 +117,7 @@ gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: Empty file gist.revision.empty-file: Empty file
gist.revision.no-changes: No changes gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s
settings: Settings settings: Settings
settings.email: Email settings.email: Email
@@ -117,6 +141,7 @@ settings.delete-ssh-key-confirm: Confirm deletion of SSH key
settings.ssh-key-added-at: Added settings.ssh-key-added-at: Added
settings.ssh-key-never-used: Never used settings.ssh-key-never-used: Never used
settings.ssh-key-last-used: Last used settings.ssh-key-last-used: Last used
settings.ssh-key-exists: SSH key already exists
settings.change-username: Change username settings.change-username: Change username
settings.create-password: Create password settings.create-password: Create password
settings.create-password-help: Create your password to login to Opengist via HTTP settings.create-password-help: Create your password to login to Opengist via HTTP
@@ -132,11 +157,53 @@ auth.username: Username
auth.password: Password auth.password: Password
auth.register-instead: Register instead auth.register-instead: Register instead
auth.login-instead: Login instead auth.login-instead: Login instead
auth.github-oauth: Continue with GitHub account auth.oauth: Continue with %s account
auth.gitlab-oauth: Continue with GitLab account auth.mfa: Multi-factor authentication
auth.gitea-oauth: Continue with Gitea account auth.mfa.passkey: Passkey
auth.mfa.passkeys: Passkeys
auth.mfa.use-passkey: Use passkey
auth.mfa.bind-passkey: Bind passkey
auth.mfa.login-with-passkey: Login with passkey
auth.mfa.waiting-for-passkey-input: Waiting for input from browser interaction...
auth.mfa.use-passkey-to-finish: Use a passkey to finish authentication
auth.mfa.passkeys-help: Add a passkey to log to your account and to use as an MFA method.
auth.mfa.passkey-name: Name
auth.mfa.delete-passkey: Delete
auth.mfa.passkey-added-at: Added
auth.mfa.passkey-never-used: Never used
auth.mfa.passkey-last-used: Last used
auth.mfa.delete-passkey-confirm: Confirm deletion of passkey
auth.totp: Time based one-time password (TOTP)
auth.totp.help: TOTP is a two-factor authentication method that uses a shared secret to generate a one-time password.
auth.totp.use: Use TOTP
auth.totp.regenerate-recovery-codes: Regenerate recovery codes
auth.totp.already-enabled: TOTP is already enabled
auth.totp.invalid-secret: Invalid TOTP secret
auth.totp.invalid-code: Invalid TOTP code
auth.totp.code-used: The recovery code %s was used, it is now invalid. You may want to disable MFA for now or regenerate your codes.
auth.totp.disabled: TOTP successfully disabled
auth.totp.disable: Disable TOTP
auth.totp.enter-code: Enter the code from the Authenticator app
auth.totp.enter-recovery-key: or a recovery key if you lost your device
auth.totp.code: Code
auth.totp.submit: Submit
auth.totp.proceed: Proceed
auth.totp.save-recovery-codes: Save your recovery codes in a safe place. You can use these codes to recover access to your account if you lose access to your authenticator app.
auth.totp.scan-qr-code: Scan the QR code below with your authenticator app to enable two-factor authentication or enter the following string, then confirm with the generated code.
error: Error error: Error
error.page-not-found: Page not found
error.bad-request: Bad request
error.signup-disabled: Signing up is disabled
error.signup-disabled-form: Signing up via registration form is disabled
error.login-disabled-form: Logging in via login form is disabled
error.complete-oauth-login: "Cannot complete user auth: %s"
error.oauth-unsupported: Unsupported provider
error.cannot-bind-data: Cannot bind data
error.invalid-number: Invalid number
error.invalid-character-unescaped: Invalid character unescaped
error.not-in-mfa-session: User is not in a MFA session
header.menu.all: All header.menu.all: All
header.menu.new: New header.menu.new: New
@@ -163,6 +230,8 @@ admin.general: General
admin.users: Users admin.users: Users
admin.gists: Gists admin.gists: Gists
admin.configuration: Configuration admin.configuration: Configuration
admin.invitations: Invitations
admin.invitations.create: Create invitation
admin.versions: Versions admin.versions: Versions
admin.ssh_keys: SSH keys admin.ssh_keys: SSH keys
admin.stats: Stats admin.stats: Stats
@@ -173,6 +242,7 @@ admin.actions.git-gc: Garbage collect all git repositories
admin.actions.sync-previews: Synchronize all gists previews admin.actions.sync-previews: Synchronize all gists previews
admin.actions.reset-hooks: Reset Git server hooks for all repositories admin.actions.reset-hooks: Reset Git server hooks for all repositories
admin.actions.index-gists: Index all gists admin.actions.index-gists: Index all gists
admin.actions.sync-gist-languages: Synchronize all gists languages
admin.id: ID admin.id: ID
admin.user: User admin.user: User
admin.delete: Delete admin.delete: Delete
@@ -184,6 +254,8 @@ admin.disable-signup: Disable signup
admin.disable-signup_help: Forbid the creation of new accounts. admin.disable-signup_help: Forbid the creation of new accounts.
admin.require-login: Require login admin.require-login: Require login
admin.require-login_help: Enforce users to be logged in to see gists. admin.require-login_help: Enforce users to be logged in to see gists.
admin.allow-gists-without-login: Allow individual gists without login
admin.allow-gists-without-login_help: Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
admin.disable-login: Disable login form admin.disable-login: Disable login form
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead. admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar admin.disable-gravatar: Disable Gravatar
@@ -196,3 +268,57 @@ admin.gists.private: Private ?
admin.gists.nb-files: Nb. files admin.gists.nb-files: Nb. files
admin.gists.nb-likes: Nb. likes admin.gists.nb-likes: Nb. likes
admin.gists.delete_confirm: Do you want to delete this gist ? admin.gists.delete_confirm: Do you want to delete this gist ?
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
admin.invitations.max_uses: Max uses
admin.invitations.expires_at: Expires at
admin.invitations.code: Code
admin.invitations.copy_link: Copy link
admin.invitations.uses: Uses
admin.invitations.expired: Expired
admin.invitations.delete_confirm: Do you want to delete this invitation ?
flash.admin.user-deleted: User has been deleted
flash.admin.gist-deleted: Gist has been deleted
flash.admin.invitation-created: Invitation has been created
flash.admin.invitation-deleted: Invitation has been deleted
flash.admin.sync-fs: Syncing repositories from filesystem...
flash.admin.sync-db: Syncing repositories from database...
flash.admin.git-gc: Garbage collecting repositories...
flash.admin.sync-previews: Syncing Gist previews...
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
flash.admin.index-gists: Indexing all gists...
flash.admin.sync-gist-languages: Syncing Gist languages...
flash.auth.username-exists: Username already exists
flash.auth.invalid-credentials: Invalid credentials
flash.auth.account-linked-oauth: Account linked to %s
flash.auth.account-unlinked-oauth: Account unlinked from %s
flash.auth.user-sshkeys-not-retrievable: Could not get user keys
flash.auth.user-sshkeys-not-created: Could not create ssh key
flash.auth.must-be-logged-in: You must be logged in to access gists
flash.auth.passkey-registred: Passkey %s registered
flash.auth.passkey-deleted: Passkey deleted
flash.gist.visibility-changed: Gist visibility has been changed
flash.gist.deleted: Gist has been deleted
flash.gist.fork-own-gist: Unable to fork own gists
flash.gist.forked: Gist has been forked
flash.user.email-updated: Email updated
flash.user.invalid-ssh-key: Invalid SSH key
flash.user.ssh-key-added: SSH key added
flash.user.ssh-key-deleted: SSH key deleted
flash.user.password-updated: Password updated
flash.user.username-updated: Username updated
validation.is-too-long: Field %s is too long
validation.should-not-be-empty: Field %s should not be empty
validation.should-not-include-sub-directory: Field %s should not include a sub directory
validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
validation.not-enough: Not enough %s
validation.invalid: Invalid %s
validation.invalid-gist-topics: Invalid gist topics, they must start with a letter or number, consist of 50 characters or less, and can include hyphens
html.title.admin-panel: Admin panel

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Clonar via %s
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP. gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
gist.header.clone-ssh: Clonar via SSH gist.header.clone-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH. gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
gist.header.embed: gist.header.embed: 'Embeber'
gist.header.embed-help: gist.header.embed-help: 'Embebe este gist en tu sitio web.'
gist.header.download-zip: Descargar ZIP gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato gist.raw: Sin formato
@@ -71,8 +71,8 @@ gist.forks: Bifurcaciones
gist.forks.view: Ver bifurcación gist.forks.view: Ver bifurcación
gist.forks.no: No hay bifurcaciones públicas gist.forks.no: No hay bifurcaciones públicas
gist.likes: Gustos gist.likes: Todos los me gusta
gist.likes.no: Aún no hay gustos gist.likes.no: Aún no hay me gusta
gist.revisions: Revisiones gist.revisions: Revisiones
gist.revision.revised: revisó este gist gist.revision.revised: revisó este gist
@@ -80,7 +80,7 @@ gist.revision.go-to-revision: Ir a la revisión
gist.revision.file-created: archivo creado gist.revision.file-created: archivo creado
gist.revision.file-deleted: archivo eliminado gist.revision.file-deleted: archivo eliminado
gist.revision.file-renamed: renombrado a gist.revision.file-renamed: renombrado a
gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse. gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse
gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios
gist.revision.empty-file: Archivo vacío gist.revision.empty-file: Archivo vacío
gist.revision.no-changes: Sin cambios gist.revision.no-changes: Sin cambios
@@ -115,8 +115,7 @@ auth.username: Nombre de usuario
auth.password: Contraseña auth.password: Contraseña
auth.register-instead: Registrarse en su lugar auth.register-instead: Registrarse en su lugar
auth.login-instead: Iniciar sesión en su lugar auth.login-instead: Iniciar sesión en su lugar
auth.github-oauth: Continuar con cuenta de GitHub auth.oauth: Continuar con cuenta de %s
auth.gitea-oauth: Continuar con cuenta de Gitea
error: Error error: Error
@@ -158,7 +157,6 @@ admin.delete: Eliminar
admin.created_at: Creado admin.created_at: Creado
admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno. admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno.
admin.config-link-overridden: sobrescrito
admin.disable-signup: Deshabilitar registro admin.disable-signup: Deshabilitar registro
admin.disable-signup_help: Prohibir la creación de nuevas cuentas. admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
admin.require-login: Requerir inicio de sesión admin.require-login: Requerir inicio de sesión
@@ -167,7 +165,8 @@ admin.disable-login: Deshabilitar formulario de inicio de sesión
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar. admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
admin.disable-gravatar: Deshabilitar Gravatar admin.disable-gravatar: Deshabilitar Gravatar
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar. admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
admin.allow-gists-without-login: Permitir gists individuales sin iniciar sesión
admin.allow-gists-without-login_help: Permitir ver y descargar gists individuales sin iniciar sesión, requiriendo iniciar sesión para descubrir gists.
admin.users.delete_confirm: ¿Quieres eliminar a este usuario? admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
admin.gists.title: Título admin.gists.title: Título
@@ -175,3 +174,123 @@ admin.gists.private: ¿Privado?
admin.gists.nb-files: Núm. de archivos admin.gists.nb-files: Núm. de archivos
admin.gists.nb-likes: Núm. de gustos admin.gists.nb-likes: Núm. de gustos
admin.gists.delete_confirm: ¿Quieres eliminar este gist? admin.gists.delete_confirm: ¿Quieres eliminar este gist?
gist.new.url: 'URL'
gist.new.preview: 'Previsualizar'
gist.new.create-a-new-gist: 'Crear un nuevo gist'
gist.edit.edit-gist: 'Editar %s'
gist.list.all-liked-by: 'Todos los gists que le gustaron a %s'
gist.list.all-forked-by: 'Todos los gists bifurcados por %s'
gist.list.all-from: 'Todos los gists de %s'
gist.search.found: 'gists encontrados'
gist.search.no-results: 'No se han encontrado gists'
gist.search.help.user: 'gists creados por el usuario'
gist.search.help.title: 'gists con el título indicado'
gist.search.help.filename: 'gists que contienen archivos con el nombre indicado'
gist.search.help.extension: 'gists que contienen archivos con la extensión indicada'
gist.search.help.language: 'gists que contienen archivos con el lenguaje indicado'
gist.forks.for: 'Bifurcacaiones de %s'
gist.likes.for: 'Me gusta para %s'
gist.revision-of: 'Revisión de %s'
settings.link-gitlab-account: 'Vincular cuenta de GitLab'
settings.unlink-gitlab-account: 'Desvincular cuenta de GitLab'
settings.change-username: 'Cambiar nombre de usuario'
settings.create-password: 'Crear contraseña'
settings.create-password-help: 'Crea tu contraseña para acceder a Opengist vía HTTP'
settings.change-password: 'Cambiar contraseña'
settings.change-password-help: 'Cambia tu contraseña para acceder a Opengist vía HTTP'
settings.password-label-title: 'Contraseña'
error.page-not-found: 'Página no encontrada'
error.bad-request: 'Solicitud incorrecta'
error.signup-disabled: 'El registro está deshabilitado'
error.signup-disabled-form: 'El registro mediante el formulario está deshabilitado'
error.login-disabled-form: 'El inicio de sesión mediante el formulario está deshabilitado'
error.complete-oauth-login: 'No se puede completar la autenticación del usuario: %s'
error.oauth-unsupported: 'Proveedor no compatible'
error.cannot-bind-data: 'No se puede vincular los datos'
error.invalid-number: 'Número inválido'
error.invalid-character-unescaped: 'Carácter inválido no escapado'
admin.invitations: 'Invitaciones'
admin.invitations.create: 'Crear invitación'
admin.actions.sync-previews: 'Sincronizar todas las vistas previas de gists'
admin.actions.reset-hooks: 'Resetear los hooks de Git en todos los repositorios'
admin.actions.index-gists: 'Indexar todos los gists'
admin.config-link-overriden: 'sobrescrito'
admin.invitations.help: 'Las invitaciones se pueden usar para crear una cuenta aunque el registro esté deshabilitado.'
admin.invitations.max_uses: 'Cantidad máxima de usos'
admin.invitations.expires_at: 'Expira el'
admin.invitations.code: 'Código'
admin.invitations.copy_link: 'Copiar vínculo'
admin.invitations.uses: 'Usos'
admin.invitations.expired: 'Expirado'
flash.admin.user-deleted: 'El usuario ha sido eliminado'
flash.admin.gist-deleted: 'El gist ha sido eliminado'
flash.admin.invitation-created: 'La invitación ha sido creada'
flash.admin.invitation-deleted: 'La invitación ha sido eliminada'
flash.admin.sync-fs: 'Sincronizando repositorios desde el sistema de archivos...'
flash.admin.sync-db: 'Sincronizando repositorios desde la base de datos...'
flash.admin.git-gc: 'Recolectando basura en los repositorios...'
flash.admin.sync-previews: 'Sincronizando vistas previas de gists...'
flash.admin.reset-hooks: 'Reseteando hooks del servidor Git en todos los repositorios...'
flash.admin.index-gists: 'Indexando todos los gists...'
flash.auth.username-exists: 'El nombre de usuario ya existe'
flash.auth.invalid-credentials: 'Credenciales incorrectas'
flash.auth.account-linked-oauth: 'Cuenta vinculada a %s'
flash.auth.account-unlinked-oauth: 'Cuenta desvinculada de %s'
flash.auth.user-sshkeys-not-retrievable: 'No se pudo obtener las claves del usuario'
flash.auth.user-sshkeys-not-created: 'No se pudo crear la aclave ssh'
flash.auth.must-be-logged-in: 'Debes estar logueaado para acceder a los gists'
flash.gist.visibility-changed: 'La visibilidad del Gist ha sido modificada'
flash.gist.deleted: 'El gist fue eliminado'
flash.gist.fork-own-gist: 'No se puede bifurcar gists propios'
flash.gist.forked: 'El gist ha sido bifurcado'
flash.user.email-updated: 'Correo actualizado'
flash.user.invalid-ssh-key: 'Clave SSH inválida'
flash.user.ssh-key-added: 'Clave SSH añadida'
flash.user.ssh-key-deleted: 'Clave SSH eliminada'
flash.user.password-updated: 'Contraseña actualizada'
flash.user.username-updated: 'Nombre de usuario actualizado'
validation.is-too-long: 'El campo %s es demasiado largo'
validation.should-not-be-empty: 'El campo %s no puede estar vacío'
validation.should-not-include-sub-directory: 'El campo %s no puede incluir un sub directorio'
validation.should-only-contain-alphanumeric-characters: 'El campo %s solo puede contener caracteres alfanuméricos'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'El campo %s solo puede contener caracteres alfanuméricos y guiones'
validation.not-enough: 'No hay suficiente %s'
validation.invalid: '%s inválido'
html.title.admin-panel: 'Panel de administración'
auth.mfa: Autenticación mult-factor
auth.mfa.passkey: Clave de acceso
auth.mfa.passkeys: Claves de acceso
auth.mfa.use-passkey: Utilizar clave de acceso
auth.mfa.bind-passkey: Vincular clave de acceso
auth.mfa.login-with-passkey: Ingresar con clave de acceso
auth.mfa.waiting-for-passkey-input: Esperando interacción del navegador...
auth.mfa.use-passkey-to-finish: Usa una clave de acceso para completar la autenticación
auth.mfa.passkeys-help: Agrega una clave de acceso para iniciar sesión en tu cuenta y usarla como método MFA.
auth.mfa.passkey-name: Nombre
auth.mfa.delete-passkey: Eliminar
auth.mfa.passkey-added-at: Agregado
auth.mfa.passkey-never-used: Nunca utilizado
auth.mfa.passkey-last-used: Último uso
auth.mfa.delete-passkey-confirm: Confirmar eliminación de clave de acceso
auth.totp.enter-recovery-key: o una clave de recuperación si perdiste tu dispositivo
auth.totp.code: Código
auth.totp.submit: Enviar
auth.totp.proceed: Proceder
auth.totp.save-recovery-codes: Guarda tus códigos de recuperación en un lugar seguro. Puedes usarlos para recuperar el acceso a tu cuenta si pierdes el acceso a tu app de autenticación.
auth.totp.scan-qr-code: Escanea el código QR con tu app de autenticación para habilitar la autenticación de doble factor o ingresa la siguiente cadena y confirma con el código generado.
error.not-in-mfa-session: El usuario no está en una sesión MFA
settings.ssh-key-exists: La clave SSH ya existe
auth.totp: Contraseña de un solo uso basada en tiempo (TOTP)
auth.totp.help: TOTP es un método de autenticación doble factor que utiliza una clave compartida para generar una contraseña de un solo uso.
auth.totp.use: Usar TOTP
auth.totp.regenerate-recovery-codes: Regenerar códigos de recuperación
auth.totp.already-enabled: TOTP ya está habilitado
auth.totp.invalid-secret: Clave TOTP inválido
auth.totp.invalid-code: Código TOTP inválido
auth.totp.code-used: El código de recuperación %s fue utilizado, ahora es inválido. Puedes desactivar MFA por ahora o regenerar tus códigos.
auth.totp.disabled: TOTP deshabilitado exitosamente
auth.totp.disable: Deshabilitar TOTP
auth.totp.enter-code: Ingresa el código de la app de autenticación
gist.delete.confirm: ¿Estás seguro que deseas eliminar este gist?
flash.auth.passkey-registred: Clave de acceso %s registrada
flash.auth.passkey-deleted: Clave de acceso eliminada

View File

@@ -115,8 +115,7 @@ auth.username: Nom d'utilisateur
auth.password: Mot de passe auth.password: Mot de passe
auth.register-instead: Je préfère m'inscrire auth.register-instead: Je préfère m'inscrire
auth.login-instead: Je préfère me connecter auth.login-instead: Je préfère me connecter
auth.github-oauth: Continuer avec un compte GitHub auth.oauth: Continuer avec un compte %s
auth.gitea-oauth: Continuer avec un compte Gitea
error: Erreur error: Erreur
@@ -167,7 +166,8 @@ admin.disable-login: Désactiver le formulaire de connexion
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place. admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
admin.disable-gravatar: Désactiver Gravatar admin.disable-gravatar: Désactiver Gravatar
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar. admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
admin.allow-gists-without-login: Autoriser les gists individuelles sans login
admin.allow-gists-without-login_help: Autoriser la visualisation et le téléchargement de gists individuels sans connexion, tout en exigeant une connexion pour la découverte de gists.
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ? admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
admin.gists.title: Titre admin.gists.title: Titre
@@ -188,10 +188,74 @@ settings.create-password-help: Créer un mot de passe pour se connecter à Openg
settings.change-password: Changer le mot de passe settings.change-password: Changer le mot de passe
settings.change-password-help: Changer le mot de passe pour se connecter à Opengist via HTTP settings.change-password-help: Changer le mot de passe pour se connecter à Opengist via HTTP
settings.password-label-title: Mot de passe settings.password-label-title: Mot de passe
auth.gitlab-oauth: Continuer avec un compte GitLab
admin.actions.sync-previews: Synchroniser l'aperçu des gists admin.actions.sync-previews: Synchroniser l'aperçu des gists
admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôts admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôts
gist.new.url: URL gist.new.url: URL
gist.search.no-results: Aucun gist trouvé gist.search.no-results: Aucun gist trouvé
settings.unlink-gitlab-account: Détacher le compte GitLab settings.unlink-gitlab-account: Détacher le compte GitLab
admin.actions.index-gists: Indexer tous les gists admin.actions.index-gists: Indexer tous les gists
gist.new.preview: 'Aperçu'
gist.new.create-a-new-gist: 'Créer un nouveau gist'
gist.edit.edit-gist: 'Modifier %s'
gist.list.all-liked-by: 'Tous les gists aimés par %s'
gist.list.all-forked-by: 'Tous les gists forkées par %s'
gist.list.all-from: 'Tous les gists de %S'
gist.forks.for: 'Forks pour %s'
gist.likes.for: 'J''aimes pour %s'
gist.revision-of: 'Révisions pour %s'
error.page-not-found: 'Page non trouvée'
error.bad-request: 'Requête erronée'
error.signup-disabled: 'L''inscription est désactivée'
error.signup-disabled-form: 'L''inscription via le formulaire d''enregistrement est désactivée'
error.login-disabled-form: 'La connexion via le formulaire de connexion est désactivée'
error.complete-oauth-login: 'Impossible de terminer l''authentification de l''utilisateur : %s'
error.oauth-unsupported: 'Fournisseur d''authentification non supporté'
error.cannot-bind-data: 'Impossible de lier les données'
error.invalid-number: 'Nombre invalide'
error.invalid-character-unescaped: 'Caractère non protégé invalide'
admin.invitations: 'Invitations'
admin.invitations.create: 'Créer une invitation'
admin.invitations.help: 'Les invitations peuvent être utilisées pour créer un compte même si l''inscription est désactivée.'
admin.invitations.max_uses: 'Utilisations maximales'
admin.invitations.expires_at: 'Expire le'
admin.invitations.code: 'Code'
admin.invitations.copy_link: 'Copier le lien'
admin.invitations.uses: 'Utilisations'
admin.invitations.expired: 'Expiré'
flash.admin.user-deleted: 'L''utilisateur a été supprimé'
flash.admin.gist-deleted: 'Le gist a été supprimée'
flash.admin.invitation-created: 'L''invitation a été créée'
flash.admin.invitation-deleted: 'L''invitation a été supprimée'
flash.admin.sync-fs: 'Synchronisation des dépôts à partir du système de fichiers...'
flash.admin.sync-db: 'Synchronisation des dépôts à partir de la base de données...'
flash.admin.git-gc: 'Nettoyage des dépôts...'
flash.admin.sync-previews: 'Synchronisation des aperçus du Gist...'
flash.admin.reset-hooks: 'Réinitialisation des hooks du serveur Git pour tous les dépôts...'
flash.admin.index-gists: 'Indexation de tous les gists...'
flash.auth.username-exists: 'Nom d''utilisateur déjà utilisé'
flash.auth.invalid-credentials: 'Identifiants non valides'
flash.auth.account-linked-oauth: 'Compte lié à %s'
flash.auth.account-unlinked-oauth: 'Compte dissocié de %s'
flash.auth.user-sshkeys-not-retrievable: 'Impossible d''obtenir les clés de l''utilisateur'
flash.auth.user-sshkeys-not-created: 'Impossible de créer une clé ssh'
flash.auth.must-be-logged-in: 'Vous devez être connecté pour accéder aux gists'
flash.gist.visibility-changed: 'La visibilité du gist a été modifiée'
flash.gist.deleted: 'Le gist a été supprimé'
flash.gist.fork-own-gist: 'Impossible de forker ses propres gists'
flash.gist.forked: 'Le gist a été forké'
flash.user.email-updated: 'Email mis à jour'
flash.user.invalid-ssh-key: 'Clé SSH invalide'
flash.user.ssh-key-added: 'Clé SSH ajoutée'
flash.user.ssh-key-deleted: 'Clé SSH supprimée'
flash.user.password-updated: 'Mot de passe mis à jour'
flash.user.username-updated: 'Nom d''utilisateur mis à jour'
validation.is-too-long: 'Le champ %s est trop long'
validation.should-not-be-empty: 'Le champ %s ne doit pas être vide'
validation.should-not-include-sub-directory: 'Le champ %s ne doit pas inclure de sous-répertoire'
validation.should-only-contain-alphanumeric-characters: 'Le champ %s ne doit contenir que des caractères alphanumériques.'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Le champ %s ne doit contenir que des caractères alphanumériques et des tirets.'
validation.not-enough: 'Pas assez de %s'
validation.invalid: '%s non valide'
html.title.admin-panel: 'Administration'
settings.ssh-key-exists: La clé SSH existe déjà
gist.delete.confirm: Voulez-vous supprimer ce Gist ?

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: "Clone-ozás ezzel: %s"
gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel. gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel.
gist.header.clone-ssh: Clone-ozás SSH-n keresztül gist.header.clone-ssh: Clone-ozás SSH-n keresztül
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
gist.header.embed: gist.header.embed: ''
gist.header.embed-help: gist.header.embed-help: ''
gist.header.download-zip: ZIP archívum letöltése gist.header.download-zip: ZIP archívum letöltése
gist.raw: Eredeti gist.raw: Eredeti
@@ -30,6 +30,7 @@ gist.no-content: Nincs tartalom
gist.new.new_gist: Új gist gist.new.new_gist: Új gist
gist.new.title: Cím gist.new.title: Cím
gist.new.description: Leírás gist.new.description: Leírás
gist.new.url: URL
gist.new.filename-with-extension: Fájlnév kiterjesztéssel gist.new.filename-with-extension: Fájlnév kiterjesztéssel
gist.new.indent-mode: Indentáció típusa gist.new.indent-mode: Indentáció típusa
gist.new.indent-mode-space: Szóköz gist.new.indent-mode-space: Szóköz
@@ -67,6 +68,14 @@ gist.list.files: fájlok
gist.list.last-active: Utoljára aktív gist.list.last-active: Utoljára aktív
gist.list.no-gists: Nincsenek gistek gist.list.no-gists: Nincsenek gistek
gist.search.found: találat
gist.search.no-results: Nincsenek találatok
gist.search.help.user: létrehozva e felhasználó által
gist.search.help.title: gistek egyező címmel
gist.search.help.filename: gistek melyek tartalmaznak fájlt egyező névvel
gist.search.help.extension: gistek melyek tartalmaznak fájlt egyező kiterjesztéssel
gist.search.help.language: gistek melyek tartalmaznak fájlt egyező nyelvvel
gist.forks: Forkok gist.forks: Forkok
gist.forks.view: Fork megtekintése gist.forks.view: Fork megtekintése
gist.forks.no: Nincsenek nyilvános forkok gist.forks.no: Nincsenek nyilvános forkok
@@ -92,8 +101,10 @@ settings.email-help: Commitoknál és Gravatarnál van használva
settings.email-set: Email beállítása settings.email-set: Email beállítása
settings.link-accounts: Fiókok összekötése settings.link-accounts: Fiókok összekötése
settings.link-github-account: GitHub fiók hozzáadása settings.link-github-account: GitHub fiók hozzáadása
settings.link-gitlab-account: GitLab fiók hozzáadása
settings.link-gitea-account: Gitea fiók hozzáadása settings.link-gitea-account: Gitea fiók hozzáadása
settings.unlink-github-account: GitHub fiók eltávolítása settings.unlink-github-account: GitHub fiók eltávolítása
settings.unlink-gitlab-account: GitLab fiók eltávolítása
settings.unlink-gitea-account: Gitea fiók eltávolítása settings.unlink-gitea-account: Gitea fiók eltávolítása
settings.delete-account: Fiók törlése settings.delete-account: Fiók törlése
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod? settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
@@ -106,6 +117,7 @@ settings.delete-ssh-key-confirm: Erősítsd meg az SSH kulcs törlését
settings.ssh-key-added-at: "Hozzáadva:" settings.ssh-key-added-at: "Hozzáadva:"
settings.ssh-key-never-used: Sosem használt settings.ssh-key-never-used: Sosem használt
settings.ssh-key-last-used: "Utoljára használva:" settings.ssh-key-last-used: "Utoljára használva:"
settings.change-username: Felhasználónév megváltoztatása
settings.create-password: Jelszó létrehozása settings.create-password: Jelszó létrehozása
settings.create-password-help: Hozz létre egy jelszót, hogy bejelentkezhess az OpenGist-be HTTP-n keresztül settings.create-password-help: Hozz létre egy jelszót, hogy bejelentkezhess az OpenGist-be HTTP-n keresztül
settings.change-password: Jelszó megváltoztatása settings.change-password: Jelszó megváltoztatása
@@ -120,8 +132,7 @@ auth.username: Felhasználónév
auth.password: Jelszó auth.password: Jelszó
auth.register-instead: Vagy regisztrálj auth.register-instead: Vagy regisztrálj
auth.login-instead: Vagy jelentkezz be auth.login-instead: Vagy jelentkezz be
auth.github-oauth: Folytatás GitHub fiókkal auth.oauth: Folytatás %s fiókkal
auth.gitea-oauth: Folytatás Gitea fiókkal
error: Hiba error: Hiba
@@ -157,6 +168,9 @@ admin.actions: Műveletek
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
admin.actions.sync-db: Gistek szinkronizálása az adatbázissal admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
admin.actions.git-gc: Használatlan git repository-k eltávolítása admin.actions.git-gc: Használatlan git repository-k eltávolítása
admin.actions.sync-previews: Gist előnézetek szinkronizálása
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
admin.actions.index-gists: Gistek indexelése
admin.id: Azonosító admin.id: Azonosító
admin.user: Felhasználó admin.user: Felhasználó
admin.delete: Törlés admin.delete: Törlés
@@ -172,7 +186,8 @@ admin.disable-login: Bejelentkezés űrlap letiltása
admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette. admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette.
admin.disable-gravatar: Gravatar kikapcsolása admin.disable-gravatar: Gravatar kikapcsolása
admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató. admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót? admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót?
admin.gists.title: Cím admin.gists.title: Cím
@@ -180,3 +195,66 @@ admin.gists.private: Privát ?
admin.gists.nb-files: Fájlok száma admin.gists.nb-files: Fájlok száma
admin.gists.nb-likes: Kedv. száma admin.gists.nb-likes: Kedv. száma
admin.gists.delete_confirm: Biztosan törlöd a gistet? admin.gists.delete_confirm: Biztosan törlöd a gistet?
gist.new.preview: ''
gist.new.create-a-new-gist: ''
gist.edit.edit-gist: ''
gist.list.all-liked-by: ''
gist.list.all-forked-by: ''
gist.list.all-from: ''
gist.forks.for: ''
gist.likes.for: ''
gist.revision-of: ''
error.page-not-found: ''
error.bad-request: ''
error.signup-disabled: ''
error.signup-disabled-form: ''
error.login-disabled-form: ''
error.complete-oauth-login: ''
error.oauth-unsupported: ''
error.cannot-bind-data: ''
error.invalid-number: ''
error.invalid-character-unescaped: ''
admin.invitations: ''
admin.invitations.create: ''
admin.invitations.help: ''
admin.invitations.max_uses: ''
admin.invitations.expires_at: ''
admin.invitations.code: ''
admin.invitations.copy_link: ''
admin.invitations.uses: ''
admin.invitations.expired: ''
flash.admin.user-deleted: ''
flash.admin.gist-deleted: ''
flash.admin.invitation-created: ''
flash.admin.invitation-deleted: ''
flash.admin.sync-fs: ''
flash.admin.sync-db: ''
flash.admin.git-gc: ''
flash.admin.sync-previews: ''
flash.admin.reset-hooks: ''
flash.admin.index-gists: ''
flash.auth.username-exists: ''
flash.auth.invalid-credentials: ''
flash.auth.account-linked-oauth: ''
flash.auth.account-unlinked-oauth: ''
flash.auth.user-sshkeys-not-retrievable: ''
flash.auth.user-sshkeys-not-created: ''
flash.auth.must-be-logged-in: ''
flash.gist.visibility-changed: ''
flash.gist.deleted: ''
flash.gist.fork-own-gist: ''
flash.gist.forked: ''
flash.user.email-updated: ''
flash.user.invalid-ssh-key: ''
flash.user.ssh-key-added: ''
flash.user.ssh-key-deleted: ''
flash.user.password-updated: ''
flash.user.username-updated: ''
validation.is-too-long: ''
validation.should-not-be-empty: ''
validation.should-not-include-sub-directory: ''
validation.should-only-contain-alphanumeric-characters: ''
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
validation.not-enough: ''
validation.invalid: ''
html.title.admin-panel: ''

View File

@@ -0,0 +1,269 @@
gist.public: 'Pubblico'
gist.unlisted: 'Non in lista'
gist.private: 'Privato'
gist.header.like: 'Mi piace'
gist.header.unlike: 'Non mi piace più'
gist.header.fork: 'Forka'
gist.header.edit: 'Modifica'
gist.header.delete: 'Elimina'
gist.header.forked-from: 'Forkato da'
gist.header.last-active: 'Ultima attività'
gist.header.select-tab: 'Seleziona una scheda'
gist.header.code: 'Codice'
gist.header.revisions: 'Revisioni'
gist.header.revision: 'Revisione'
gist.header.clone-http: 'Clona tramite %s'
gist.header.clone-http-help: 'Clona con Git usando l''autenticazione HTTP basic.'
gist.header.clone-ssh: 'Clona tramite SSH'
gist.header.clone-ssh-help: 'Clona con Git usando una chiave SSH.'
gist.header.embed: 'Incorpora'
gist.header.embed-help: 'Incorpora questo gist nel tuo sito.'
gist.header.download-zip: 'Scarica ZIP'
gist.raw: 'Raw'
gist.file-truncated: 'Questo file è stato troncato.'
gist.watch-full-file: 'Visualizza il file completo.'
gist.file-not-valid: 'Questo file non è un CSV valido.'
gist.no-content: 'Nessun file trovato'
gist.new.new_gist: 'Nuovo gist'
gist.new.title: 'Titolo'
gist.new.description: 'Descrizione'
gist.new.url: 'URL'
gist.new.filename-with-extension: 'Nome del file con l''estensione'
gist.new.indent-mode: 'Modalità indentazione'
gist.new.indent-mode-space: 'Spazio'
gist.new.indent-mode-tab: 'Tabulazione'
gist.new.indent-size: 'Dimensione d''indentazione'
gist.new.wrap-mode: 'Modalità a capo'
gist.new.wrap-mode-no: 'Non andare a capo'
gist.new.wrap-mode-soft: 'Vai a capo dove necessario'
gist.new.add-file: 'Aggiungi un file'
gist.new.create-public-button: 'Crea un gist pubblico'
gist.new.create-unlisted-button: 'Crea un gist non in lista'
gist.new.create-private-button: 'Crea un gist privato'
gist.new.preview: 'Anteprima'
gist.new.create-a-new-gist: 'Crea un nuovo gist'
gist.edit.editing: 'Modificando'
gist.edit.edit-gist: 'Modifica %s'
gist.edit.change-visibility: 'Crea'
gist.edit.delete: 'Elimina'
gist.edit.cancel: 'Annulla'
gist.edit.save: 'Salva'
gist.list.joined: 'Unito'
gist.list.all: 'Tutti i gists'
gist.list.search-results: 'Risultati della ricerca'
gist.list.sort: 'Ordina'
gist.list.sort-by-created: 'creazione'
gist.list.sort-by-updated: 'aggiornamento'
gist.list.order-by-asc: 'Meno recente'
gist.list.order-by-desc: 'Più recente'
gist.list.select-tab: 'Seleziona una scheda'
gist.list.liked: 'Gists che mi piacciono'
gist.list.likes: 'mi piace'
gist.list.forked: 'Forkati'
gist.list.forked-from: 'Forkato da'
gist.list.forks: 'forks'
gist.list.files: 'files'
gist.list.last-active: 'Ultima volta attivo'
gist.list.no-gists: 'Nessun gist'
gist.list.all-liked-by: 'Tutti i gists che piacciono a %s'
gist.list.all-forked-by: 'Tutti i gists forkati da %s'
gist.list.all-from: 'Tutti i gists di %s'
gist.search.found: 'gists trovati'
gist.search.no-results: 'Nessun gist trovato'
gist.search.help.user: 'utente che ha creato il gist'
gist.search.help.title: 'titolo del gist'
gist.search.help.filename: 'nome di file nel gist'
gist.search.help.extension: 'estensione del file nel gist'
gist.search.help.language: 'linguaggio del file nel gist'
gist.forks: 'Forks'
gist.forks.view: 'Visualizza fork'
gist.forks.no: 'Nessun fork pubblico'
gist.forks.for: 'Forks di %s'
gist.likes: 'Mi piace'
gist.likes.no: 'Ancora nessun mi piace'
gist.likes.for: 'Mi piace per %s'
gist.revisions: 'Revisioni'
gist.revision.revised: 'ha revisionato questo gist'
gist.revision.go-to-revision: 'Vai alla revisione'
gist.revision.file-created: 'file creato'
gist.revision.file-deleted: 'file eliminato'
gist.revision.file-renamed: 'rinominato come'
gist.revision.diff-truncated: 'Il diff è troppo grande per essere visualizzato'
gist.revision.file-renamed-no-changes: 'File rinominato senza modifiche'
gist.revision.empty-file: 'File vuoto'
gist.revision.no-changes: 'Nessuna modifica'
gist.revision.no-revisions: 'Nessuna revisione da mostrare'
gist.revision-of: 'Revisioni di %s'
settings: 'Impostazioni'
settings.email: 'Email'
settings.email-help: 'Usato per i commits e per Gravatar'
settings.email-set: 'Imposta email'
settings.link-accounts: 'Collega accounts'
settings.link-github-account: 'Collega account di GitHub'
settings.link-gitlab-account: 'Collega account di GitLab'
settings.link-gitea-account: 'Collega account di Gitea'
settings.unlink-github-account: 'Scollega account di GitHub'
settings.unlink-gitlab-account: 'Scollega account di GitLab'
settings.unlink-gitea-account: 'Scollega account di Gitea'
settings.delete-account: 'Elimina account'
settings.delete-account-confirm: 'Sei sicuro di voler eliminare il tuo account?'
settings.add-ssh-key: 'Aggiungi chiave SSH'
settings.add-ssh-key-help: 'Utilizzata soltanto per pullare/pushare gists con Git tramite SSH'
settings.add-ssh-key-title: 'Titolo'
settings.add-ssh-key-content: 'Chiave'
settings.delete-ssh-key: 'Elimina'
settings.delete-ssh-key-confirm: 'Conferma eliminazione della chiave SSH'
settings.ssh-key-added-at: 'Aggiunta'
settings.ssh-key-never-used: 'Mai usata'
settings.ssh-key-last-used: 'Usata l''ultima volta'
settings.change-username: 'Cambia nome utente'
settings.create-password: 'Crea password'
settings.create-password-help: 'Crea la tua password per entrare in Opengist tramite HTTP'
settings.change-password: 'Cambia password'
settings.change-password-help: 'Cambia la tua password per entrare in Opengist tramite HTTP'
settings.password-label-title: 'Password'
auth.signup-disabled: 'L''amministratore ha disabilitato la registrazione'
auth.login: 'Entra'
auth.signup: 'Registrati'
auth.new-account: 'Nuovo account'
auth.username: 'Nome utente'
auth.password: 'Password'
auth.register-instead: 'Non hai ancora un account?'
auth.login-instead: 'Hai già un account?'
auth.oauth: 'Continua con l''account %s'
error: 'Errore'
error.page-not-found: 'Pagina non trovata'
error.bad-request: 'Richiesta errata'
error.signup-disabled: 'La registrazione è disabilitata'
error.signup-disabled-form: 'La registrazione tramtie form è disabilitata'
error.login-disabled-form: 'Il login tramite form è disabilitato'
error.complete-oauth-login: "Impossibile completare l'autenticazione di %s"
error.oauth-unsupported: 'Provider non supportato'
error.cannot-bind-data: 'Impossibile abbinare i dati'
error.invalid-number: 'Numero non valido'
error.invalid-character-unescaped: 'Caratteri non escapati non validi'
header.menu.all: 'Tutti'
header.menu.new: 'Nuovi'
header.menu.search: 'Cerca'
header.menu.my-gists: 'I miei gists'
header.menu.liked: 'Gists che mi piacciono'
header.menu.admin: 'Amministrazione'
header.menu.settings: 'Impostazioni'
header.menu.logout: 'Esci'
header.menu.register: 'Registrati'
header.menu.login: 'Entra'
header.menu.light: 'Chiaro'
header.menu.dark: 'Scuro'
header.menu.system: 'Sistema'
footer.powered-by: 'Creato da %s'
pagination.older: 'Più vecchi'
pagination.newer: 'Più nuovi'
pagination.previous: 'Precedente'
pagination.next: 'Successiva'
admin.admin_panel: 'Pannello amministrazione'
admin.general: 'Generale'
admin.users: 'Utenti'
admin.gists: 'Gists'
admin.configuration: 'Configurazione'
admin.invitations: 'Inviti'
admin.invitations.create: 'Crea un invito'
admin.versions: 'Versioni'
admin.ssh_keys: 'Chiavi SSH'
admin.stats: 'Statistiche'
admin.actions: 'Azioni'
admin.actions.sync-fs: 'Sincronizza gists dal filesystem'
admin.actions.sync-db: 'Sincronizza gists dal database'
admin.actions.git-gc: 'Esegui la garbage collection da tutti i repositories'
admin.actions.sync-previews: 'Sincronizza tutte le anteprime dei gists'
admin.actions.reset-hooks: 'Resetta tutti gli hook del server Git per tutti i repositories'
admin.actions.index-gists: 'Indicizza tutti i gists'
admin.id: 'ID'
admin.user: 'Utente'
admin.delete: 'Elimina'
admin.created_at: 'Creato'
admin.config-link: 'Questa configurazione può essere %s da un file di configurazione YAML o da delle variabili d''ambiente.'
admin.config-link-overriden: 'sovrascritta'
admin.disable-signup: 'Disabilita la registrazione'
admin.disable-signup_help: 'Blocca la creazione di nuovi accounts.'
admin.require-login: 'Richiedi login'
admin.require-login_help: 'Obbliga gli utenti ad essere loggati per vedere i gists.'
admin.allow-gists-without-login: 'Permetti di creare gists individuali senza login'
admin.allow-gists-without-login_help: 'Permetti di visualizzare e scaricare gists individuali senza essere loggati, ma richiedi il login per scoprire nuovi gists.'
admin.disable-login: 'Disabilita form di login'
admin.disable-login_help: 'Blocca il login tramite form per forzare l''accesso tramite Oauth.'
admin.disable-gravatar: 'Disabilita Gravatar'
admin.disable-gravatar_help: 'Disabilita Gravatar come provider di avatar.'
admin.users.delete_confirm: 'Vuoi eliminare questo utente?'
admin.gists.title: 'Titolo'
admin.gists.private: 'Privato?'
admin.gists.nb-files: 'N. files'
admin.gists.nb-likes: 'N. mi piace'
admin.gists.delete_confirm: 'Vuoi eliminare questo gist?'
admin.invitations.help: 'Gli inviti possono essere usati per creare un account anche se la registazione è disabilitata.'
admin.invitations.max_uses: 'Utenti massimi'
admin.invitations.expires_at: 'Scade il'
admin.invitations.code: 'Codice'
admin.invitations.copy_link: 'Copia link'
admin.invitations.uses: 'Usa'
admin.invitations.expired: 'Scaduto'
flash.admin.user-deleted: 'L''utente è stato eliminato'
flash.admin.gist-deleted: 'Il gist è stato eliminato'
flash.admin.invitation-created: 'L''invito è stato creato'
flash.admin.invitation-deleted: 'L''invito è stato eliminato'
flash.admin.sync-fs: 'Sincronizzando i repositories dal filesystem...'
flash.admin.sync-db: 'Sincronizzando i repositories dal database...'
flash.admin.git-gc: 'Eseguendo il garbage collector dei repositories...'
flash.admin.sync-previews: 'Sincronizzando le anteprime dei gists...'
flash.admin.reset-hooks: 'Resettando gli hook di Git per tutti i repositories...'
flash.admin.index-gists: 'Indicizzando tutti i gists...'
flash.auth.username-exists: 'Il nome utente esiste già'
flash.auth.invalid-credentials: 'Credenziali errate'
flash.auth.account-linked-oauth: 'Account collegato a %s'
flash.auth.account-unlinked-oauth: 'Account scollegato da %s'
flash.auth.user-sshkeys-not-retrievable: 'Impossibile ottenere le chiavi dell''utente'
flash.auth.user-sshkeys-not-created: 'Impossibile creare chiave SSH'
flash.auth.must-be-logged-in: 'Devi essere loggato per visualizzare questi gists'
flash.gist.visibility-changed: 'La visibilità del gist è stata modificata'
flash.gist.deleted: 'Il gist è stato eliminato'
flash.gist.fork-own-gist: 'Impossibile forkare i propri gists'
flash.gist.forked: 'Il gist è stato forkato'
flash.user.email-updated: 'Email aggiornata'
flash.user.invalid-ssh-key: 'Chiave SSH non valida'
flash.user.ssh-key-added: 'Chiave SSH aggiunta'
flash.user.ssh-key-deleted: 'Chiave SSH eliminata'
flash.user.password-updated: 'Password aggiornata'
flash.user.username-updated: 'Nome utente aggiornato'
validation.is-too-long: 'Il campo %s è troppo lungo'
validation.should-not-be-empty: 'Il campo %s non può essere vuoto'
validation.should-not-include-sub-directory: 'Il campo %s non può contenere una sottocartella'
validation.should-only-contain-alphanumeric-characters: 'Il campo %s deve contenere solo caratteri alfanumerici'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Il campo %s può contenere solo caratteri alfanumerici e trattini'
validation.not-enough: 'Non abbastanza %s'
validation.invalid: '%s non valido'
html.title.admin-panel: 'Pannello amministratore'
settings.ssh-key-exists: Questa chiave SSH esiste già

View File

@@ -0,0 +1,307 @@
gist.public: 'Publiczny'
gist.unlisted: 'Niepubliczny'
gist.private: 'Prywatny'
gist.header.like: 'Polub'
gist.header.unlike: 'Cofnij polubienie'
gist.header.fork: 'Zforkuj'
gist.header.edit: 'Edytuj'
gist.header.delete: 'Usuń'
gist.header.forked-from: 'Zforkowane z'
gist.header.last-active: 'Ostatnio aktywny'
gist.header.select-tab: 'Wybierz kartę'
gist.header.code: 'Kod'
gist.header.revisions: 'Rewizje'
gist.header.revision: 'Rewizja'
gist.header.clone-http: 'Sklonuj za pomocą %s'
gist.header.clone-http-help: 'Sklonuj za pomocą Git używając podstawowej autoryzacji HTTP.'
gist.header.clone-ssh: 'Sklonuj za pomocą SSH'
gist.header.clone-ssh-help: 'Sklonuj za pomocą Git używając klucza SSH.'
gist.header.embed: 'Osadź'
gist.header.embed-help: 'Wstaw ten Gist na twoją stronę.'
gist.header.download-zip: 'Pobierz ZIP'
gist.raw: 'Surowy'
gist.file-truncated: 'Ten plik został przycięty.'
gist.watch-full-file: 'Zobacz pełny plik.'
gist.file-not-valid: 'Ten plik nie jest poprawnym plikiem CSV.'
gist.no-content: 'Nie znaleziono plików'
gist.new.new_gist: 'Nowy Gist'
gist.new.title: 'Tytuł'
gist.new.description: 'Opis'
gist.new.url: 'URL'
gist.new.filename-with-extension: 'Nazwa pliku z rozszerzeniem'
gist.new.indent-mode: 'Tryb wcięcia'
gist.new.indent-mode-space: 'Spacje'
gist.new.indent-mode-tab: 'Tabulatory'
gist.new.indent-size: 'Wielkość wcięcia'
gist.new.wrap-mode: 'Tryb zawijania'
gist.new.wrap-mode-no: 'Bez zawijania'
gist.new.wrap-mode-soft: 'Miękkie zawijanie'
gist.new.add-file: 'Dodaj plik'
gist.new.create-public-button: 'Stwórz publiczny Gist'
gist.new.create-unlisted-button: 'Stwórz niepubliczny Gist'
gist.new.create-private-button: 'Stwórz prywatny Gist'
gist.new.preview: 'Podgląd'
gist.new.create-a-new-gist: 'Stwórz nowy Gist'
gist.edit.editing: 'Edytowanie'
gist.edit.edit-gist: 'Edytuj %s'
gist.edit.change-visibility: 'Zmień widoczność na'
gist.edit.delete: 'Usuń'
gist.edit.cancel: 'Anuluj'
gist.edit.save: 'Zapisz'
gist.delete.confirm: 'Czy na pewno chcesz usunąć ten Gist?'
gist.list.joined: 'Dołączono'
gist.list.all: 'Wszystkie Gisty'
gist.list.search-results: 'Wyniki wyszukiwania'
gist.list.sort: 'Sortuj'
gist.list.sort-by-created: 'utworzono'
gist.list.sort-by-updated: 'zaktualizowano'
gist.list.order-by-asc: 'Najdawniej'
gist.list.order-by-desc: 'Ostatnio'
gist.list.select-tab: 'Wybierz kartę'
gist.list.liked: 'Polubiane'
gist.list.likes: 'polubień'
gist.list.forked: 'Zforkowane'
gist.list.forked-from: 'Zforkowane z'
gist.list.forks: 'forków'
gist.list.files: 'plików'
gist.list.last-active: 'Ostatnio aktywne'
gist.list.no-gists: 'Brak Gistów'
gist.list.all-liked-by: 'Wszystkie Gisty polubione przez %s'
gist.list.all-forked-by: 'Wszystkie Gisty zforkowane przez %s'
gist.list.all-from: 'Wszystkie Gisty od %s'
gist.search.found: 'Gistów znaleziono'
gist.search.no-results: 'Nie znaleziono żadnych Gistów'
gist.search.help.user: 'Gisty stworzone przez użytkownika'
gist.search.help.title: 'Gisty z podanym tytułem'
gist.search.help.filename: 'Gisty zawierające pliki z podanym tytułem'
gist.search.help.extension: 'Gisty zawierające pliki z podanym rozszerzeniem'
gist.search.help.language: 'Gisty zawierające pliki z podanym językiem'
gist.forks: 'Forki'
gist.forks.view: 'Zobacz forka'
gist.forks.no: 'Brak publicznych forków'
gist.forks.for: 'Forki dla %s'
gist.likes: 'Polubienia'
gist.likes.no: 'Brak polubień'
gist.likes.for: 'Polubienia dla %s'
gist.revisions: 'Rewizje'
gist.revision.revised: 'zrewidował ten Gist'
gist.revision.go-to-revision: 'Przejdź do rewizji'
gist.revision.file-created: 'stworzono plik'
gist.revision.file-deleted: 'usunięto plik'
gist.revision.file-renamed: 'zmieniono nazwę na'
gist.revision.diff-truncated: 'Porównanie jest za duże do pokazania'
gist.revision.file-renamed-no-changes: 'Zmieniono nazwę pliku bez modyfikacji zawartości'
gist.revision.empty-file: 'Pusty plik'
gist.revision.no-changes: 'Brak zmian'
gist.revision.no-revisions: 'Brak rewizji do pokazania'
gist.revision-of: 'Rewizja %s'
settings: 'Ustawienia'
settings.email: 'Email'
settings.email-help: 'Używany do commitów i Gravatar'
settings.email-set: 'Ustaw email'
settings.link-accounts: 'Połącz konta'
settings.link-github-account: 'Połącz konto GitHub'
settings.link-gitlab-account: 'Połącz konto GitLab'
settings.link-gitea-account: 'Połącz konto Gitea'
settings.unlink-github-account: 'Odłącz konto GitHub'
settings.unlink-gitlab-account: 'Odłącz konto GitLab'
settings.unlink-gitea-account: 'Odłącz konto Gitea'
settings.delete-account: 'Usuń konto'
settings.delete-account-confirm: 'Czy na pewno chcesz usunąć swoje konto?'
settings.add-ssh-key: 'Dodaj klucz SSH'
settings.add-ssh-key-help: 'Używany tylko do operacji pull/push za pomocą Git przez SSH'
settings.add-ssh-key-title: 'Tytuł'
settings.add-ssh-key-content: 'Klucz'
settings.delete-ssh-key: 'Usuń'
settings.delete-ssh-key-confirm: 'Potwierdź usunięcie klucza SSH'
settings.ssh-key-added-at: 'Dodany'
settings.ssh-key-never-used: 'Nigdy nie użyty'
settings.ssh-key-last-used: 'Ostatnio użyty'
settings.ssh-key-exists: 'Klucz SSH już istnieje'
settings.change-username: 'Zmień nazwę użytkownika'
settings.create-password: 'Stwórz hasło'
settings.create-password-help: 'Stwórz swoje hasło do logowania się do Opengist przez HTTP'
settings.change-password: 'Zmień hasło'
settings.change-password-help: 'Zmień swoje hasło do logowania się do Opengist przez HTTP'
settings.password-label-title: 'Hasło'
auth.signup-disabled: 'Rejestracja została wyłączona przez administratora'
auth.login: 'Zaloguj się'
auth.signup: 'Zarejestruj się'
auth.new-account: 'Nowe konto'
auth.username: 'Nazwa użytkownika'
auth.password: 'Hasło'
auth.register-instead: 'Lub zarejestruj się'
auth.login-instead: 'Lub zaloguj się'
auth.oauth: 'Kontynuuj z kontem %s'
auth.mfa: 'Weryfikacja wieloskładnikowa'
auth.mfa.passkey: 'Klucz Passkey'
auth.mfa.passkeys: 'Klucze Passkey'
auth.mfa.use-passkey: 'Użyj klucza Passkey'
auth.mfa.bind-passkey: 'Powiąż klucz Passkey'
auth.mfa.login-with-passkey: 'Zaloguj się za pomocą klucza Passkey'
auth.mfa.waiting-for-passkey-input: 'Oczekiwanie na wejście z interakcji przeglądarki...'
auth.mfa.use-passkey-to-finish: 'Użyj klucza Passkey aby dokończyć logowanie'
auth.mfa.passkeys-help: 'Dodaj klucz Passkey aby logować się nim do swojego konta i aby używać go jako weryfikacji wieloskładnikowej.'
auth.mfa.passkey-name: 'Nazwa'
auth.mfa.delete-passkey: 'Usuń'
auth.mfa.passkey-added-at: 'Dodany'
auth.mfa.passkey-never-used: 'Nigdy nie użyty'
auth.mfa.passkey-last-used: 'Ostatnio użyty'
auth.mfa.delete-passkey-confirm: 'Potwierdź usunięcie klucza Passkey'
auth.totp: 'Time based one-time password (TOTP)'
auth.totp.help: 'TOTP to metoda weryfikacji dwuskładnikowej, która używa współdzielonego sekretu do generowania hasła jednorazowego użytku.'
auth.totp.use: 'Użyj TOTP'
auth.totp.regenerate-recovery-codes: 'Wygeneruje ponownie kody odzyskiwania'
auth.totp.already-enabled: 'TOTP jest już włączone'
auth.totp.invalid-secret: 'Niepoprawny sekret TOTP'
auth.totp.invalid-code: 'Niepoprawny kod TOTP'
auth.totp.code-used: 'Kod odzyskiwania %s został użyty, jest teraz nieważny. Możesz chcieć wyłączyć weryfikację wieloskładnikową na teraz lub wygenerować swoje kody ponownie.'
auth.totp.disabled: 'TOTP zostało pomyślnie wyłączone'
auth.totp.disable: 'Wyłącz TOTP'
auth.totp.enter-code: 'Wpisz kod z aplikacji uwierzytelniajacej'
auth.totp.enter-recovery-key: 'lub kod odzyskiwania jeśli zgubiłeś swoje urządzenie'
auth.totp.code: 'Kod'
auth.totp.submit: 'Prześlij'
auth.totp.proceed: 'Dalej'
auth.totp.save-recovery-codes: 'Zapis swoje kody odzyskiwania w bezpiecznym miejscu. Możesz użyć tych kodów, aby odzyskać dostęp do swojego konta jeśli stracisz dostęp do swojej aplikacji uwierzytelniajacej.'
auth.totp.scan-qr-code: 'Zeskanuj kod QR poniżej używając swojej aplikacji uwierzytelniajacej, aby włączyć weryfikację dwuskładnikową lub wpisz następujący ciąg i potwierdź go wygenerowanym kodem.'
error: 'Błąd'
error.page-not-found: 'Nie znaleziono strony'
error.bad-request: 'Złe żądanie'
error.signup-disabled: 'Rejestracja jest wyłączona'
error.signup-disabled-form: 'Rejestracja za pomocą formularza rejestracyjnego jest wyłączona'
error.login-disabled-form: 'Logowanie za pomocą formularza logowania jest wyłączone'
error.complete-oauth-login: "Nie można ukończyć logowania: %s"
error.oauth-unsupported: 'Niewspierany dostawca'
error.cannot-bind-data: 'Nie można powiązać danych'
error.invalid-number: 'Niepoprawna liczba'
error.invalid-character-unescaped: 'Nieprawidłowy niechroniony znak'
error.not-in-mfa-session: 'Użytkownik nie jest w sesji uwierzytelnienia wieloskładnikowego'
header.menu.all: 'Wszystko'
header.menu.new: 'Nowy'
header.menu.search: 'Szukaj'
header.menu.my-gists: 'Moje Gisty'
header.menu.liked: 'Polubione'
header.menu.admin: 'Admin'
header.menu.settings: 'Ustawienia'
header.menu.logout: 'Wyloguj się'
header.menu.register: 'Zarejestruj się'
header.menu.login: 'Zaloguj się'
header.menu.light: 'Jasny'
header.menu.dark: 'Ciemny'
header.menu.system: 'Systemowy'
footer.powered-by: 'Obsługiwane przez %s'
pagination.older: 'Starsze'
pagination.newer: 'Nowsze'
pagination.previous: 'Poprzedni'
pagination.next: 'Nastepny'
admin.admin_panel: 'Panel administracyjny'
admin.general: 'Ogólne'
admin.users: 'Użytkownicy'
admin.gists: 'Gisty'
admin.configuration: 'Konfiguracja'
admin.invitations: 'Zaproszenia'
admin.invitations.create: 'Stwórz zaproszenie'
admin.versions: 'Wersje'
admin.ssh_keys: 'Klucze SSH'
admin.stats: 'Statystyki'
admin.actions: 'Akcje'
admin.actions.sync-fs: 'Synchronizuj Gisty z systemu plików'
admin.actions.sync-db: 'Synchronizuj Gisty z bazy danych'
admin.actions.git-gc: 'Zbierz śmieci we wszystkich repozytoriach Git'
admin.actions.sync-previews: 'Synchronizuj podglądy wszystkich Gistów'
admin.actions.reset-hooks: 'Zresetuj hooki serwera Git dla wszystkich repozytoriów'
admin.actions.index-gists: 'Indeksuj wszystkie Gisty'
admin.id: 'ID'
admin.user: 'Użytkownik'
admin.delete: 'Usuń'
admin.created_at: 'Utworzono'
admin.config-link: 'Ta konfiguracja może zostać %s przez plik konfiguracyjny YAML i/lub zmienne środowiskowe.'
admin.config-link-overriden: 'nadpisana'
admin.disable-signup: 'Wyłącz rejestrację'
admin.disable-signup_help: 'Zabroń tworzenia nowych kont.'
admin.require-login: 'Wymagaj logowania'
admin.require-login_help: 'Wymagaj od użytkowników zalogowania się, aby mogli zobaczyć Gisty.'
admin.allow-gists-without-login: 'Zezwól na indywidualne Gisty bez logowania'
admin.allow-gists-without-login_help: 'Zezwalaj na przeglądanie i pobieranie pojedynczych Gistów bez logowania, ale wymagaj zalogowania się w celu odkrywania Gistów.'
admin.disable-login: 'Wyłącz formularz logowania'
admin.disable-login_help: 'Zabroń logowania się za pomocą formularza logowania, aby wymusić korzystanie z dostawców OAuth.'
admin.disable-gravatar: 'Wyłącz Gravatar'
admin.disable-gravatar_help: 'Wyłącz używanie Gravatar jako dostawcy awatarów.'
admin.users.delete_confirm: 'Czy chcesz usunąć tego użytkownika?'
admin.gists.title: 'Tytuł'
admin.gists.private: 'Prywatny?'
admin.gists.nb-files: 'Liczba plików'
admin.gists.nb-likes: 'Liczba polubień'
admin.gists.delete_confirm: 'Czy chcesz usunąć tego Gista?'
admin.invitations.help: 'Zaproszenia mogą być używane, aby stworzyć konto nawet, jeśli rejestracja jest wyłączona.'
admin.invitations.max_uses: 'Maksymalna liczba użyć'
admin.invitations.expires_at: 'Wygasa'
admin.invitations.code: 'Kod'
admin.invitations.copy_link: 'Kopiuj link'
admin.invitations.uses: 'Użyć'
admin.invitations.expired: 'Wygasło'
flash.admin.user-deleted: 'Użytkownik został usunięty'
flash.admin.gist-deleted: 'Gist został usunięty'
flash.admin.invitation-created: 'Zaproszenie zostało stworzone'
flash.admin.invitation-deleted: 'Zaproszenie zostało usunięte'
flash.admin.sync-fs: 'Synchronizowanie repozytoriów z systemu plików...'
flash.admin.sync-db: 'Synchronizowanie repozytoriów z bazy danych...'
flash.admin.git-gc: 'Zbieranie śmieci w repozytoriach...'
flash.admin.sync-previews: 'Synchronizowanie podglądów Gistów...'
flash.admin.reset-hooks: 'Resetowanie hooków serwera Git dla wszystkich repozytoriów...'
flash.admin.index-gists: 'Indeksowanie wszystkich Gistów...'
flash.auth.username-exists: 'Nazwa użytkownika już istnieje'
flash.auth.invalid-credentials: 'Niepoprawne dane logowania'
flash.auth.account-linked-oauth: 'Konto połączone z %s'
flash.auth.account-unlinked-oauth: 'Konto odłączone od %s'
flash.auth.user-sshkeys-not-retrievable: 'Nie można uzyskać kluczy użytkownika'
flash.auth.user-sshkeys-not-created: 'Nie można stworzyć klucza SSH'
flash.auth.must-be-logged-in: 'Musisz być zalogowany, aby widzieć Gisty'
flash.auth.passkey-registred: 'Zarejestrowano klucz Passkey %s'
flash.auth.passkey-deleted: 'Usunięto klucz Passkey'
flash.gist.visibility-changed: 'Widoczność Gista została zmieniona'
flash.gist.deleted: 'Gist został usunięty'
flash.gist.fork-own-gist: 'Nie można forkować własnych Gistów'
flash.gist.forked: 'Gist został zforkowany'
flash.user.email-updated: 'Email został zaktualizowany'
flash.user.invalid-ssh-key: 'niepoprawny klucz SSH'
flash.user.ssh-key-added: 'Dodano klucz SSH'
flash.user.ssh-key-deleted: 'Usunięto klucz SSH'
flash.user.password-updated: 'Zaktualizowano hasło'
flash.user.username-updated: 'Zaktualizowano nazwę użytkownika'
validation.is-too-long: 'Pole %s jest za długie'
validation.should-not-be-empty: 'Pole %s nie może być puste'
validation.should-not-include-sub-directory: 'Pole %s nie może zawierać podfolderu'
validation.should-only-contain-alphanumeric-characters: 'Pole %s może tylko zawierać znaki alfanumeryczne'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Pole %s może tylko zawierać znaki alfanumeryczne i myślniki'
validation.not-enough: 'Nie wystarczająco %s'
validation.invalid: 'Niepoprawny %s'
html.title.admin-panel: 'Panel administracyjny'
admin.invitations.delete_confirm: Czy chcesz usunąć to zaproszenie?

View File

@@ -17,8 +17,6 @@ gist.header.clone-http: Clonar via %s
gist.header.clone-http-help: Clonar com Git usando autenticação básica HTTP. gist.header.clone-http-help: Clonar com Git usando autenticação básica HTTP.
gist.header.clone-ssh: Clonar via SSH gist.header.clone-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar com Git usando uma chave SSH. gist.header.clone-ssh-help: Clonar com Git usando uma chave SSH.
gist.header.share: Compartilhar
gist.header.share-help: Copiar link para compartilhar este gist.
gist.header.download-zip: Baixar ZIP gist.header.download-zip: Baixar ZIP
gist.raw: Bruto gist.raw: Bruto
@@ -115,8 +113,7 @@ auth.username: Nome de usuário
auth.password: Senha auth.password: Senha
auth.register-instead: Registrar-se no lugar auth.register-instead: Registrar-se no lugar
auth.login-instead: Entrar no lugar auth.login-instead: Entrar no lugar
auth.github-oauth: Continuar com conta do GitHub auth.oauth: Continuar com conta do %s
auth.gitea-oauth: Continuar com conta do Gitea
error: Erro error: Erro
@@ -158,7 +155,6 @@ admin.delete: Excluir
admin.created_at: Criado admin.created_at: Criado
admin.config-link: Esta configuração pode ser %s por um arquivo de configuração YAML e/ou variáveis de ambiente. admin.config-link: Esta configuração pode ser %s por um arquivo de configuração YAML e/ou variáveis de ambiente.
admin.config-link-overridden: sobrescrito
admin.disable-signup: Desabilitar registro admin.disable-signup: Desabilitar registro
admin.disable-signup_help: Proibir a criação de novas contas. admin.disable-signup_help: Proibir a criação de novas contas.
admin.require-login: Exigir login admin.require-login: Exigir login
@@ -167,7 +163,8 @@ admin.disable-login: Desabilitar formulário de login
admin.disable-login_help: Proibir o login através do formulário de login para forçar o uso de provedores de OAuth no lugar. admin.disable-login_help: Proibir o login através do formulário de login para forçar o uso de provedores de OAuth no lugar.
admin.disable-gravatar: Desabilitar Gravatar admin.disable-gravatar: Desabilitar Gravatar
admin.disable-gravatar_help: Desabilitar o uso do Gravatar como provedor de avatar. admin.disable-gravatar_help: Desabilitar o uso do Gravatar como provedor de avatar.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Quer excluir este usuário? admin.users.delete_confirm: Quer excluir este usuário?
admin.gists.title: Título admin.gists.title: Título
@@ -175,3 +172,88 @@ admin.gists.private: Privado
admin.gists.nb-files: Núm. de arquivos admin.gists.nb-files: Núm. de arquivos
admin.gists.nb-likes: Núm. de curtidas admin.gists.nb-likes: Núm. de curtidas
admin.gists.delete_confirm: Quer excluir este gist? admin.gists.delete_confirm: Quer excluir este gist?
flash.admin.index-gists: ''
gist.header.embed: ''
gist.header.embed-help: ''
gist.new.url: ''
gist.list.all-liked-by: ''
gist.new.preview: ''
gist.new.create-a-new-gist: ''
gist.edit.edit-gist: ''
gist.list.all-forked-by: ''
gist.list.all-from: ''
gist.search.found: ''
gist.search.no-results: ''
gist.search.help.user: ''
gist.search.help.title: ''
gist.search.help.filename: ''
gist.search.help.extension: ''
gist.search.help.language: ''
gist.forks.for: ''
gist.likes.for: ''
gist.revision-of: ''
settings.link-gitlab-account: ''
settings.unlink-gitlab-account: ''
settings.change-username: ''
settings.create-password: ''
settings.create-password-help: ''
settings.change-password: ''
settings.change-password-help: ''
settings.password-label-title: ''
error.page-not-found: ''
error.bad-request: ''
error.signup-disabled: ''
error.signup-disabled-form: ''
error.login-disabled-form: ''
error.complete-oauth-login: ''
error.oauth-unsupported: ''
error.cannot-bind-data: ''
error.invalid-number: ''
error.invalid-character-unescaped: ''
admin.invitations: ''
admin.invitations.create: ''
admin.actions.sync-previews: ''
admin.actions.reset-hooks: ''
admin.actions.index-gists: ''
admin.config-link-overriden: ''
validation.invalid: ''
admin.invitations.help: ''
admin.invitations.max_uses: ''
admin.invitations.expires_at: ''
admin.invitations.code: ''
admin.invitations.copy_link: ''
admin.invitations.uses: ''
admin.invitations.expired: ''
flash.admin.user-deleted: ''
flash.admin.gist-deleted: ''
flash.admin.invitation-created: ''
flash.admin.invitation-deleted: ''
flash.admin.sync-fs: ''
flash.admin.sync-db: ''
flash.admin.git-gc: ''
flash.admin.sync-previews: ''
flash.admin.reset-hooks: ''
flash.auth.username-exists: ''
flash.auth.invalid-credentials: ''
flash.auth.account-linked-oauth: ''
flash.auth.account-unlinked-oauth: ''
flash.auth.user-sshkeys-not-retrievable: ''
flash.auth.user-sshkeys-not-created: ''
flash.auth.must-be-logged-in: ''
flash.gist.visibility-changed: ''
flash.gist.deleted: ''
flash.gist.fork-own-gist: ''
flash.gist.forked: ''
flash.user.email-updated: ''
flash.user.invalid-ssh-key: ''
flash.user.ssh-key-added: ''
flash.user.ssh-key-deleted: ''
flash.user.password-updated: ''
flash.user.username-updated: ''
validation.is-too-long: ''
validation.should-not-be-empty: ''
validation.should-not-include-sub-directory: ''
validation.should-only-contain-alphanumeric-characters: ''
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
validation.not-enough: ''
html.title.admin-panel: ''

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Клонировать с помощью %s
gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP. gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP.
gist.header.clone-ssh: Клонировать c помощью SSH gist.header.clone-ssh: Клонировать c помощью SSH
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH. gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
gist.header.embed: gist.header.embed: 'Встроить'
gist.header.embed-help: gist.header.embed-help: 'Встроить этот фрагмент в ваш веб-сайт.'
gist.header.download-zip: Скачать ZIP-архив gist.header.download-zip: Скачать ZIP-архив
gist.raw: Исходник gist.raw: Исходник
@@ -115,8 +115,7 @@ auth.username: Имя пользователя
auth.password: Пароль auth.password: Пароль
auth.register-instead: Зарегистрироваться auth.register-instead: Зарегистрироваться
auth.login-instead: Войти auth.login-instead: Войти
auth.github-oauth: Войти с помощью доступа GitHub auth.oauth: Войти с помощью доступа %s
auth.gitea-oauth: Войти с помощью доступа Gitea
error: Ошибка error: Ошибка
@@ -167,7 +166,8 @@ admin.disable-login: Запретить авторизацию по паролю
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub. admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
admin.disable-gravatar: Запретить Gravatar admin.disable-gravatar: Запретить Gravatar
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля. admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя? admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
admin.gists.title: Название admin.gists.title: Название
@@ -175,3 +175,85 @@ admin.gists.private: Приватный
admin.gists.nb-files: Файлов admin.gists.nb-files: Файлов
admin.gists.nb-likes: Понравилось admin.gists.nb-likes: Понравилось
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент? admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
gist.new.url: 'URL'
gist.new.preview: 'Предпросмотр'
gist.new.create-a-new-gist: 'Создать новый фрагмент'
gist.edit.edit-gist: 'Редактировать %s'
gist.list.all-liked-by: 'Все фрагменты, понравившиеся %s'
gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s'
gist.list.all-from: 'Все фрагменты от %s'
gist.search.found: 'фрагментов найдено'
gist.search.no-results: 'Не найден ни один фрагмент'
gist.search.help.user: 'фрагментов создано пользователем'
gist.search.help.title: ''
gist.search.help.filename: ''
gist.search.help.extension: ''
gist.search.help.language: ''
gist.forks.for: ''
gist.likes.for: ''
gist.revision-of: ''
settings.link-gitlab-account: ''
settings.unlink-gitlab-account: ''
settings.change-username: ''
settings.create-password: ''
settings.create-password-help: ''
settings.change-password: ''
settings.change-password-help: ''
settings.password-label-title: ''
error.page-not-found: ''
error.bad-request: ''
error.signup-disabled: ''
error.signup-disabled-form: ''
error.login-disabled-form: ''
error.complete-oauth-login: ''
error.oauth-unsupported: ''
error.cannot-bind-data: ''
error.invalid-number: ''
error.invalid-character-unescaped: ''
admin.invitations: ''
admin.invitations.create: ''
admin.actions.sync-previews: ''
admin.actions.reset-hooks: ''
admin.actions.index-gists: ''
validation.should-not-be-empty: ''
admin.invitations.help: ''
admin.invitations.max_uses: ''
admin.invitations.expires_at: ''
admin.invitations.code: ''
admin.invitations.copy_link: ''
admin.invitations.uses: ''
admin.invitations.expired: ''
flash.admin.user-deleted: ''
flash.admin.gist-deleted: ''
flash.admin.invitation-created: ''
flash.admin.invitation-deleted: ''
flash.admin.sync-fs: ''
flash.admin.sync-db: ''
flash.admin.git-gc: ''
flash.admin.sync-previews: ''
flash.admin.reset-hooks: ''
flash.admin.index-gists: ''
flash.auth.username-exists: ''
flash.auth.invalid-credentials: ''
flash.auth.account-linked-oauth: ''
flash.auth.account-unlinked-oauth: ''
flash.auth.user-sshkeys-not-retrievable: ''
flash.auth.user-sshkeys-not-created: ''
flash.auth.must-be-logged-in: ''
flash.gist.visibility-changed: ''
flash.gist.deleted: ''
flash.gist.fork-own-gist: ''
flash.gist.forked: ''
flash.user.email-updated: ''
flash.user.invalid-ssh-key: ''
flash.user.ssh-key-added: ''
flash.user.ssh-key-deleted: ''
flash.user.password-updated: ''
flash.user.username-updated: ''
validation.is-too-long: ''
validation.should-not-include-sub-directory: ''
validation.should-only-contain-alphanumeric-characters: ''
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
validation.not-enough: ''
validation.invalid: ''
html.title.admin-panel: ''

Some files were not shown because too many files have changed in this diff Show More