Compare commits

...

250 Commits

Author SHA1 Message Date
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
Thomas Miceli
3444fb9b75 v1.5.3 2023-11-20 18:49:46 +01:00
Thomas Miceli
be46304e23 Display OAuth errors (#159) 2023-11-20 18:41:01 +01:00
Thomas Miceli
5fa55dfbba Tiny UI fixes (#158) 2023-11-20 18:28:13 +01:00
Thomas Miceli
09fb647f03 Fix: bare first branch name, truncated output hanging (#157) 2023-11-20 18:03:59 +01:00
Thomas Miceli
d518a44d32 Create/change account password (#156) 2023-11-20 18:03:28 +01:00
Thomas Miceli
dcacde0959 Fix home user directory detection handling (#145) 2023-10-31 15:23:15 +09:00
Manuel Vergara
064d4d53f6 Add spanish translation (#139) 2023-10-31 15:22:58 +09:00
Thomas Miceli
aec7ee2708 v1.5.2 2023-10-16 12:26:05 +02:00
Thomas Miceli
10fd170833 Fix markdown render dark background (#137) 2023-10-16 12:20:09 +02:00
Slava Krampetz
ba03b8df38 Add ru-RU translation (#135) 2023-10-15 18:09:54 +02:00
Thomas Miceli
ef45f3d0ca config.yml with Docker (#131) 2023-10-15 08:14:34 +02:00
Thomas Miceli
b1acea9f1c Better password hashes error handling (#132) 2023-10-13 05:36:00 +02:00
Gary Wang
7059d5c834 Add zh-CN translation and minor UI fix (#130) 2023-10-12 14:13:39 +02:00
Thomas Miceli
1539499294 Longer title and description (#129) 2023-10-04 18:48:02 +02:00
Thomas Miceli
6f587f4757 Fix private gist visibility (#128) 2023-10-04 18:47:50 +02:00
Thomas Miceli
632206e172 v1.5.1 2023-09-29 16:59:09 +02:00
WilliamNT
2eeb9283f0 Added hungarian translations (#123) 2023-09-29 16:39:55 +02:00
Thomas Miceli
d137820037 Add missing $ in templates (#122) 2023-09-29 06:32:09 +02:00
Thomas Miceli
4eedfdcf6f Fix login page disabled depending on locale (#120) 2023-09-28 20:09:08 +02:00
Thomas Miceli
bae18ecb0a Detect .c and .h files (#119) 2023-09-28 20:08:57 +02:00
Thomas Miceli
2b9eb8e127 v1.5.0 2023-09-26 15:34:43 +02:00
Thomas Miceli
05523f6bb1 Merge dev-1.5 in master 2023-09-26 15:19:35 +02:00
Thomas Miceli
30ca090e74 Add binaries cross compile in CD (#113) 2023-09-26 15:13:58 +02:00
Thomas Miceli
fa8e068e24 Add Run with Systemd docs (#111)
Co-authored-by: Cyberes <64224601+cyberes@users.noreply.github.com>
2023-09-25 22:09:52 +02:00
Thomas Miceli
72275e7573 Add documentation (#110) 2023-09-25 18:57:47 +02:00
Thomas Miceli
5b278e2e86 Change gist init url to /init (#109) 2023-09-25 18:43:55 +02:00
Thomas Miceli
6c450c6f3b Delete gists when user is deleted (#108) 2023-09-25 18:43:36 +02:00
Thomas Miceli
dd050bb6a0 Fix CI 2023-09-25 16:08:26 +02:00
Thomas Miceli
c7a6b05c6d Added some info about OIDC 2023-09-25 15:58:05 +02:00
Thomas Miceli
35297a287a Implement OIDC auth (#98) 2023-09-25 13:08:06 +02:00
Thomas Miceli
85b51bf3c9 Merge branch 'master' of github.com:Maronato/opengist into Maronato-master 2023-09-25 13:07:48 +02:00
Thomas Miceli
9dff67f003 Various bug fixes (#105) 2023-09-22 17:31:19 +02:00
Thomas Miceli
a5ea522e45 Add translation system (#104) 2023-09-22 17:26:09 +02:00
Thomas Miceli
61e274e56d Added new logo (#103) 2023-09-19 15:48:19 +02:00
Thomas Miceli
c20ed60913 Update Go & deps. version (#102) 2023-09-18 18:17:11 +02:00
Thomas Miceli
b31d95c7f6 Remove TLS server (#101) 2023-09-18 18:06:27 +02:00
Thomas Miceli
689fd21afa Merge branch 'jolheiser-modernc' into dev-1.5 2023-09-17 03:23:37 +02:00
Thomas Miceli
be3580f7b1 Resolve merge 2023-09-17 03:23:20 +02:00
Thomas Miceli
6085471b81 Adapt find command for Windows users (#89) 2023-09-17 03:03:54 +02:00
Thomas Miceli
3943b53163 Enhance Go CI (#99) 2023-09-17 02:55:17 +02:00
Thomas Miceli
fe674ac88b Add git, auth and gists tests (#97) 2023-09-17 00:59:47 +02:00
Gustavo Maronato
9c29e86222 trigger action 2023-09-15 19:49:18 -03:00
Gustavo Maronato
933ba2da0d errors should not be capitalized 2023-09-15 19:48:09 -03:00
Gustavo Maronato
4d0b75ed0e fix wrong config key for discovery-url 2023-09-15 19:11:33 -03:00
Gustavo Maronato
1dcb900cf3 implement OIDC auth 2023-09-15 18:56:14 -03:00
Thomas Miceli
46dea89b41 Create gists from git http server endpoint (#95) 2023-09-09 19:39:57 +02:00
Thomas Miceli
977fc9db28 Improved git http semantics and repo obfuscation (#94) 2023-09-07 11:46:34 +02:00
Thomas Miceli
3e83700fc2 Miscellaneous front changes (#93)
* Fix fork icon

* Added alt images descriptions

* Add avatars next to gists links

* Fix avatar for nil user

* Slightly different blue primary color

* Reduced main width

* "New" button redirects to login when not logged in
2023-09-06 23:36:44 +02:00
Thomas Miceli
0d7305d9ba Use dayjs instead of moment (#92) 2023-09-05 15:22:24 +02:00
Thomas Miceli
d4eed91130 Split hljs into a new file; improved dev vite server system (#91) 2023-09-05 15:22:09 +02:00
Thomas Miceli
ffafde2b3e Run git gc for repositories (#90) 2023-09-04 11:11:54 +02:00
Thomas Miceli
a7b346d8df Tweaked project structure (#88) 2023-09-03 00:30:57 +02:00
Thomas Miceli
25316d7bf2 Added private visibility
* Changed gist type and added HTML button on creation

* Adapted label and edit button

* Changed rules for git HTTP and SSH

* Adapt Readme features
2023-09-02 03:58:37 +02:00
joe
4f623881ac fix typo on admin index page (#85) 2023-08-12 22:53:17 +02:00
Thomas Miceli
b5cd49db4c Download file, button groups, fix unknown file reading (#84) 2023-07-26 15:43:07 +02:00
Thomas Miceli
89685bfac6 Remove CONFIG env var 2023-07-26 10:54:16 +02:00
John Olheiser
319a89387a fix: retain visibility when editing (#83)
* fix: retain visibility when editing

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

* review(thomiceli): remove private conversion in dto

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

---------

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-07-19 17:28:42 +02:00
Thomas Miceli
24fc6dd8e4 v1.4.2 2023-07-17 04:28:13 +02:00
Thomas Miceli
cc6110bb4e Remove Dev Docker image (#80) 2023-07-17 04:16:08 +02:00
Thomas Miceli
2890c60124 Warning message on OAuth unlink (#79) 2023-07-17 04:07:10 +02:00
Thomas Miceli
5bb5886770 Make unlisted gists not SEO crawlable (#78) 2023-07-17 03:58:45 +02:00
Thomas Miceli
038d81df2d Add external url to HTML links & redirects (#75) 2023-07-03 16:31:12 +02:00
Thomas Miceli
7515e82d34 Revert redirection when not logged to /all (#76) 2023-07-03 16:31:03 +02:00
Thomas Miceli
add0299442 v1.4.1 2023-06-25 11:46:15 +02:00
Thomas Miceli
06b752f567 Fixes unable to access '/root/.config/git/attributes': Permission denied (#71) 2023-06-25 11:40:28 +02:00
Thomas Miceli
a35b64455a v1.4.0 2023-06-23 14:27:23 +02:00
Thomas Miceli
9470622125 Search page more explicit 2023-06-23 13:59:07 +02:00
Thomas Miceli
7b5d035a32 Search gists (#68) 2023-06-21 18:19:17 +02:00
John Olheiser
98c5cd1794 chore: use npx (#66)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-06-21 18:18:07 +02:00
Thomas Miceli
0936cbc455 Fix Docker entrypoint typo 2023-06-21 17:53:22 +02:00
John Olheiser
fc421a68b5 refactor!: prefix DEV env var and deprecate CONFIG (#64)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-06-18 17:01:27 +02:00
Thomas Miceli
62711ff491 Change docker tag 2023-06-18 12:54:33 +02:00
Thomas Miceli
da19e486f2 Customise UID/GID for Docker (#63) 2023-06-18 12:50:36 +02:00
John Olheiser
98c85de3d6 fix: gitea config url for avatar (#61)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-06-18 12:38:57 +02:00
jolheiser
af3aab21e3 chore: use glebarez mirror
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-06-16 11:13:25 -05:00
jolheiser
fb407bcbce feat: non-cgo sqlite
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-06-16 11:08:33 -05:00
Thomas Miceli
3366cde385 Sqlite journal mode (#54) 2023-06-09 15:25:41 +02:00
Thomas Miceli
b2a56fe5a0 Better ci (#56) 2023-06-09 15:01:14 +02:00
Thomas Miceli
24e3de8fc1 Better config (#50) 2023-06-07 20:50:30 +02:00
dependabot[bot]
c517c2d9c9 Bump vite from 4.2.1 to 4.2.3 (#49)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.2.1 to 4.2.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.2.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.2.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>
2023-06-06 12:01:06 +02:00
Thomas Miceli
8880e00f48 Fix dark mode flickering (#44) 2023-06-01 19:04:12 +02:00
Thomas Miceli
da970d7272 Fix URL join (#43) 2023-05-29 22:39:30 +02:00
Thomas Miceli
62f91c5ed2 Small fixes (#42)
* UI color fixes
* Typos
* Fancy badge in README
2023-05-27 20:20:20 +02:00
Thomas Miceli
0a542431c9 v1.3.0 2023-05-27 14:12:34 +02:00
Thomas Miceli
cecc06b332 Light mode (#38) 2023-05-27 13:58:08 +02:00
Thomas Miceli
4a75a50370 Disable Gravatar (#37)
* Disable Gravatar
* Lowercase emails
* Add migration
2023-05-26 09:15:37 +02:00
Thomas Miceli
7cc77d80dc Small footer change 2023-05-24 16:44:06 +02:00
Thomas Miceli
df22506bdd Append logs to stdout 2023-05-24 16:40:27 +02:00
Thomas Miceli
026bb7304c Update go module name 2023-05-15 21:07:29 +02:00
Thomas Miceli
0cae152e03 Fix admin settings initialization (#31) 2023-05-12 12:06:55 +02:00
Thomas Miceli
5fe84164a5 Better UI for admin settings (#30) 2023-05-11 15:16:05 +02:00
Thomas Miceli
089d321898 Syntax highlighting + fix escaping in Markdown code (#29) 2023-05-07 18:54:09 +02:00
Thomas Miceli
1f74affde4 Merge pull request #26 from thomiceli/feature/better-oauth
Feature/better oauth
2023-05-07 11:02:30 +02:00
Thomas Miceli
49807d04c7 Disable login form via admin panel 2023-05-06 18:53:59 +02:00
Thomas Miceli
67a06cea0a Merge pull request #25 from thomiceli/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.4.0 to 0.7.0
2023-05-06 03:11:34 +02:00
dependabot[bot]
d866a6f2f2 Bump golang.org/x/net from 0.4.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.4.0 to 0.7.0.
- [Commits](https://github.com/golang/net/compare/v0.4.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 01:08:12 +00:00
Thomas Miceli
3cf5bc8b76 First account registering with OAuth is now admin 2023-05-04 11:48:26 +02:00
Thomas Miceli
2782655545 v1.2.0 2023-05-01 03:09:12 +02:00
Thomas Miceli
0362cd8a6a Merge pull request #22 from thomiceli/fix/ssh-keys
Fix SSH pubkey detection
2023-05-01 03:04:36 +02:00
Thomas Miceli
58d40e211f Fix SSH pubkey detection 2023-05-01 02:55:34 +02:00
Thomas Miceli
713b5d623e Merge pull request #21 from josefandersson/oath-respect-external-url-config
Respect ExternalUrl for OAuth
2023-05-01 02:03:24 +02:00
Josef Andersson
5040a2fe3c Respect ExternalUrl for oauth 2023-04-30 12:09:42 +02:00
Thomas Miceli
5ba90af04c Merge pull request #19 from thomiceli/feature/all-private
Restrict/unrestrict gists visibility to anonymous users
2023-04-29 20:22:34 +02:00
Thomas Miceli
c45b418bd0 Merge pull request #20 from thomiceli/fix/trim-filenames
Trim filenames
2023-04-29 20:21:13 +02:00
Thomas Miceli
2b3043ad5d Trim filenames 2023-04-28 20:41:12 +02:00
Thomas Miceli
333efeacbf Add require login feature to see gists 2023-04-28 20:31:10 +02:00
Thomas Miceli
64d0818c9f CI status on README 2023-04-26 23:55:37 +02:00
Thomas Miceli
fe19e64dd4 Adding Golang CI 2023-04-26 23:31:18 +02:00
Thomas Miceli
91db4dcd30 Satisfy Staticcheck 2023-04-26 23:13:11 +02:00
Thomas Miceli
09507500a9 v1.1.1 2023-04-20 21:54:54 +02:00
Thomas Miceli
14dac703c8 Fix git zombie process 2023-04-20 21:48:25 +02:00
Thomas Miceli
fddc50e3cc v1.1.0 2023-04-18 03:11:28 +02:00
Thomas Miceli
5bc1697774 Migration system & fix ssh key table constraint 2023-04-18 02:33:19 +02:00
Thomas Miceli
7807110845 Small change in README 2023-04-17 22:17:08 +02:00
Thomas Miceli
c7866e18e4 Fix HTML titles in admin panel 2023-04-17 22:13:19 +02:00
Thomas Miceli
4c43f1aa8a Rename config.go to settings.go 2023-04-17 22:09:40 +02:00
Thomas Miceli
485f209319 Merge pull request #13 from thomiceli/feature/oauth
Adding Github & Gitea OAuth provider
2023-04-17 22:07:24 +02:00
Thomas Miceli
9d20465eb9 Update README.md 2023-04-17 22:00:39 +02:00
Thomas Miceli
884195aed4 Trim gitea url 2023-04-17 21:31:40 +02:00
Thomas Miceli
6a0fd92516 Block Oauth when signup is disabled 2023-04-17 20:33:24 +02:00
Thomas Miceli
2d42cf798c Include HTML buttons 2023-04-17 20:25:35 +02:00
Thomas Miceli
4008b7ce38 Client key and secret Oauth in config 2023-04-17 19:11:32 +02:00
Thomas Miceli
a6c5696ceb Support Github and Gitea OAuth providers 2023-04-17 14:25:39 +02:00
Thomas Miceli
47cbf5e7ef Merge pull request #11 from thomiceli/feature/admin-panel-changes
Settings on admin panel
2023-04-17 00:50:36 +02:00
Thomas Miceli
8534a4880b Fix admin panel url 2023-04-17 00:35:04 +02:00
Thomas Miceli
dba3f4be44 Create admin settings and moved signup disable setting there 2023-04-17 00:17:06 +02:00
Thomas Miceli
dbdd38694d Change /admin route to /admin-panel (#1) 2023-04-16 17:48:21 +02:00
6543
19db4c223b nits to improve git backend (#7) 2023-04-16 16:14:12 +02:00
Lucien Davison
31aae8a305 fix: do not truncate raw files (#4) 2023-04-14 10:55:54 +02:00
Thomas Miceli
864880b442 v1.0.1 2023-04-12 13:34:22 +02:00
Thomas Miceli
51dadcecef Change redirections when not logged in 2023-04-12 13:30:51 +02:00
Thomas Miceli
ab59d7956a Updated base footer 2023-04-12 13:25:41 +02:00
Thomas Miceli
8e481d70f0 Update README.md 2023-04-12 12:50:47 +02:00
193 changed files with 20528 additions and 9103 deletions

2
.gitattributes vendored
View File

@@ -1,2 +1,4 @@
templates/**/* linguist-vendored
public/**/*.css linguist-vendored
public/**/*.scss linguist-vendored
*.config.js linguist-vendored

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 }}

138
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,138 @@
name: "Go CI"
on:
push:
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.23
uses: actions/setup-go@v4
with:
go-version: "1.23"
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
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.23
uses: actions/setup-go@v4
with:
go-version: "1.23"
- 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.23"]
database: ["sqlite"]
runs-on: ${{ matrix.os }}
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 }}
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

@@ -1,12 +1,37 @@
name: Docker
name: Release
on:
release:
types: [published]
workflow_dispatch:
jobs:
docker:
binaries-build-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: "1.23"
- name: Cross compile build
run: make all_crosscompile
- name: Upload Release Assets
uses: softprops/action-gh-release@v1
with:
files: |
build/*.tar.gz
build/*.zip
build/checksums.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker-build-release:
runs-on: ubuntu-latest
permissions:
contents: read

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ gist.db
public/assets/*
public/manifest.json
opengist
build/
docs/.vitepress/dist/
docs/.vitepress/cache/

358
CHANGELOG.md Normal file
View File

@@ -0,0 +1,358 @@
# Changelog
## [1.8.0](https://github.com/thomiceli/opengist/compare/v1.7.5...v1.8.0) - 2024-09-12
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)
- Create/change account password (#156)
- Display OAuth error messages when HTTP 400 (#159)
### Fixed
- Git bare repository branch name creation (#157)
- Git file truncated output hanging (#157)
- Home user directory detection handling (#145)
- UI changes (#158)
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
### Added
- zh-CN translation (#130)
- ru-RU translation (#135)
- config.yml usage in the Docker container (#131)
- Longer title and description (#129)
### Fixed
- Private gist visibility (#128)
- Dark background color in Markdown rendering (#137)
- Error handling for password hashes (#132)
## [1.5.1](https://github.com/thomiceli/opengist/compare/v1.5.0...v1.5.1) - 2023-09-29
### Added
- Hungarian translations (#123)
### Fixed
- .c and .h syntax highlighting (#119)
- Login page disabled depending on locale (#120)
- Syntax error on templates when calling locale function (#122)
## [1.5.0](https://github.com/thomiceli/opengist/compare/v1.4.2...v1.5.0) - 2023-09-26
### Added
- Private Gist visibility (#87)
- Create gists from a special Git HTTP server remote URL (#95)
- OIDC provider integration (#98)
- Translation system (#104)
- Run `git gc` on all repositories as admin (#90)
- Unit and integration tests (#97)
- Documentation (#110, #111)
- New logo (#103)
### Changed
- Use Non-CGO SQLite instead of CGO SQLite (#100)
- Various UI changes (#84, #93)
- Improved CI/CD pipeline (#99, #113)
- Improved git http semantics and repo obfuscation (#94)
- Updated Go deps (#102)
### Fixed
- Find command for Windows users (#89)
- Retain visibility when editing a gist (#83)
- Typo on admin index page (#85)
- ViteJS dev server (#91)
- Bugs (#105)
### Breaking changes
- Removed CONFIG env var
- Removed TLS server (#101)
## [1.4.2](https://github.com/thomiceli/opengist/compare/v1.4.1...v1.4.2) - 2023-07-17
### Added
- External url to HTML links & redirects (#75)
- Make unlisted gists not SEO crawlable (#78)
- Warning message on OAuth unlink (#79)
### Changed
- Redirect to `/all` when not logged in (#76)
- Removed Dev Docker image (#80)
## [1.4.1](https://github.com/thomiceli/opengist/compare/v1.4.0...v1.4.1) - 2023-06-25
### ⚠️ Docker users ⚠️
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
`docker-compose.yml` file or any other Docker related configuration.
Please make a backup of your Opengist data directory before updating.
### Fixed
- Git message remote: `warning: unable to access '/root/.config/git/attributes': Permission denied` (#71)
## [1.4.0](https://github.com/thomiceli/opengist/compare/v1.3.0...v1.4.0) - 2023-06-23
### ⚠️ Docker users ⚠️
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
`docker-compose.yml` file or any other Docker related configuration.
Please make a backup of your Opengist data directory before updating.
### Added
- Search gists, browse users snippets, likes and forks (#68)
- SQLite WAL journal mode by default (#54)
- Change SQLite journal mode via configuration (#54)
- Configuration via environment variables (#50)
- Docker dev image (#56)
- Choose Docker container/volumes owner via UID/GID (#63)
### Changed
- Docker volume changed from `/root/.opengist` to `/opengist` (#63)
- `DEV` environment variable renamed to `OG_DEV` (#64)
- Use `npx` in Makefile instead of `./node_modules/.bin` (#66)
- DEPRECATED: `OG_CONFIG` environment variable (#64)
### Fixed
- Gitea URL joins (#43, #61)
- Dark mode flickering (#44)
- Typos (#42)
## [1.3.0](https://github.com/thomiceli/opengist/compare/v1.2.0...v1.3.0) - 2023-05-27
### Added
- Disable login form via admin panel
- Syntax highlighting in Markdown code block (#29)
- Better UI for admin settings (#30)
- Disable Gravatar (#37)
- Swap between dark and light theme (#38)
### Changed
- Logs are now also appended to stdout
- Golang module name is now `github.com/thomiceli/opengist`
### Fixed
- First account registering with OAuth is now admin
- Fix HTML entities escaping in Markdown code block (#29)
## [1.2.0](https://github.com/thomiceli/opengist/compare/v1.1.1...v1.2.0) - 2023-05-01
### Added
- Restrict or unrestrict snippets visibility to anonymous users (#19)
- Go CI with Staticcheck
### Changed
- Filenames are now trimmed when creating a snippet (#20)
- SSH public key comments are now trimmed when adding a new key (#22)
### Fixed
- Respect ExternalUrl for OAuth (#21)
- SSH public key detection (#22)
## [1.1.1](https://github.com/thomiceli/opengist/compare/v1.1.0...v1.1.1) - 2023-04-20
### Fixed
- Git processes are now correctly killed
## [1.1.0](https://github.com/thomiceli/opengist/compare/v1.0.1...v1.1.0) - 2023-04-18
### Added
- GitHub and Gitea OAuth2 login
- Database migration system
### Changed
- Admin panel route from `/admin` route to `/admin-panel`
- Moved disable signup option to admin panel
### Fixed
- Truncate raw file (#4)
- Fix SSH key table constraints on user delete
## [1.0.1](https://github.com/thomiceli/opengist/compare/v1.0.0...v1.0.1) - 2023-04-12
### Changed
- Updated base footer
- Changed redirections when not logged in
## 1.0.0 - 2023-04-10
- Initial release

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.19-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,13 +27,27 @@ WORKDIR /opengist
COPY . .
FROM base AS dev
EXPOSE 6157 2222 16157
VOLUME /opengist
RUN git config --global --add safe.directory /opengist
CMD ["make", "watch"]
FROM base AS build
RUN make
FROM alpine:3.17
FROM alpine:3.19 as prod
RUN apk update && \
apk add --no-cache \
shadow \
openssl \
openssh \
curl \
@@ -36,10 +59,17 @@ RUN apk update && \
musl-dev \
libstdc++
WORKDIR /opengist
RUN addgroup -S opengist && \
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist
COPY --from=build /opengist/opengist .
COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml
WORKDIR /app/opengist
COPY --from=build --chown=opengist:opengist /opengist/opengist .
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222
VOLUME /root/.opengist
CMD ["./opengist"]
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,9 +1,14 @@
.PHONY: all install build_frontend build_backend build build_docker watch_frontend watch_backend watch clean clean_docker
.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: install build
all: clean install build
all_crosscompile: clean install build_frontend build_crosscompile
install:
@echo "Installing NPM dependencies..."
@@ -13,34 +18,62 @@ install:
build_frontend:
@echo "Building frontend assets..."
./node_modules/.bin/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
build_crosscompile:
@bash ./scripts/build-all.sh
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..."
./node_modules/.bin/vite dev --port 16157
npx vite -c public/vite.config.js dev --port 16157 --host
watch_backend:
@echo "Building Opengist binary..."
DEV=1 ./node_modules/.bin/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 ./watch.sh
@sh ./scripts/watch.sh
clean:
@echo "Cleaning up build artifacts..."
@rm -f $(BINARY_NAME) public/manifest.json
@rm -rf public/assets
@rm -rf public/assets build
clean_docker:
@echo "Cleaning up Docker image..."
@docker rmi $(BINARY_NAME)
check_changes:
@echo "Checking for changes..."
@git --no-pager diff --exit-code || (echo "There are unstaged changes detected." && exit 1)
go_mod:
@go mod download
@go mod tidy
fmt:
@go fmt ./...
test:
@OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
check-tr:
@bash ./scripts/check-translations.sh

180
README.md
View File

@@ -1,84 +1,94 @@
# 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 similar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
[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)
A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomice.li).
* [Features](#features)
* [Install](#install)
* [With Docker](#with-docker)
* [From source](#from-source)
* [Configuration](#configuration)
* [Administration](#administration)
* [Use Nginx as a reverse proxy](#use-nginx-as-a-reverse-proxy)
* [Use Fail2ban](#use-fail2ban)
* [License](#license)
## Features
* Create public or unlisted snippets
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support
* Search code in snippets; browse users snippets, likes and forks
* Embed snippets in other websites
* Revisions history
* Like / Fork snippets
* Search for all snippets or for certain users snippets
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* Avatars
* Responsive UI
* Enable or disable signups
* Admin panel : delete users/gists; clean database/filesystem by syncing gists
* SQLite database
* Logging
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users
* Docker support
* [More...](/docs/introduction.md#features)
#### Todo
- [ ] Light mode
- [ ] Tests
- [ ] Search for snippets
- [ ] Embed snippets
- [ ] Filesystem/Redis support for user sessions
- [ ] Have a cool logo
## Install
## Quick start
### With Docker
A Docker [image](https://github.com/users/thomiceli/packages/container/package/opengist), available for each release, can be pulled
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```
docker pull ghcr.io/thomiceli/opengist:1
```shell
docker pull ghcr.io/thomiceli/opengist:1.7
```
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`
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
image: ghcr.io/thomiceli/opengist:1.7
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:/root/.opengist"
environment:
CONFIG: |
log-level: info
- "$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.8.0/opengist1.8.0-linux-amd64.tar.gz
tar xzvf opengist1.8.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```
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.19+), [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
@@ -89,87 +99,15 @@ make
Opengist is now running on port 6157, you can browse http://localhost:6157
## Configuration
---
Opengist can be configured using YAML. The full configuration file is [config.yml](config.yml), each default key/value
pair can be overridden.
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
### With docker
## Documentation
Add a `CONFIG` environment variable in the `docker-compose.yml` file to the `opengist` service :
```diff
environment:
CONFIG: |
log-level: info
ssh.git-enabled: false
disable-signup: true
# ...
```
### With binary
Create a `config.yml` file (you can reuse this [one](config.yml)) and run Opengist binary with the `--config` flag :
```shell
./opengist --config /path/to/config.yml
```
The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
## Administration
### Use Nginx as a reverse proxy
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
```
server {
listen 80;
server_name opengist.example.com;
location / {
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;
}
}
```
Then run :
```shell
service nginx restart
```
### Use Fail2ban
Fail2ban can be used to ban IPs that try to bruteforce the login page.
Log level must be set at least to `warn`.
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
```ini
[Definition]
failregex = Invalid .* authentication attempt from <HOST>
ignoreregex =
```
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
```ini
[opengist]
enabled = true
filter = opengist
logpath = /home/*/.opengist/log/opengist.log
maxretry = 10
findtime = 3600
bantime = 600
banaction = iptables-allports
port = anyport
```
Then run
```shell
service fail2ban restart
```
## License
Opengist is licensed under the [AGPL-3.0 license](LICENSE).
Opengist is licensed under the [AGPL-3.0 license](/LICENSE).

View File

@@ -1,18 +1,42 @@
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
# Learn more about Opengist configuration here:
# 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: 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
external-url:
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file
# Prevents the creation of new accounts (either `true` or `false`). Default: false
disable-signup: false
# 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)
# SQLite: file name
# 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
@@ -25,15 +49,6 @@ http.port: 6157
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
http.git-enabled: true
# Enable or disable TLS (either `true` or `false`). Default: false
http.tls-enabled: false
# Path to the TLS certificate file if TLS is enabled
http.cert-file:
# Path to the TLS key file if TLS is enabled
http.key-file:
# SSH built-in server configuration
# Note: it is not using the SSH daemon from your machine (yet)
@@ -56,3 +71,47 @@ 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.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:
# 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.0
images:
- name: ghcr.io/thomiceli/opengist
newTag: 1.8.0
patches:
# Add your ingress
- path: ingress.yaml
- patch: |-
- op: add
path: /spec/rules/0/host
value: opengist.mydomain.com
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: opengist
```
`namespace.yaml`:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: opengist
```
`ingress.yaml`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: nginx
tls:
- hosts:
- opengist.mydomain.com
secretName: opengist-tls
```

29
deploy/deployment.yaml Normal file
View File

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

20
deploy/ingress.yaml Normal file
View File

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

11
deploy/kustomization.yaml Normal file
View File

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

15
deploy/pvc.yaml Normal file
View File

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

14
deploy/service.yaml Normal file
View File

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

18
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
export USER=opengist
UID=${UID:-1000}
GID=${GID:-1000}
groupmod -o -g "$GID" $USER
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
})

View File

@@ -1,8 +1,9 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./templates/**/*.html",
"./.vitepress/theme/*.vue",
],
theme: {
colors: {
@@ -22,10 +23,8 @@ module.exports = {
800: "#232429",
900: "#131316"
},
emerald: colors.emerald,
rose: colors.rose,
primary: colors.sky,
slate: colors.slate
indigo: colors.indigo,
},
extend: {
borderWidth: {
@@ -33,5 +32,6 @@ module.exports = {
}
},
},
plugins: [require("@tailwindcss/typography"),require('@tailwindcss/forms')],
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.0</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,29 @@
# Fail2ban setup
Fail2ban can be used to ban IPs that try to bruteforce the login page.
Log level must be set at least to `warn`.
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
```ini
[Definition]
failregex = Invalid .* authentication attempt from <HOST>
ignoreregex =
```
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
```ini
[opengist]
enabled = true
filter = opengist
logpath = /home/*/.opengist/log/opengist.log
maxretry = 10
findtime = 3600
bantime = 600
banaction = iptables-allports
port = anyport
```
Then run
```shell
service fail2ban restart
```

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

@@ -0,0 +1,46 @@
# Use Nginx as a reverse proxy
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;
server_name opengist.example.com;
location / {
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;
}
}
```
### 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

@@ -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

@@ -0,0 +1,92 @@
# Run with Systemd
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
sudo cp opengist /usr/local/bin
sudo mkdir -p /var/lib/opengist
sudo cp config.yml /etc/opengist
```
Edit the Opengist home directory configuration in `/etc/opengist/config.yml` like:
```shell
opengist-home: /var/lib/opengist
```
Create a new user to run Opengist:
```shell
sudo useradd --system opengist
sudo mkdir -p /var/lib/opengist
sudo chown -R opengist:opengist /var/lib/opengist
```
Then create a service file at `/etc/systemd/system/opengist.service`:
```ini
[Unit]
Description=opengist Server
After=network.target
[Service]
Type=simple
User=opengist
Group=opengist
ExecStart=opengist --config /etc/opengist/config.yml
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
Finally, start the service:
```shell
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

@@ -0,0 +1,42 @@
---
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: `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.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

@@ -0,0 +1,72 @@
# Configuration
Opengist provides flexible configuration options through either a YAML file and/or environment variables.
You would only need to specify the configuration options you want to change — for any config option left untouched,
Opengist will simply apply the default values.
The [configuration cheat sheet](cheat-sheet.md) lists all available configuration options.
## Configuration via YAML file
The configuration file must be specified when launching the application, using the `--config` flag followed by the path
to your YAML file.
Usage with Docker Compose :
```yml
services:
opengist:
# ...
volumes:
# ...
- "/path/to/config.yml:/config.yml"
```
Usage via command line :
```shell
./opengist --config /path/to/config.yml
```
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
Usage with Docker Compose :
```yml
services:
opengist:
# ...
environment:
OG_LOG_LEVEL: "info"
# etc.
```
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,31 @@
# 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
```

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,41 @@
# 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.
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
```
#### 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

4
docs/index.md Normal file
View File

@@ -0,0 +1,4 @@
---
layout: home
navbar: false
---

7
docs/installation.md Normal file
View File

@@ -0,0 +1,7 @@
# Install Opengist
There are several ways to install Opengist, depending on your preferences and your environment.
- [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.0/opengist1.8.0-linux-amd64.tar.gz
tar xzvf opengist1.8.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```

View File

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

View File

@@ -0,0 +1,19 @@
# Installation from source
Requirements:
* [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.23+)
* [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone https://github.com/thomiceli/opengist
cd opengist
git checkout v1.8.0 # 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.0/opengist1.8.0-linux-amd64.tar.gz
tar xzvf opengist1.8.0-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

@@ -0,0 +1,42 @@
# Init Gists via Git
Opengist allows you to create new snippets via Git over HTTP.
Simply init a new Git repository where your file(s) is/are located:
```shell
git init
git add .
git commit -m "My cool snippet"
```
Then add this Opengist special remote URL and push your changes:
```shell
git remote add origin http://localhost:6157/init
git push -u origin master
```
Log in with your Opengist account credentials, and your snippet will be created at the specified URL:
```shell
Username for 'http://localhost:6157': thomas
Password for 'http://thomas@localhost:6157':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
remote:
remote: If you want to keep working with your gist, you could set the remote URL via:
remote: git remote set-url origin http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
remote:
To http://localhost:6157/init
* [new branch] master -> master
```
<video controls="controls" src="https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066" />

View File

@@ -1,8 +0,0 @@
//go:build fs_embed
package main
import "embed"
//go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg
var dirFS embed.FS

View File

@@ -1,7 +0,0 @@
//go:build !fs_embed
package main
import "os"
var dirFS = os.DirFS(".")

121
go.mod
View File

@@ -1,35 +1,114 @@
module opengist
module github.com/thomiceli/opengist
go 1.19
go 1.23
require (
github.com/go-playground/validator/v10 v10.11.0
github.com/google/uuid v1.3.0
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo/v4 v4.10.0
github.com/rs/zerolog v1.29.0
golang.org/x/crypto v0.2.0
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/blevesearch/bleve/v2 v2.4.2
github.com/dustin/go-humanize v1.0.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.22.1
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.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.9.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.28.0
golang.org/x/text v0.19.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/sqlite v1.3.2
gorm.io/gorm v1.23.5
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/bits-and-blooms/bitset v1.14.3 // indirect
github.com/blevesearch/bleve_index_api v1.1.12 // 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.10 // 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.15 // indirect
github.com/blevesearch/zapx/v16 v16.1.7 // 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/dlclark/regexp2 v1.11.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // 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/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.1 // 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.17 // indirect
github.com/mattn/go-sqlite3 v1.14.13 // 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.4.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.2.0 // 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-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/time v0.7.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.33.1 // indirect
)

349
go.sum
View File

@@ -1,110 +1,303 @@
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA=
github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.4.2 h1:NooYP1mb3c0StkiY9/xviiq2LGSaE8BQBCc/pirMx0U=
github.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8=
github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY=
github.com/blevesearch/bleve_index_api v1.1.12/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.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
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.15 h1:JydcGIq279tmTrfBBSPDF/VOiCMBLQ+rJulTXrGFlGA=
github.com/blevesearch/zapx/v15 v15.3.15/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/blevesearch/zapx/v16 v16.1.7 h1:I07qV6l1rPda19zyof9Q2J9E8cjZ57pQhNY0+ePI5vM=
github.com/blevesearch/zapx/v16 v16.1.7/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/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
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/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.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.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=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
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.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
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/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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
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/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.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/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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
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-20210806184541-e5e7981a1069/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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
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.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
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.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/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.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
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.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
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.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.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.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")
}
}

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

@@ -0,0 +1,185 @@
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")).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 := 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,109 +2,153 @@ package config
import (
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"io"
"net/url"
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/utils"
"gopkg.in/yaml.v3"
)
var OpengistVersion = "1.0.0"
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 {
LogLevel string `yaml:"log-level"`
ExternalUrl string `yaml:"external-url"`
DisableSignup bool `yaml:"disable-signup"`
OpengistHome string `yaml:"opengist-home"`
DBFilename string `yaml:"db-filename"`
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
HttpHost string `yaml:"http.host"`
HttpPort string `yaml:"http.port"`
HttpGit bool `yaml:"http.git-enabled"`
HttpTLSEnabled bool `yaml:"http.tls-enabled"`
HttpCertFile string `yaml:"http.cert-file"`
HttpKeyFile string `yaml:"http.key-file"`
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"`
SshGit bool `yaml:"ssh.git-enabled"`
SshHost string `yaml:"ssh.host"`
SshPort string `yaml:"ssh.port"`
SshExternalDomain string `yaml:"ssh.external-domain"`
SshKeygen string `yaml:"ssh.keygen-executable"`
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"`
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"`
SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"`
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"`
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) {
homeDir, err := os.UserHomeDir()
c := &config{}
if err != nil {
return c, err
}
c.SecretKey = ""
c.LogLevel = "warn"
c.DisableSignup = false
c.OpengistHome = filepath.Join(homeDir, ".opengist")
c.DBFilename = "opengist.db"
c.LogOutput = "stdout,file"
c.OpengistHome = ""
c.DBUri = "opengist.db"
c.IndexEnabled = true
c.IndexDirname = "opengist.index"
c.SqliteJournalMode = "WAL"
c.HttpHost = "0.0.0.0"
c.HttpPort = "6157"
c.HttpGit = true
c.HttpTLSEnabled = false
c.SshGit = true
c.SshHost = "0.0.0.0"
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
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 configPath != "" {
absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath)
file, err := os.Open(absolutePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
fmt.Println("No YML config file found at " + absolutePath)
} else {
fmt.Println("Using config file: " + absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
if err = d.Decode(&c); err != nil {
return err
}
defer file.Close()
}
} else {
fmt.Println("No config file specified. Using default values.")
if err = loadConfigFromYaml(c, configPath, out); err != nil {
return err
}
// Override default values with environment variables (as yaml)
configEnv := os.Getenv("CONFIG")
if configEnv != "" {
fmt.Println("Using config from environment variable: CONFIG")
d := yaml.NewDecoder(strings.NewReader(configEnv))
if err = d.Decode(&c); err != nil {
return err
if err = loadConfigFromEnv(c, out); err != nil {
return err
}
if c.OpengistHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration")
}
c.OpengistHome = filepath.Join(homeDir, ".opengist")
}
if err = checks(c); err != nil {
return err
}
C = c
if err = migrateConfig(); err != nil {
return err
}
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err
}
return nil
}
@@ -112,22 +156,62 @@ 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
}
if os.Getenv("DEV") == "1" {
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
} else {
log.Logger = zerolog.New(file).Level(level).With().Timestamp().Logger()
var logWriters []io.Writer
logOutputTypes := utils.RemoveDuplicates[string](
strings.Split(strings.ToLower(C.LogOutput), ","),
)
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)
}
}
@@ -145,8 +229,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,3 +240,135 @@ func GetHomeDir() string {
absolutePath, _ := filepath.Abs(C.OpengistHome)
return filepath.Clean(absolutePath)
}
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)
file, err := os.Open(absolutePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
} else {
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
if err = d.Decode(&c); err != nil {
return err
}
defer file.Close()
}
} else {
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
}
return nil
}
func loadConfigFromEnv(c *config, out io.Writer) error {
v := reflect.ValueOf(c).Elem()
var envVars []string
for i := 0; i < v.NumField(); i++ {
tag := v.Type().Field(i).Tag.Get("env")
if tag == "" {
continue
}
envValue := os.Getenv(strings.ToUpper(tag))
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())
}
}
if len(envVars) > 0 {
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
} else {
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
}
return nil
}
func checks(c *config) error {
if _, err := url.Parse(c.ExternalUrl); err != nil {
return err
}
if _, err := url.Parse(c.GiteaUrl); err != nil {
return err
}
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
return err
}
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

@@ -0,0 +1,83 @@
package db
import (
"gorm.io/gorm/clause"
)
type AdminSetting struct {
Key string `gorm:"index:,unique"`
Value string
}
const (
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
return setting.Value, err
}
func GetSettings() (map[string]string, error) {
var settings []AdminSetting
err := db.Find(&settings).Error
if err != nil {
return nil, err
}
result := make(map[string]string)
for _, setting := range settings {
result[setting.Key] = setting.Value
}
return result, nil
}
func UpdateSetting(key string, value string) error {
return db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}}, // key column
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}).Create(&AdminSetting{
Key: key,
Value: value,
}).Error
}
func setSetting(key string, value string) error {
return db.FirstOrCreate(&AdminSetting{Key: key, Value: value}, &AdminSetting{Key: key}).Error
}
func initAdminSettings(settings map[string]string) error {
for key, value := range settings {
if err := setSetting(key, value); err != nil {
if !IsUniqueConstraintViolation(err) {
return err
}
}
}
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
}

245
internal/db/db.go Normal file
View File

@@ -0,0 +1,245 @@
package db
import (
"errors"
"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"
"gorm.io/gorm"
)
var db *gorm.DB
const (
SQLite databaseType = iota
PostgreSQL
MySQL
)
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{}
if !strings.Contains(uri, "://") {
info.Type = SQLite
if uri == "file::memory:" {
info.Database = "file::memory:"
return info, nil
}
info.Database = filepath.Join(config.GetHomeDir(), uri)
return info, nil
}
u, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("invalid URI: %v", err)
}
switch u.Scheme {
case "postgres", "postgresql":
info.Type = PostgreSQL
case "mysql", "mariadb":
info.Type = MySQL
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, "/")
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
}
if err = db.SetupJoinTable(&User{}, "Liked", &Like{}); err != nil {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}); err != nil {
return err
}
if err = applyMigrations(db, dbInfo); err != nil {
return err
}
// Default admin setting values
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingAllowGistsWithoutLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})
}
func Close() error {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func CountAll(table interface{}) (int64, error) {
var count int64
err := db.Model(table).Count(&count).Error
return count, err
}
func IsUniqueConstraintViolation(err error) bool {
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)
}
sharedCacheStr := ""
if sharedCache {
sharedCacheStr = "&cache=shared"
}
db, err = gorm.Open(sqlite.Open(dbInfo.Database+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &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{})
}

644
internal/db/gist.go Normal file
View File

@@ -0,0 +1,644 @@
package db
import (
"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 Visibility // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
NbLikes int
NbForks int
CreatedAt int64
UpdatedAt int64
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint
}
type Like struct {
UserID uint `gorm:"primaryKey"`
GistID uint `gorm:"primaryKey"`
CreatedAt int64
}
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
// Decrement fork counter if the gist was forked
err := tx.Model(&Gist{}).
Omit("updated_at").
Where("id = ?", gist.ForkedID).
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).Error
return err
}
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.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
return gist, err
}
func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
Where("gists.id = ?", gistId).
First(&gist).Error
return gist, err
}
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11).
Offset(offset * 10).
Order(sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func GetAllGists(offset int) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").
Limit(11).
Offset(offset * 10).
Order("id asc").
Find(&gists).Error
return gists, err
}
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id").
Joins("join users on likes.user_id = users.id")
}
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := likedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := likedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := forkedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := forkedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func GetAllGistsRows() ([]*Gist, error) {
var gists []*Gist
err := db.Table("gists").
Preload("User").
Find(&gists).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
}
func (gist *Gist) CreateForked() error {
return db.Create(&gist).Error
}
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 {
return err
}
return db.Delete(&gist).Error
}
func (gist *Gist) SetLastActiveNow() error {
return db.Model(&Gist{}).
Where("id = ?", gist.ID).
Update("updated_at", time.Now().Unix()).Error
}
func (gist *Gist) AppendUserLike(user *User) error {
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1).Error
if err != nil {
return err
}
return db.Model(&gist).Omit("updated_at").Association("Likes").Append(user)
}
func (gist *Gist) RemoveUserLike(user *User) error {
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1).Error
if err != nil {
return err
}
return db.Model(&gist).Omit("updated_at").Association("Likes").Delete(user)
}
func (gist *Gist) IncrementForkCount() error {
return db.Model(&gist).Omit("updated_at").Update("nb_forks", gist.NbForks+1).Error
}
func (gist *Gist) GetForkParent(user *User) (*Gist, error) {
fork := new(Gist)
err := db.Preload("User").
Where("forked_id = ? and user_id = ?", gist.ID, user.ID).
First(&fork).Error
return fork, err
}
func (gist *Gist) GetUsersLikes(offset int) ([]*User, error) {
var users []*User
err := db.Model(&gist).
Where("gist_id = ?", gist.ID).
Limit(31).
Offset(offset * 30).
Association("Likes").Find(&users)
return users, err
}
func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
var gists []*Gist
err := db.Model(&gist).Preload("User").
Where("forked_id = ?", gist.ID).
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
Limit(11).
Offset(offset * 10).
Order("updated_at desc").
Find(&gists).Error
return gists, err
}
func (gist *Gist) CanWrite(user *User) bool {
return !(user == nil) && (gist.UserID == user.ID)
}
func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
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, &git.RevisionNotFoundError{}
}
return nil, err
}
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
}
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
content, truncated, err := git.GetFileContent(gist.User.Username, gist.Uuid, revision, filename, truncate)
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
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)
}
func (gist *Gist) NbCommits() (string, error) {
return git.CountCommits(gist.User.Username, gist.Uuid)
}
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
return err
}
for _, file := range *files {
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) 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)
}
func (gist *Gist) UpdateServerInfo() error {
return git.UpdateServerInfo(gist.User.Username, gist.Uuid)
}
func (gist *Gist) RPC(service string) ([]byte, error) {
return git.RPC(gist.User.Username, gist.Uuid, service)
}
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
if err != nil {
return err
}
gist.NbFiles = len(filesStr)
if len(filesStr) == 0 {
gist.Preview = ""
gist.PreviewFilename = ""
} else {
file, err := gist.File("HEAD", filesStr[0], true)
if err != nil {
return err
}
split := strings.Split(file.Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = file.Content
}
gist.PreviewFilename = file.Filename
}
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 -- //
type GistDTO struct {
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
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 {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string `validate:"required"`
}
func (dto *GistDTO) ToGist() *Gist {
return &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)
}

115
internal/db/migration.go Normal file
View File

@@ -0,0 +1,115 @@
package db
import (
"fmt"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type MigrationVersion struct {
ID uint `gorm:"primaryKey"`
Version uint
}
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")
return err
}
// Get the current migration version
var currentVersion MigrationVersion
db.First(&currentVersion)
// Define migrations
migrations := []struct {
Version uint
Func func(*gorm.DB) error
}{
{1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails},
// Add more migrations here as needed
}
// Apply migrations
for _, m := range migrations {
if m.Version > currentVersion.Version {
tx := db.Begin()
if err := tx.Error; err != nil {
log.Fatal().Err(err).Msg("Error starting transaction")
return err
}
if err := m.Func(db); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback()
return err
} else {
if err = tx.Commit().Error; err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
return err
}
currentVersion.Version = m.Version
db.Save(&currentVersion)
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
}
}
}
return nil
}
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
createSQL := `
CREATE TABLE ssh_keys_temp (
id integer primary key,
title text,
content text,
sha text,
created_at integer,
last_used_at integer,
user_id integer
constraint fk_users_ssh_keys references users(id) on update cascade on delete cascade
);
`
if err := db.Exec(createSQL).Error; err != nil {
return err
}
// Copy data from the old table to the new table
copySQL := `INSERT INTO ssh_keys_temp SELECT * FROM ssh_keys;`
if err := db.Exec(copySQL).Error; err != nil {
return err
}
// Drop the old table
dropSQL := `DROP TABLE ssh_keys;`
if err := db.Exec(dropSQL).Error; err != nil {
return err
}
// Rename the new table to the original table name
renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;`
return db.Exec(renameSQL).Error
}
func v2_lowercaseEmails(db *gorm.DB) error {
// Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error
}

View File

@@ -1,6 +1,12 @@
package models
package db
import "time"
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"time"
)
type SSHKey struct {
ID uint `gorm:"primaryKey"`
@@ -13,6 +19,16 @@ type SSHKey struct {
User User `validate:"-" `
}
func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil {
return err
}
sha := sha256.Sum256(pubKey.Marshal())
sshKey.SHA = base64.StdEncoding.EncodeToString(sha[:])
return nil
}
func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) {
var sshKeys []*SSHKey
err := db.
@@ -32,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
return sshKey, err
}
func GetSSHKeyByContent(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 {
@@ -49,9 +64,9 @@ func (sshKey *SSHKey) Delete() error {
return db.Delete(&sshKey).Error
}
func SSHKeyLastUsedNow(sshKeyID uint) error {
func SSHKeyLastUsedNow(sshKeyContent string) error {
return db.Model(&SSHKey{}).
Where("id = ?", sshKeyID).
Where("content = ?", sshKeyContent).
Update("last_used_at", time.Now().Unix()).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 ""
}

234
internal/db/user.go Normal file
View File

@@ -0,0 +1,234 @@
package db
import (
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex,size:191"`
Password string
IsAdmin bool
CreatedAt int64
Email string
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"`
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
}
func (user *User) BeforeDelete(tx *gorm.DB) error {
// Decrement likes counter for all gists liked by this user
// The likes will be automatically deleted by the foreign key constraint
err := tx.Model(&Gist{}).
Omit("updated_at").
Where("id IN (?)", tx.
Select("gist_id").
Table("likes").
Where("user_id = ?", user.ID),
).
UpdateColumn("nb_likes", gorm.Expr("nb_likes - 1")).
Error
if err != nil {
return err
}
// Decrement forks counter for all gists forked by this user
err = tx.Model(&Gist{}).
Omit("updated_at").
Where("id IN (?)", tx.
Select("forked_id").
Table("gists").
Where("user_id = ?", user.ID),
).
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
Error
if err != nil {
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
}
func UserExists(username string) (bool, error) {
var count int64
err := db.Model(&User{}).Where("username like ?", username).Count(&count).Error
return count > 0, err
}
func GetAllUsers(offset int) ([]*User, error) {
var users []*User
err := db.
Limit(11).
Offset(offset * 10).
Order("id asc").
Find(&users).Error
return users, err
}
func GetUserByUsername(username string) (*User, error) {
user := new(User)
err := db.
Where("username like ?", username).
First(&user).Error
return user, err
}
func GetUserById(userId uint) (*User, error) {
user := new(User)
err := db.
Where("id = ?", userId).
First(&user).Error
return user, err
}
func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error) {
var users []*User
emails := make([]string, 0, len(emailsSet))
for email := range emailsSet {
emails = append(emails, email)
}
err := db.
Where("email IN ?", emails).
Find(&users).Error
if err != nil {
return nil, err
}
userMap := make(map[string]*User)
for _, user := range users {
userMap[user.Email] = user
}
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.
Where("content = ?", sshKey).
Where("user_id = ?", userId).
First(&key).Error
return key, err
}
func GetUserByProvider(id string, provider string) (*User, error) {
user := new(User)
var err 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":
err = db.Where("oidc_id = ?", id).First(&user).Error
}
return user, err
}
func (user *User) Create() error {
return db.Create(&user).Error
}
func (user *User) Update() error {
return db.Save(&user).Error
}
func (user *User) Delete() error {
return db.Delete(&user).Error
}
func (user *User) SetAdmin() error {
return db.Model(&user).Update("is_admin", true).Error
}
func (user *User) HasLiked(gist *Gist) (bool, error) {
association := db.Model(&gist).Where("user_id = ?", user.ID).Association("Likes")
if association.Error != nil {
return false, association.Error
}
if association.Count() == 0 {
return false, nil
}
return true, nil
}
func (user *User) DeleteProviderID(provider string) error {
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(providerIDField, nil).
Update("avatar_url", nil).
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,alphanumdash,notreserved"`
Password string `form:"password" validate:"required"`
}
func (dto *UserDTO) ToUser() *User {
return &User{
Username: dto.Username,
Password: dto.Password,
}
}

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,17 +1,57 @@
package git
import (
"bufio"
"bytes"
"context"
"fmt"
"opengist/internal/config"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
)
var (
ReposDirectory = "repos"
)
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(), "repos", strings.ToLower(user), gist)
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
}
func RepositoryUrl(ctx echo.Context, user string, gist string) string {
httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
httpProtocol = "https"
}
var baseHttpUrl string
// if a custom external url is set, use it
if config.C.ExternalUrl != "" {
baseHttpUrl = config.C.ExternalUrl
} else {
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
}
return fmt.Sprintf("%s/%s/%s", baseHttpUrl, user, gist)
}
func TmpRepositoryPath(gistId string) string {
@@ -26,22 +66,23 @@ 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)
err := cmd.Run()
if err != nil {
cmd := exec.Command("git", args...)
if err := cmd.Run(); err != nil {
return err
}
return copyFiles(repositoryPath)
return CreateDotGitFiles(user, gist)
}
func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) {
func CountCommits(user string, gist string) (string, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
@@ -62,8 +103,9 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
cmd := exec.Command(
"git",
"ls-tree",
revision,
"--name-only",
"--",
revision,
)
cmd.Dir = repositoryPath
@@ -76,15 +118,134 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
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: 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)
var maxBytes int64 = -1
if truncate {
maxBytes = 2 << 18
maxBytes = truncateLimit
}
cmd := exec.Command(
// Set up a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
cmd := exec.CommandContext(
ctx,
"git",
"--no-pager",
"show",
@@ -92,16 +253,39 @@ func GetFileContent(user string, gist string, revision string, filename string,
)
cmd.Dir = repositoryPath
stdout, _ := cmd.StdoutPipe()
err := cmd.Start()
output, err := cmd.Output()
if err != nil {
return "", false, err
}
return truncateCommandOutput(stdout, maxBytes)
content, truncated, err := truncateCommandOutput(bytes.NewReader(output), maxBytes)
if err != nil {
return "", false, err
}
return content, truncated, nil
}
func GetLog(user string, gist string, skip string) ([]*Commit, error) {
func GetFileSize(user string, gist string, revision string, filename string) (uint64, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
"git",
"cat-file",
"-s",
revision+":"+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)
cmd := exec.Command(
@@ -113,7 +297,7 @@ func GetLog(user string, gist string, skip string) ([]*Commit, error) {
"--no-color",
"-p",
"--skip",
skip,
strconv.Itoa(skip),
"--format=format:c %H%na %aN%nm %ae%nt %at",
"--shortstat",
"HEAD",
@@ -124,11 +308,17 @@ func GetLog(user string, gist string, skip string) ([]*Commit, error) {
if err != nil {
return nil, err
}
defer func(cmd *exec.Cmd) {
waitErr := cmd.Wait()
if waitErr != nil {
err = waitErr
}
}(cmd)
return parseLog(stdout, 2<<18), nil
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()
@@ -146,13 +336,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
return err
}
// remove every file (and not the .git directory!)
cmd = exec.Command("find", ".", "-maxdepth", "1", "-type", "f", "-delete")
cmd.Dir = tmpRepositoryPath
if err = cmd.Run(); 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 {
@@ -173,7 +363,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
return err
}
return copyFiles(repositoryPathDst)
return CreateDotGitFiles(userDst, gistDst)
}
func SetFileContent(gistTmpId string, filename string, content string) error {
@@ -226,7 +416,6 @@ func Push(gistTmpId string) error {
if err != nil {
return err
}
return os.RemoveAll(tmpRepositoryPath)
}
@@ -251,6 +440,67 @@ func RPC(user string, gist string, service string) ([]byte, error) {
return stdout, err
}
func GcRepos() error {
subdirs, err := os.ReadDir(filepath.Join(config.GetHomeDir(), ReposDirectory))
if err != nil {
return err
}
for _, subdir := range subdirs {
if !subdir.IsDir() {
continue
}
subRoot := filepath.Join(config.GetHomeDir(), ReposDirectory, subdir.Name())
gitRepos, err := os.ReadDir(subRoot)
if err != nil {
log.Warn().Err(err).Msg("Cannot read directory")
continue
}
for _, repo := range gitRepos {
if !repo.IsDir() {
continue
}
repoPath := filepath.Join(subRoot, repo.Name())
log.Info().Msg("Running git gc for repository " + repoPath)
cmd := exec.Command("git", "gc")
cmd.Dir = repoPath
err = cmd.Run()
if err != nil {
log.Warn().Err(err).Msg("Cannot run git gc for repository " + repoPath)
continue
}
}
}
return err
}
func HasNoCommits(user string, gist string) (bool, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command("git", "rev-parse", "--all")
cmd.Dir = repositoryPath
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return false, err
}
if out.String() == "" {
return true, nil // No commits exist
}
return false, nil // Commits exist
}
func GetGitVersion() (string, error) {
cmd := exec.Command("git", "--version")
stdout, err := cmd.Output()
@@ -266,16 +516,33 @@ func GetGitVersion() (string, error) {
return versionFields[2], nil
}
func copyFiles(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()
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", "pre-receive"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
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_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
if err != nil {
return err
}
if _, err = preReceiveDst.WriteString(preReceive); err != nil {
if _, err = preReceiveDst.WriteString(content); err != nil {
return err
}
defer preReceiveDst.Close()
@@ -283,29 +550,21 @@ func copyFiles(repositoryPath string) error {
return nil
}
const preReceive = `#!/bin/sh
func removeFilesExceptGit(dir string) error {
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && filepath.Base(path) == ".git" {
return filepath.SkipDir
}
if !d.IsDir() {
return os.Remove(path)
}
return nil
})
}
disallowed_files=""
while read -r old_rev new_rev ref
do
while IFS= read -r file
do
case $file in
*/*)
disallowed_files="${disallowed_files}${file} "
;;
esac
done <<EOF
$(git diff --name-only "$old_rev" "$new_rev")
EOF
done
if [ -n "$disallowed_files" ]; then
echo "Pushing files in folders is not allowed:"
for file in $disallowed_files; do
echo " $file"
done
exit 1
fi
const hookTemplate = `#!/bin/sh
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
`

View File

@@ -0,0 +1,242 @@
package git
import (
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"os"
"os/exec"
"path"
"strings"
"testing"
)
func TestInitDeleteRepository(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
cmd.Dir = RepositoryPath("thomas", "gist1")
out, err := cmd.Output()
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"), "git-daemon-export-ok"))
require.NoError(t, err, "git-daemon-export-ok file not found")
err = DeleteRepository("thomas", "gist1")
require.NoError(t, err, "Could not delete repository")
require.NoDirExists(t, RepositoryPath("thomas", "gist1"), "Repository should not exist")
}
func TestCommits(t *testing.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)
hasNoCommits, err = HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has no commits")
require.False(t, hasNoCommits, "Repository should have commits")
nbCommits, err := CountCommits("thomas", "gist1")
require.NoError(t, err, "Could not count commits")
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
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) {
SetupTest(t)
defer TeardownTest(t)
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",
})
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")
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false)
require.NoError(t, err, "Could not get content")
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "I love Opengist\n", content, "Content is not correct")
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
require.NoError(t, err, "Could not get content")
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{
"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",
})
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")
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
require.NoError(t, err, "Could not get content")
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct")
commits, err := GetLog("thomas", "gist1", 0)
require.NoError(t, err, "Could not get log")
require.Equal(t, 2, len(commits), "Commits count are not correct")
require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct")
require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct")
require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct")
require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct")
require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct")
require.Contains(t, commits[0].Files, File{
Filename: "my_renamed_file.txt",
OldFilename: "my_file.txt",
Content: "",
Truncated: false,
IsCreated: false,
IsDeleted: false,
}, "File my_renamed_file.txt is not correct")
require.Contains(t, commits[0].Files, File{
Filename: "rip.txt",
OldFilename: "",
Content: `@@ -1 +0,0 @@
-byebye
\ No newline at end of file
`,
Truncated: false,
IsCreated: false,
IsDeleted: true,
}, "File rip.txt is not correct")
require.Contains(t, commits[0].Files, File{
Filename: "my_other_file.txt",
OldFilename: "my_other_file.txt",
Content: `@@ -1,2 +1,2 @@
I really
-hate Opengist
\ No newline at end of file
+like Opengist actually
\ No newline at end of file
`,
Truncated: false,
IsCreated: false,
IsDeleted: false,
}, "File my_other_file.txt is not correct")
require.Contains(t, commits[0].Files, File{
Filename: "new_file.txt",
OldFilename: "",
Content: `@@ -0,0 +1 @@
+Wait now there is a new file
\ No newline at end of file
`,
Truncated: false,
IsCreated: true,
IsDeleted: false,
}, "File new_file.txt is not correct")
commitsSkip1, err := GetLog("thomas", "gist1", 1)
require.NoError(t, err, "Could not get log")
require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct")
}
func TestGitGc(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
err := GcRepos()
require.NoError(t, err, "Could not run git gc")
}
func TestFork(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "I love Opengist\n",
})
err := ForkClone("thomas", "gist1", "thomas", "gist2")
require.NoError(t, err, "Could not fork repository")
files1, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
require.NoError(t, err, "Could not get files of repository")
files2, err := GetFilesOfRepository("thomas", "gist2", "HEAD")
require.NoError(t, err, "Could not get files of repository")
require.Equal(t, files1, files2, "Files are not the same")
}
func TestTruncate(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "A",
})
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
require.NoError(t, err, "Could not get content")
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, 1, len(content), "Content size is not correct")
var builder strings.Builder
for i := 0; i < truncateLimit+10; i++ {
builder.WriteString("A")
}
str := builder.String()
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": str,
})
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
require.NoError(t, err, "Could not get content")
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{
"my_file.txt": "AA\n" + str,
})
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
require.NoError(t, err, "Could not get content")
require.True(t, truncated, "Content should be truncated")
require.Equal(t, 2, len(content), "Content size is not correct")
}
func TestGitInitBranchNames(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
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")
config.C.GitDefaultBranch = "main"
err = InitRepository("thomas", "gist2")
require.NoError(t, err)
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")
}

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 {
@@ -46,7 +47,7 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
if err != nil {
return "", false, err
}
truncated := len(buf) >= int(maxBytes)
truncated := maxBytes > 0 && len(buf) >= int(maxBytes)
// Remove the last line if it's truncated
if truncated {
// Find the index of the last newline character
@@ -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 = line[12 : len(line)-1]
case strings.HasPrefix(line, "rename to "):
currentFile.Filename = line[10 : len(line)-1]
parseRename = false
case strings.HasPrefix(line, "copy from "):
currentFile.OldFilename = line[10 : len(line)-1]
case strings.HasPrefix(line, "copy to "):
currentFile.Filename = 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 := 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 := 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
}

140
internal/i18n/locale.go Normal file
View File

@@ -0,0 +1,140 @@
package i18n
import (
"fmt"
"github.com/thomiceli/opengist/internal/i18n/locales"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
"gopkg.in/yaml.v3"
"html/template"
"io"
"io/fs"
"path/filepath"
"strings"
)
var Locales = NewLocaleStore()
type LocaleStore struct {
Locales map[string]*Locale
}
type Locale struct {
Code string
Name string
Messages map[string]string
}
// NewLocaleStore creates a new LocaleStore
func NewLocaleStore() *LocaleStore {
return &LocaleStore{
Locales: make(map[string]*Locale),
}
}
// loadLocaleFromYAML loads a single Locale from a given YAML file
func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
a, err := locales.Files.Open(path)
if err != nil {
return err
}
data, err := io.ReadAll(a)
if err != nil {
return err
}
tag, err := language.Parse(localeCode)
if err != nil {
return err
}
name := display.Self.Name(tag)
if tag == language.AmericanEnglish {
name = "English"
} else if tag == language.EuropeanSpanish {
name = "Español"
}
locale := &Locale{
Code: localeCode,
Name: cases.Title(language.English).String(name),
Messages: make(map[string]string),
}
err = yaml.Unmarshal(data, &locale.Messages)
if err != nil {
return err
}
store.Locales[localeCode] = locale
return nil
}
func (store *LocaleStore) LoadAll() error {
return fs.WalkDir(locales.Files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
localeKey := strings.TrimSuffix(path, filepath.Ext(path))
err := store.loadLocaleFromYAML(localeKey, path)
if err != nil {
return err
}
}
return nil
})
}
func (store *LocaleStore) GetLocale(lang string) (*Locale, error) {
_, ok := store.Locales[lang]
if !ok {
return nil, fmt.Errorf("locale %s not found", lang)
}
return store.Locales[lang], nil
}
func (store *LocaleStore) HasLocale(lang string) bool {
_, ok := store.Locales[lang]
return ok
}
func (store *LocaleStore) MatchTag(langs []language.Tag) string {
for _, lang := range langs {
if store.HasLocale(lang.String()) {
return lang.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]
if message == "" {
return Locales.Locales["en-US"].Tr(key, args...)
}
if len(args) == 0 {
return template.HTML(message)
}
return template.HTML(fmt.Sprintf(message, args...))
}

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

@@ -0,0 +1,305 @@
gist.public: Public
gist.unlisted: Unlisted
gist.private: Private
gist.header.like: Like
gist.header.unlike: Unlike
gist.header.fork: Fork
gist.header.edit: Edit
gist.header.delete: Delete
gist.header.forked-from: Forked from
gist.header.last-active: Last active
gist.header.select-tab: Select a tab
gist.header.code: Code
gist.header.revisions: Revisions
gist.header.revision: Revision
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.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 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
gist.new.indent-mode-tab: Tab
gist.new.indent-size: Indent size
gist.new.wrap-mode: Wrap mode
gist.new.wrap-mode-no: No wrap
gist.new.wrap-mode-soft: Soft wrap
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.list.joined: Joined
gist.list.all: All gists
gist.list.search-results: Search results
gist.list.sort: Sort
gist.list.sort-by-created: created
gist.list.sort-by-updated: updated
gist.list.order-by-asc: Least recently
gist.list.order-by-desc: Recently
gist.list.select-tab: Select a tab
gist.list.liked: Liked
gist.list.likes: likes
gist.list.forked: Forked
gist.list.forked-from: Forked from
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
gist.revision.go-to-revision: Go to revision
gist.revision.file-created: file created
gist.revision.file-deleted: file deleted
gist.revision.file-renamed: renamed to
gist.revision.diff-truncated: Diff is too large to be shown
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
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 ?
settings.add-ssh-key: Add SSH key
settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH
settings.add-ssh-key-title: Title
settings.add-ssh-key-content: Key
settings.delete-ssh-key: Delete
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
settings.change-password-help: Change your password to login to Opengist via HTTP
settings.password-label-title: Password
auth.signup-disabled: Administrator has disabled signing up
auth.login: Login
auth.signup: Register
auth.new-account: New account
auth.username: Username
auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
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
header.menu.search: Search
header.menu.my-gists: My gists
header.menu.liked: Liked
header.menu.admin: Admin
header.menu.settings: Settings
header.menu.logout: Logout
header.menu.register: Register
header.menu.login: Login
header.menu.light: Light
header.menu.dark: Dark
header.menu.system: System
footer.powered-by: Powered by %s
pagination.older: Older
pagination.newer: Newer
pagination.previous: Previous
pagination.next: Next
admin.admin_panel: Admin panel
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 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
admin.created_at: Created
admin.config-link: This configuration can be %s by a YAML config file and/or environment variables.
admin.config-link-overriden: overridden
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
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
admin.users.delete_confirm: Do you want to delete this user ?
admin.gists.title: Title
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
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

@@ -0,0 +1,259 @@
gist.public: Público
gist.unlisted: No listado
gist.private: Privado
gist.header.like: Me gusta
gist.header.unlike: No me gusta
gist.header.fork: Bifurcar
gist.header.edit: Editar
gist.header.delete: Eliminar
gist.header.forked-from: Bifurcado desde
gist.header.last-active: Última actividad
gist.header.select-tab: Seleccionar pestaña
gist.header.code: Código
gist.header.revisions: Revisiones
gist.header.revision: Revisión
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.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato
gist.file-truncated: Este archivo ha sido truncado.
gist.watch-full-file: Ver el archivo completo.
gist.file-not-valid: Este archivo no es un archivo CSV válido.
gist.no-content: Sin contenido
gist.new.new_gist: Nuevo gist
gist.new.title: Título
gist.new.description: Descripción
gist.new.filename-with-extension: Nombre de archivo con extensión
gist.new.indent-mode: Modo de sangrado
gist.new.indent-mode-space: Espacio
gist.new.indent-mode-tab: Tabulación
gist.new.indent-size: Tamaño de sangrado
gist.new.wrap-mode: Modo de ajuste
gist.new.wrap-mode-no: Sin ajuste
gist.new.wrap-mode-soft: Ajuste suave
gist.new.add-file: Agregar archivo
gist.new.create-public-button: Crear gist público
gist.new.create-unlisted-button: Crear gist no listado
gist.new.create-private-button: Crear gist privado
gist.edit.editing: Editando
gist.edit.change-visibility: Hacer
gist.edit.delete: Eliminar
gist.edit.cancel: Cancelar
gist.edit.save: Guardar
gist.list.joined: Unido
gist.list.all: Todos los gists
gist.list.search-results: Resultados de búsqueda
gist.list.sort: Ordenar
gist.list.sort-by-created: creado
gist.list.sort-by-updated: actualizado
gist.list.order-by-asc: Menos reciente
gist.list.order-by-desc: Recientemente
gist.list.select-tab: Seleccionar pestaña
gist.list.liked: Gustado
gist.list.likes: gustos
gist.list.forked: Bifurcado
gist.list.forked-from: Bifurcado desde
gist.list.forks: bifurcaciones
gist.list.files: archivos
gist.list.last-active: Última actividad
gist.list.no-gists: Sin gists
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.revisions: Revisiones
gist.revision.revised: revisó este gist
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.file-renamed-no-changes: Archivo renombrado sin cambios
gist.revision.empty-file: Archivo vacío
gist.revision.no-changes: Sin cambios
gist.revision.no-revisions: No hay revisiones para mostrar
settings: Configuración
settings.email: Correo electrónico
settings.email-help: Usado para confirmaciones y Gravatar
settings.email-set: Establecer correo electrónico
settings.link-accounts: Enlazar cuentas
settings.link-github-account: Enlazar cuenta de GitHub
settings.link-gitea-account: Enlazar cuenta de Gitea
settings.unlink-github-account: Desenlazar cuenta de GitHub
settings.unlink-gitea-account: Desenlazar cuenta de Gitea
settings.delete-account: Eliminar cuenta
settings.delete-account-confirm: ¿Estás seguro de que quieres eliminar tu cuenta?
settings.add-ssh-key: Agregar clave SSH
settings.add-ssh-key-help: Usado solo para extraer/push gists usando Git a través de SSH
settings.add-ssh-key-title: Título
settings.add-ssh-key-content: Clave
settings.delete-ssh-key: Eliminar
settings.delete-ssh-key-confirm: Confirmar eliminación de clave SSH
settings.ssh-key-added-at: Añadido
settings.ssh-key-never-used: Nunca usado
settings.ssh-key-last-used: Último uso
auth.signup-disabled: El administrador ha deshabilitado el registro
auth.login: Iniciar sesión
auth.signup: Registrarse
auth.new-account: Nueva cuenta
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.oauth: Continuar con cuenta de %s
error: Error
header.menu.all: Todos
header.menu.new: Nuevo
header.menu.search: Buscar
header.menu.my-gists: Mis gists
header.menu.liked: Gustados
header.menu.admin: Administrador
header.menu.settings: Configuración
header.menu.logout: Cerrar sesión
header.menu.register: Registrarse
header.menu.login: Iniciar sesión
header.menu.light: Claro
header.menu.dark: Oscuro
header.menu.system: Sistema
footer.powered-by: Desarrollado por %s
pagination.older: Anterior
pagination.newer: Siguiente
pagination.previous: Anterior
pagination.next: Siguiente
admin.admin_panel: Panel de administración
admin.general: General
admin.users: Usuarios
admin.gists: Gists
admin.configuration: Configuración
admin.versions: Versiones
admin.ssh_keys: Claves SSH
admin.stats: Estadísticas
admin.actions: Acciones
admin.actions.sync-fs: Sincronizar gists desde el sistema de archivos
admin.actions.sync-db: Sincronizar gists desde la base de datos
admin.actions.git-gc: Recolectar basura en los repositorios Git
admin.id: ID
admin.user: Usuario
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.disable-signup: Deshabilitar registro
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
admin.require-login: Requerir inicio de sesión
admin.require-login_help: Obligar a los usuarios a iniciar sesión para ver gists.
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:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
admin.gists.title: Título
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: ''
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.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: ''
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,260 @@
gist.public: Public
gist.unlisted: Non répertorié
gist.private: Privé
gist.header.like: J'aime
gist.header.unlike: Je n'aime plus
gist.header.fork: Fork
gist.header.edit: Éditer
gist.header.delete: Supprimer
gist.header.forked-from: Forké de
gist.header.last-active: Dernière activité
gist.header.select-tab: Sélectionner un onglet
gist.header.code: Code
gist.header.revisions: Révisions
gist.header.revision: Révision
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.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: Aucun fichier
gist.new.new_gist: Nouveau gist
gist.new.title: Titre
gist.new.description: Description
gist.new.filename-with-extension: Nom de fichier avec extension
gist.new.indent-mode: Mode d'indentation
gist.new.indent-mode-space: Espace
gist.new.indent-mode-tab: Tabulation
gist.new.indent-size: Taille d'indentation
gist.new.wrap-mode: Mode d'enroulement
gist.new.wrap-mode-no: Sans enroulement
gist.new.wrap-mode-soft: Enroulement doux
gist.new.add-file: Ajouter un fichier
gist.new.create-public-button: Créer un gist public
gist.new.create-unlisted-button: Créer un gist non repertorié
gist.new.create-private-button: Créer un gist privé
gist.edit.editing: Édition de
gist.edit.change-visibility: Rendre
gist.edit.delete: Supprimer
gist.edit.cancel: Annuler
gist.edit.save: Sauvegarder
gist.list.joined: Inscrit
gist.list.all: Tous les gists
gist.list.search-results: Résultats de recherche
gist.list.sort: Trier
gist.list.sort-by-created: créé
gist.list.sort-by-updated: mis à jour
gist.list.order-by-asc: Le moins récemment
gist.list.order-by-desc: Récemment
gist.list.select-tab: Sélectionner un onglet
gist.list.liked: Aimé
gist.list.likes: j'aimes
gist.list.forked: Forké
gist.list.forked-from: Forké de
gist.list.forks: forks
gist.list.files: fichiers
gist.list.last-active: Dernière activité
gist.list.no-gists: Aucun gist
gist.forks: Forks
gist.forks.view: Voir le fork
gist.forks.no: Pas de forks publics
gist.likes: J'aime
gist.likes.no: Aucun j'aime pour le moment
gist.revisions: Révisions
gist.revision.revised: a révisé ce gist
gist.revision.go-to-revision: Aller à la révision
gist.revision.file-created: fichier créé
gist.revision.file-deleted: fichier supprimé
gist.revision.file-renamed: renommé en
gist.revision.diff-truncated: Révision trop volumineuse pour être affichée
gist.revision.file-renamed-no-changes: Fichier renommé sans modifications
gist.revision.empty-file: Fichier vide
gist.revision.no-changes: Aucun changement
gist.revision.no-revisions: Aucune révision à afficher
settings: Paramètres
settings.email: Email
settings.email-help: Utilisé pour les commits et Gravatar
settings.email-set: Définir l'email
settings.link-accounts: Lier les comptes
settings.link-github-account: Lier le compte GitHub
settings.link-gitea-account: Lier le compte Gitea
settings.unlink-github-account: Détacher le compte GitHub
settings.unlink-gitea-account: Détacher le compte Gitea
settings.delete-account: Supprimer le compte
settings.delete-account-confirm: Êtes-vous sûr de vouloir supprimer votre compte ?
settings.add-ssh-key: Ajouter une clé SSH
settings.add-ssh-key-help: Utilisé uniquement pour pull/push des gists avec Git via SSH
settings.add-ssh-key-title: Titre
settings.add-ssh-key-content: Clé
settings.delete-ssh-key: Supprimer
settings.delete-ssh-key-confirm: Confirmer la suppression de la clé SSH
settings.ssh-key-added-at: Ajouté
settings.ssh-key-never-used: Jamais utilisé
settings.ssh-key-last-used: Dernière utilisation
auth.signup-disabled: L'administrateur a désactivé l'inscription
auth.login: Connexion
auth.signup: Inscription
auth.new-account: Nouveau compte
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.oauth: Continuer avec un compte %s
error: Erreur
header.menu.all: Tous
header.menu.new: Nouveau
header.menu.search: Recherche
header.menu.my-gists: Mes gists
header.menu.liked: Aimés
header.menu.admin: Admin
header.menu.settings: Paramètres
header.menu.logout: Déconnexion
header.menu.register: Inscription
header.menu.login: Connexion
header.menu.light: Clair
header.menu.dark: Sombre
header.menu.system: Système
footer.powered-by: Propulsé par %s
pagination.older: Plus ancien
pagination.newer: Plus récent
pagination.previous: Précédent
pagination.next: Suivant
admin.admin_panel: Panneau d'administration
admin.general: Général
admin.users: Utilisateurs
admin.gists: Gists
admin.configuration: Configuration
admin.versions: Versions
admin.ssh_keys: Clés SSH
admin.stats: Statistiques
admin.actions: Actions
admin.actions.sync-fs: Synchroniser les gists depuis le système de fichiers
admin.actions.sync-db: Synchroniser les gists depuis la base de données
admin.actions.git-gc: Nettoyage des dépôts git
admin.id: ID
admin.user: Utilisateur
admin.delete: Supprimer
admin.created_at: Créé
admin.config-link: Cette configuration peut être %s par un fichier de configuration YAML et/ou des variables d'environnement.
admin.config-link-overriden: remplacée
admin.disable-signup: Désactiver l'inscription
admin.disable-signup_help: Interdire la création de nouveaux comptes.
admin.require-login: Exiger la connexion
admin.require-login_help: Obliger les utilisateurs à être connectés pour voir les gists.
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
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à

View File

@@ -0,0 +1,6 @@
package locales
import "embed"
//go:embed *.yml
var Files embed.FS

View File

@@ -0,0 +1,260 @@
gist.public: Nyilvános
gist.unlisted: Nem listázott
gist.private: Privát
gist.header.like: Tetszik
gist.header.unlike: Nem tetszik
gist.header.fork: Fork
gist.header.edit: Szerkesztés
gist.header.delete: Törlés
gist.header.forked-from: "Forkolva innen:"
gist.header.last-active: Utoljára aktív
gist.header.select-tab: Fül választása
gist.header.code: Kód
gist.header.revisions: Revíziók
gist.header.revision: Revízió
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.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: ZIP archívum letöltése
gist.raw: Eredeti
gist.file-truncated: Ennek a fájlnak nem az egész tartalma lett megjelenítve.
gist.watch-full-file: Tekintsd meg a fájl egész tartalmát.
gist.file-not-valid: Ez nem egy érvényes CSV fájl.
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
gist.new.indent-mode-tab: Tabulátor
gist.new.indent-size: Indentáció méret
gist.new.wrap-mode: Sortörés típusa
gist.new.wrap-mode-no: Nincs sortörés
gist.new.wrap-mode-soft: Csak megjelenítéskor
gist.new.add-file: Fájl hozzáadása
gist.new.create-public-button: Nyilvános gist létrehozása
gist.new.create-unlisted-button: Nem listázott gist létrehozása
gist.new.create-private-button: Privát gist létrehozása
gist.edit.editing: Szerkesztés
gist.edit.change-visibility: "Állítsd erre:"
gist.edit.delete: Törlés
gist.edit.cancel: Mégse
gist.edit.save: Mentés
gist.list.joined: Hozzáadva
gist.list.all: Összes gist
gist.list.search-results: Keresési erdemények
gist.list.sort: Rendezés
gist.list.sort-by-created: létrehozva
gist.list.sort-by-updated: módosítva
gist.list.order-by-asc: Utolsótól
gist.list.order-by-desc: Újabbtól
gist.list.select-tab: Fül választása
gist.list.liked: Tetszik
gist.list.likes: Kedvelések
gist.list.forked: Forkolva
gist.list.forked-from: "Forkolva innen:"
gist.list.forks: forkok
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
gist.likes: Kedvelések
gist.likes.no: Még nincsenek kedvelések
gist.revisions: Revíziók
gist.revision.revised: gist felülvizsgálása
gist.revision.go-to-revision: Revízióhoz ugrás
gist.revision.file-created: fájl létrehozva
gist.revision.file-deleted: fájl törölve
gist.revision.file-renamed: "fájl átnevezve erre: "
gist.revision.diff-truncated: Nem a teljes Diff lett megjelítve, mert túl hosszú lenne
gist.revision.file-renamed-no-changes: Fájl átnevezve változtatások nélkül
gist.revision.empty-file: Üres fájl
gist.revision.no-changes: Nincsenek változtatások
gist.revision.no-revisions: Nincsenek megjeleníthető revíziók
settings: Beállítások
settings.email: Email
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?
settings.add-ssh-key: SSH kulcs hozzáadása
settings.add-ssh-key-help: Csak SSH-n kersztül történő pull/push műveleteknél van használva
settings.add-ssh-key-title: Cím
settings.add-ssh-key-content: Kulcs
settings.delete-ssh-key: Törlés
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
auth.signup: Regisztráció
auth.new-account: Új fiók
auth.username: Felhasználónév
auth.password: Jelszó
auth.register-instead: Vagy regisztrálj
auth.login-instead: Vagy jelentkezz be
auth.oauth: Folytatás %s fiókkal
error: Hiba
header.menu.all: Minden
header.menu.new: Új
header.menu.search: Keresés
header.menu.my-gists: Gistjeim
header.menu.liked: Kedvelt
header.menu.admin: Admin
header.menu.settings: Beállítások
header.menu.logout: Kijelentkezés
header.menu.register: Regisztráció
header.menu.login: Bejelentkezés
header.menu.light: Világos
header.menu.dark: Sötét
header.menu.system: Rendszer
footer.powered-by: Ez az oldal %s alapú
pagination.older: Régebbi
pagination.newer: Újabb
pagination.previous: Előző
pagination.next: Következő
admin.admin_panel: Admin felület
admin.general: Általános
admin.users: Felhasználók
admin.gists: Gistek
admin.configuration: Konfiguráció
admin.versions: Verziók
admin.ssh_keys: SSH kulcsok
admin.stats: Statisztikák
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
admin.created_at: Létrehozva
admin.config-link: Ezt a konfigurációt %s a YAML alapú konfigurációs fájl és/vagy környezeti változók.
admin.config-link-overriden: felülírhatja
admin.disable-signup: Regisztrációk letiltása
admin.disable-signup_help: Letiltja a regisztráció lehetőségét.
admin.require-login: Bejelentkezés szükséges
admin.require-login_help: Csak bejelentkezett felhasználók láthatják a gisteket.
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
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,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

@@ -0,0 +1,259 @@
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: Клонировать c помощью SSH
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
gist.header.embed: 'Встроить'
gist.header.embed-help: 'Встроить этот фрагмент в ваш веб-сайт.'
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.new.title: Название
gist.new.description: Описание
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.new.create-unlisted-button: Создать скрытый фрагмент
gist.new.create-private-button: Создать приватный фрагмент
gist.edit.editing: Редактирование
gist.edit.change-visibility: Применить
gist.edit.delete: Удалить
gist.edit.cancel: Отмена
gist.edit.save: Сохранить
gist.list.joined: Зарегистрирован
gist.list.all: Все фрагменты
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: Нет фрагментов
gist.forks: Форки
gist.forks.view: Посмотреть форк
gist.forks.no: Нет форков
gist.likes: Нравятся
gist.likes.no: Нет
gist.revisions: Ревизии
gist.revision.revised: ревизий этого фрагмента
gist.revision.go-to-revision: К ревизии
gist.revision.file-created: файл создан
gist.revision.file-deleted: файл удалён
gist.revision.file-renamed: переименован в
gist.revision.diff-truncated: Разница (diff) обрезана, так как результат слишком большой для показа
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-set: Сохранить адрес
settings.link-accounts: Привязка доступов
settings.link-github-account: Привязать доступ GitHub
settings.link-gitea-account: Привязать доступ Gitea
settings.unlink-github-account: Отвязать доступ GitHub
settings.unlink-gitea-account: Отвязать доступ Gitea
settings.delete-account: Удалить аккаунт
settings.delete-account-confirm: Вы уверены что хотите удалить свой аккаунт?
settings.add-ssh-key: Добавить ключ SSH
settings.add-ssh-key-help: Нужен только для работы с фрагментами через 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: Последнее использование
auth.signup-disabled: Регистрация запрещена Администратором сервиса
auth.login: Вход
auth.signup: Регистрация
auth.new-account: Новый аккаунт
auth.username: Имя пользователя
auth.password: Пароль
auth.register-instead: Зарегистрироваться
auth.login-instead: Войти
auth.oauth: Войти с помощью доступа %s
error: Ошибка
header.menu.all: Все
header.menu.new: Новый
header.menu.search: Поиск
header.menu.my-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: Фрагменты
admin.configuration: Настройки
admin.versions: Версии
admin.ssh_keys: Ключи SSH
admin.stats: Статистика
admin.actions: Действия
admin.actions.sync-fs: Синхронизировать фрагменты из файловой системы
admin.actions.sync-db: Синхронизировать фрагменты с базой данных
admin.actions.git-gc: Сборка мусора в репозиториях Git
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: Запретить просмотр фрагментов без авторизации.
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: Название
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

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