Compare commits

..

140 Commits

Author SHA1 Message Date
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
Thomas Miceli
a6c4183aac v1.6.1 2024-01-06 14:36:03 +01:00
Thomas Miceli
7fc8577ce0 Translated using Weblate (French) (#201)
Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/
2024-01-06 14:35:08 +01:00
Thomas Miceli
a1524af7a9 Fix directory renaming on username change (#205)
* src/dest dirs have to be lowercase
* if the src dir doesn't exist, don't rename
2024-01-06 14:35:08 +01:00
Thomas Miceli
10cf7e6e25 Add Healthcheck on Docker (#204) 2024-01-06 14:35:08 +01:00
Thomas Miceli
7ce94eea59 Ignore .yml files for Github Actions 2024-01-05 04:36:05 +01:00
Thomas Miceli
8eb8f4e231 v1.6.0 2024-01-04 18:06:19 +01:00
Thomas Miceli
af19268d6f Add some docs (#198) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4215d7e43b Update dependencies (#197)
Go 1.20 -> 1.21
JS package-lock
Nodejs Docker 18 -> 20
Alpine Docker 3.17 -> 3.19
2024-01-04 18:06:19 +01:00
Thomas Miceli
d85917bfb2 Small fixes (#196) 2024-01-04 18:06:19 +01:00
Chiawei Chen
7c1d6e8bfd chore: update taiwan translation (#195) 2024-01-04 18:06:19 +01:00
Matheus C. França
3a2fd2374a Add pt-BR translation (#193)
new translation
2024-01-04 18:06:19 +01:00
Thomas Miceli
87a6113cc7 Add Gist code search (#194) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4cb7dc2d30 Fix reverse proxy subpath support (#192) 2024-01-04 18:06:19 +01:00
Thomas Miceli
f52310a841 Add 2 new admin actions (#191)
* Synchronize all gists previews
* Reset Git server hooks for all repositories
2024-01-04 18:06:19 +01:00
Thomas Miceli
97707f7cca Change username setting (#190) 2024-01-04 18:06:19 +01:00
Thomas Miceli
5058ca8f27 Optimize multiple file rendering (#189) 2024-01-04 18:06:19 +01:00
Thomas Miceli
b3a856a05e Optimize reading gist files content (#186) 2024-01-04 18:06:19 +01:00
WilliamNT
f557bd45df Updated the hungarian translation file (#185) 2024-01-04 18:06:19 +01:00
Jacob Hands
2f8435892e Add config for default branch name (#171)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Jacob Hands
4bba26daf6 Add log output config option (#172)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Thomas Miceli
3c97901995 Bug fixes (#184)
* Fix gist content when going back to editing

* Fix not outputting non-truncated large files for editon/zip download

* Allow dashes in usernames

* Delete keys associated to deleted user

* Fix error message when there is no files in gist

* Show if there is not files in gist preview

* Fix log parsing for the 11th empty commit
2024-01-04 18:06:19 +01:00
Thomas Miceli
3828022a1c Add custom urls for gists (#183) 2024-01-04 18:06:19 +01:00
Thomas Miceli
85e2da054b Add clickable Markdown checkboxes (#182) 2024-01-04 18:06:19 +01:00
Thomas Miceli
0753c5cb54 Add embedded gists & JSON gist data/metadata (#179) 2024-01-04 18:06:19 +01:00
Thomas Miceli
845e28dd59 Move code rendering to the backend & frontend improvements (#176)
Added Chroma & Goldmark

Added Mermaidjs

More languages supported

Add default values for gist links input

Added copy code from markdown blocks
2024-01-04 18:06:19 +01:00
Chiawei Chen
eff88711ea Trivial Typo: Change 'Gitlab' to 'GitLab' (#177) 2024-01-04 18:06:19 +01:00
Thomas Miceli
8466e50cc3 Add GitLab OAuth provider (#174) 2024-01-04 18:06:19 +01:00
Thomas Miceli
c9fd58c904 Update JS dependencies versions (#175) 2024-01-04 18:06:19 +01:00
Thomas Miceli
47869a77c9 Add healthcheck endpoint (#170) 2024-01-04 18:06:19 +01:00
John Olheiser
246f12c8cb feat: default visibility (#155)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2024-01-04 18:06:19 +01:00
Chiawei Chen
943212e492 feat: add traditional chinese translation (#166) 2024-01-04 18:06:19 +01:00
Pavel Vácha
7a6fb98223 Add Czech translation (#164) 2024-01-04 18:06:19 +01:00
176 changed files with 14975 additions and 7020 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: thomiceli

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,50 +4,105 @@ on:
branches:
- master
- 'dev-*'
workflow_dispatch:
pull_request:
paths-ignore:
- '**.yml'
- '**.md'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.23"
- name: Lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.54
skip-pkg-cache: true
args: --out-format=colored-line-number --timeout=20m
version: v1.60
args: --out-format=colored-line-number --timeout=20m
- name: Format
run: make fmt check_changes
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.23"
- name: Check
- name: Check Go modules
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:
name: Test
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.20", "1.21"]
go: ["1.23"]
database: ["sqlite"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -59,5 +114,25 @@ jobs:
go-version: ${{ matrix.go }}
- 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
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.23"
- name: Cross compile build
run: make all_crosscompile

2
.gitignore vendored
View File

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

View File

@@ -1,5 +1,245 @@
# Changelog
## [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
See here how to [update](/docs/update.md) Opengist.
### Added
- Healthcheck on Docker container (#204)
- Translations:
- fr-FR (#201)
### Fixed
- Directory renaming on username change (#205)
## [1.6.0](https://github.com/thomiceli/opengist/compare/v1.5.3...v1.6.0) - 2024-01-04
See here how to [update](/docs/update.md) Opengist.
### Added
- Embedded gists (#179)
- Gist code search (#194)
- Custom URLS for gists (#183)
- Gist JSON data/metadata (#179)
- Keep default visibility when creating a gist on the UI (#155)
- Health check endpoint (#170)
- GitLab OAuth2 login (#174)
- Syntax highlighting for more file types (#176)
- Checkable Markdown checkboxes (#182)
- Config:
- Log output (#172)
- Default git branch name (#171)
- Change username setting (#190)
- Admin actions:
- Synchronize all gists previews (#191)
- Reset Git server hooks for all repositories (#191)
- Index all gists (#194)
- Translations:
- cs-CZ (#164)
- zh-TW (#166, #195)
- hu-HU (#185)
- pt-BR (#193)
- Docs (#198)
### Changed
- Updated dependencies (#197):
- Go `1.20` -> `1.21`
- JavaScript packages
- NodeJS Docker image `18` -> `20`
- Alpine Docker image `3.17` -> `3.19`
### Fixed
- Fix reverse proxy subpath support (#192)
- Fix undecoded gist content when going back to editing in the UI (#184)
- Fix outputting non-truncated large files for editon/zip download (#184)
- Allow dashes in usernames (#184)
- Delete SSH keys associated to deleted user (#184)
- Better error message when there is no files in gist (#184)
- Show if there is no files in gist preview (#184)
- Log parsing for the 11th empty commit (#184)
- Optimize reading gist files content (#186)
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
### Added
- es-ES translation (#139)

View File

@@ -1,16 +1,25 @@
FROM alpine:3.17 AS build
FROM alpine:3.19 AS base
RUN apk update && \
apk add --no-cache \
make \
gcc \
musl-dev \
libstdc++
apk add --no-cache \
make \
shadow \
openssl \
openssh \
curl \
wget \
git \
gnupg \
xz \
gcc \
musl-dev \
libstdc++
COPY --from=golang:1.20-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 CGO_ENABLED=0
COPY --from=node:18-alpine /usr/local/ /usr/local/
COPY --from=node:20-alpine /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules"
ENV PATH="/usr/local/bin:${PATH}"
@@ -18,10 +27,21 @@ WORKDIR /opengist
COPY . .
FROM base AS dev
EXPOSE 6157 2222 16157
VOLUME /opengist
CMD ["make", "watch"]
FROM base AS build
RUN make
FROM alpine:3.17 as run
FROM alpine:3.19 as prod
RUN apk update && \
apk add --no-cache \
@@ -38,7 +58,7 @@ RUN apk update && \
libstdc++
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
@@ -49,4 +69,5 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222
VOLUME /opengist
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
ENTRYPOINT ["./docker/entrypoint.sh"]

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
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
@@ -15,11 +18,12 @@ install:
build_frontend:
@echo "Building frontend assets..."
npx vite build
npx vite -c public/vite.config.js build
@EMBED=1 npx postcss 'public/assets/embed-*.css' -c public/postcss.config.js --replace # until we can .nest { @tailwind } in Sass
build_backend:
@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
@@ -30,16 +34,23 @@ build_docker:
@echo "Building Docker image..."
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:
@echo "Building frontend assets..."
npx vite dev --port 16157
npx vite -c public/vite.config.js dev --port 16157 --host
watch_backend:
@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:
@bash ./scripts/watch.sh
@sh ./scripts/watch.sh
clean:
@echo "Cleaning up build artifacts..."
@@ -62,4 +73,7 @@ fmt:
@go fmt ./...
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,33 +1,34 @@
# 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.
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)
![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 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
* Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* 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
* Search for snippets ; browse users snippets, likes and forks
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, Gitea, and OpenID Connect
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users
* Docker support
* [More...](/docs/index.md#features)
* [More...](/docs/introduction.md#features)
## Quick start
@@ -36,7 +37,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 :
```shell
docker pull ghcr.io/thomiceli/opengist:1
docker pull ghcr.io/thomiceli/opengist:1.8
```
It can be used in a `docker-compose.yml` file :
@@ -46,11 +47,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
```yml
version: "3"
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
image: ghcr.io/thomiceli/opengist:1.8
container_name: opengist
restart: unless-stopped
ports:
@@ -77,9 +76,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.5.2/opengist1.5.2-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.8.4/opengist1.8.4-linux-amd64.tar.gz
tar xzvf opengist1.5.2-linux-amd64.tar.gz
tar xzvf opengist1.8.4-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
@@ -89,7 +88,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
### From source
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.20+), [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
git clone https://github.com/thomiceli/opengist
@@ -100,10 +99,13 @@ make
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
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

View File

@@ -1,25 +1,43 @@
# 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
# 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
# Public URL for the Git HTTP/SSH connection.
# If not set, uses the URL from the request
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file
# Public URL to access to Opengist
external-url:
# Directory where Opengist will store its data. Default: ~/.opengist/
opengist-home:
# Name of the SQLite database file. Default: opengist.db
db-filename: opengist.db
# Secret key used for session store & encrypt MFA data on database. Default: <randomized 32 bytes>
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
index.enabled: true
# Name of the directory where the code search index is stored. Default: opengist.index
index.dirname: opengist.index
# Default branch name used by Opengist when initializing Git repositories.
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
git.default-branch:
# Set the journal mode for SQLite. Default: WAL
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
# For SQLite databases only.
sqlite.journal-mode: WAL
# HTTP server configuration
# Host to bind to. Default: 0.0.0.0
http.host: 0.0.0.0
@@ -53,22 +71,48 @@ ssh.external-domain:
# Path or alias to ssh-keygen executable. Default: ssh-keygen
ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
github.client-key:
github.secret:
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
gitlab.client-key:
gitlab.secret:
# URL of the Gitlab instance. Default: 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
gitea.client-key:
gitea.secret:
# URL of the Gitea instance. Default: 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:
oidc.client-key:
oidc.secret:
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
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.8.4
images:
- name: ghcr.io/thomiceli/opengist
newTag: 1.8.4
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" /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"

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.8.4</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,13 @@
# Healthcheck
A healthcheck is a simple HTTP GET request to the `/healthcheck` endpoint. It returns a `200 OK` response if the server is healthy.
## Example
```shell
curl http://localhost:6157/healthcheck
```
```json
{"database":"ok","opengist":"ok","time":"2024-01-04T05:18:33+01:00"}
```

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

@@ -1,6 +1,10 @@
# Use Nginx as a reverse proxy
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
Configure Nginx to proxy requests to Opengist. Here are example configuration file to use Opengist on a subdomain or on a subpath.
Make sure you set the base url for Opengist via the [configuration](/docs/configuration/cheat-sheet.md).
### Subdomain
```
server {
listen 80;
@@ -16,7 +20,27 @@ server {
}
```
Then run :
```shell
service nginx restart
### Subpath
```
server {
listen 80;
server_name example.com;
location /opengist/ {
rewrite ^/opengist(/.*)$ $1 break;
proxy_pass http://127.0.0.1:6157;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /opengist;
}
}
```
---
To apply changes:
```shell
sudo systemctl restart nginx
```

View File

@@ -1,39 +0,0 @@
# Use OAuth providers
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
## Github
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new)
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
github.client-key: <key>
github.secret: <secret>
```
## Gitea
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
gitea.client-key: <key>
gitea.secret: <secret>
# URL of the Gitea instance. Default: https://gitea.com/
gitea.url: http://localhost:3000
```
## OpenID Connect
* Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.domain/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) :
```yaml
oidc.client-key: <key>
oidc.secret: <secret>
# 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
```

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.
## As root
On Unix distributions with systemd, place the Opengist binary like:
```shell
@@ -45,3 +46,47 @@ systemctl daemon-reload
systemctl enable --now 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,25 +1,43 @@
---
aside: false
---
# Configuration Cheat Sheet
| 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`. |
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
| 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. |
| 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.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| 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.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| 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. |
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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`. |
| 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. |
| secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. |
| db-uri | OG_DB_URI | `opengist.db` | URI of the database. |
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) |
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. |
| 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) |
| 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.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| 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.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| 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
```
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
@@ -46,3 +46,27 @@ Usage via command line :
```shell
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

@@ -0,0 +1,77 @@
# Use OAuth providers
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
## GitHub
* 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`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml
github.client-key: <key>
github.secret: <secret>
```
```shell
OG_GITHUB_CLIENT_KEY=<key>
OG_GITHUB_SECRET=<secret>
```
## GitLab
* 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`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml
gitlab.client-key: <key>
gitlab.secret: <secret>
# URL of the GitLab instance. Default: 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
* 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`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml
gitea.client-key: <key>
gitea.secret: <secret>
# URL of the Gitea instance. Default: https://gitea.com/
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
* Add a new OAuth app in Application settings of your OIDC provider
* 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](cheat-sheet.md) :
```yaml
oidc.client-key: <key>
oidc.secret: <secret>
# 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
```
```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,52 +1,4 @@
# Opengist
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](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Syntax highlighting ; markdown & CSV support
* Like / Fork snippets
* Search for snippets ; browse users snippets, likes and forks
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, 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.20** or later is recommended as the app has not been tested with older Git versions.
[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)
---
layout: home
navbar: false
---

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 :
```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
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.5.1/opengist1.5.1-linux-amd64.tar.gz
tar xzvf opengist1.5.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.20+)
* [Go](https://go.dev/doc/install) (1.20+)
* [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
- [Docker](installation/docker.md)
- [Source](installation/source.md)
- [Binary](installation/binary.md)

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.8.4/opengist1.8.4-linux-amd64.tar.gz
tar xzvf opengist1.8.4-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.8.4 # optional, to checkout the latest release
make
./opengist
```
Opengist is now running on port 6157, you can browse http://localhost:6157

55
docs/introduction.md Normal file
View File

@@ -0,0 +1,55 @@
# 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
* 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

58
docs/update.md Normal file
View File

@@ -0,0 +1,58 @@
# Update Opengist
## Make a backup
Before updating, always make sure to backup the Opengist home directory, where all the data is stored.
You can do so by copying the `~/.opengist` directory (default location).
```shell
cp -r ~/.opengist ~/.opengist.bak
```
## Install the new version
### With Docker
Pull the last version of Opengist
```shell
docker pull ghcr.io/thomiceli/opengist:1
```
And restart the container, using `docker compose up -d` for example if you use docker compose.
### Via binary
Stop the running instance; then like your first installation of Opengist, 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.8.4/opengist1.8.4-linux-amd64.tar.gz
tar xzvf opengist1.8.4-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```
### From source
Stop the running instance; then pull the last changes from the master branch, and build the new version.
```shell
git switch master
git pull
make
./opengist
```
## Restore the backup
If you have any issue with the new version, you can restore the backup you made before updating.
```shell
rm -rf ~/.opengist
cp -r ~/.opengist.bak ~/.opengist
```
Then run the old version of Opengist again.

11
docs/usage/embed.md Normal file
View File

@@ -0,0 +1,11 @@
# Embed a Gist to your webpage
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
```html
<script src="http://opengist.url/user/gist-url.js"></script>
<!-- Dark mode: -->
<script src="http://opengist.url/user/gist-url.js?dark"></script>
```

37
docs/usage/gist-json.md Normal file
View File

@@ -0,0 +1,37 @@
# Retrieve Gist as JSON
To retrieve a Gist as JSON, you can add `.json` to the end of the URL of your gist:
```shell
curl http://opengist.url/thomas/my-gist.json | jq '.'
```
It returns a JSON object with the following structure similar to this one:
```json
{
"created_at": "2023-04-12T13:15:20+02:00",
"description": "",
"embed": {
"css": "http://localhost:6157/assets/embed-94abc261.css",
"html": "<div class=\"opengist-embed\" id=\"my-gist\">\n <div class=\"html \">\n \n <div class=\"rounded-md border-1 border-gray-100 dark:border-gray-800 overflow-auto mb-4\">\n <div class=\"border-b-1 border-gray-100 dark:border-gray-700 text-xs p-2 pl-4 bg-gray-50 dark:bg-gray-800 text-gray-400\">\n <a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist#file-hello-md\"><span class=\"font-bold text-gray-700 dark:text-gray-200\">hello.md</span> · 21 B · Markdown</a>\n <span class=\"float-right\"><a target=\"_blank\" href=\"http://localhost:6157\">Hosted via Opengist</a> · <span class=\"text-gray-700 dark:text-gray-200 font-bold\"><a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist/raw/HEAD/hello.md\">view raw</a></span></span>\n </div>\n \n \n \n <div class=\"chroma markdown markdown-body p-8\"><h1>Welcome to Opengist</h1>\n</div>\n \n\n </div>\n \n </div>\n</div>\n",
"js": "http://localhost:6157/thomas/my-gist.js",
"js_dark": "http://localhost:6157/thomas/my-gist.js?dark"
},
"files": [
{
"filename": "hello.md",
"size": 21,
"human_size": "21 B",
"content": "# Welcome to Opengist",
"truncated": false,
"type": "Markdown"
}
],
"id": "my-gist",
"owner": "thomas",
"title": "hello.md",
"uuid": "8622b297bce54b408e36d546cef8019d",
"visibility": "public"
}
```

View File

@@ -0,0 +1,26 @@
# 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 visibility
```shell
git push -o visibility=public
git push -o visibility=unlisted
git push -o visibility=private
```

View File

@@ -0,0 +1,23 @@
# Import Gists from GitHub
After running Opengist at least once, you can import your Gists from GitHub using this script:
```shell
github_user=user # replace with your GitHub username
opengist_url="http://user:password@opengist.url/init" # replace user, password and Opengist url
curl -s https://api.github.com/users/"$github_user"/gists?per_page=100 | jq '.[] | .git_pull_url' -r | while read url; do
git clone "$url"
repo_dir=$(basename "$url" .git)
# Add remote, push, and remove the directory
if [ -d "$repo_dir" ]; then
cd "$repo_dir"
git remote add gist "$opengist_url"
git push -u gist --all
cd ..
rm -rf "$repo_dir"
fi
done
```

View File

@@ -39,4 +39,4 @@ To http://localhost:6157/init
* [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" />

120
go.mod
View File

@@ -1,54 +1,114 @@
module github.com/thomiceli/opengist
go 1.20
go 1.23
require (
github.com/glebarez/go-sqlite v1.21.2
github.com/glebarez/sqlite v1.9.0
github.com/go-playground/validator/v10 v10.15.4
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/blevesearch/bleve/v2 v2.4.3
github.com/dustin/go-humanize v1.0.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.23.0
github.com/go-webauthn/webauthn v0.11.2
github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-memdb v1.3.4
github.com/labstack/echo/v4 v4.11.1
github.com/markbates/goth v1.78.0
github.com/rs/zerolog v1.30.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.13.0
golang.org/x/text v0.13.0
github.com/labstack/echo/v4 v4.12.0
github.com/markbates/goth v1.80.0
github.com/pquerna/otp v1.4.0
github.com/rs/zerolog v1.33.0
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
go.abhg.dev/goldmark/mermaid v0.5.0
golang.org/x/crypto v0.29.0
golang.org/x/text v0.20.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.4
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.10
gorm.io/gorm v1.25.12
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/bits-and-blooms/bitset v1.17.0 // 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/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.0.11 // indirect
github.com/blevesearch/zapx/v11 v11.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/v14 v14.3.10 // 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/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/dlclark/regexp2 v1.11.4 // 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/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/protobuf v1.5.3 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.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/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/now v1.1.5 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // 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/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/reflect2 v1.0.2 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
modernc.org/libc v1.24.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.9.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/memory v1.7.1 // indirect
modernc.org/sqlite v1.25.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.34.1 // indirect
)

726
go.sum
View File

@@ -1,144 +1,135 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
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/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
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.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
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.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
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.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4=
github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY=
github.com/blevesearch/bleve_index_api v1.1.13 h1:+nrA6oRJr85aCPyqaeZtsruObwKojutfonHJin/BP48=
github.com/blevesearch/bleve_index_api v1.1.13/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
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/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY=
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/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/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/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.11 h1:SJI97toEFTtA9WsDZxkyGTaBWFdWl1n2LEDCXLCq/AU=
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/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/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
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/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE=
github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
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/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
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/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
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/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
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/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/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/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
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/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
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/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
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/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/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.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
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/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
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/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
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=
@@ -147,387 +138,166 @@ github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYi
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
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/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
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-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
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.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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
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/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
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.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
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/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
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.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.10 h1:7Lggqempgy496c0WfHXsYWxk3Th+ZcW66/21QhVFdeE=
gorm.io/driver/postgres v1.5.10/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.22.3 h1:C7AW89Zw3kygesTQWBzApwIn9ldM+cb/plrTIKq41Os=
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/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
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=

177
internal/actions/actions.go Normal file
View File

@@ -0,0 +1,177 @@
package actions
import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"os"
"path/filepath"
"strings"
"sync"
)
type ActionStatus struct {
Running bool
}
const (
SyncReposFromFS = iota
SyncReposFromDB
GitGcRepos
SyncGistPreviews
ResetHooks
IndexGists
)
var (
mutex sync.Mutex
actions = make(map[int]ActionStatus)
)
func updateActionStatus(actionType int, running bool) {
actions[actionType] = ActionStatus{
Running: running,
}
}
func IsRunning(actionType int) bool {
mutex.Lock()
defer mutex.Unlock()
return actions[actionType].Running
}
func Run(actionType int) {
mutex.Lock()
if actions[actionType].Running {
mutex.Unlock()
return
}
updateActionStatus(actionType, true)
mutex.Unlock()
defer func() {
mutex.Lock()
updateActionStatus(actionType, false)
mutex.Unlock()
}()
var functionToRun func()
switch actionType {
case SyncReposFromFS:
functionToRun = syncReposFromFS
case SyncReposFromDB:
functionToRun = syncReposFromDB
case GitGcRepos:
functionToRun = gitGcRepos
case SyncGistPreviews:
functionToRun = syncGistPreviews
case ResetHooks:
functionToRun = resetHooks
case IndexGists:
functionToRun = indexGists
default:
log.Error().Msg("Unknown action type")
}
functionToRun()
}
func syncReposFromFS() {
log.Info().Msg("Syncing repositories from filesystem...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
// if repository does not exist, delete gist from database
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
if err2 := gist.Delete(); err2 != nil {
log.Error().Err(err2).Msgf("Cannot delete gist %d", gist.ID)
}
}
}
}
func syncReposFromDB() {
log.Info().Msg("Syncing repositories from database...")
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
if err != nil {
log.Error().Err(err).Msg("Cannot read repos directories")
return
}
for _, e := range entries {
path := strings.Split(e, string(os.PathSeparator))
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
if gist.ID == 0 {
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot delete repository %s/%s", path[len(path)-2], path[len(path)-1])
}
}
}
}
func gitGcRepos() {
log.Info().Msg("Garbage collecting all repositories...")
if err := git.GcRepos(); err != nil {
log.Error().Err(err).Msg("Error garbage collecting repositories")
}
}
func syncGistPreviews() {
log.Info().Msg("Syncing all Gist previews...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
if err = gist.UpdatePreviewAndCount(false); err != nil {
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
}
}
}
func resetHooks() {
log.Info().Msg("Resetting Git server hooks for all repositories...")
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
if err != nil {
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])
}
}
}
func indexGists() {
log.Info().Msg("Indexing all Gists...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
log.Info().Msgf("Indexing gist %d", gist.ID)
indexedGist, err := gist.ToIndexedGist()
if err != nil {
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
continue
}
if err = index.AddInIndex(indexedGist); err != nil {
log.Error().Err(err).Msgf("Cannot index gist %d", gist.ID)
}
}
}

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,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/db"
"github.com/thomiceli/opengist/internal/utils"
"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 := utils.Argon2id.Hash(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, false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
}
}

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

@@ -0,0 +1,189 @@
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/memdb"
"github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web"
"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 web.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, false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database")
}
if err := memdb.Setup(); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize in memory 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,12 +2,15 @@ package config
import (
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -15,17 +18,29 @@ import (
"gopkg.in/yaml.v3"
)
var OpengistVersion = "1.5.3"
var OpengistVersion = ""
var C *config
var SecretKey []byte
// Not using nested structs because the library
// doesn't support dot notation in this case sadly
type config struct {
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
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"`
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"`
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
@@ -42,21 +57,42 @@ type config struct {
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
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"`
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
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"`
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
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) {
c := &config{}
c.SecretKey = ""
c.LogLevel = "warn"
c.LogOutput = "stdout,file"
c.OpengistHome = ""
c.DBFilename = "opengist.db"
c.DBUri = "opengist.db"
c.IndexEnabled = true
c.IndexDirname = "opengist.index"
c.SqliteJournalMode = "WAL"
@@ -69,23 +105,26 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
c.GiteaUrl = "http://gitea.com"
c.GitlabName = "GitLab"
c.GiteaUrl = "https://gitea.com"
c.GiteaName = "Gitea"
return c, nil
}
func InitConfig(configPath string) error {
func InitConfig(configPath string, out io.Writer) error {
// Default values
c, err := configWithDefaults()
if err != nil {
return err
}
if err = loadConfigFromYaml(c, configPath); err != nil {
if err = loadConfigFromYaml(c, configPath, out); err != nil {
return err
}
if err = loadConfigFromEnv(c); err != nil {
if err = loadConfigFromEnv(c, out); err != nil {
return err
}
@@ -104,6 +143,13 @@ func InitConfig(configPath string) error {
C = c
if err = migrateConfig(); err != nil {
return err
}
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err
}
return nil
}
@@ -111,21 +157,61 @@ func InitLog() {
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
panic(err)
}
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
var level zerolog.Level
level, err = zerolog.ParseLevel(C.LogLevel)
level, err := zerolog.ParseLevel(C.LogLevel)
if err != nil {
level = zerolog.InfoLevel
}
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
var logWriters []io.Writer
logOutputTypes := utils.RemoveDuplicates[string](
strings.Split(strings.ToLower(C.LogOutput), ","),
)
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
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 {
logOutputType = strings.TrimSpace(logOutputType)
if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
defer func() { log.Warn().Msg("Invalid log output type: " + logOutputType) }()
continue
}
switch logOutputType {
case "stdout":
logWriters = append(logWriters, consoleWriter)
defer func() { log.Debug().Msg("Logging to stdout") }()
case "file":
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
logWriters = append(logWriters, file)
defer func() { log.Debug().Msg("Logging to file: " + file.Name()) }()
}
}
if len(logWriters) == 0 {
logWriters = append(logWriters, consoleWriter)
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
}
multi := zerolog.MultiLevelWriter(logWriters...)
log.Logger = zerolog.New(multi).Level(level).With().Caller().Timestamp().Logger()
if !slices.Contains([]string{"debug", "info", "warn", "error", "fatal"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel)
}
}
@@ -144,8 +230,8 @@ func CheckGitVersion(version string) (bool, error) {
return false, fmt.Errorf("invalid minor version number")
}
// Check if version is prior to 2.20
if major < 2 || (major == 2 && minor < 20) {
// Check if version is prior to 2.28
if major < 2 || (major == 2 && minor < 28) {
return false, nil
}
return true, nil
@@ -156,7 +242,16 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath)
}
func loadConfigFromYaml(c *config, configPath string) error {
func SetupSecretKey() {
if C.SecretKey == "" {
path := filepath.Join(GetHomeDir(), "opengist-secret.key")
SecretKey, _ = utils.GenerateSecretKey(path)
} else {
SecretKey = []byte(C.SecretKey)
}
}
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if configPath != "" {
absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath)
@@ -165,9 +260,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
if !os.IsNotExist(err) {
return err
}
fmt.Println("No YAML config file found at " + absolutePath)
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
} else {
fmt.Println("Using YAML config file: " + absolutePath)
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
@@ -177,13 +272,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
defer file.Close()
}
} else {
fmt.Println("No YAML config file specified.")
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
}
return nil
}
func loadConfigFromEnv(c *config) error {
func loadConfigFromEnv(c *config, out io.Writer) error {
v := reflect.ValueOf(c).Elem()
var envVars []string
@@ -195,28 +290,69 @@ func loadConfigFromEnv(c *config) error {
}
envValue := os.Getenv(strings.ToUpper(tag))
if envValue == "" {
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
continue
}
switch v.Field(i).Kind() {
case reflect.String:
v.Field(i).SetString(envValue)
envVars = append(envVars, tag)
case reflect.Bool:
boolVal, err := strconv.ParseBool(envValue)
if err != nil {
return err
}
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 {
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
} else {
fmt.Println("No environment variables config specified.")
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
}
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,21 @@ import (
)
type AdminSetting struct {
Key string `gorm:"uniqueIndex"`
Key string `gorm:"index:,unique"`
Value string
}
const (
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
)
func GetSetting(key string) (string, error) {
var setting AdminSetting
err := db.Where("key = ?", key).First(&setting).Error
err := db.Where("`key` = ?", key).First(&setting).Error
return setting.Value, err
}
@@ -48,7 +49,7 @@ func UpdateSetting(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 {
@@ -62,3 +63,21 @@ func initAdminSettings(settings map[string]string) error {
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,134 @@ package db
import (
"errors"
"strings"
msqlite "github.com/glebarez/go-sqlite"
"fmt"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm/logger"
"net/url"
"path/filepath"
"slices"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
func Setup(dbPath string, sharedCache bool) error {
var err error
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
const (
SQLite databaseType = iota
PostgreSQL
MySQL
)
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
type databaseType int
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{}
u, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("invalid URI: %v", err)
}
sharedCacheStr := ""
if sharedCache {
sharedCacheStr = "&cache=shared"
if u.Scheme == "" {
info.Type = SQLite
info.Database = filepath.Join(config.GetHomeDir(), uri)
return info, nil
}
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
}); err != 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, sharedCache bool) error {
dbInfo, err := parseDBURI(dbUri)
if err != nil {
return err
}
log.Info().Msgf("Setting up a %s database connection", dbInfo.Type)
var setupFunc func(databaseInfo, bool) 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, sharedCache)
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 {
return err
}
@@ -42,20 +138,21 @@ func Setup(dbPath string, sharedCache bool) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}); err != nil {
return err
}
if err = ApplyMigrations(db); err != nil {
if err = applyMigrations(db, dbInfo); err != nil {
return err
}
// Default admin setting values
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingAllowGistsWithoutLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})
}
@@ -74,9 +171,85 @@ func CountAll(table interface{}) (int64, error) {
}
func IsUniqueConstraintViolation(err error) bool {
var sqliteErr *msqlite.Error
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
return true
}
return false
return errors.Is(err, gorm.ErrDuplicatedKey)
}
func Ping() error {
sql, err := db.DB()
if err != nil {
return err
}
return sql.Ping()
}
func setupSQLite(dbInfo databaseInfo, sharedCache bool) error {
var err error
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)
}
u, err := url.Parse(dbInfo.Database)
if err != nil {
return err
}
u.Scheme = "file"
q := u.Query()
q.Set("_fk", "true")
q.Set("_journal_mode", journalMode)
if sharedCache {
q.Set("cache", "shared")
}
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, sharedCache bool) 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, sharedCache bool) 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{})
}

View File

@@ -1,22 +1,75 @@
package db
import (
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
"fmt"
"os/exec"
"path/filepath"
"strings"
"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/index"
"gorm.io/gorm"
)
type Visibility int
const (
PublicVisibility Visibility = iota
UnlistedVisibility
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 {
switch v {
case PublicVisibility:
return UnlistedVisibility
case UnlistedVisibility:
return PrivateVisibility
default:
return PublicVisibility
}
}
func ParseVisibility[T string | int](v T) (Visibility, error) {
switch s := fmt.Sprint(v); s {
case "0", "public":
return PublicVisibility, nil
case "1", "unlisted":
return UnlistedVisibility, nil
case "2", "private":
return PrivateVisibility, nil
default:
return -1, fmt.Errorf("unknown visibility %q", s)
}
}
type Gist struct {
ID uint `gorm:"primaryKey"`
Uuid string
Title string
URL string
Preview string
PreviewFilename string
Description string
Private int // 0: public, 1: unlisted, 2: private
Private Visibility // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
@@ -48,7 +101,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
Where("gists.uuid = ? AND users.username like ?", 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").
First(&gist).Error
@@ -177,6 +230,25 @@ func GetAllGistsRows() ([]*Gist, error) {
return gists, err
}
func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
var gists []uint
err := db.Table("gists").
Where("gists.private = 0 or gists.user_id = ?", userId).
Pluck("gists.id", &gists).Error
return gists, err
}
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("id in ?", ids).
Find(&gists).Error
return gists, err
}
func (gist *Gist) Create() error {
// avoids foreign key constraint error because the default value in the struct is 0
return db.Omit("forked_id").Create(&gist).Error
@@ -190,6 +262,10 @@ func (gist *Gist) Update() error {
return db.Omit("forked_id").Save(&gist).Error
}
func (gist *Gist) UpdateNoTimestamps() error {
return db.Omit("forked_id", "updated_at").Save(&gist).Error
}
func (gist *Gist) Delete() error {
err := gist.DeleteRepository()
if err != nil {
@@ -266,33 +342,29 @@ func (gist *Gist) InitRepository() error {
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 {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) Files(revision string) ([]*git.File, error) {
var files []*git.File
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
if err != nil {
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
return nil, nil
return nil, &git.RevisionNotFoundError{}
}
return nil, err
}
for _, fileStr := range filesStr {
file, err := gist.File(revision, fileStr, true)
if err != nil {
return nil, err
}
files = append(files, file)
var files []*git.File
for _, fileCat := range filesCat {
files = append(files, &git.File{
Filename: fileCat.Name,
Size: fileCat.Size,
HumanSize: humanize.IBytes(fileCat.Size),
Content: fileCat.Content,
Truncated: fileCat.Truncated,
})
}
return files, err
}
@@ -305,13 +377,26 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
return nil, nil
}
var size uint64
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
if err != nil {
return nil, err
}
return &git.File{
Filename: filename,
Size: size,
HumanSize: humanize.IBytes(size),
Content: content,
Truncated: truncated,
}, err
}
func (gist *Gist) FileNames(revision string) ([]string, error) {
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
}
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
return git.GetLog(gist.User.Username, gist.Uuid, skip)
}
@@ -321,7 +406,7 @@ func (gist *Gist) NbCommits() (string, error) {
}
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email); err != nil {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
return err
}
@@ -342,6 +427,26 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
return git.Push(gist.Uuid)
}
func (gist *Gist) AddAndCommitFile(file *FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, false); err != nil {
return err
}
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
}
if err := git.AddAll(gist.Uuid); err != nil {
return err
}
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
return err
}
return git.Push(gist.Uuid)
}
func (gist *Gist) ForkClone(username string, uuid string) error {
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
}
@@ -354,7 +459,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
return git.RPC(gist.User.Username, gist.Uuid, service)
}
func (gist *Gist) UpdatePreviewAndCount() error {
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
if err != nil {
return err
@@ -380,7 +485,54 @@ func (gist *Gist) UpdatePreviewAndCount() error {
gist.PreviewFilename = file.Filename
}
return gist.Update()
if withTimestampUpdate {
return gist.Update()
}
return gist.UpdateNoTimestamps()
}
func (gist *Gist) VisibilityStr() string {
switch gist.Private {
case PublicVisibility:
return "public"
case UnlistedVisibility:
return "unlisted"
case PrivateVisibility:
return "private"
default:
return ""
}
}
func (gist *Gist) Identifier() string {
if gist.URL != "" {
return gist.URL
}
return gist.Uuid
}
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
files, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
languages := make([]string, 0, len(files))
for _, file := range files {
var lexer chroma.Lexer
if lexer = lexers.Get(file.Filename); lexer == nil {
lexer = lexers.Fallback
}
fileType := lexer.Config().Name
if lexer.Config().Name == "fallback" || lexer.Config().Name == "plaintext" {
fileType = "Text"
}
languages = append(languages, fileType)
}
return languages, nil
}
// -- DTO -- //
@@ -388,10 +540,15 @@ func (gist *Gist) UpdatePreviewAndCount() error {
type GistDTO struct {
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
Private int `validate:"number,min=0,max=2" form:"private"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
VisibilityDTO
}
type VisibilityDTO struct {
Private Visibility `validate:"number,min=0,max=2" form:"private"`
}
type FileDTO struct {
@@ -404,11 +561,84 @@ func (dto *GistDTO) ToGist() *Gist {
Title: dto.Title,
Description: dto.Description,
Private: dto.Private,
URL: dto.URL,
}
}
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title
gist.Description = dto.Description
gist.URL = dto.URL
return gist
}
// -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
files, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
exts := make([]string, 0, len(files))
wholeContent := ""
for _, file := range files {
wholeContent += file.Content
exts = append(exts, filepath.Ext(file.Filename))
}
fileNames, err := gist.FileNames("HEAD")
if err != nil {
return nil, err
}
langs, err := gist.GetLanguagesFromFiles()
if err != nil {
return nil, err
}
indexedGist := &index.Gist{
GistID: gist.ID,
Username: gist.User.Username,
Title: gist.Title,
Content: wholeContent,
Filenames: fileNames,
Extensions: exts,
Languages: langs,
CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt,
}
return indexedGist, nil
}
func (gist *Gist) AddInIndex() {
if !index.Enabled() {
return
}
go func() {
indexedGist, err := gist.ToIndexedGist()
if err != nil {
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
return
}
err = index.AddInIndex(indexedGist)
if err != nil {
log.Error().Err(err).Msgf("Error adding gist %d to index", gist.ID)
}
}()
}
func (gist *Gist) RemoveFromIndex() {
if !index.Enabled() {
return
}
go func() {
err := index.RemoveFromIndex(gist.ID)
if err != nil {
log.Error().Err(err).Msgf("Error remove gist %d from index", gist.ID)
}
}()
}

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

@@ -11,7 +11,19 @@ type MigrationVersion struct {
Version uint
}
func ApplyMigrations(db *gorm.DB) error {
func applyMigrations(db *gorm.DB, dbInfo *databaseInfo) error {
switch dbInfo.Type {
case SQLite:
return applySqliteMigrations(db)
case PostgreSQL, MySQL:
return nil
default:
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
}
}
func applySqliteMigrations(db *gorm.DB) error {
// Create migration table if it doesn't exist
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
log.Fatal().Err(err).Msg("Error creating migration version table")

View File

@@ -19,7 +19,7 @@ type SSHKey struct {
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))
if err != nil {
return err
@@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
return sshKey, err
}
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
sshKey := new(SSHKey)
err := db.
Where("content like ?", sshKeyContent+"%").
First(&sshKey).Error
return sshKey, err
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
var count int64
err := db.Model(&SSHKey{}).
Where("content = ?", sshKeyContent).
Count(&count).Error
return count > 0, err
}
func (sshKey *SSHKey) Create() error {

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

@@ -0,0 +1,122 @@
package db
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
ogtotp "github.com/thomiceli/opengist/internal/auth/totp"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/utils"
"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 := utils.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 := utils.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 := utils.Argon2id.Verify(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 := utils.Argon2id.Hash(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

@@ -6,7 +6,7 @@ import (
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex"`
Username string `gorm:"uniqueIndex,size:191"`
Password string
IsAdmin bool
CreatedAt int64
@@ -14,12 +14,14 @@ type User struct {
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GitlabID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
Gists []Gist `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"`
Gists []Gist `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"`
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
}
func (user *User) BeforeDelete(tx *gorm.DB) error {
@@ -52,6 +54,16 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
return err
}
err = tx.Where("user_id = ?", user.ID).Delete(&SSHKey{}).Error
if err != nil {
return err
}
err = tx.Where("user_id = ?", user.ID).Delete(&WebAuthnCredential{}).Error
if err != nil {
return err
}
// Delete all gists created by this user
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
}
@@ -100,7 +112,6 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
err := db.
Where("email IN ?", emails).
Find(&users).Error
if err != nil {
return nil, err
}
@@ -113,6 +124,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
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) {
key := new(SSHKey)
err := db.
@@ -129,6 +149,8 @@ func GetUserByProvider(id string, provider string) (*User, error) {
switch provider {
case "github":
err = db.Where("github_id = ?", id).First(&user).Error
case "gitlab":
err = db.Where("gitlab_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
case "openid-connect":
@@ -167,20 +189,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
}
func (user *User) DeleteProviderID(provider string) error {
switch provider {
case "github":
providerIDFields := map[string]string{
"github": "github_id",
"gitlab": "gitlab_id",
"gitea": "gitea_id",
"openid-connect": "oidc_id",
}
if providerIDField, ok := providerIDFields[provider]; ok {
return db.Model(&user).
Update("github_id", nil).
Update("avatar_url", nil).
Error
case "gitea":
return db.Model(&user).
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
case "openid-connect":
return db.Model(&user).
Update("oidc_id", nil).
Update(providerIDField, nil).
Update("avatar_url", nil).
Error
}
@@ -188,10 +206,23 @@ func (user *User) DeleteProviderID(provider string) error {
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 -- //
type UserDTO struct {
Username string `form:"username" validate:"required,max=24,alphanum,notreserved"`
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
Password string `form:"password" validate:"required"`
}

View File

@@ -0,0 +1,149 @@
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":
case "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":
case "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

@@ -1,12 +1,12 @@
package git
import (
"bufio"
"bytes"
"context"
"fmt"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"io"
"net/url"
"os"
"os/exec"
"path"
@@ -14,6 +14,10 @@ import (
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
)
var (
@@ -21,6 +25,14 @@ var (
)
const truncateLimit = 2 << 18
const diffSize = 2 << 12
const maxFilesPerDiffCommit = 10
type RevisionNotFoundError struct{}
func (m *RevisionNotFoundError) Error() string {
return "revision not found"
}
func RepositoryPath(user string, gist string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
@@ -55,28 +67,20 @@ func TmpRepositoriesPath() string {
func InitRepository(user string, gist string) error {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
"git",
"init",
"--bare",
repositoryPath,
)
var args []string
args = append(args, "init")
if config.C.GitDefaultBranch != "" {
args = append(args, "--initial-branch", config.C.GitDefaultBranch)
}
args = append(args, "--bare", repositoryPath)
cmd := exec.Command("git", args...)
if err := cmd.Run(); err != nil {
return err
}
return createDotGitFiles(repositoryPath)
}
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))
return CreateDotGitFiles(user, gist)
}
func CountCommits(user string, gist string) (string, error) {
@@ -112,9 +116,126 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
}
slice := strings.Split(string(stdout), "\n")
for i, s := range slice {
slice[i] = convertOctalToUTF8(s)
}
return slice[:len(slice)-1], nil
}
type catFileBatch struct {
Name, Hash, Content string
Size uint64
Truncated bool
}
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
repositoryPath := RepositoryPath(user, gist)
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
lsTreeCmd.Dir = repositoryPath
lsTreeOutput, err := lsTreeCmd.Output()
if err != nil {
return nil, err
}
fileMap := make([]*catFileBatch, 0)
lines := strings.Split(string(lsTreeOutput), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 4 {
continue // Skip lines that don't have enough fields
}
hash := fields[2]
size, err := strconv.ParseUint(fields[3], 10, 64)
if err != nil {
continue // Skip lines with invalid size field
}
name := strings.Join(fields[4:], " ") // File name may contain spaces
fileMap = append(fileMap, &catFileBatch{
Hash: hash,
Size: size,
Name: convertOctalToUTF8(name),
})
}
catFileCmd := exec.Command("git", "cat-file", "--batch")
catFileCmd.Dir = repositoryPath
stdin, err := catFileCmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := catFileCmd.StdoutPipe()
if err != nil {
return nil, err
}
if err = catFileCmd.Start(); err != nil {
return nil, err
}
reader := bufio.NewReader(stdout)
for _, file := range fileMap {
_, err = stdin.Write([]byte(file.Hash + "\n"))
if err != nil {
return nil, err
}
header, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
parts := strings.Fields(header)
if len(parts) > 3 {
continue // Not a valid header, skip this entry
}
size, err := strconv.ParseUint(parts[2], 10, 64)
if err != nil {
return nil, err
}
sizeToRead := size
if truncate && sizeToRead > truncateLimit {
sizeToRead = truncateLimit
}
// Read exactly size bytes from header, or the max allowed if truncated
content := make([]byte, sizeToRead)
if _, err = io.ReadFull(reader, content); err != nil {
return nil, err
}
file.Content = string(content)
if truncate && size > truncateLimit {
// skip other bytes if truncated
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
return nil, err
}
file.Truncated = true
}
// Read the blank line following the content
if _, err := reader.ReadByte(); err != nil {
return nil, err
}
}
if err = stdin.Close(); err != nil {
return nil, err
}
if err = catFileCmd.Wait(); err != nil {
return nil, err
}
return fileMap, nil
}
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
repositoryPath := RepositoryPath(user, gist)
@@ -132,7 +253,7 @@ func GetFileContent(user string, gist string, revision string, filename string,
"git",
"--no-pager",
"show",
revision+":"+filename,
revision+":"+convertURLToOctal(filename),
)
cmd.Dir = repositoryPath
@@ -149,6 +270,25 @@ func GetFileContent(user string, gist string, revision string, filename string,
return content, truncated, nil
}
func GetFileSize(user string, gist string, revision string, filename string) (uint64, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
"git",
"cat-file",
"-s",
revision+":"+convertURLToOctal(filename),
)
cmd.Dir = repositoryPath
stdout, err := cmd.Output()
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
}
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
repositoryPath := RepositoryPath(user, gist)
@@ -179,10 +319,10 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
}
}(cmd)
return parseLog(stdout, truncateLimit), err
return parseLog(stdout, maxFilesPerDiffCommit, diffSize)
}
func CloneTmp(user string, gist string, gistTmpId string, email string) error {
func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error {
repositoryPath := RepositoryPath(user, gist)
tmpPath := TmpRepositoriesPath()
@@ -200,11 +340,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
return err
}
// remove every file (and not the .git directory!)
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
return err
// remove every file (keep the .git directory)
// useful when user wants to edit multiple files from an existing gist
if remove {
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
return err
}
}
cmd = exec.Command("git", "config", "--local", "user.name", user)
cmd.Dir = tmpRepositoryPath
if err = cmd.Run(); err != nil {
@@ -225,7 +367,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
return err
}
return createDotGitFiles(repositoryPathDst)
return CreateDotGitFiles(userDst, gistDst)
}
func SetFileContent(gistTmpId string, filename string, content string) error {
@@ -278,7 +420,6 @@ func Push(gistTmpId string) error {
if err != nil {
return err
}
return os.RemoveAll(tmpRepositoryPath)
}
@@ -379,22 +520,28 @@ func GetGitVersion() (string, error) {
return versionFields[2], nil
}
func createDotGitFiles(repositoryPath string) error {
func CreateDotGitFiles(user string, gist string) error {
repositoryPath := RepositoryPath(user, gist)
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f1.Close()
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil {
return err
if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
for _, hook := range []string{"pre-receive", "post-receive"} {
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
return err
}
}
}
return nil
}
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
if err != nil {
return err
}
@@ -422,57 +569,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
do
if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then
# This is the first commit, so we check all the files in that commit
changed_files=$(git ls-tree -r --name-only "$new_rev")
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
needsQuoting := false
for _, r := range name {
if r > 127 {
needsQuoting = true
break
}
}
while IFS= read -r file
do
case $file in
*/*)
disallowed_files="${disallowed_files}${file} "
;;
esac
done <<EOF
$changed_files
EOF
done
if !needsQuoting {
return name
}
if [ -n "$disallowed_files" ]; then
echo ""
echo "Pushing files in folders is not allowed:"
for file in $disallowed_files; do
echo " $file"
done
echo ""
exit 1
fi
`
const postReceive = `#!/bin/sh
while read oldrev newrev refname; do
if ! git rev-parse --verify --quiet HEAD &>/dev/null; then
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
quoted := fmt.Sprintf("%q", name)
return strings.Trim(quoted, `"`)
}
func convertURLToOctal(name string) string {
decoded, err := url.QueryUnescape(name)
if err != nil {
return name
}
return convertUTF8ToOctal(decoded)
}
const hookTemplate = `#!/bin/sh
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
`

View File

@@ -1,42 +1,18 @@
package git
import (
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"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) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
cmd.Dir = RepositoryPath("thomas", "gist1")
@@ -44,9 +20,6 @@ func TestInitDeleteRepository(t *testing.T) {
require.NoError(t, err, "Could not run git command")
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"))
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) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
hasNoCommits, err := HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has 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")
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.Equal(t, "1", nbCommits, "Repository should have 1 commit")
commitToBare(t, "thomas", "gist1", nil)
CommitToBare(t, "thomas", "gist1", nil)
nbCommits, err = CountCommits("thomas", "gist1")
require.NoError(t, err, "Could not count commits")
require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
}
func TestContent(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(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_other_file.txt": `I really
hate Opengist`,
"rip.txt": "byebye",
"中文名.txt": "中文内容",
})
files, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
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)
require.NoError(t, err, "Could not get content")
@@ -104,16 +78,22 @@ hate Opengist`,
require.False(t, truncated, "Content should not be truncated")
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_other_file.txt": `I really
like Opengist actually`,
"new_file.txt": "Wait now there is a new file",
"中文名.txt": "中文内容",
})
files, err = GetFilesOfRepository("thomas", "gist1", "HEAD")
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)
require.NoError(t, err, "Could not get content")
@@ -152,7 +132,7 @@ like Opengist actually`,
require.Contains(t, commits[0].Files, File{
Filename: "my_other_file.txt",
OldFilename: "",
OldFilename: "my_other_file.txt",
Content: `@@ -1,2 +1,2 @@
I really
-hate Opengist
@@ -183,18 +163,18 @@ like Opengist actually`,
}
func TestGitGc(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
err := GcRepos()
require.NoError(t, err, "Could not run git gc")
}
func TestFork(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(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",
})
@@ -211,10 +191,10 @@ func TestFork(t *testing.T) {
}
func TestTruncate(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "A",
})
@@ -228,7 +208,7 @@ func TestTruncate(t *testing.T) {
builder.WriteString("A")
}
str := builder.String()
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": str,
})
@@ -237,7 +217,7 @@ func TestTruncate(t *testing.T) {
require.True(t, truncated, "Content should be truncated")
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,
})
@@ -247,52 +227,23 @@ func TestTruncate(t *testing.T) {
require.Equal(t, 2, len(content), "Content size is not correct")
}
func TestInitViaGitInit(t *testing.T) {
setup(t)
defer teardown(t)
func TestGitInitBranchNames(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
e := echo.New()
cmd := exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = RepositoryPath("thomas", "gist1")
out, err := cmd.Output()
require.NoError(t, err, "Could not run git command")
require.Equal(t, "refs/heads/master", strings.TrimSpace(string(out)), "Repository should have master branch as default")
// 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)
config.C.GitDefaultBranch = "main"
err = InitRepository("thomas", "gist2")
require.NoError(t, err)
}
func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com")
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")
}
cmd = exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = RepositoryPath("thomas", "gist2")
out, err = cmd.Output()
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")
}

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,17 +6,18 @@ import (
"encoding/csv"
"fmt"
"io"
"regexp"
"strings"
)
type File struct {
Filename string
OldFilename string
Content string
Truncated bool
IsCreated bool
IsDeleted bool
Filename string `json:"filename"`
Size uint64 `json:"size"`
HumanSize string `json:"human_size"`
OldFilename string `json:"-"`
Content string `json:"content"`
Truncated bool `json:"truncated"`
IsCreated bool `json:"-"`
IsDeleted bool `json:"-"`
}
type CsvFile struct {
@@ -61,124 +62,287 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
return string(buf), truncated, nil
}
func parseLog(out io.Reader, maxBytes int) []*Commit {
scanner := bufio.NewScanner(out)
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
var commits []*Commit
var currentCommit *Commit
var currentFile *File
var isContent bool
var bytesRead = 0
scanNext := true
var headerParsed = false
var skipped = false
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 {
if scanNext && !scanner.Scan() {
break
for isFragment {
currentFile.Truncated = true
// Read the next line
_, isFragment, err = input.ReadLine()
if err != nil {
return nil, false, err
}
}
scanNext = true
// new commit found
currentFile = nil
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}}
sb.Reset()
scanner.Scan()
currentCommit.AuthorName = string(scanner.Bytes()[2:])
// Read the next line
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
if err == io.EOF {
return lineBytes, isFragment, err
}
return nil, false, err
}
scanner.Scan()
currentCommit.AuthorEmail = string(scanner.Bytes()[2:])
// End of file
if len(lineBytes) == 0 {
return lineBytes, false, err
}
if lineBytes[0] == 'd' {
return lineBytes, false, err
}
scanner.Scan()
currentCommit.Timestamp = string(scanner.Bytes()[2:])
scanner.Scan()
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one
if scanner.Bytes()[0] != ' ' {
commits = append(commits, currentCommit)
// avoid scanning the next line, as we already did it
scanNext = false
if currFileLineCount >= maxBytes {
currentFile.Truncated = true
continue
}
changed := scanner.Bytes()[1:]
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
currentCommit.Changed = string(changed)
// twice because --shortstat adds a new line
scanner.Scan()
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
line := string(lineBytes)
if isFragment {
currentFile.Truncated = true
for isFragment {
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err)
}
}
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) {

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,107 @@
package hooks
import (
"bufio"
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/utils"
"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 := utils.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 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"
)
var title = cases.Title(language.English)
var Locales = NewLocaleStore()
type LocaleStore struct {
@@ -59,7 +58,7 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
locale := &Locale{
Code: localeCode,
Name: title.String(name),
Name: cases.Title(language.English).String(name),
Messages: make(map[string]string),
}
@@ -112,6 +111,20 @@ func (store *LocaleStore) MatchTag(langs []language.Tag) string {
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 {
message := l.Messages[key]

View File

@@ -0,0 +1,261 @@
gist.public: Veřejný
gist.unlisted: Neveřejný
gist.private: Privátní
gist.header.like: To se mi líbí
gist.header.unlike: Už se mi nelíbí
gist.header.fork: Fork
gist.header.edit: Upravit
gist.header.delete: Smazat
gist.header.forked-from: Forkováno z
gist.header.last-active: Naposledy aktivní
gist.header.select-tab: Vyberte záložku
gist.header.code: Kód
gist.header.revisions: Revize
gist.header.revision: Revize
gist.header.clone-http: Klonovat pomocí %s
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-help: Klonovat s pomocí Git pomocí klíče SSH.
gist.header.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: Stáhnout ZIP
gist.raw: Raw
gist.file-truncated: Tento soubor byl zkrácen.
gist.watch-full-file: Zobrazit celý soubor.
gist.file-not-valid: Tento soubor není validní CSV.
gist.no-content: Žádný obsah
gist.new.new_gist: Nový gist
gist.new.title: Titulek
gist.new.description: Popis
gist.new.filename-with-extension: Název s příponou
gist.new.indent-mode: Režim odsazení
gist.new.indent-mode-space: Mezery
gist.new.indent-mode-tab: Tabulátory
gist.new.indent-size: Velikost odsazení
gist.new.wrap-mode: Režim zalamování
gist.new.wrap-mode-no: Bez zalamování
gist.new.wrap-mode-soft: Měkké zalamování
gist.new.add-file: Přidat soubor
gist.new.create-public-button: Vytvořit veřejný gist
gist.new.create-unlisted-button: Vytvořit neveřejný gist
gist.new.create-private-button: Vytvořit soukromý gist
gist.edit.editing: Úprava
gist.edit.change-visibility: Změnit viditelnost
gist.edit.delete: Smazat
gist.edit.cancel: Zrušit
gist.edit.save: Uložit
gist.list.joined: Připojeno
gist.list.all: Všechny gisty
gist.list.search-results: Výsledky hledání
gist.list.sort: Seřadit
gist.list.sort-by-created: Vytvořeno
gist.list.sort-by-updated: Aktualizováno
gist.list.order-by-asc: Nejméně nedávno
gist.list.order-by-desc: Nedávno
gist.list.select-tab: Vyberte záložku
gist.list.liked: Líbí se
gist.list.likes: Lajky
gist.list.forked: Forkováno
gist.list.forked-from: Forkováno z
gist.list.forks: Forky
gist.list.files: Soubory
gist.list.last-active: Naposledy aktivní
gist.list.no-gists: Žádné gisty
gist.forks: Forky
gist.forks.view: Zobrazit forky
gist.forks.no: Žádné veřejné forky
gist.likes: Lajky
gist.likes.no: Zatím žádné lajky
gist.revisions: Revize
gist.revision.revised: revidoval tento gist
gist.revision.go-to-revision: Přejít na revizi
gist.revision.file-created: vytvořil soubor
gist.revision.file-deleted: smazal soubor
gist.revision.file-renamed: přejmenováno na
gist.revision.diff-truncated: Diff je příliš velký na zobrazení
gist.revision.file-renamed-no-changes: Soubor přejmenován beze změn
gist.revision.empty-file: Prázdný soubor
gist.revision.no-changes: Žádné změny
gist.revision.no-revisions: Žádné revize k zobrazení
settings: Nastavení
settings.email: Email
settings.email-help: Používá se pro commity a Gravatary
settings.email-set: Nastavit email
settings.link-accounts: Propojit účty
settings.link-github-account: Propojit účet na GitHubu
settings.link-gitea-account: Propojit účet na Gitea
settings.unlink-github-account: Odpojit účet na GitHubu
settings.unlink-gitea-account: Odpojit účet na Gitea
settings.delete-account: Smazat účet
settings.delete-account-confirm: Opravdu chcete smazat svůj účet?
settings.add-ssh-key: Přidat SSH klíč
settings.add-ssh-key-help: Používá se pouze k tahání/pushování gistů pomocí Gitu přes SSH
settings.add-ssh-key-title: Titulek
settings.add-ssh-key-content: Obsah
settings.delete-ssh-key: Smazat
settings.delete-ssh-key-confirm: Potvrdit smazání SSH klíče
settings.ssh-key-added-at: Přidáno
settings.ssh-key-never-used: Nikdy nepoužito
settings.ssh-key-last-used: Naposledy použito
settings.create-password: Vytvořit heslo
settings.create-password-help: Vytvořte si heslo pro přihlášení do Opengist pomocí HTTP
settings.change-password: Změnit heslo
settings.change-password-help: Změňte své heslo pro přihlášení do Opengist pomocí HTTP
settings.password-label-title: Heslo
auth.signup-disabled: Správce zakázal registraci
auth.login: Přihlásit se
auth.signup: Registrovat
auth.new-account: Nový účet
auth.username: Uživatelské jméno
auth.password: Heslo
auth.register-instead: Raději se zaregistrovat
auth.login-instead: Raději se přihlásit
auth.oauth: Pokračovat s účtem na %s
error: Chyba
header.menu.all: Všechno
header.menu.new: Nové
header.menu.search: Hledat
header.menu.my-gists: Moje gisty
header.menu.liked: Lajknuté
header.menu.admin: Administrace
header.menu.settings: Nastavení
header.menu.logout: Odhlásit se
header.menu.register: Registrovat
header.menu.login: Přihlásit se
header.menu.light: Světlý
header.menu.dark: Tmavý
header.menu.system: Systém
footer.powered-by: Vytvořeno pomocí %s
pagination.older: Starší
pagination.newer: Novější
pagination.previous: Předchozí
pagination.next: Další
admin.admin_panel: Administrační panel
admin.general: Obecné
admin.users: Uživatelé
admin.gists: Gisty
admin.configuration: Konfigurace
admin.versions: Verze
admin.ssh_keys: SSH klíče
admin.stats: Statistiky
admin.actions: Akce
admin.actions.sync-fs: Synchronizovat gisty ze souborového systému
admin.actions.sync-db: Synchronizovat gisty z databáze
admin.actions.git-gc: Garbage collect git repozitářů
admin.id: ID
admin.user: Uživatel
admin.delete: Smazat
admin.created_at: Vytvořeno
admin.config-link: Tato konfigurace může být %s pomocí YAML konfiguračního souboru a/nebo prostřednictvím proměnných prostředí.
admin.config-link-overriden: přepsána
admin.disable-signup: Zakázat registraci
admin.disable-signup_help: Zakázat vytváření nových účtů.
admin.require-login: Vyžadovat přihlášení
admin.require-login_help: Vynutit, aby uživatelé byli přihlášeni k zobrazení gistů.
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-gravatar: Zakázat Gravatar
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.gists.title: Titulek
admin.gists.private: Soukromé?
admin.gists.nb-files: Počet souborů
admin.gists.nb-likes: Počet lajků
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

@@ -17,19 +17,20 @@ gist.header.clone-http: Clone via %s
gist.header.clone-http-help: Clone with Git using HTTP basic authentication.
gist.header.clone-ssh: Clone via SSH
gist.header.clone-ssh-help: Clone with Git using an SSH key.
gist.header.share: Share
gist.header.share-help: Copy shareable link for this gist.
gist.header.embed: Embed
gist.header.embed-help: Embed this gist to your website.
gist.header.download-zip: Download ZIP
gist.raw: Raw
gist.file-truncated: This file has been truncated.
gist.watch-full-file: View the full file.
gist.file-not-valid: This file is not a valid CSV file.
gist.no-content: No content
gist.no-content: No files found
gist.new.new_gist: New gist
gist.new.title: Title
gist.new.description: Description
gist.new.url: URL
gist.new.filename-with-extension: Filename with extension
gist.new.indent-mode: Indent mode
gist.new.indent-mode-space: Space
@@ -42,12 +43,16 @@ gist.new.add-file: Add file
gist.new.create-public-button: Create public gist
gist.new.create-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist
gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist
gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
gist.edit.change-visibility: Make
gist.edit.delete: Delete
gist.edit.cancel: Cancel
gist.edit.save: Save
gist.delete.confirm: Are you sure you want to delete this gist ?
gist.list.joined: Joined
gist.list.all: All gists
@@ -66,13 +71,26 @@ gist.list.forks: forks
gist.list.files: files
gist.list.last-active: Last active
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.search.found: gists found
gist.search.no-results: No gists found
gist.search.help.user: gists created by user
gist.search.help.title: gists with given title
gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language
gist.forks: Forks
gist.forks.view: View fork
gist.forks.no: No public forks
gist.forks.for: Forks for %s
gist.likes: Likes
gist.likes.no: No likes yet
gist.likes.for: Likes for %s
gist.revisions: Revisions
gist.revision.revised: revised this gist
@@ -85,6 +103,7 @@ gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: Empty file
gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s
settings: Settings
settings.email: Email
@@ -92,8 +111,10 @@ settings.email-help: Used for commits and Gravatar
settings.email-set: Set email
settings.link-accounts: Link accounts
settings.link-github-account: Link GitHub account
settings.link-gitlab-account: Link GitLab account
settings.link-gitea-account: Link Gitea account
settings.unlink-github-account: Unlink GitHub account
settings.unlink-gitlab-account: Unlink GitLab account
settings.unlink-gitea-account: Unlink Gitea account
settings.delete-account: Delete account
settings.delete-account-confirm: Are you sure you want to delete your account ?
@@ -106,6 +127,8 @@ settings.delete-ssh-key-confirm: Confirm deletion of SSH key
settings.ssh-key-added-at: Added
settings.ssh-key-never-used: Never used
settings.ssh-key-last-used: Last used
settings.ssh-key-exists: SSH key already exists
settings.change-username: Change username
settings.create-password: Create password
settings.create-password-help: Create your password to login to Opengist via HTTP
settings.change-password: Change password
@@ -120,10 +143,53 @@ auth.username: Username
auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
auth.github-oauth: Continue with GitHub account
auth.gitea-oauth: Continue with Gitea account
auth.oauth: Continue with %s account
auth.mfa: Multi-factor authentication
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.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.new: New
@@ -150,13 +216,18 @@ admin.general: General
admin.users: Users
admin.gists: Gists
admin.configuration: Configuration
admin.invitations: Invitations
admin.invitations.create: Create invitation
admin.versions: Versions
admin.ssh_keys: SSH keys
admin.stats: Stats
admin.actions: Actions
admin.actions.sync-fs: Synchronize gists from filesystem
admin.actions.sync-db: Synchronize gists from database
admin.actions.git-gc: Garbage collect git repositories
admin.actions.git-gc: Garbage collect all git repositories
admin.actions.sync-previews: Synchronize all gists previews
admin.actions.reset-hooks: Reset Git server hooks for all repositories
admin.actions.index-gists: Index all gists
admin.id: ID
admin.user: User
admin.delete: Delete
@@ -168,6 +239,8 @@ admin.disable-signup: Disable signup
admin.disable-signup_help: Forbid the creation of new accounts.
admin.require-login: Require login
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_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar
@@ -180,3 +253,55 @@ admin.gists.private: Private ?
admin.gists.nb-files: Nb. files
admin.gists.nb-likes: Nb. likes
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.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
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-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
gist.header.share: Compartir
gist.header.share-help: Copiar enlace para compartir este gist.
gist.header.embed: 'Embeber'
gist.header.embed-help: 'Embebe este gist en tu sitio web.'
gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato
@@ -71,8 +71,8 @@ gist.forks: Bifurcaciones
gist.forks.view: Ver bifurcación
gist.forks.no: No hay bifurcaciones públicas
gist.likes: Gustos
gist.likes.no: Aún no hay gustos
gist.likes: Todos los me gusta
gist.likes.no: Aún no hay me gusta
gist.revisions: Revisiones
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-deleted: archivo eliminado
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.empty-file: Archivo vacío
gist.revision.no-changes: Sin cambios
@@ -115,8 +115,7 @@ auth.username: Nombre de usuario
auth.password: Contraseña
auth.register-instead: Registrarse en su lugar
auth.login-instead: Iniciar sesión en su lugar
auth.github-oauth: Continuar con cuenta de GitHub
auth.gitea-oauth: Continuar con cuenta de Gitea
auth.oauth: Continuar con cuenta de %s
error: Error
@@ -158,7 +157,6 @@ admin.delete: Eliminar
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-overridden: sobrescrito
admin.disable-signup: Deshabilitar registro
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
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-gravatar: Deshabilitar Gravatar
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.gists.title: Título
@@ -175,3 +174,123 @@ admin.gists.private: ¿Privado?
admin.gists.nb-files: Núm. de archivos
admin.gists.nb-likes: Núm. de gustos
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

@@ -17,15 +17,15 @@ gist.header.clone-http: Cloner via %s
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic.
gist.header.clone-ssh: Cloner via SSH
gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH.
gist.header.share: Partager
gist.header.share-help: Copier le lien partageable de ce gist.
gist.header.embed: Intégrer
gist.header.embed-help: Intégrer ce gist dans une page web.
gist.header.download-zip: Télécharger en ZIP
gist.raw: Brut
gist.file-truncated: Ce fichier a été tronqué.
gist.watch-full-file: Voir le fichier complet.
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
gist.no-content: Pas de contenu
gist.no-content: Aucun fichier
gist.new.new_gist: Nouveau gist
gist.new.title: Titre
@@ -115,8 +115,7 @@ auth.username: Nom d'utilisateur
auth.password: Mot de passe
auth.register-instead: Je préfère m'inscrire
auth.login-instead: Je préfère me connecter
auth.github-oauth: Continuer avec un compte GitHub
auth.gitea-oauth: Continuer avec un compte Gitea
auth.oauth: Continuer avec un compte %s
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-gravatar: Désactiver Gravatar
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.gists.title: Titre
@@ -175,3 +175,87 @@ admin.gists.private: Privé ?
admin.gists.nb-files: Nb. de fichiers
admin.gists.nb-likes: Nb. de j'aime
admin.gists.delete_confirm: Voulez-vous supprimer ce gist ?
gist.search.help.user: gists créés par un utilisateur
gist.search.help.title: gists avec un titre spécifique
gist.search.help.extension: gists qui ont des fichiers avec une extension spécifique
gist.search.found: gists trouvés
gist.search.help.filename: gists qui ont des fichiers avec un nom spécifique
settings.link-gitlab-account: Lier le compte GitLab
gist.search.help.language: gists qui ont des fichiers écrits en un langage spécifique
settings.change-username: Changer le nom d'utilisateur
settings.create-password: Créer un mot de passe
settings.create-password-help: Créer un mot de passe pour se connecter à Opengist via HTTP
settings.change-password: Changer le mot de passe
settings.change-password-help: Changer le mot de passe pour se connecter à Opengist via HTTP
settings.password-label-title: Mot de passe
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
gist.new.url: URL
gist.search.no-results: Aucun gist trouvé
settings.unlink-gitlab-account: Détacher le compte GitLab
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-ssh: Clone-ozás SSH-n keresztül
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
gist.header.share: Megosztás
gist.header.share-help: Másold ki ennek a gistnek a megosztható linkjét
gist.header.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: ZIP archívum letöltése
gist.raw: Eredeti
@@ -30,6 +30,7 @@ gist.no-content: Nincs tartalom
gist.new.new_gist: Új gist
gist.new.title: Cím
gist.new.description: Leírás
gist.new.url: URL
gist.new.filename-with-extension: Fájlnév kiterjesztéssel
gist.new.indent-mode: Indentáció típusa
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.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.view: Fork megtekintése
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.link-accounts: Fiókok összekötése
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.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.delete-account: Fiók törlése
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
@@ -106,6 +117,12 @@ 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-never-used: Sosem használt
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-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-help: Változtasd meg a jelszót, amivel bejelentkezel az OpenGist-be HTTP-n keresztül
settings.password-label-title: Jelszó
auth.signup-disabled: Az adminisztrátor kikapcsolta a regisztrációkat
auth.login: Bejelentkezés
@@ -115,8 +132,7 @@ auth.username: Felhasználónév
auth.password: Jelszó
auth.register-instead: Vagy regisztrálj
auth.login-instead: Vagy jelentkezz be
auth.github-oauth: Folytatás GitHub fiókkal
auth.gitea-oauth: Folytatás Gitea fiókkal
auth.oauth: Folytatás %s fiókkal
error: Hiba
@@ -152,6 +168,9 @@ admin.actions: Műveletek
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
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.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.user: Felhasználó
admin.delete: Törlés
@@ -167,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-gravatar: Gravatar kikapcsolása
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.gists.title: Cím
@@ -175,3 +195,66 @@ admin.gists.private: Privát ?
admin.gists.nb-files: Fájlok száma
admin.gists.nb-likes: Kedv. száma
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

@@ -0,0 +1,259 @@
gist.public: Público
gist.unlisted: Não listado
gist.private: Privado
gist.header.like: Curtir
gist.header.unlike: Não curtir
gist.header.fork: Bifurcar
gist.header.edit: Editar
gist.header.delete: Excluir
gist.header.forked-from: Bifurcado de
gist.header.last-active: Última atividade
gist.header.select-tab: Selecionar aba
gist.header.code: Código
gist.header.revisions: Revisões
gist.header.revision: Revisão
gist.header.clone-http: Clonar via %s
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-help: Clonar com Git usando uma chave SSH.
gist.header.download-zip: Baixar ZIP
gist.raw: Bruto
gist.file-truncated: Este arquivo foi truncado.
gist.watch-full-file: Ver arquivo completo.
gist.file-not-valid: Este arquivo não é um arquivo CSV válido.
gist.no-content: Sem conteúdo
gist.new.new_gist: Novo gist
gist.new.title: Título
gist.new.description: Descrição
gist.new.filename-with-extension: Nome do arquivo com extensão
gist.new.indent-mode: Modo de indentação
gist.new.indent-mode-space: Espaço
gist.new.indent-mode-tab: Tabulação
gist.new.indent-size: Tamanho da indentação
gist.new.wrap-mode: Modo de quebra
gist.new.wrap-mode-no: Sem quebra
gist.new.wrap-mode-soft: Quebra suave
gist.new.add-file: Adicionar arquivo
gist.new.create-public-button: Criar gist público
gist.new.create-unlisted-button: Criar gist não listado
gist.new.create-private-button: Criar gist privado
gist.edit.editing: Editando
gist.edit.change-visibility: Alterar visibilidade
gist.edit.delete: Excluir
gist.edit.cancel: Cancelar
gist.edit.save: Salvar
gist.list.joined: Juntou-se
gist.list.all: Todos os gists
gist.list.search-results: Resultados da busca
gist.list.sort: Ordenar
gist.list.sort-by-created: criado
gist.list.sort-by-updated: atualizado
gist.list.order-by-asc: Menos recente
gist.list.order-by-desc: Mais recente
gist.list.select-tab: Selecionar aba
gist.list.liked: Curtido
gist.list.likes: curtidas
gist.list.forked: Bifurcado
gist.list.forked-from: Bifurcado de
gist.list.forks: bifurcações
gist.list.files: arquivos
gist.list.last-active: Última atividade
gist.list.no-gists: Sem gists
gist.forks: Bifurcações
gist.forks.view: Ver bifurcação
gist.forks.no: Não há bifurcações públicas
gist.likes: Curtidas
gist.likes.no: Ainda não há curtidas
gist.revisions: Revisões
gist.revision.revised: revisou este gist
gist.revision.go-to-revision: Ir para a revisão
gist.revision.file-created: arquivo criado
gist.revision.file-deleted: arquivo excluído
gist.revision.file-renamed: renomeado para
gist.revision.diff-truncated: Diferença truncada porque é muito grande para ser exibida.
gist.revision.file-renamed-no-changes: Arquivo renomeado sem alterações
gist.revision.empty-file: Arquivo vazio
gist.revision.no-changes: Sem alterações
gist.revision.no-revisions: Não há revisões para mostrar
settings: Configurações
settings.email: E-mail
settings.email-help: Usado para confirmações e Gravatar
settings.email-set: Configurar e-mail
settings.link-accounts: Vincular contas
settings.link-github-account: Vincular conta do GitHub
settings.link-gitea-account: Vincular conta do Gitea
settings.unlink-github-account: Desvincular conta do GitHub
settings.unlink-gitea-account: Desvincular conta do Gitea
settings.delete-account: Excluir conta
settings.delete-account-confirm: Tem certeza de que deseja excluir sua conta?
settings.add-ssh-key: Adicionar chave SSH
settings.add-ssh-key-help: Usado apenas para extrair/puxar gists usando Git via SSH
settings.add-ssh-key-title: Título
settings.add-ssh-key-content: Chave
settings.delete-ssh-key: Excluir
settings.delete-ssh-key-confirm: Confirmar exclusão da chave SSH
settings.ssh-key-added-at: Adicionado
settings.ssh-key-never-used: Nunca usado
settings.ssh-key-last-used: Último uso
auth.signup-disabled: O administrador desabilitou o registro
auth.login: Entrar
auth.signup: Cadastrar-se
auth.new-account: Nova conta
auth.username: Nome de usuário
auth.password: Senha
auth.register-instead: Registrar-se no lugar
auth.login-instead: Entrar no lugar
auth.oauth: Continuar com conta do %s
error: Erro
header.menu.all: Todos
header.menu.new: Novo
header.menu.search: Buscar
header.menu.my-gists: Meus gists
header.menu.liked: Curtidos
header.menu.admin: Administrador
header.menu.settings: Configurações
header.menu.logout: Sair
header.menu.register: Registrar-se
header.menu.login: Entrar
header.menu.light: Claro
header.menu.dark: Escuro
header.menu.system: Sistema
footer.powered-by: Desenvolvido por %s
pagination.older: Anterior
pagination.newer: Próximo
pagination.previous: Anterior
pagination.next: Próximo
admin.admin_panel: Painel de administração
admin.general: Geral
admin.users: Usuários
admin.gists: Gists
admin.configuration: Configuração
admin.versions: Versões
admin.ssh_keys: Chaves SSH
admin.stats: Estatísticas
admin.actions: Ações
admin.actions.sync-fs: Sincronizar gists do sistema de arquivos
admin.actions.sync-db: Sincronizar gists do banco de dados
admin.actions.git-gc: Coletar lixo nos repositórios Git
admin.id: ID
admin.user: Usuário
admin.delete: Excluir
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.disable-signup: Desabilitar registro
admin.disable-signup_help: Proibir a criação de novas contas.
admin.require-login: Exigir login
admin.require-login_help: Obrigar os usuários a fazerem login para ver gists.
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-gravatar: Desabilitar Gravatar
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.gists.title: Título
admin.gists.private: Privado
admin.gists.nb-files: Núm. de arquivos
admin.gists.nb-likes: Núm. de curtidas
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-ssh: Клонировать c помощью SSH
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
gist.header.share: Поделиться
gist.header.share-help: Скопировать ссылку на фрагмент.
gist.header.embed: 'Встроить'
gist.header.embed-help: 'Встроить этот фрагмент в ваш веб-сайт.'
gist.header.download-zip: Скачать ZIP-архив
gist.raw: Исходник
@@ -115,8 +115,7 @@ auth.username: Имя пользователя
auth.password: Пароль
auth.register-instead: Зарегистрироваться
auth.login-instead: Войти
auth.github-oauth: Войти с помощью доступа GitHub
auth.gitea-oauth: Войти с помощью доступа Gitea
auth.oauth: Войти с помощью доступа %s
error: Ошибка
@@ -167,7 +166,8 @@ admin.disable-login: Запретить авторизацию по паролю
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
admin.disable-gravatar: Запретить Gravatar
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
admin.gists.title: Название
@@ -175,3 +175,85 @@ admin.gists.private: Приватный
admin.gists.nb-files: Файлов
admin.gists.nb-likes: Понравилось
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: ''

View File

@@ -0,0 +1,268 @@
gist.public: Herkese Açık
gist.unlisted: Listelenmemiş
gist.private: Gizli
gist.header.like: Beğen
gist.header.unlike: Beğenmekten Vazgeç
gist.header.fork: Çatalla
gist.header.edit: Düzenle
gist.header.delete: Sil
gist.header.forked-from: Çatallandı
gist.header.last-active: Son aktivite
gist.header.select-tab: Bir sekme seç
gist.header.code: Kod
gist.header.revisions: Revizyonlar
gist.header.revision: Revizyon
gist.header.clone-http: '%s ile klonla'
gist.header.clone-http-help: HTTP temel kimlik doğrulamasını kullanarak Git ile klonlayın.
gist.header.clone-ssh: SSH aracılığıyla klonla
gist.header.clone-ssh-help: Bir SSH anahtarı kullanarak Git ile klonlayın.
gist.header.embed: Göm
gist.header.embed-help: Bu gisti web sitenize yerleştirin.
gist.header.download-zip: ZIP'i İndir
gist.raw: Ham
gist.file-truncated: Bu dosya kısaltılmıştır.
gist.watch-full-file: Dosyanın tamamını görüntüleyin.
gist.file-not-valid: Bu dosya geçerli bir CSV dosyası değildir.
gist.no-content: Dosya bulunamadı
gist.new.new_gist: Yeni gist
gist.new.title: Başlık
gist.new.description: ıklama
gist.new.url: URL
gist.new.filename-with-extension: Dosya adı ve uzantısı
gist.new.indent-mode: Girinti modu
gist.new.indent-mode-space: Boşluk
gist.new.indent-mode-tab: Sekme
gist.new.indent-size: Girinti boyutu
gist.new.wrap-mode: 'Satır kaydırma modu'
gist.new.wrap-mode-no: 'Kaydırma yok'
gist.new.wrap-mode-soft: 'Yumuşak kaydırma'
gist.new.add-file: Dosya ekle
gist.new.create-public-button: Herkese açık gist oluştur
gist.new.create-unlisted-button: Liste dışı gist oluştur
gist.new.create-private-button: Gizli gist oluştur
gist.new.preview: Önizle
gist.new.create-a-new-gist: Yeni bir gist oluştur
gist.edit.editing: Düzenleniyor
gist.edit.edit-gist: '%s düzenle'
gist.edit.change-visibility: 'Yap'
gist.edit.delete: Sil
gist.edit.cancel: İptal Et
gist.edit.save: Kaydet
gist.list.joined: Katıldı
gist.list.all: Tüm gistler
gist.list.search-results: Arama sonuçları
gist.list.sort: Sırala
gist.list.sort-by-created: oluşturuldu
gist.list.sort-by-updated: güncellendi
gist.list.order-by-asc: En eski
gist.list.order-by-desc: Yakın zamanda
gist.list.select-tab: Bir sekme seçin
gist.list.liked: Beğenildi
gist.list.likes: beğeniler
gist.list.forked: Çatallandı
gist.list.forked-from: Çatallandı
gist.list.forks: çatallar
gist.list.files: dosyalar
gist.list.last-active: Son aktivite
gist.list.no-gists: Gist yok
gist.list.all-liked-by: '%s tarafından beğenilen tüm gistler'
gist.list.all-forked-by: '%s tarafından çatallanan tüm gistler'
gist.list.all-from: '%s oluşturduğu tüm gistler'
gist.search.found: gist bulundu
gist.search.no-results: Hiç gist bulunamadı
gist.search.help.user: kullanıcı tarafından oluşturulan gistler
gist.search.help.title: belirtilen isme sahip gistler
gist.search.help.filename: belirtilen isimde dosyaları olan gistler
gist.search.help.extension: belirtilen uzantıya sahip dosyalara sahip gistler
gist.search.help.language: belirtilen dilde dosyaları olan gistler
gist.forks: Çatallar
gist.forks.view: Çatalı görüntüle
gist.forks.no: Herkes açık çatal yok
gist.forks.for: '%s için çatallar'
gist.likes: Beğeniler
gist.likes.no: Henüz kimse beğenmedi
gist.likes.for: '%s için beğeniler'
gist.revisions: Düzenlemeler
gist.revision.revised: bu gisti düzenledi
gist.revision.go-to-revision: Düzenlemeye git
gist.revision.file-created: dosya oluşturuldu
gist.revision.file-deleted: dosya silindi
gist.revision.file-renamed: yeniden adlandırıldı
gist.revision.diff-truncated: Fark gösterilemeyecek kadar büyük
gist.revision.file-renamed-no-changes: Dosya değişiklik yapılmadan yeniden adlandırıldı
gist.revision.empty-file: Boş dosya
gist.revision.no-changes: Değişiklik yok
gist.revision.no-revisions: Gösterilecek düzenleme yok
gist.revision-of: '%s düzenlemesi'
settings: Ayarlar
settings.email: E-Posta
settings.email-help: Commit ve Gravatar için kullanılır
settings.email-set: E-postayı ayarla
settings.link-accounts: Hesapları bağla
settings.link-github-account: GitHub hesabını bağla
settings.link-gitlab-account: GitLab hesabını bağla
settings.link-gitea-account: Gitea hesabını bağla
settings.unlink-github-account: GitHub hesabının bağlantısını kaldır
settings.unlink-gitlab-account: GitLab hesabının bağlantısını kaldır
settings.unlink-gitea-account: Gitea hesabının bağlantısını kaldır
settings.delete-account: Hesabı sil
settings.delete-account-confirm: Hesabını silmek istediğinden emin misin?
settings.add-ssh-key: SSH anahtarı ekle
settings.add-ssh-key-help: Sadece SSH üzerinden Git kullanarak gistlerin pull/push işlemlerinde kullanılır
settings.add-ssh-key-title: Başlık
settings.add-ssh-key-content: Anahtar
settings.delete-ssh-key: Sil
settings.delete-ssh-key-confirm: SSH anahtarının silinmesini onaylayın
settings.ssh-key-added-at: Eklendi
settings.ssh-key-never-used: Hiç kullanılmadı
settings.ssh-key-last-used: Son kullanma
settings.change-username: Kullanıcı adını değiştir
settings.create-password: Parola oluştur
settings.create-password-help: HTTP üzerinden Opengist'e giriş yapmak için parolanızı oluşturun
settings.change-password: Parolayı değiştir
settings.change-password-help: HTTP üzerinden Opengist'e giriş yapmak için parolanızı değiştirin
settings.password-label-title: Parola
auth.signup-disabled: Yönetici kaydolmayı devre dışı bıraktı
auth.login: Giriş
auth.signup: Kaydol
auth.new-account: Yeni hesap
auth.username: Kullanıcı adı
auth.password: Parola
auth.register-instead: Bunun yerine kayıt olun
auth.login-instead: Bunun yerine giriş yapın
auth.oauth: '%s hesabı ile devam et'
error: Hata
error.page-not-found: Sayfa bulunamadı
error.bad-request: Geçersiz istek
error.signup-disabled: Kullanıcı kaydı devre dışı bırakıldı
error.signup-disabled-form: Kayıt formu aracılığıyla kullanıcı kaydı devre dışı bırakıldı
error.login-disabled-form: Giriş formu üzerinden giriş yapma devre dışı bırakıldı
error.complete-oauth-login: "Kullanıcı kimlik doğrulaması tamamlanamıyor: %s"
error.oauth-unsupported: Desteklenmeyen sağlayıcı
error.cannot-bind-data: Veri bağlanamıyor
error.invalid-number: Geçersiz numara
error.invalid-character-unescaped: Geçersiz karakter için kaçış işlemi yapılamadı
header.menu.all: Tümü
header.menu.new: Yeni
header.menu.search: Ara
header.menu.my-gists: Gistlerim
header.menu.liked: Beğenilen
header.menu.admin: Yönetici
header.menu.settings: Ayarlar
header.menu.logout: Çıkış
header.menu.register: Kaydol
header.menu.login: Giriş
header.menu.light: ık
header.menu.dark: Koyu
header.menu.system: Sistem
footer.powered-by: '%s tarafından desteklenmektedir'
pagination.older: Daha eski
pagination.newer: Daha yeni
pagination.previous: Önceki
pagination.next: Sonraki
admin.admin_panel: Yönetici paneli
admin.general: Genel
admin.users: Kullanıcılar
admin.gists: Gistler
admin.configuration: Yapılandırma
admin.invitations: Davetler
admin.invitations.create: Davet oluştur
admin.versions: Sürümler
admin.ssh_keys: SSH anahtarları
admin.stats: İstatistikler
admin.actions: Eylemler
admin.actions.sync-fs: Gistleri dosya sisteminden senkronize et
admin.actions.sync-db: Gistleri veri tabanından senkronize et
admin.actions.git-gc: Tüm Git depolarındaki gereksiz verileri temizle
admin.actions.sync-previews: Tüm gist önizlemelerini senkronize et
admin.actions.reset-hooks: Tüm depolar için Git sunucu kancalarını sıfırla
admin.actions.index-gists: Tüm gistleri indeksle
admin.id: ID
admin.user: Kullanıcı
admin.delete: Sil
admin.created_at: Oluşturulma
admin.config-link: Bu yapılandırma, bir YAML yapılandırma dosyası ve/veya ortam değişkenleri tarafından %s.
admin.config-link-overriden: geçersiz kılınabilir
admin.disable-signup: Kayıt işlemini devre dışı bırak
admin.disable-signup_help: Yeni hesap oluşturulmasını yasaklar.
admin.require-login: Giriş gerektir
admin.require-login_help: Gistleri görebilmek için kullanıcıların oturum açmasını zorunlu kılar.
admin.disable-login: Giriş formunu devre dışı bırak
admin.disable-login_help: Giriş formunu kullanarak giriş yapmayı yasaklar ve bunun yerine OAuth sağlayıcılarının kullanılmasını zorunlu hale getirir.
admin.disable-gravatar: Gravatar'ı devre dışı bırak
admin.disable-gravatar_help: Gravatar'ın avatar sağlayıcı olarak kullanımını devre dışı bırakır.
admin.allow-gists-without-login: Bireysel gistlere oturum açmadan izin ver
admin.allow-gists-without-login_help: Bireysel gistlerin oturum açmadan görüntülenmesine ve indirilmesine izin verir, ancak gistleri keşfetmek için oturum açmayı zorunlu kılar.
admin.users.delete_confirm: Bu kullanıcıyı silmek istiyor musun?
admin.gists.title: Başlık
admin.gists.private: Gizlilik
admin.gists.nb-files: Dosyalar
admin.gists.nb-likes: Beğeniler
admin.gists.delete_confirm: Bu gisti silmek istiyor musun?
admin.invitations.help: Kayıt olma özelliği devre dışı bırakılmış olsa bile, hesap oluşturmak için davetiyeler kullanılabilir.
admin.invitations.max_uses: Maksimum kullanım
admin.invitations.expires_at: Sona erme tarihi
admin.invitations.code: Kod
admin.invitations.copy_link: Linki kopyala
admin.invitations.uses: Kullanım
admin.invitations.expired: Süresi bitti
flash.admin.user-deleted: Kullanıcı silindi
flash.admin.gist-deleted: Gist silindi
flash.admin.invitation-created: Davetiye oluşturuldu
flash.admin.invitation-deleted: Davetiye silindi
flash.admin.sync-fs: Depolar dosya sisteminden senkronize ediliyor...
flash.admin.sync-db: Depolar veri tabanından senkronize ediliyor...
flash.admin.git-gc: Depolardan gereksiz veriler temizleniyor...
flash.admin.sync-previews: Gist önizlemeleri senkronize ediliyor...
flash.admin.reset-hooks: Tüm depolar için Git sunucusu kancaları sıfırlanıyor...
flash.admin.index-gists: Tüm gistler indeksleniyor...
flash.auth.username-exists: Kullanıcı adı zaten mevcut
flash.auth.invalid-credentials: Geçersiz kimlik bilgileri
flash.auth.account-linked-oauth: Hesap %s ile bağlantılı
flash.auth.account-unlinked-oauth: Hesabın %s ile bağlantısı kaldırıldı
flash.auth.user-sshkeys-not-retrievable: Kullanıcı anahtarları alınamadı
flash.auth.user-sshkeys-not-created: SSH anahtarı oluşturulamadı
flash.auth.must-be-logged-in: Gistlere erişmek için giriş yapmış olmanız gerekli
flash.gist.visibility-changed: Gist görünürlüğü değiştirildi
flash.gist.deleted: Gist silindi
flash.gist.fork-own-gist: Kendi gistlerin çatallanamaz
flash.gist.forked: Gist çatallandı
flash.user.email-updated: E-posta güncellendi
flash.user.invalid-ssh-key: Geçersiz SSH anahtarı
flash.user.ssh-key-added: SSH anahtarı eklendi
flash.user.ssh-key-deleted: SSH anahtarı silindi
flash.user.password-updated: Parola güncellendi
flash.user.username-updated: Kullanıcı adı güncellendi
validation.is-too-long: '%s alanı çok uzun'
validation.should-not-be-empty: '%s alanı boş olmamalı'
validation.should-not-include-sub-directory: '%s alanı bir alt dizin içermemeli'
validation.should-only-contain-alphanumeric-characters: '%s alanı yalnızca alfanümerik karakterler içermelidir'
validation.should-only-contain-alphanumeric-characters-and-dashes: '%s alanı yalnızca alfanümerik karakterler ve tire içermelidir'
validation.not-enough: Yeterli %s yok
validation.invalid: Geçersiz %s
html.title.admin-panel: Yönetici paneli
settings.ssh-key-exists: SSH anahtarı zaten mevcut

View File

@@ -0,0 +1,269 @@
gist.public: Публічний
gist.unlisted: Прихований
gist.private: Приватний
gist.header.like: Подобається
gist.header.unlike: Не подобається
gist.header.fork: Створити форк
gist.header.edit: Редагувати
gist.header.delete: Видалити
gist.header.forked-from: Форк з
gist.header.last-active: Остання активність
gist.header.select-tab: Перейти
gist.header.code: Код
gist.header.revisions: Версії
gist.header.revision: Версія
gist.header.clone-http: Клонувати за допомогою %s
gist.header.clone-http-help: Клонувати за допомогою Git з використанням аутентифікації HTTP.
gist.header.clone-ssh: Клонувати за допомогою SSH
gist.header.clone-ssh-help: Клонувати за допомогою Git з використанням ключа SSH.
gist.header.embed: 'Вбудувати'
gist.header.embed-help: 'Вбудувати цей gist до вашого веб-сайту.'
gist.header.download-zip: Скачати ZIP-архів
gist.raw: Неформатований
gist.file-truncated: Цей файл було обрізано.
gist.watch-full-file: Перегляд всього файла.
gist.file-not-valid: Невалідний CSV.
gist.no-content: Немає даних
gist.new.new_gist: Новий gist
gist.new.title: Назва
gist.new.description: Опис
gist.new.url: URL
gist.new.filename-with-extension: Ім'я файла з розширенням
gist.new.indent-mode: Режим відступів
gist.new.indent-mode-space: Пробіли
gist.new.indent-mode-tab: Табуляція
gist.new.indent-size: Розмір відступа
gist.new.wrap-mode: Переноси рядків
gist.new.wrap-mode-no: Без переносів
gist.new.wrap-mode-soft: М'які переноси
gist.new.add-file: Додати файл
gist.new.create-public-button: Створити публічний gist
gist.new.create-unlisted-button: Створити прихований gist
gist.new.create-private-button: Створити приватний gist
gist.new.preview: Перегляд
gist.new.create-a-new-gist: Створити новий gist
gist.edit.editing: Редагування
gist.edit.edit-gist: Редагувати %s
gist.edit.change-visibility: Зробити
gist.edit.delete: Видалити
gist.edit.cancel: Скасувати
gist.edit.save: Зберегти
gist.list.joined: Зареєстрован
gist.list.all: Всі gist
gist.list.search-results: Результати пошуку
gist.list.sort: Сортування
gist.list.sort-by-created: створені
gist.list.sort-by-updated: оновлені
gist.list.order-by-asc: Нещодавні знизу
gist.list.order-by-desc: Нещодавні зверху
gist.list.select-tab: Оберіть вкладку
gist.list.liked: Вподобані
gist.list.likes: вподобань
gist.list.forked: Форки
gist.list.forked-from: Форки з
gist.list.forks: форк(-ів)
gist.list.files: файл(-ів)
gist.list.last-active: Остання активність
gist.list.no-gists: Немає gists
gist.list.all-liked-by: Всі gists вподобані %s
gist.list.all-forked-by: Всі gists форкнуті by %s
gist.list.all-from: Всі gists від %s
gist.search.found: gists знайдено
gist.search.no-results: Не знайдено gists
gist.search.help.user: gists створені користувачем
gist.search.help.title: gists з наданим ім'ям
gist.search.help.filename: gists мають файли з наданим ім'ям
gist.search.help.extension: gists мають файли з наданим розширенням
gist.search.help.language: gists мають файли з наданою мовою
gist.forks: Форки
gist.forks.view: Подивитися форк
gist.forks.no: Немає форків
gist.forks.for: Форки для %s
gist.likes: Подобається
gist.likes.no: Ще немає вподобань
gist.likes.for: Вподобання для %s
gist.revisions: Ревизії
gist.revision.revised: ревизій цього gist
gist.revision.go-to-revision: До ревизії
gist.revision.file-created: файл створено
gist.revision.file-deleted: файл видалено
gist.revision.file-renamed: перейменовано в
gist.revision.diff-truncated: Різниця завелика для відображення
gist.revision.file-renamed-no-changes: Файл перейменовано без змін
gist.revision.empty-file: Пустий файл
gist.revision.no-changes: Без змін
gist.revision.no-revisions: Немає ревізій для відображення
gist.revision-of: Ревізії %s
settings: Налаштування
settings.email: Адреса електронної пошти
settings.email-help: Використовується для коммітів та Gravatar
settings.email-set: Зберегти адресу електронної пошти
settings.link-accounts: Підключення акаунтів
settings.link-github-account: Підключити GitHub акаунт
settings.link-gitlab-account: Підключити GitLab акаунт
settings.link-gitea-account: Підключити Gitea акаунт
settings.unlink-github-account: Відключити GitHub акаунт
settings.unlink-gitlab-account: Відключити GitLab акаунт
settings.unlink-gitea-account: Відключити Gitea акаунт
settings.delete-account: Видалити акаунт
settings.delete-account-confirm: Ви впевненні, що хочете видалити свій акаунт?
settings.add-ssh-key: Додати SSH ключ
settings.add-ssh-key-help: Використовується только для pull/push gists при використанні Git з SSH
settings.add-ssh-key-title: Назва
settings.add-ssh-key-content: Ключ
settings.delete-ssh-key: Видалити
settings.delete-ssh-key-confirm: Підтвердьте видалення ключа SSH
settings.ssh-key-added-at: Додано
settings.ssh-key-never-used: Не використовувася
settings.ssh-key-last-used: Останнє використання
settings.ssh-key-exists: SSH ключ вже існує
settings.change-username: Змінити им'я користувача
settings.create-password: Створити пароль
settings.create-password-help: Створити ваш пароль для логіну в Opengist через HTTP
settings.change-password: Створити пароль
settings.change-password-help: Змінити ваш пароль для логіну в Opengist через HTTP
settings.password-label-title: Пароль
auth.signup-disabled: Адміністратор відключив реєстрацію
auth.login: Вхід
auth.signup: Реєстрація
auth.new-account: Новий акаунт
auth.username: Им'я користувача
auth.password: Пароль
auth.register-instead: Зареєструватися
auth.login-instead: Увійти
auth.oauth: Продовжити з %s акаунтом
error: Помилка
error.page-not-found: Сторінка не знайдена
error.bad-request: Невірний запрос
error.signup-disabled: Реєстрацію вимкнено
error.signup-disabled-form: Реєстрацію через форму вимкнено
error.login-disabled-form: Логін через форму логіна вимкнено
error.complete-oauth-login: "Неможливо виконати авторизацію користувача: %s"
error.oauth-unsupported: Провайдер не підтримується
error.cannot-bind-data: Не вдається зв'язати дані
error.invalid-number: Недійсний номер
error.invalid-character-unescaped: Неправильний символ не екранований
header.menu.all: Все
header.menu.new: Новий
header.menu.search: Пошук
header.menu.my-gists: Мої gists
header.menu.liked: Сподобалися
header.menu.admin: Адміністрування
header.menu.settings: Налаштування
header.menu.logout: Вийти
header.menu.register: Реєстрація
header.menu.login: Увійти
header.menu.light: Світла
header.menu.dark: Темна
header.menu.system: Системна
footer.powered-by: Працює на %s
pagination.older: Пізніше
pagination.newer: Новіше
pagination.previous: Попередня
pagination.next: Наступна
admin.admin_panel: Панель управління
admin.general: Загальні
admin.users: Користувачі
admin.gists: Gists
admin.configuration: Конфігурація
admin.invitations: Запрошення
admin.invitations.create: Створити запрошення
admin.versions: Версії
admin.ssh_keys: Ключі SSH
admin.stats: Статистика
admin.actions: Дії
admin.actions.sync-fs: Синхронізувати gists з файлової системи
admin.actions.sync-db: Синхронізувати gists з базою даних
admin.actions.git-gc: Збір сміття з репозиторіїв Git
admin.actions.sync-previews: Синхронізувати всі gists перегляди
admin.actions.reset-hooks: Скинути серверні Git hooks для всіх репозиторіїв
admin.actions.index-gists: Проіндексувати всі gists
admin.id: ID
admin.user: Користувач
admin.delete: Видалити
admin.created_at: Створено
admin.config-link: Ця конфігурація може бути %s за допомогою YAML файла та/або змінних середовища
admin.config-link-overriden: перевизначено
admin.disable-signup: Вимкнути реєстрацію
admin.disable-signup_help: Заборонити створення нових акаунтів
admin.require-login: Вимагати авторизацію
admin.require-login_help: Вимагати авторизації для перегляду gists
admin.allow-gists-without-login: Дозволити перегляд індивідуальних gists без авторизації
admin.allow-gists-without-login_help: Дозволити перегляд і скачування індивідуальних gists без авторизації, але вимагати авторизацію для перегляду переліку gists.
admin.disable-login: Вимкнути форму авторизації
admin.disable-login_help: Заборонити авторизацію по паролю та замість цього примушувати використовувати провайдерів OAuth.
admin.disable-gravatar: Заборонити Gravatar
admin.disable-gravatar_help: Вимкнути використання Gravatar як провайдера аватарів.
admin.users.delete_confirm: Ви впевнені, що хочете видалити цього користувача?
admin.gists.title: Назва
admin.gists.private: Чи приватний
admin.gists.nb-files: Файлів
admin.gists.nb-likes: Вподобань
admin.gists.delete_confirm: Ви впевнені, що хочете видалити цей gist?
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: Gist був видалений
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 переглядів...
flash.admin.reset-hooks: Скидання cерверниз Git hooks для всіх репозиторіїв...
flash.admin.index-gists: Індексація всіх gists...
flash.auth.username-exists: Це ім'я користувача вже існує
flash.auth.invalid-credentials: Недійсні облікові дані
flash.auth.account-linked-oauth: Акаунт підключено до %s
flash.auth.account-unlinked-oauth: Акаунт відключено від %s
flash.auth.user-sshkeys-not-retrievable: Не зміг отримати ключі користувача
flash.auth.user-sshkeys-not-created: Не зміг створити ssh ключ
flash.auth.must-be-logged-in: Ви маєте бути авторизовані для доступу до gists
flash.gist.visibility-changed: Параметри перегляду gist змінено
flash.gist.deleted: Gist було видалено
flash.gist.fork-own-gist: Неможливо форкнути власні gists
flash.gist.forked: Gist було форкнуто
flash.user.email-updated: Email оновлено
flash.user.invalid-ssh-key: Недійсний SSH ключ
flash.user.ssh-key-added: SSH ключ додано
flash.user.ssh-key-deleted: SSH key видалено
flash.user.password-updated: Пароль оновлено
flash.user.username-updated: Ім'я користувача оновлено
validation.is-too-long: Поле %s занадто велике
validation.should-not-be-empty: Поле %s не має бути пустим
validation.should-not-include-sub-directory: Поле %s неповинно включати піддиректорії
validation.should-only-contain-alphanumeric-characters: Поле %s має містити лише буквено-цифрові символи
validation.should-only-contain-alphanumeric-characters-and-dashes: Поле %s має містити лише буквено-цифрові символи та тире
validation.not-enough: Недостатньо %s
validation.invalid: Недійсний %s
html.title.admin-panel: Панель адміністратора

View File

@@ -1,6 +1,6 @@
gist.public: 公开
gist.unlisted: 非列出
gist.private:
gist.private:
gist.header.like: 喜欢
gist.header.unlike: 取消喜欢
@@ -9,16 +9,16 @@ gist.header.edit: 编辑
gist.header.delete: 删除
gist.header.forked-from: 派生自
gist.header.last-active: 最后活跃于
gist.header.select-tab: Select a tab
gist.header.select-tab: 选择一个选项
gist.header.code: 代码
gist.header.revisions: 修订
gist.header.revision: 修订
gist.header.clone-http: 通过 %s 克隆
gist.header.clone-http-help: 使用 Git 通过 HTTP 基础认证克隆。
gist.header.clone-http-help: 使用 HTTP 基础认证通过 Git 克隆。
gist.header.clone-ssh: 通过 SSH 克隆
gist.header.clone-ssh-help: 使用 Git 通过 SSH 密钥克隆。
gist.header.share: 分享
gist.header.share-help: 此 Gist 复制可供分享的链接。
gist.header.clone-ssh-help: 使用 SSH 密钥通过 Git 克隆。
gist.header.embed: '嵌入'
gist.header.embed-help: '将此 Gist 嵌入到您的网站。'
gist.header.download-zip: 下载 ZIP
gist.raw: 原始文件
@@ -27,10 +27,10 @@ gist.watch-full-file: 查看完整文件。
gist.file-not-valid: 此文件不是有效的 CSV 文件。
gist.no-content: 没有内容
gist.new.new_gist: 建 Gist
gist.new.new_gist: 建 Gist
gist.new.title: 标题
gist.new.description: 描述
gist.new.filename-with-extension: 文件名与扩展
gist.new.filename-with-extension: 带扩展名的文件名
gist.new.indent-mode: 缩进模式
gist.new.indent-mode-space: 空格
gist.new.indent-mode-tab: 制表符
@@ -39,17 +39,17 @@ gist.new.wrap-mode: 换行模式
gist.new.wrap-mode-no: 不自动换行
gist.new.wrap-mode-soft: 软换行
gist.new.add-file: 添加文件
gist.new.create-public-button: 创建公开 Gist
gist.new.create-unlisted-button: 创建非列出 Gist
gist.new.create-private-button: 创建私 Gist
gist.new.create-public-button: 创建公开 Gist
gist.new.create-unlisted-button: 创建非列出 Gist
gist.new.create-private-button: 创建私密的 Gist
gist.edit.editing: 编辑
gist.edit.editing: 正在编辑
gist.edit.change-visibility: 设为
gist.edit.delete: 删除
gist.edit.cancel: 取消
gist.edit.save: 保存
gist.list.joined: Joined
gist.list.joined: 加入于
gist.list.all: 所有 Gists
gist.list.search-results: 搜索结果
gist.list.sort: 排序
@@ -57,7 +57,7 @@ gist.list.sort-by-created: 创建
gist.list.sort-by-updated: 更新
gist.list.order-by-asc: 最早
gist.list.order-by-desc: 最近
gist.list.select-tab: Select a tab
gist.list.select-tab: 选择一个标签
gist.list.liked: 已喜欢
gist.list.likes: 喜欢
gist.list.forked: 已派生
@@ -65,30 +65,30 @@ gist.list.forked-from: 派生自
gist.list.forks: 派生
gist.list.files: 文件
gist.list.last-active: 最后活跃于
gist.list.no-gists: 没有 Gist
gist.list.no-gists: 没有任何 Gist
gist.forks: 派生
gist.forks.view: 查看派生
gist.forks.no: 公开派生
gist.forks.no: 没有任何公开派生
gist.likes: 喜欢
gist.likes.no: 还没有喜欢
gist.likes.no: 目前还没有任何喜欢
gist.revisions: 修订
gist.revision.revised: 修订了这个 Gist
gist.revision.go-to-revision: 跳至此修订
gist.revision.file-created: file created
gist.revision.file-deleted: file deleted
gist.revision.go-to-revision: 转到此修订
gist.revision.file-created: 文件已创建
gist.revision.file-deleted: 文件已删除
gist.revision.file-renamed: 重命名为
gist.revision.diff-truncated: 由于变更差异过大,显示内容已被截断
gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: 文件
gist.revision.no-changes: 没有变更
gist.revision.diff-truncated: 差异内容过多而无法显示
gist.revision.file-renamed-no-changes: 文件已重命名,但内容与之前没有差异
gist.revision.empty-file: 文件为空
gist.revision.no-changes: 没有任何变更
gist.revision.no-revisions: 无可供显示的修订
settings: 设置
settings.email: 邮箱
settings.email-help: 用于提交 Gravatar
settings.email: 电子邮箱
settings.email-help: 用于提交 Gravatar 头像
settings.email-set: 设置邮箱地址
settings.link-accounts: 关联账号
settings.link-github-account: 关联 GitHub 账号
@@ -98,16 +98,16 @@ settings.unlink-gitea-account: 解除关联 Gitea 账号
settings.delete-account: 删除账号
settings.delete-account-confirm: 您确认要删除您的账号吗?
settings.add-ssh-key: 添加 SSH 密钥
settings.add-ssh-key-help: 用于使用 Git 通过 SSH 拉取与推送 Gist
settings.add-ssh-key-title: 标题
settings.add-ssh-key-help: 用于使用 Git 通过 SSH 拉取与推送 Gist
settings.add-ssh-key-title: 名称
settings.add-ssh-key-content: 密钥
settings.delete-ssh-key: 删除
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
settings.ssh-key-added-at: 添加
settings.delete-ssh-key-confirm: 确认删除 SSH 密钥
settings.ssh-key-added-at: 添加
settings.ssh-key-never-used: 从未使用过
settings.ssh-key-last-used: 最后使用于
auth.signup-disabled: 管理员已禁用注册
auth.signup-disabled: 管理员已禁用注册
auth.login: 登录
auth.signup: 注册
auth.new-account: 新建账号
@@ -115,28 +115,27 @@ auth.username: 用户名
auth.password: 密码
auth.register-instead: 转到注册
auth.login-instead: 转到登录
auth.github-oauth: 使用 GitHub 账号继续
auth.gitea-oauth: 使用 Gitea 账号继续
auth.oauth: 使用 %s 账号继续
error: 错误
header.menu.all: 全部
header.menu.new:
header.menu.new:
header.menu.search: 搜索
header.menu.my-gists: 我的 Gists
header.menu.liked: Liked
header.menu.liked: 喜欢的 Gists
header.menu.admin: 管理
header.menu.settings: 设置
header.menu.logout: 登出
header.menu.register: 注册
header.menu.login: 登录
header.menu.light: 亮色
header.menu.dark: 暗色
header.menu.system: 系统
header.menu.light: 亮色模式
header.menu.dark: 暗色模式
header.menu.system: 跟随系统
footer.powered-by: 由 %s 强力驱动
pagination.older: 更早
pagination.newer: 更新
pagination.older: 下一页
pagination.newer: 上一页
pagination.previous: 上一页
pagination.next: 下一页
@@ -157,21 +156,142 @@ admin.user: 用户
admin.delete: 删除
admin.created_at: 创建于
admin.config-link: 此配置可通过 YAML 配置和/或环境变量进行 %s 。
admin.config-link: 此配置可通过 YAML 配置文件和/或环境变量进行 %s 。
admin.config-link-overriden: 覆盖
admin.disable-signup: 注册
admin.disable-signup_help: 止创建新的账号。
admin.require-login: 要求登录
admin.disable-signup: 止用户注册
admin.disable-signup_help: 止创建新的账号。
admin.require-login: 登录访问限制
admin.require-login_help: 强制用户登录后才能查看 Gist。
admin.disable-login: 禁用登录表单
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
admin.users.delete_confirm: 你想要删除此用户吗?
admin.allow-gists-without-login: 允许未登录状态下访问单个 Gists
admin.allow-gists-without-login_help: 允许在不登录的情况下查看和下载 Gist同时需要登录才能使用 Gists 的发现功能。
admin.users.delete_confirm: 您想要删除此用户吗?
admin.gists.title: 标题
admin.gists.private: 私有?
admin.gists.private: 访问权限
admin.gists.nb-files: 文件数
admin.gists.nb-likes: 喜欢数
admin.gists.delete_confirm: 想要删除此 Gist 吗?
admin.gists.delete_confirm: 想要删除此 Gist 吗?
gist.new.url: '自定义 URL 路径名'
gist.new.preview: '预览'
error.page-not-found: '页面未找到'
gist.new.create-a-new-gist: '创建一个新的 Gist'
gist.edit.edit-gist: '编辑 %s'
gist.list.all-liked-by: '%s 喜欢的所有 Gists'
gist.list.all-forked-by: '%s 派生的所有 Gists'
gist.list.all-from: '来自于 %s 的所有 Gists'
gist.search.found: '个已找到的 Gist(s)'
gist.search.no-results: '没有找到任何 Gist'
gist.search.help.user: '由用户创建的 Gists'
gist.search.help.title: '包含指定标题的 Gists'
gist.search.help.filename: '包含指定文件名的 Gists'
gist.search.help.extension: '包含指定扩展名的 Gists'
gist.search.help.language: '包含指定编程语言的 Gists'
gist.forks.for: '%s 的派生'
gist.likes.for: '%s 的喜欢'
gist.revision-of: '%s 的修订'
settings.link-gitlab-account: '关联 GitLab 账号'
settings.unlink-gitlab-account: '解除关联 GitLab 账号'
settings.change-username: '修改用户名'
settings.create-password: '创建密码'
settings.create-password-help: '创建您的密码以通过 HTTP 的方式登录到 Opengist'
settings.change-password: '修改密码'
settings.change-password-help: '修改您通过 HTTP 登录到 Opengist 的密码'
settings.password-label-title: '密码'
error.bad-request: '请求错误'
error.signup-disabled: '注册已被禁用'
error.signup-disabled-form: '通过用户名密码注册已被禁用'
error.login-disabled-form: '通过用户名密码登录已被禁用'
error.complete-oauth-login: '用户认证未能通过:%s'
error.oauth-unsupported: '不支持的认证提供商'
error.cannot-bind-data: '无法绑定数据'
error.invalid-number: '数字格式不正确'
error.invalid-character-unescaped: '包含未转义的无效字符'
admin.invitations: '邀请'
admin.invitations.create: '创建邀请'
admin.actions.sync-previews: '同步所有 Gists 预览'
admin.actions.reset-hooks: '重置所有存储库的 Git 服务 hooks'
admin.actions.index-gists: '索引所有 Gists'
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: 'Gist 已被删除'
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 预览...'
flash.admin.reset-hooks: '正在重置所有存储库的 Git 服务挂钩...'
flash.admin.index-gists: '正在索引所有 Gists...'
flash.auth.username-exists: '用户名已存在'
flash.auth.invalid-credentials: '无效的凭证'
flash.auth.account-linked-oauth: '帐户已关联到 %s'
flash.auth.account-unlinked-oauth: '帐户与 %s 解除关联'
flash.auth.user-sshkeys-not-retrievable: '无法获取用户密钥'
flash.auth.user-sshkeys-not-created: '无法创建 SSH 密钥'
flash.auth.must-be-logged-in: '您必须登录才能访问 Gists'
flash.gist.visibility-changed: 'Gist 可见性已更改'
flash.gist.deleted: 'Gist 已被删除'
flash.gist.fork-own-gist: '无法派生自己的 Gists'
flash.gist.forked: 'Gist 已被派生'
flash.user.email-updated: '电子邮件已更新'
flash.user.invalid-ssh-key: 'SSH 密钥无效'
flash.user.ssh-key-added: 'SSH 密钥已添加'
flash.user.ssh-key-deleted: 'SSH 密钥已删除'
flash.user.password-updated: '密码已更新'
flash.user.username-updated: '用户名已更新'
validation.is-too-long: '字段 %s 太长'
validation.should-not-be-empty: '字段 %s 不能为空'
validation.should-not-include-sub-directory: '字段 %s 不应包含子目录'
validation.should-only-contain-alphanumeric-characters: '字段 %s 只能包含字母、数字、字符'
validation.should-only-contain-alphanumeric-characters-and-dashes: '字段 %s 应仅包含字母、数字、字符和 -'
validation.not-enough: '%s 长度过短'
validation.invalid: '%s 无效'
html.title.admin-panel: '管理面板'
settings.ssh-key-exists: SSH 密钥已经存在
auth.mfa.passkey-name: 名称
auth.mfa.use-passkey-to-finish: 使用通行密钥完成身份验证
auth.mfa.delete-passkey: 删除
auth.mfa.passkey-added-at: 密钥已添加
auth.mfa.passkey-never-used: 从未使用过
auth.mfa.passkey-last-used: 最后使用于
auth.mfa.delete-passkey-confirm: 确认删除密钥
error.not-in-mfa-session: 用户不在 MFA 会话中
auth.mfa.waiting-for-passkey-input: 等待浏览器的交互输入...
auth.mfa.passkeys-help: 添加通行密钥以登录您的帐户并用作 MFA 方法。
flash.auth.passkey-registred: 密钥 %s 已注册
flash.auth.passkey-deleted: 密钥已删除
auth.mfa.use-passkey: 使用通行密钥
auth.mfa.bind-passkey: 绑定通行密钥
auth.mfa.login-with-passkey: 使用通行密钥登录
auth.mfa: 多因素认证
auth.mfa.passkey: 通行密钥
auth.mfa.passkeys: 通行密钥
gist.delete.confirm: 确定要删除此 Gist 吗?
auth.totp.scan-qr-code: 使用您的身份验证应用扫描下面的二维码以启用双因素认证,或者输入以下字符串,然后用生成的代码进行确认。
auth.totp: 基于时间的一次性密码算法TOTP
auth.totp.help: TOTP 是一种双因素认证方法,它使用共享密钥生成一次性密码。
auth.totp.use: 使用 TOTP
auth.totp.regenerate-recovery-codes: 重新生成恢复码
auth.totp.already-enabled: TOTP 已启用
auth.totp.invalid-secret: 无效的 TOTP 密钥
auth.totp.code-used: 恢复代码 %s 已被使用且失效。您可能需要暂时禁用 MFA 或重新生成代码。
auth.totp.invalid-code: 无效的 TOTP 代码
auth.totp.disabled: 成功禁用 TOTP
auth.totp.disable: 禁用 TOTP
auth.totp.enter-code: 请输入来自身份验证器应用程序的代码
auth.totp.enter-recovery-key: 如果您丢失了设备,可以使用恢复密钥
auth.totp.code: 代码
auth.totp.submit: 提交
auth.totp.proceed: 继续
auth.totp.save-recovery-codes: 请将您的恢复代码保存在安全的地方。在您无法访问身份验证应用程序时,可以使用这些代码恢复访问。
admin.invitations.delete_confirm: 您想要删除此邀请吗?

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