Compare commits

...

68 Commits

Author SHA1 Message Date
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
120 changed files with 6573 additions and 3171 deletions

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

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

View File

@@ -5,6 +5,8 @@ on:
- master
- 'dev-*'
pull_request:
paths-ignore:
- '**.yml'
jobs:
lint:
@@ -13,10 +15,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.21
- name: Set up Go 1.22
uses: actions/setup-go@v4
with:
go-version: "1.21"
go-version: "1.22"
- name: Lint
uses: golangci/golangci-lint-action@v3
@@ -34,20 +36,23 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.21
- name: Set up Go 1.22
uses: actions/setup-go@v4
with:
go-version: "1.21"
go-version: "1.22"
- name: Check
- name: Check Go modules
run: make go_mod check_changes
- name: Check translations
run: make check-tr
test:
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.21"]
go: ["1.22"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout

View File

@@ -13,10 +13,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.21
- name: Set up Go 1.22
uses: actions/setup-go@v4
with:
go-version: "1.21"
go-version: "1.22"
- name: Cross compile build
run: make all_crosscompile

2
.gitignore vendored
View File

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

View File

@@ -1,5 +1,122 @@
# Changelog
## [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.

View File

@@ -1,14 +1,23 @@
FROM alpine:3.19 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.21-alpine /usr/local/go/ /usr/local/go/
COPY --from=golang:1.22-alpine /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0
COPY --from=node:20-alpine /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules"
@@ -18,10 +27,23 @@ 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.19 as run
FROM alpine:3.19 as prod
RUN apk update && \
apk add --no-cache \
@@ -49,4 +71,5 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222
VOLUME /opengist
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
ENTRYPOINT ["./docker/entrypoint.sh"]

View File

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

View File

@@ -1,12 +1,12 @@
# Opengist
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" />
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
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.
[Documentation](/docs) • [Demo](https://opengist.thomice.li)
[Home Page](https://opengist.io) • [Documentation](https://opengist.io/docs) • [Discord](https://discord.gg/9Pm3X5scZT) • [Demo](https://demo.opengist.io)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/thomiceli/opengist?sort=semver)
@@ -37,7 +37,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell
docker pull ghcr.io/thomiceli/opengist:1
docker pull ghcr.io/thomiceli/opengist:1.7
```
It can be used in a `docker-compose.yml` file :
@@ -51,7 +51,7 @@ version: "3"
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
image: ghcr.io/thomiceli/opengist:1.7
container_name: opengist
restart: unless-stopped
ports:
@@ -78,9 +78,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.6.0/opengist1.6.0-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.7.5/opengist1.7.5-linux-amd64.tar.gz
tar xzvf opengist1.6.0-linux-amd64.tar.gz
tar xzvf opengist1.7.5-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
@@ -90,7 +90,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
### From source
Requirements : [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.21+), [Node.js](https://nodejs.org/en/download/) (16+)
Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.22+), [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
@@ -101,10 +101,13 @@ make
Opengist is now running on port 6157, you can browse http://localhost:6157
---
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
## Documentation
The documentation is available in [/docs](/docs) directory.
The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
## License

View File

@@ -2,7 +2,7 @@
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
# Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
log-level: warn
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
@@ -78,15 +78,33 @@ 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.7.5
images:
- name: ghcr.io/thomiceli/opengist
newTag: 1.7.5
patches:
# Add your ingress
- path: ingress.yaml
- patch: |-
- op: add
path: /spec/rules/0/host
value: opengist.mydomain.com
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: opengist
```
`namespace.yaml`:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: opengist
```
`ingress.yaml`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: nginx
tls:
- hosts:
- opengist.mydomain.com
secretName: opengist-tls
```

29
deploy/deployment.yaml Normal file
View File

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

20
deploy/ingress.yaml Normal file
View File

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

11
deploy/kustomization.yaml Normal file
View File

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

15
deploy/pvc.yaml Normal file
View File

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

14
deploy/service.yaml Normal file
View File

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

View File

@@ -0,0 +1,89 @@
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: 'Admin panel', link: '/admin-panel'},
{text: 'OAuth Providers', link: '/oauth-providers'},
{text: 'Custom assets', link: '/custom-assets'},
{text: 'Custom links', link: '/custom-links'},
{text: 'Cheat Sheet', link: '/cheat-sheet'},
], collapsed: false
},
{
text: 'Usage', base: '/docs/usage', items: [
{text: 'Init via Git', link: '/init-via-git'},
{text: 'Embed Gist', link: '/embed'},
{text: 'Gist as JSON', link: '/gist-json'},
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
{text: 'Git push options', link: '/git-push-options'},
], collapsed: false
},
{
text: 'Administration', base: '/docs/administration', items: [
{text: 'Run with systemd', link: '/run-with-systemd'},
{text: 'Reverse proxy', items: [
{text: 'Nginx', link: '/nginx-reverse-proxy'},
{text: 'Traefik', link: '/traefik-reverse-proxy'},
], collapsed: true},
{text: 'Fail2ban', link: '/fail2ban-setup'},
{text: 'Healthcheck', link: '/healthcheck'},
], collapsed: false
},
{
text: 'Contributing', base: '/docs/contributing', items: [
{text: 'Community', link: '/community'},
{text: 'Development', link: '/development'},
], collapsed: false
},
]},
socialLinks: [
{icon: 'github', link: 'https://github.com/thomiceli/opengist'}
],
editLink: {
pattern: 'https://github.com/thomiceli/opengist/edit/stable/docs/:path'
},
// @ts-ignore
lastUpdated: true,
},
head: [
['link', {rel: 'icon', href: '/favicon.svg'}],
],
ignoreDeadLinks: true
})

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

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

View File

@@ -0,0 +1,101 @@
<script>
import { withBase } from 'vitepress';
import './theme.css'
export default {
setup() {
return { withBase };
},
};
</script>
<template>
<main class="home">
<header class="hero">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto lg:text-center">
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="" >
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
<span class="pr-1">Released 1.7.5</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,7 @@
# Reset a user password
To reset a user password, run the following command using the Opengist binary:
```bash
./opengist admin reset-password <username> <new-password>
```

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,41 @@
---
aside: false
---
# Configuration Cheat Sheet
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|--------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| 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. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| 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. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. |
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| 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

@@ -27,7 +27,7 @@ Usage via command line :
./opengist --config /path/to/config.yml
```
You can start by copying and/or modifying the provided [config.yml](/config.yml) file.
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
## Configuration via Environment Variables

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

@@ -2,51 +2,76 @@
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
## Github
## GitHub
* Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
* Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
```yaml
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](/docs/configuration/cheat-sheet.md) :
* 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](/docs/configuration/cheat-sheet.md) :
* 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](/docs/configuration/cheat-sheet.md) :
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](cheat-sheet.md) :
```yaml
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.22+)
* [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone git@github.com:thomiceli/opengist.git
cd opengist
make watch
```
Opengist is now running on port 6157, you can browse http://localhost:6157

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
# 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
version: "3"
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
- "6157:6157" # HTTP port
- "2222:2222" # SSH port, can be removed if you don't use SSH
volumes:
- "$HOME/.opengist:/opengist"
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.22+)
* [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.7.5 # 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 database
* Logging
* Docker support
## System requirements
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
Version **2.28** or later is recommended as the app has not been tested with older Git versions and some features would not work.
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
## Components
* Backend Web Framework: [Echo](https://echo.labstack.com/)
* ORM: [GORM](https://gorm.io/)
* Frontend libraries:
* [TailwindCSS](https://tailwindcss.com/)
* [CodeMirror](https://codemirror.net/)
* [Day.js](https://day.js.org/)
* and [others](/package.json)

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

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

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

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

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

73
go.mod
View File

@@ -1,41 +1,44 @@
module github.com/thomiceli/opengist
go 1.21
go 1.22
require (
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.12.0
github.com/blevesearch/bleve/v2 v2.3.10
github.com/alecthomas/chroma/v2 v2.14.0
github.com/blevesearch/bleve/v2 v2.4.0
github.com/dustin/go-humanize v1.0.1
github.com/glebarez/go-sqlite v1.22.0
github.com/glebarez/sqlite v1.10.0
github.com/go-playground/validator/v10 v10.16.0
github.com/google/uuid v1.5.0
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.21.0
github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/hashicorp/go-memdb v1.3.4
github.com/labstack/echo/v4 v4.11.4
github.com/markbates/goth v1.78.0
github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.6.0
github.com/labstack/echo/v4 v4.12.0
github.com/markbates/goth v1.80.0
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.2
github.com/yuin/goldmark v1.7.1
github.com/yuin/goldmark-emoji v1.0.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.5.0
golang.org/x/crypto v0.17.0
golang.org/x/text v0.14.0
golang.org/x/crypto v0.23.0
golang.org/x/text v0.15.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.5
gorm.io/gorm v1.25.10
)
require (
github.com/RoaringBitmap/roaring v1.7.0 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/blevesearch/bleve_index_api v1.1.4 // indirect
github.com/blevesearch/geo v0.1.18 // indirect
github.com/blevesearch/bleve_index_api v1.1.8 // indirect
github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.16 // 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.5 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 // 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
@@ -45,44 +48,48 @@ require (
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.13 // indirect
github.com/blevesearch/zapx/v16 v16.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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.2.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // 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.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.15.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect
modernc.org/libc v1.38.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
modernc.org/libc v1.51.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.30.0 // indirect
)

597
go.sum
View File

@@ -1,68 +1,34 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/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.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
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.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
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.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
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.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
github.com/blevesearch/bleve_index_api v1.1.4 h1:n9Ilxlb80g9DAhchR95IcVrzohamDSri0wPnkKnva50=
github.com/blevesearch/bleve_index_api v1.1.4/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
github.com/blevesearch/bleve/v2 v2.4.0 h1:2xyg+Wv60CFHYccXc+moGxbL+8QKT/dZK09AewHgKsg=
github.com/blevesearch/bleve/v2 v2.4.0/go.mod h1:IhQHoFAbHgWKYavb9rQgQEJJVMuY99cKdQ0wPpst2aY=
github.com/blevesearch/bleve_index_api v1.1.8 h1:rJUccYfWqRY2/BGowlsv1lwrLKYK/zPE6hgNn1pTGdk=
github.com/blevesearch/bleve_index_api v1.1.8/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.16 h1:lfzXzzjO1mAf15MRiRY5yz6KVGr02CyRrr7m0z70Ih8=
github.com/blevesearch/go-faiss v1.0.16/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
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.5 h1:5SsNQmR8v1bojtGQ1zFhZravcMg5rdiX8AVu6LwlVtc=
github.com/blevesearch/scorch_segment_api/v2 v2.2.5/go.mod h1:8N2ytOlBCdurlxDgbqsfeR1oTKRN0ZVIKdUUP1VFZNc=
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 h1:UfbyRpIMdcaNsgciGYS9Pib7N3xd3EEw8KKbd/aDBlA=
github.com/blevesearch/scorch_segment_api/v2 v2.2.13/go.mod h1:osG1bAUONZB2r/ozUJwjbuOzPvdrULWaLOm+vsMANsk=
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=
@@ -81,136 +47,69 @@ github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz7
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/blevesearch/zapx/v16 v16.1.0 h1:bHsyowFqU0QA+uVDJCjifv9OvPGb8htkV52Yc/wT6xs=
github.com/blevesearch/zapx/v16 v16.1.0/go.mod h1:P0h9lKRyl4EKksAWfxwCQ5I5pLB9jH2XD8bhYHuIYuc=
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
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.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/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.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0=
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/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/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
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.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -221,14 +120,11 @@ github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYi
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -237,31 +133,20 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/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/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
github.com/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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -275,361 +160,105 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
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/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/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.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.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.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
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.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/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.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
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.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA=
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.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.51.0 h1:kjSHjz1guHbI5iRdi6nEr/wIKSN6X4vzLd6TJMN+lHA=
modernc.org/libc v1.51.0/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.30.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM=
modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -17,12 +17,12 @@ type ActionStatus struct {
}
const (
SyncReposFromFS = iota
SyncReposFromDB = iota
GitGcRepos = iota
SyncGistPreviews = iota
ResetHooks = iota
IndexGists = iota
SyncReposFromFS = iota
SyncReposFromDB
GitGcRepos
SyncGistPreviews
ResetHooks
IndexGists
)
var (
@@ -74,7 +74,7 @@ func Run(actionType int) {
case IndexGists:
functionToRun = indexGists
default:
panic("unhandled default case")
log.Error().Msg("Unknown action type")
}
functionToRun()

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
}

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

@@ -0,0 +1,50 @@
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,
},
}
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
},
}

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"
"path/filepath"
)
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()
if err := db.Setup(filepath.Join(config.GetHomeDir(), config.C.DBFilename), false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
}
}

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

@@ -0,0 +1,177 @@
package cli
import (
"fmt"
"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"
"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.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()
}
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), 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 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

@@ -10,6 +10,7 @@ import (
"slices"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -17,7 +18,7 @@ import (
"gopkg.in/yaml.v3"
)
var OpengistVersion = "1.6.0"
var OpengistVersion = ""
var C *config
@@ -52,14 +53,25 @@ type config struct {
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) {
@@ -83,23 +95,26 @@ func configWithDefaults() (*config, error) {
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 err = loadConfigFromYaml(c, configPath); err != nil {
if err = loadConfigFromYaml(c, configPath, out); err != nil {
return err
}
if err = loadConfigFromEnv(c); err != nil {
if err = loadConfigFromEnv(c, out); err != nil {
return err
}
@@ -118,6 +133,9 @@ func InitConfig(configPath string) error {
C = c
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err
}
return nil
}
@@ -136,6 +154,21 @@ func InitLog() {
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) {
@@ -145,7 +178,7 @@ func InitLog() {
switch logOutputType {
case "stdout":
logWriters = append(logWriters, zerolog.NewConsoleWriter())
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)
@@ -157,14 +190,14 @@ func InitLog() {
}
}
if len(logWriters) == 0 {
logWriters = append(logWriters, zerolog.NewConsoleWriter())
logWriters = append(logWriters, consoleWriter)
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
}
multi := zerolog.MultiLevelWriter(logWriters...)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
log.Logger = zerolog.New(multi).Level(level).With().Caller().Timestamp().Logger()
if !slices.Contains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
if !slices.Contains([]string{"debug", "info", "warn", "error", "fatal"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel)
}
}
@@ -195,7 +228,7 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath)
}
func loadConfigFromYaml(c *config, configPath string) error {
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if configPath != "" {
absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath)
@@ -204,9 +237,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
if !os.IsNotExist(err) {
return err
}
fmt.Println("No YAML config file found at " + absolutePath)
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
} else {
fmt.Println("Using YAML config file: " + absolutePath)
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
@@ -216,13 +249,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
defer file.Close()
}
} else {
fmt.Println("No YAML config file specified.")
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
}
return nil
}
func loadConfigFromEnv(c *config) error {
func loadConfigFromEnv(c *config, out io.Writer) error {
v := reflect.ValueOf(c).Elem()
var envVars []string
@@ -234,28 +267,69 @@ func loadConfigFromEnv(c *config) error {
}
envValue := os.Getenv(strings.ToUpper(tag))
if envValue == "" {
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
continue
}
switch v.Field(i).Kind() {
case reflect.String:
v.Field(i).SetString(envValue)
envVars = append(envVars, tag)
case reflect.Bool:
boolVal, err := strconv.ParseBool(envValue)
if err != nil {
return err
}
v.Field(i).SetBool(boolVal)
envVars = append(envVars, tag)
case reflect.Slice:
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
prefix := strings.ToUpper(tag) + "_"
var sliceValue reflect.Value
elemType := v.Type().Field(i).Type.Elem()
for index := 0; ; index++ {
allFieldsPresent := true
elemValue := reflect.New(elemType).Elem()
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
elemField := elemValue.Type().Field(j)
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
envValue, present := os.LookupEnv(envName)
if !present {
allFieldsPresent = false
break
}
envVars = append(envVars, envName)
elemValue.Field(j).SetString(envValue)
}
if !allFieldsPresent {
break
}
if sliceValue.Kind() != reflect.Slice {
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
}
sliceValue = reflect.Append(sliceValue, elemValue)
}
if sliceValue.IsValid() {
v.Field(i).Set(sliceValue)
}
}
default:
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
}
envVars = append(envVars, tag)
}
if len(envVars) > 0 {
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
} else {
fmt.Println("No environment variables config specified.")
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
}
return nil

View File

@@ -10,10 +10,11 @@ type AdminSetting struct {
}
const (
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
)
func GetSetting(key string) (string, error) {
@@ -62,3 +63,21 @@ func initAdminSettings(settings map[string]string) error {
return nil
}
type DBAuthInfo struct{}
func (auth DBAuthInfo) RequireLogin() (bool, error) {
s, err := GetSetting(SettingRequireLogin)
if err != nil {
return true, err
}
return s == "1", nil
}
func (auth DBAuthInfo) AllowGistsWithoutLogin() (bool, error) {
s, err := GetSetting(SettingAllowGistsWithoutLogin)
if err != nil {
return false, err
}
return s == "1", nil
}

View File

@@ -42,7 +42,7 @@ func Setup(dbPath string, sharedCache bool) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil {
return err
}
@@ -52,10 +52,11 @@ func Setup(dbPath string, sharedCache bool) error {
// Default admin setting values
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingAllowGistsWithoutLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})
}

View File

@@ -2,18 +2,17 @@ package db
import (
"fmt"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/index"
"os/exec"
"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"
)
@@ -25,6 +24,19 @@ const (
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:
@@ -38,11 +50,11 @@ func (v Visibility) Next() Visibility {
func ParseVisibility[T string | int](v T) (Visibility, error) {
switch s := fmt.Sprint(v); s {
case "0":
case "0", "public":
return PublicVisibility, nil
case "1":
case "1", "unlisted":
return UnlistedVisibility, nil
case "2":
case "2", "private":
return PrivateVisibility, nil
default:
return -1, fmt.Errorf("unknown visibility %q", s)
@@ -330,10 +342,6 @@ func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
}
func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
@@ -530,13 +538,17 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
// -- 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"`
Private Visibility `validate:"number,min=0,max=2" form:"private"`
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
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 {

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

@@ -0,0 +1,87 @@
package db
import (
"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
err := db.
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc").
Order("id asc").
Find(&invitations).Error
return invitations, err
}
func GetInvitationByID(id uint) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("id = ?", id).
First(&invitation).Error
return invitation, err
}
func GetInvitationByCode(code string) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("code = ?", code).
First(&invitation).Error
return invitation, err
}
func InvitationCodeExists(code string) (bool, error) {
var count int64
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
return count > 0, err
}
func (i *Invitation) Create() error {
i.Code = generateRandomCode()
return db.Create(&i).Error
}
func (i *Invitation) Update() error {
return db.Save(&i).Error
}
func (i *Invitation) Delete() error {
return db.Delete(&i).Error
}
func (i *Invitation) IsExpired() bool {
return i.ExpiresAt < time.Now().Unix()
}
func (i *Invitation) IsMaxedOut() bool {
return i.NbMax > 0 && i.NbUsed >= i.NbMax
}
func (i *Invitation) IsUsable() bool {
return !i.IsExpired() && !i.IsMaxedOut()
}
func (i *Invitation) Use() error {
i.NbUsed++
return i.Update()
}
func generateRandomCode() string {
const charset = "0123456789ABCDEF"
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
result := make([]byte, 16)
for i := range result {
result[i] = charset[seededRand.Intn(len(charset))]
}
return string(result)
}

View File

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

View File

@@ -118,6 +118,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
return userMap, nil
}
func GetUserFromSSHKey(sshKey string) (*User, error) {
user := new(User)
err := db.
Joins("JOIN ssh_keys ON users.id = ssh_keys.user_id").
Where("ssh_keys.content = ?", sshKey).
First(&user).Error
return user, err
}
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
key := new(SSHKey)
err := db.

View File

@@ -24,6 +24,8 @@ var (
)
const truncateLimit = 2 << 18
const diffSize = 2 << 12
const maxFilesPerDiffCommit = 10
type RevisionNotFoundError struct{}
@@ -80,16 +82,6 @@ func InitRepository(user string, gist string) error {
return CreateDotGitFiles(user, gist)
}
func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
repositoryPath := RepositoryPath(user, gist)
if err := InitRepository(user, gist); err != nil {
return err
}
repositoryUrl := RepositoryUrl(ctx, user, gist)
return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl))
}
func CountCommits(user string, gist string) (string, error) {
repositoryPath := RepositoryPath(user, gist)
@@ -323,7 +315,7 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
}
}(cmd)
return parseLog(stdout, truncateLimit), err
return parseLog(stdout, maxFilesPerDiffCommit, diffSize)
}
func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error {
@@ -424,7 +416,6 @@ func Push(gistTmpId string) error {
if err != nil {
return err
}
return os.RemoveAll(tmpRepositoryPath)
}
@@ -534,8 +525,12 @@ func CreateDotGitFiles(user string, gist string) error {
}
defer f1.Close()
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil {
return err
if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
for _, hook := range []string{"pre-receive", "post-receive"} {
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
return err
}
}
}
return nil
@@ -570,57 +565,6 @@ func removeFilesExceptGit(dir string) error {
})
}
const preReceive = `#!/bin/sh
disallowed_files=""
while read -r old_rev new_rev ref
do
if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then
# This is the first commit, so we check all the files in that commit
changed_files=$(git ls-tree -r --name-only "$new_rev")
else
# This is not the first commit, so we compare it with its predecessor
changed_files=$(git diff --name-only "$old_rev" "$new_rev")
fi
while IFS= read -r file
do
case $file in
*/*)
disallowed_files="${disallowed_files}${file} "
;;
esac
done <<EOF
$changed_files
EOF
done
if [ -n "$disallowed_files" ]; then
echo ""
echo "Pushing files in folders is not allowed:"
for file in $disallowed_files; do
echo " $file"
done
echo ""
exit 1
fi
`
const postReceive = `#!/bin/sh
while read oldrev newrev refname; do
if ! git rev-parse --verify --quiet HEAD &>/dev/null; then
git symbolic-ref HEAD "$refname"
fi
done
echo ""
echo "Your new repository has been created here: %s"
echo ""
echo "If you want to keep working with your gist, you could set the remote URL via:"
echo "git remote set-url origin %s"
echo ""
rm -f $0
const hookTemplate = `#!/bin/sh
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
`

View File

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

View File

@@ -6,7 +6,6 @@ import (
"encoding/csv"
"fmt"
"io"
"regexp"
"strings"
)
@@ -63,129 +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
}
scanNext = true
for isFragment {
currentFile.Truncated = true
// new commit found
currentFile = nil
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}}
scanner.Scan()
currentCommit.AuthorName = string(scanner.Bytes()[2:])
scanner.Scan()
currentCommit.AuthorEmail = string(scanner.Bytes()[2:])
scanner.Scan()
currentCommit.Timestamp = string(scanner.Bytes()[2:])
scanner.Scan()
if len(scanner.Bytes()) == 0 {
commits = append(commits, currentCommit)
break
// Read the next line
_, isFragment, err = input.ReadLine()
if err != nil {
return nil, false, err
}
}
// 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)
sb.Reset()
// avoid scanning the next line, as we already did it
scanNext = false
// Read the next line
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
if err == io.EOF {
return lineBytes, isFragment, err
}
return nil, false, err
}
// End of file
if len(lineBytes) == 0 {
return lineBytes, false, err
}
if lineBytes[0] == 'd' {
return lineBytes, false, err
}
if currFileLineCount >= maxBytes {
currentFile.Truncated = true
continue
}
changed := scanner.Bytes()[1:]
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
currentCommit.Changed = string(changed)
// twice because --shortstat adds a new line
scanner.Scan()
scanner.Scan()
// commit header parsed
// files changes inside the commit
for {
line := scanner.Bytes()
// end of content of file
if len(line) == 0 {
isContent = false
if currentFile != nil {
currentCommit.Files = append(currentCommit.Files, *currentFile)
}
break
}
// new file found
if bytes.HasPrefix(line, []byte("diff --git")) {
// current file is finished, we can add it to the commit
if currentFile != nil {
currentCommit.Files = append(currentCommit.Files, *currentFile)
}
// create a new file
isContent = false
bytesRead = 0
currentFile = &File{}
filenameRegex := regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
matches := filenameRegex.FindStringSubmatch(string(line))
if len(matches) == 3 {
currentFile.Filename = matches[2]
if matches[1] != matches[2] {
currentFile.OldFilename = matches[1]
}
}
scanner.Scan()
continue
}
if bytes.HasPrefix(line, []byte("new")) {
currentFile.IsCreated = true
}
if bytes.HasPrefix(line, []byte("deleted")) {
currentFile.IsDeleted = true
}
// file content found
if line[0] == '@' {
isContent = true
}
if isContent {
currentFile.Content += string(line) + "\n"
bytesRead += len(line)
if bytesRead > maxBytes {
currentFile.Truncated = true
currentFile.Content = ""
isContent = false
line := string(lineBytes)
if isFragment {
currentFile.Truncated = true
for isFragment {
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err)
}
}
scanner.Scan()
}
commits = append(commits, currentCommit)
if len(line) > maxBytes {
currentFile.Truncated = true
line = line[:maxBytes]
}
currentFile.Content += line + "\n"
}
}
return commits
func skipToNextCommit(input *bufio.Reader) (line string, err error) {
// need to skip until the next cmdDiffHead
var isFragment, wasFragment bool
var lineBytes []byte
for {
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
return "", err
}
if wasFragment {
wasFragment = isFragment
continue
}
if bytes.HasPrefix(lineBytes, []byte("c")) {
break
}
wasFragment = isFragment
}
line = string(lineBytes)
if isFragment {
var tail string
tail, err = input.ReadString('\n')
if err != nil {
return "", err
}
line += tail
}
return line, err
}
func ParseCsv(file *File) (*CsvFile, error) {

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Klonovat pomocí %s
gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP.
gist.header.clone-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.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: Stáhnout ZIP
gist.raw: Raw
@@ -122,8 +122,7 @@ 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.github-oauth: Pokračovat s účtem na GitHubu
auth.gitea-oauth: Pokračovat s účtem na Gitea
auth.oauth: Pokračovat s účtem na %s
error: Chyba
@@ -174,7 +173,8 @@ admin.disable-login: Zakázat přihlášení
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
admin.disable-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
@@ -182,3 +182,80 @@ 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

@@ -43,8 +43,11 @@ 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
@@ -67,6 +70,9 @@ 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
@@ -79,9 +85,11 @@ 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
@@ -94,6 +102,7 @@ gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: Empty file
gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s
settings: Settings
settings.email: Email
@@ -117,6 +126,7 @@ 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
@@ -132,11 +142,19 @@ auth.username: Username
auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
auth.github-oauth: Continue with GitHub account
auth.gitlab-oauth: Continue with GitLab account
auth.gitea-oauth: Continue with Gitea account
auth.oauth: Continue with %s account
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
header.menu.all: All
header.menu.new: New
@@ -163,6 +181,8 @@ 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
@@ -184,6 +204,8 @@ admin.disable-signup: Disable signup
admin.disable-signup_help: Forbid the creation of new accounts.
admin.require-login: Require login
admin.require-login_help: Enforce users to be logged in to see gists.
admin.allow-gists-without-login: Allow individual gists without login
admin.allow-gists-without-login_help: Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
admin.disable-login: Disable login form
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar
@@ -196,3 +218,52 @@ 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.gist.visibility-changed: Gist visibility has been changed
flash.gist.deleted: Gist has been deleted
flash.gist.fork-own-gist: Unable to fork own gists
flash.gist.forked: Gist has been forked
flash.user.email-updated: Email updated
flash.user.invalid-ssh-key: Invalid SSH key
flash.user.ssh-key-added: SSH key added
flash.user.ssh-key-deleted: SSH key deleted
flash.user.password-updated: Password updated
flash.user.username-updated: Username updated
validation.is-too-long: Field %s is too long
validation.should-not-be-empty: Field %s should not be empty
validation.should-not-include-sub-directory: Field %s should not include a sub directory
validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
validation.not-enough: Not enough %s
validation.invalid: Invalid %s
html.title.admin-panel: Admin panel

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Clonar via %s
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
gist.header.clone-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
gist.header.embed:
gist.header.embed-help:
gist.header.embed: ''
gist.header.embed-help: ''
gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato
@@ -115,8 +115,7 @@ auth.username: Nombre de usuario
auth.password: Contraseña
auth.register-instead: Registrarse en su lugar
auth.login-instead: Iniciar sesión en su lugar
auth.github-oauth: Continuar con cuenta de GitHub
auth.gitea-oauth: Continuar con cuenta de Gitea
auth.oauth: Continuar con cuenta de %s
error: Error
@@ -158,7 +157,6 @@ admin.delete: Eliminar
admin.created_at: Creado
admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno.
admin.config-link-overridden: sobrescrito
admin.disable-signup: Deshabilitar registro
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
admin.require-login: Requerir inicio de sesión
@@ -167,7 +165,8 @@ admin.disable-login: Deshabilitar formulario de inicio de sesión
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
admin.disable-gravatar: Deshabilitar Gravatar
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
admin.gists.title: Título
@@ -175,3 +174,86 @@ 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

@@ -17,8 +17,8 @@ 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:
gist.header.embed-help:
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
@@ -115,8 +115,7 @@ auth.username: Nom d'utilisateur
auth.password: Mot de passe
auth.register-instead: Je préfère m'inscrire
auth.login-instead: Je préfère me connecter
auth.github-oauth: Continuer avec un compte GitHub
auth.gitea-oauth: Continuer avec un compte Gitea
auth.oauth: Continuer avec un compte %s
error: Erreur
@@ -167,7 +166,8 @@ admin.disable-login: Désactiver le formulaire de connexion
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
admin.disable-gravatar: Désactiver Gravatar
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
admin.allow-gists-without-login: Autoriser les gists individuelles sans login
admin.allow-gists-without-login_help: Autoriser la visualisation et le téléchargement de gists individuels sans connexion, tout en exigeant une connexion pour la découverte de gists.
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
admin.gists.title: Titre
@@ -175,3 +175,86 @@ 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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,267 @@
gist.public: Herkese Açık
gist.unlisted: Liste Dışı
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: Çatallı
gist.header.last-active: Son aktif
gist.header.select-tab: Bir sekme seç
gist.header.code: Kod
gist.header.revisions: Revizyonlar
gist.header.revision: Revizyon
gist.header.clone-http: \%s aracılığıyla 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: Yerleştirme
gist.header.embed-help: Bu gisti web sitenize yerleştirin.
gist.header.download-zip: ZIP'i indirin
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: Description
gist.new.url: URL
gist.new.filename-with-extension: Uzantılı dosya adı
gist.new.indent-mode: Girinti modu
gist.new.indent-mode-space: Boşluk
gist.new.indent-mode-tab: Tab
gist.new.indent-size: Girinti boyutu
gist.new.wrap-mode: ''
gist.new.wrap-mode-no: ''
gist.new.wrap-mode-soft: ''
gist.new.add-file: Add file
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: Ön izle
gist.new.create-a-new-gist: Yeni bir gist oluştur
gist.edit.editing: Düzenleme
gist.edit.edit-gist: '%s düzenle'
gist.edit.change-visibility: ''
gist.edit.delete: Delete
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: düzenlendi
gist.list.order-by-asc: En son yakın zamanda
gist.list.order-by-desc: Son zamanlarda
gist.list.select-tab: Bir sekme seçin
gist.list.liked: Beğenildi
gist.list.likes: beğeniler
gist.list.forked: Çatallı
gist.list.forked-from: çatallandı
gist.list.forks: çatallar
gist.list.files: files
gist.list.last-active: Son aktif
gist.list.no-gists: Gistler yok
gist.list.all-liked-by: '%s tarafından beğenilen tüm gistler'
gist.list.all-forked-by: '%s tarafından beğenilen tüm çatallar'
gist.list.all-from: '%s tüm gistleri'
gist.search.found: bulunan gistler
gist.search.no-results: Hiç gist bulunamadı
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.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
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
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.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.allow-gists-without-login:
admin.allow-gists-without-login_help:
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.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,269 @@
gist.public: Публічний
gist.unlisted: Прихований
gist.private: Приватний
gist.header.like: Подобається
gist.header.unlike: Не подобається
gist.header.fork: Створити форк
gist.header.edit: Редагувати
gist.header.delete: Видалити
gist.header.forked-from: Форк з
gist.header.last-active: Остання активність
gist.header.select-tab: Перейти
gist.header.code: Код
gist.header.revisions: Версії
gist.header.revision: Версія
gist.header.clone-http: Клонувати за допомогою %s
gist.header.clone-http-help: Клонувати за допомогою Git з використанням аутентифікації HTTP.
gist.header.clone-ssh: Клонувати за допомогою SSH
gist.header.clone-ssh-help: Клонувати за допомогою Git з використанням ключа SSH.
gist.header.embed: 'Вбудувати'
gist.header.embed-help: 'Вбудувати цей gist до вашого веб-сайту.'
gist.header.download-zip: Скачати ZIP-архів
gist.raw: Неформатований
gist.file-truncated: Цей файл було обрізано.
gist.watch-full-file: Перегляд всього файла.
gist.file-not-valid: Невалідний CSV.
gist.no-content: Немає даних
gist.new.new_gist: Новий gist
gist.new.title: Назва
gist.new.description: Опис
gist.new.url: URL
gist.new.filename-with-extension: Ім'я файла з розширенням
gist.new.indent-mode: Режим відступів
gist.new.indent-mode-space: Пробіли
gist.new.indent-mode-tab: Табуляція
gist.new.indent-size: Розмір відступа
gist.new.wrap-mode: Переноси рядків
gist.new.wrap-mode-no: Без переносів
gist.new.wrap-mode-soft: М'які переноси
gist.new.add-file: Додати файл
gist.new.create-public-button: Створити публічний gist
gist.new.create-unlisted-button: Створити прихований gist
gist.new.create-private-button: Створити приватний gist
gist.new.preview: Перегляд
gist.new.create-a-new-gist: Створити новий gist
gist.edit.editing: Редагування
gist.edit.edit-gist: Редагувати %s
gist.edit.change-visibility: Зробити
gist.edit.delete: Видалити
gist.edit.cancel: Скасувати
gist.edit.save: Зберегти
gist.list.joined: Зареєстрован
gist.list.all: Всі gist
gist.list.search-results: Результати пошуку
gist.list.sort: Сортування
gist.list.sort-by-created: створені
gist.list.sort-by-updated: оновлені
gist.list.order-by-asc: Нещодавні знизу
gist.list.order-by-desc: Нещодавні зверху
gist.list.select-tab: Оберіть вкладку
gist.list.liked: Вподобані
gist.list.likes: вподобань
gist.list.forked: Форки
gist.list.forked-from: Форки з
gist.list.forks: форк(-ів)
gist.list.files: файл(-ів)
gist.list.last-active: Остання активність
gist.list.no-gists: Немає gists
gist.list.all-liked-by: Всі gists вподобані %s
gist.list.all-forked-by: Всі gists форкнуті by %s
gist.list.all-from: Всі gists від %s
gist.search.found: gists знайдено
gist.search.no-results: Не знайдено gists
gist.search.help.user: gists створені користувачем
gist.search.help.title: gists з наданим ім'ям
gist.search.help.filename: gists мають файли з наданим ім'ям
gist.search.help.extension: gists мають файли з наданим розширенням
gist.search.help.language: gists мають файли з наданою мовою
gist.forks: Форки
gist.forks.view: Подивитися форк
gist.forks.no: Немає форків
gist.forks.for: Форки для %s
gist.likes: Подобається
gist.likes.no: Ще немає вподобань
gist.likes.for: Вподобання для %s
gist.revisions: Ревизії
gist.revision.revised: ревизій цього gist
gist.revision.go-to-revision: До ревизії
gist.revision.file-created: файл створено
gist.revision.file-deleted: файл видалено
gist.revision.file-renamed: перейменовано в
gist.revision.diff-truncated: Різниця завелика для відображення
gist.revision.file-renamed-no-changes: Файл перейменовано без змін
gist.revision.empty-file: Пустий файл
gist.revision.no-changes: Без змін
gist.revision.no-revisions: Немає ревізій для відображення
gist.revision-of: Ревізії %s
settings: Налаштування
settings.email: Адреса електронної пошти
settings.email-help: Використовується для коммітів та Gravatar
settings.email-set: Зберегти адресу електронної пошти
settings.link-accounts: Підключення акаунтів
settings.link-github-account: Підключити GitHub акаунт
settings.link-gitlab-account: Підключити GitLab акаунт
settings.link-gitea-account: Підключити Gitea акаунт
settings.unlink-github-account: Відключити GitHub акаунт
settings.unlink-gitlab-account: Відключити GitLab акаунт
settings.unlink-gitea-account: Відключити Gitea акаунт
settings.delete-account: Видалити акаунт
settings.delete-account-confirm: Ви впевненні, що хочете видалити свій акаунт?
settings.add-ssh-key: Додати SSH ключ
settings.add-ssh-key-help: Використовується только для pull/push gists при використанні Git з SSH
settings.add-ssh-key-title: Назва
settings.add-ssh-key-content: Ключ
settings.delete-ssh-key: Видалити
settings.delete-ssh-key-confirm: Підтвердьте видалення ключа SSH
settings.ssh-key-added-at: Додано
settings.ssh-key-never-used: Не використовувася
settings.ssh-key-last-used: Останнє використання
settings.ssh-key-exists: SSH ключ вже існує
settings.change-username: Змінити им'я користувача
settings.create-password: Створити пароль
settings.create-password-help: Створити ваш пароль для логіну в Opengist через HTTP
settings.change-password: Створити пароль
settings.change-password-help: Змінити ваш пароль для логіну в Opengist через HTTP
settings.password-label-title: Пароль
auth.signup-disabled: Адміністратор відключив реєстрацію
auth.login: Вхід
auth.signup: Реєстрація
auth.new-account: Новий акаунт
auth.username: Им'я користувача
auth.password: Пароль
auth.register-instead: Зареєструватися
auth.login-instead: Увійти
auth.oauth: Продовжити з %s акаунтом
error: Помилка
error.page-not-found: Сторінка не знайдена
error.bad-request: Невірний запрос
error.signup-disabled: Реєстрацію вимкнено
error.signup-disabled-form: Реєстрацію через форму вимкнено
error.login-disabled-form: Логін через форму логіна вимкнено
error.complete-oauth-login: "Неможливо виконати авторизацію користувача: %s"
error.oauth-unsupported: Провайдер не підтримується
error.cannot-bind-data: Не вдається зв'язати дані
error.invalid-number: Недійсний номер
error.invalid-character-unescaped: Неправильний символ не екранований
header.menu.all: Все
header.menu.new: Новий
header.menu.search: Пошук
header.menu.my-gists: Мої gists
header.menu.liked: Сподобалися
header.menu.admin: Адміністрування
header.menu.settings: Налаштування
header.menu.logout: Вийти
header.menu.register: Реєстрація
header.menu.login: Увійти
header.menu.light: Світла
header.menu.dark: Темна
header.menu.system: Системна
footer.powered-by: Працює на %s
pagination.older: Пізніше
pagination.newer: Новіше
pagination.previous: Попередня
pagination.next: Наступна
admin.admin_panel: Панель управління
admin.general: Загальні
admin.users: Користувачі
admin.gists: Gists
admin.configuration: Конфігурація
admin.invitations: Запрошення
admin.invitations.create: Створити запрошення
admin.versions: Версії
admin.ssh_keys: Ключі SSH
admin.stats: Статистика
admin.actions: Дії
admin.actions.sync-fs: Синхронізувати gists з файлової системи
admin.actions.sync-db: Синхронізувати gists з базою даних
admin.actions.git-gc: Збір сміття з репозиторіїв Git
admin.actions.sync-previews: Синхронізувати всі gists перегляди
admin.actions.reset-hooks: Скинути серверні Git hooks для всіх репозиторіїв
admin.actions.index-gists: Проіндексувати всі gists
admin.id: ID
admin.user: Користувач
admin.delete: Видалити
admin.created_at: Створено
admin.config-link: Ця конфігурація може бути %s за допомогою YAML файла та/або змінних середовища
admin.config-link-overriden: перевизначено
admin.disable-signup: Вимкнути реєстрацію
admin.disable-signup_help: Заборонити створення нових акаунтів
admin.require-login: Вимагати авторизацію
admin.require-login_help: Вимагати авторизації для перегляду gists
admin.allow-gists-without-login: Дозволити перегляд індивідуальних gists без авторизації
admin.allow-gists-without-login_help: Дозволити перегляд і скачування індивідуальних gists без авторизації, але вимагати авторизацію для перегляду переліку gists.
admin.disable-login: Вимкнути форму авторизації
admin.disable-login_help: Заборонити авторизацію по паролю та замість цього примушувати використовувати провайдерів OAuth.
admin.disable-gravatar: Заборонити Gravatar
admin.disable-gravatar_help: Вимкнути використання Gravatar як провайдера аватарів.
admin.users.delete_confirm: Ви впевнені, що хочете видалити цього користувача?
admin.gists.title: Назва
admin.gists.private: Чи приватний
admin.gists.nb-files: Файлів
admin.gists.nb-likes: Вподобань
admin.gists.delete_confirm: Ви впевнені, що хочете видалити цей gist?
admin.invitations.help: Запрошення можуть бути використані навіть якщо реєстрація вимкнена.
admin.invitations.max_uses: Максимальна кількість використань
admin.invitations.expires_at: Спливає
admin.invitations.code: Код
admin.invitations.copy_link: Копіювати посилання
admin.invitations.uses: Використовується
admin.invitations.expired: Сплинув
flash.admin.user-deleted: Користувач був видалений
flash.admin.gist-deleted: Gist був видалений
flash.admin.invitation-created: Запрошення було створено
flash.admin.invitation-deleted: Запрошення було видалено
flash.admin.sync-fs: Синхронізація репозиторіїв за файловою системою...
flash.admin.sync-db: Синхронізація репозиторіїв за базою даних...
flash.admin.git-gc: Збір сміття з репозиторіїв...
flash.admin.sync-previews: Синхронізація Gist переглядів...
flash.admin.reset-hooks: Скидання cерверниз Git hooks для всіх репозиторіїв...
flash.admin.index-gists: Індексація всіх gists...
flash.auth.username-exists: Це ім'я користувача вже існує
flash.auth.invalid-credentials: Недійсні облікові дані
flash.auth.account-linked-oauth: Акаунт підключено до %s
flash.auth.account-unlinked-oauth: Акаунт відключено від %s
flash.auth.user-sshkeys-not-retrievable: Не зміг отримати ключі користувача
flash.auth.user-sshkeys-not-created: Не зміг створити ssh ключ
flash.auth.must-be-logged-in: Ви маєте бути авторизовані для доступу до gists
flash.gist.visibility-changed: Параметри перегляду gist змінено
flash.gist.deleted: Gist було видалено
flash.gist.fork-own-gist: Неможливо форкнути власні gists
flash.gist.forked: Gist було форкнуто
flash.user.email-updated: Email оновлено
flash.user.invalid-ssh-key: Недійсний SSH ключ
flash.user.ssh-key-added: SSH ключ додано
flash.user.ssh-key-deleted: SSH key видалено
flash.user.password-updated: Пароль оновлено
flash.user.username-updated: Ім'я користувача оновлено
validation.is-too-long: Поле %s занадто велике
validation.should-not-be-empty: Поле %s не має бути пустим
validation.should-not-include-sub-directory: Поле %s неповинно включати піддиректорії
validation.should-only-contain-alphanumeric-characters: Поле %s має містити лише буквено-цифрові символи
validation.should-only-contain-alphanumeric-characters-and-dashes: Поле %s має містити лише буквено-цифрові символи та тире
validation.not-enough: Недостатньо %s
validation.invalid: Недійсний %s
html.title.admin-panel: Панель адміністратора

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: 通过 %s 克隆
gist.header.clone-http-help: 使用 Git 通过 HTTP 基础认证克隆。
gist.header.clone-ssh: 通过 SSH 克隆
gist.header.clone-ssh-help: 使用 Git 通过 SSH 密钥克隆。
gist.header.embed:
gist.header.embed-help:
gist.header.embed: ''
gist.header.embed-help: '在你的网页中嵌入此gist。'
gist.header.download-zip: 下载 ZIP
gist.raw: 原始文件
@@ -57,7 +57,7 @@ gist.list.sort-by-created: 创建
gist.list.sort-by-updated: 更新
gist.list.order-by-asc: 最早
gist.list.order-by-desc: 最近
gist.list.select-tab: Select a tab
gist.list.select-tab: 选择一个标签
gist.list.liked: 已喜欢
gist.list.likes: 喜欢
gist.list.forked: 已派生
@@ -115,8 +115,7 @@ auth.username: 用户名
auth.password: 密码
auth.register-instead: 转到注册
auth.login-instead: 转到登录
auth.github-oauth: 使用 GitHub 账号继续
auth.gitea-oauth: 使用 Gitea 账号继续
auth.oauth: 使用 %s 账号继续
error: 错误
@@ -167,7 +166,8 @@ admin.disable-login: 禁用登录表单
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: 你想要删除此用户吗?
admin.gists.title: 标题
@@ -175,3 +175,85 @@ admin.gists.private: 私有?
admin.gists.nb-files: 文件数
admin.gists.nb-likes: 喜欢数
admin.gists.delete_confirm: 你想要删除此 Gist 吗?
gist.new.url: 'URL'
gist.new.preview: ''
error.page-not-found: ''
gist.new.create-a-new-gist: '创建一个新的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'
gist.search.help.user: '由用户创建的gist'
gist.search.help.title: '给定标题的gist'
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.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.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

@@ -124,9 +124,7 @@ auth.username: 使用者名稱
auth.password: 密碼
auth.register-instead: 註冊
auth.login-instead: 登錄
auth.github-oauth: GitHub 帳號繼續
auth.gitlab-oauth: 用 GitLab 帳號繼續
auth.gitea-oauth: 用 Gitea 帳號繼續
auth.oauth: %s 帳號繼續
error: 錯誤
@@ -179,7 +177,8 @@ admin.disable-login: 關閉登錄頁面
admin.disable-login_help: 關閉通過登錄頁面登錄,強制使用 OAuth 提供者。
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: 標題
@@ -187,3 +186,74 @@ admin.gists.private: 是否為私人
admin.gists.nb-files: 檔案數
admin.gists.nb-likes: 喜歡
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?
gist.search.no-results: 沒有找到任何 Gists
gist.search.help.title: Gists 的標題
gist.search.help.filename: Gists 的檔案名稱
gist.search.help.language: Gists 的程式語言
admin.actions.index-gists: 索引所有的 Gists
gist.search.help.user: 由使用者建立的 Gists
gist.search.found: 已找到 Gists
gist.search.help.extension: Gists 的副檔名
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

@@ -9,25 +9,44 @@ import (
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/v2/search/query"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"strconv"
"sync/atomic"
)
var bleveIndex bleve.Index
var atomicIndexer atomic.Pointer[Indexer]
type Indexer struct {
Index bleve.Index
}
func Enabled() bool {
return config.C.IndexEnabled
}
func Open(indexFilename string) error {
var err error
bleveIndex, err = bleve.Open(indexFilename)
func Init(indexFilename string) {
atomicIndexer.Store(&Indexer{Index: nil})
go func() {
bleveIndex, err := open(indexFilename)
if err != nil {
log.Error().Err(err).Msg("Failed to open index")
(*atomicIndexer.Load()).close()
}
atomicIndexer.Store(&Indexer{Index: bleveIndex})
log.Info().Msg("Indexer initialized")
}()
}
func open(indexFilename string) (bleve.Index, error) {
bleveIndex, err := bleve.Open(indexFilename)
if err == nil {
return nil
return bleveIndex, nil
}
if !errors.Is(err, bleve.ErrorIndexPathDoesNotExist) {
return err
return nil, err
}
docMapping := bleve.NewDocumentMapping()
@@ -40,7 +59,7 @@ func Open(indexFilename string) error {
"type": unicodenorm.Name,
"form": unicodenorm.NFC,
}); err != nil {
return err
return nil, err
}
if err = mapping.AddCustomAnalyzer("gistAnalyser", map[string]interface{}{
@@ -49,43 +68,71 @@ func Open(indexFilename string) error {
"tokenizer": unicode.Name,
"token_filters": []string{"unicodeNormalize", camelcase.Name, lowercase.Name},
}); err != nil {
return err
return nil, err
}
docMapping.DefaultAnalyzer = "gistAnalyser"
bleveIndex, err = bleve.New(indexFilename, mapping)
return err
return bleve.New(indexFilename, mapping)
}
func Close() error {
return bleveIndex.Close()
func Close() {
(*atomicIndexer.Load()).close()
}
func (i *Indexer) close() {
if i == nil || i.Index == nil {
return
}
err := i.Index.Close()
if err != nil {
log.Error().Err(err).Msg("Failed to close bleve index")
}
log.Info().Msg("Indexer closed")
atomicIndexer.Store(&Indexer{Index: nil})
}
func checkForIndexer() error {
if (*atomicIndexer.Load()).Index == nil {
return errors.New("indexer is not initialized")
}
return nil
}
func AddInIndex(gist *Gist) error {
if !Enabled() {
return nil
}
if err := checkForIndexer(); err != nil {
return err
}
if gist == nil {
return errors.New("failed to add nil gist to index")
}
return bleveIndex.Index(strconv.Itoa(int(gist.GistID)), gist)
return (*atomicIndexer.Load()).Index.Index(strconv.Itoa(int(gist.GistID)), gist)
}
func RemoveFromIndex(gistID uint) error {
if !Enabled() {
return nil
}
if err := checkForIndexer(); err != nil {
return err
}
return bleveIndex.Delete(strconv.Itoa(int(gistID)))
return (*atomicIndexer.Load()).Index.Delete(strconv.Itoa(int(gistID)))
}
func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []uint, page int) ([]uint, uint64, map[string]int, error) {
if !Enabled() {
return nil, 0, nil, nil
}
if err := checkForIndexer(); err != nil {
return nil, 0, nil, err
}
var err error
var indexerQuery query.Query
@@ -98,20 +145,18 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u
indexerQuery = contentQuery
}
if len(gistsIds) > 0 {
repoQueries := make([]query.Query, 0, len(gistsIds))
repoQueries := make([]query.Query, 0, len(gistsIds))
truee := true
for _, id := range gistsIds {
f := float64(id)
qq := bleve.NewNumericRangeInclusiveQuery(&f, &f, &truee, &truee)
qq.SetField("GistID")
repoQueries = append(repoQueries, qq)
}
indexerQuery = bleve.NewConjunctionQuery(bleve.NewDisjunctionQuery(repoQueries...), indexerQuery)
truee := true
for _, id := range gistsIds {
f := float64(id)
qq := bleve.NewNumericRangeInclusiveQuery(&f, &f, &truee, &truee)
qq.SetField("GistID")
repoQueries = append(repoQueries, qq)
}
indexerQuery = bleve.NewConjunctionQuery(bleve.NewDisjunctionQuery(repoQueries...), indexerQuery)
addQuery := func(field, value string) {
if value != "" && value != "." {
q := bleve.NewMatchPhraseQuery(value)
@@ -136,7 +181,7 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u
s.Fields = []string{"GistID"}
s.IncludeLocations = false
results, err := bleveIndex.Search(s)
results, err := (*atomicIndexer.Load()).Index.Search(s)
if err != nil {
return nil, 0, nil, err
}

View File

@@ -41,6 +41,12 @@ func MarkdownFile(file *git.File) (RenderedFile, error) {
Type: "Markdown",
}, err
}
func MarkdownString(content string) (string, error) {
var buf bytes.Buffer
err := newMarkdown().Convert([]byte(content), &buf)
return buf.String(), err
}
func newMarkdown() goldmark.Markdown {
return goldmark.New(

View File

@@ -2,14 +2,16 @@ package ssh
import (
"errors"
"io"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"io"
"os/exec"
"strings"
)
func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
@@ -37,7 +39,7 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
return errors.New("gist not found")
}
requireLogin, err := db.GetSetting(db.SettingRequireLogin)
allowUnauthenticated, err := auth.ShouldAllowUnauthenticatedGistAccess(db.DBAuthInfo{}, true)
if err != nil {
return errors.New("internal server error")
}
@@ -48,11 +50,18 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
if verb == "receive-pack" ||
gist.Private == 2 ||
gist.Private == db.PrivateVisibility ||
gist.ID == 0 ||
requireLogin == "1" {
!allowUnauthenticated {
pubKey, err := db.SSHKeyExistsForUser(key, gist.UserID)
var userToCheckPermissions *db.User
if gist.Private != db.PrivateVisibility && verb == "upload-pack" {
userToCheckPermissions, _ = db.GetUserFromSSHKey(key)
} else {
userToCheckPermissions = &gist.User
}
pubKey, err := db.SSHKeyExistsForUser(key, userToCheckPermissions.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)

View File

@@ -24,9 +24,9 @@ func Start() {
sshConfig := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
_, err := db.SSHKeyDoesExists(strKey)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
exists, err := db.SSHKeyDoesExists(strKey)
if !exists || err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}

View File

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

26
internal/utils/session.go Normal file
View File

@@ -0,0 +1,26 @@
package utils
import (
"github.com/gorilla/securecookie"
"github.com/rs/zerolog/log"
"os"
)
func ReadKey(filePath string) []byte {
key, err := os.ReadFile(filePath)
if err == nil {
return key
}
key = securecookie.GenerateRandomKey(32)
if key == nil {
log.Fatal().Msg("Failed to generate a new key for sessions")
}
err = os.WriteFile(filePath, key, 0600)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to save the key to %s", filePath)
}
return key
}

View File

@@ -0,0 +1,75 @@
package utils
import (
"github.com/go-playground/validator/v10"
"github.com/thomiceli/opengist/internal/i18n"
"regexp"
"strings"
)
type OpengistValidator struct {
v *validator.Validate
}
func NewValidator() *OpengistValidator {
v := validator.New()
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
return &OpengistValidator{v}
}
func (cv *OpengistValidator) Validate(i interface{}) error {
return cv.v.Struct(i)
}
func (cv *OpengistValidator) Var(field interface{}, tag string) error {
return cv.v.Var(field, tag)
}
func ValidationMessages(err *error, locale *i18n.Locale) string {
errs := (*err).(validator.ValidationErrors)
messages := make([]string, len(errs))
for i, e := range errs {
switch e.Tag() {
case "max":
messages[i] = locale.String("validation.is-too-long", e.Field())
case "required":
messages[i] = locale.String("validation.should-not-be-empty", e.Field())
case "excludes":
messages[i] = locale.String("validation.should-not-include-sub-directory", e.Field())
case "alphanum":
messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters", e.Field())
case "alphanumdash":
case "alphanumdashorempty":
messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters-and-dashes", e.Field())
case "min":
messages[i] = locale.String("validation.not-enough", e.Field())
case "notreserved":
messages[i] = locale.String("validation.invalid", e.Field())
}
}
return strings.Join(messages, " ; ")
}
func validateReservedKeywords(fl validator.FieldLevel) bool {
name := fl.Field().String()
restrictedNames := map[string]struct{}{}
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck", "preview", "metrics"} {
restrictedNames[restrictedName] = struct{}{}
}
// if the name is not in the restricted names, it is valid
_, ok := restrictedNames[name]
return !ok
}
func validateAlphaNumDash(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}

View File

@@ -8,11 +8,11 @@ import (
"github.com/thomiceli/opengist/internal/git"
"runtime"
"strconv"
"time"
)
func adminIndex(ctx echo.Context) error {
setData(ctx, "title", "Admin panel")
setData(ctx, "htmlTitle", "Admin panel")
setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel"))
setData(ctx, "adminHeaderPage", "index")
setData(ctx, "opengistVersion", config.OpengistVersion)
@@ -51,8 +51,7 @@ func adminIndex(ctx echo.Context) error {
}
func adminUsers(ctx echo.Context) error {
setData(ctx, "title", "Users")
setData(ctx, "htmlTitle", "Users - Admin panel")
setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "adminHeaderPage", "users")
pageInt := getPage(ctx)
@@ -63,15 +62,14 @@ func adminUsers(ctx echo.Context) error {
}
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
return html(ctx, "admin_users.html")
}
func adminGists(ctx echo.Context) error {
setData(ctx, "title", "Gists")
setData(ctx, "htmlTitle", "Gists - Admin panel")
setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "adminHeaderPage", "gists")
pageInt := getPage(ctx)
@@ -82,7 +80,7 @@ func adminGists(ctx echo.Context) error {
}
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
return html(ctx, "admin_gists.html")
@@ -99,7 +97,7 @@ func adminUserDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this user", err)
}
addFlash(ctx, "User has been deleted", "success")
addFlash(ctx, tr(ctx, "flash.admin.user-deleted"), "success")
return redirect(ctx, "/admin-panel/users")
}
@@ -119,49 +117,48 @@ func adminGistDelete(ctx echo.Context) error {
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success")
return redirect(ctx, "/admin-panel/gists")
}
func adminSyncReposFromFS(ctx echo.Context) error {
addFlash(ctx, "Syncing repositories from filesystem...", "success")
addFlash(ctx, tr(ctx, "flash.admin.sync-fs"), "success")
go actions.Run(actions.SyncReposFromFS)
return redirect(ctx, "/admin-panel")
}
func adminSyncReposFromDB(ctx echo.Context) error {
addFlash(ctx, "Syncing repositories from database...", "success")
addFlash(ctx, tr(ctx, "flash.admin.sync-db"), "success")
go actions.Run(actions.SyncReposFromDB)
return redirect(ctx, "/admin-panel")
}
func adminGcRepos(ctx echo.Context) error {
addFlash(ctx, "Garbage collecting repositories...", "success")
addFlash(ctx, tr(ctx, "flash.admin.git-gc"), "success")
go actions.Run(actions.GitGcRepos)
return redirect(ctx, "/admin-panel")
}
func adminSyncGistPreviews(ctx echo.Context) error {
addFlash(ctx, "Syncing Gist previews...", "success")
addFlash(ctx, tr(ctx, "flash.admin.sync-previews"), "success")
go actions.Run(actions.SyncGistPreviews)
return redirect(ctx, "/admin-panel")
}
func adminResetHooks(ctx echo.Context) error {
addFlash(ctx, "Resetting Git server hooks for all repositories...", "success")
addFlash(ctx, tr(ctx, "flash.admin.reset-hooks"), "success")
go actions.Run(actions.ResetHooks)
return redirect(ctx, "/admin-panel")
}
func adminIndexGists(ctx echo.Context) error {
addFlash(ctx, "Indexing all gists...", "success")
addFlash(ctx, tr(ctx, "flash.admin.index-gists"), "success")
go actions.Run(actions.IndexGists)
return redirect(ctx, "/admin-panel")
}
func adminConfig(ctx echo.Context) error {
setData(ctx, "title", "Configuration")
setData(ctx, "htmlTitle", "Configuration - Admin panel")
setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "adminHeaderPage", "config")
return html(ctx, "admin_config.html")
@@ -179,3 +176,58 @@ func adminSetConfig(ctx echo.Context) error {
"success": true,
})
}
func adminInvitations(ctx echo.Context) error {
setData(ctx, "htmlTitle", trH(ctx, "admin.invitations")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "adminHeaderPage", "invitations")
var invitations []*db.Invitation
var err error
if invitations, err = db.GetAllInvitations(); err != nil {
return errorRes(500, "Cannot get invites", err)
}
setData(ctx, "invitations", invitations)
return html(ctx, "admin_invitations.html")
}
func adminInvitationsCreate(ctx echo.Context) error {
code := ctx.FormValue("code")
nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
if err != nil {
nbMax = 10
}
expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
if err != nil {
expiresAtUnix = time.Now().Unix() + 604800 // 1 week
}
invitation := &db.Invitation{
Code: code,
ExpiresAt: expiresAtUnix,
NbMax: uint(nbMax),
}
if err := invitation.Create(); err != nil {
return errorRes(500, "Cannot create invitation", err)
}
addFlash(ctx, tr(ctx, "flash.admin.invitation-created"), "success")
return redirect(ctx, "/admin-panel/invitations")
}
func adminInvitationsDelete(ctx echo.Context) error {
id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
invitation, err := db.GetInvitationByID(uint(id))
if err != nil {
return errorRes(500, "Cannot retrieve invitation", err)
}
if err := invitation.Delete(); err != nil {
return errorRes(500, "Cannot delete this invitation", err)
}
addFlash(ctx, tr(ctx, "flash.admin.invitation-deleted"), "success")
return redirect(ctx, "/admin-panel/invitations")
}

View File

@@ -6,11 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/labstack/echo/v4"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
@@ -21,9 +16,15 @@ import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/utils"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
"io"
"net/http"
"net/url"
"strings"
)
const (
@@ -33,48 +34,69 @@ const (
OpenIDConnect = "openid-connect"
)
var title = cases.Title(language.English)
func register(ctx echo.Context) error {
setData(ctx, "title", tr(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", "New account")
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
disableSignup := getData(ctx, "DisableSignup")
disableForm := getData(ctx, "DisableLoginForm")
code := ctx.QueryParam("code")
if code != "" {
if invitation, err := db.GetInvitationByCode(code); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return errorRes(500, "Cannot check for invitation code", err)
} else if invitation != nil && invitation.IsUsable() {
disableSignup = false
}
}
setData(ctx, "title", trH(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
setData(ctx, "disableForm", disableForm)
setData(ctx, "disableSignup", disableSignup)
setData(ctx, "isLoginPage", false)
return html(ctx, "auth_form.html")
}
func processRegister(ctx echo.Context) error {
if getData(ctx, "DisableSignup") == true {
return errorRes(403, "Signing up is disabled", nil)
disableSignup := getData(ctx, "DisableSignup")
code := ctx.QueryParam("code")
invitation, err := db.GetInvitationByCode(code)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return errorRes(500, "Cannot check for invitation code", err)
} else if invitation.ID != 0 && invitation.IsUsable() {
disableSignup = false
}
if disableSignup == true {
return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
}
if getData(ctx, "DisableLoginForm") == true {
return errorRes(403, "Signing up via registration form is disabled", nil)
return errorRes(403, tr(ctx, "error.signup-disabled-form"), nil)
}
setData(ctx, "title", "New account")
setData(ctx, "htmlTitle", "New account")
setData(ctx, "title", trH(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
sess := getSession(ctx)
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
return html(ctx, "auth_form.html")
}
if exists, err := db.UserExists(dto.Username); err != nil || exists {
addFlash(ctx, "Username already exists", "error")
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
return html(ctx, "auth_form.html")
}
user := dto.ToUser()
password, err := argon2id.hash(user.Password)
password, err := utils.Argon2id.Hash(user.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
@@ -90,6 +112,12 @@ func processRegister(ctx echo.Context) error {
}
}
if invitation.ID != 0 {
if err := invitation.Use(); err != nil {
return errorRes(500, "Cannot use invitation", err)
}
}
sess.Values["user"] = user.ID
saveSession(sess, ctx)
@@ -97,8 +125,8 @@ func processRegister(ctx echo.Context) error {
}
func login(ctx echo.Context) error {
setData(ctx, "title", tr(ctx, "auth.login"))
setData(ctx, "htmlTitle", "Login")
setData(ctx, "title", trH(ctx, "auth.login"))
setData(ctx, "htmlTitle", trH(ctx, "auth.login"))
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
setData(ctx, "isLoginPage", true)
return html(ctx, "auth_form.html")
@@ -106,7 +134,7 @@ func login(ctx echo.Context) error {
func processLogin(ctx echo.Context) error {
if getData(ctx, "DisableLoginForm") == true {
return errorRes(403, "Logging in via login form is disabled", nil)
return errorRes(403, tr(ctx, "error.login-disabled-form"), nil)
}
var err error
@@ -114,7 +142,7 @@ func processLogin(ctx echo.Context) error {
dto := &db.UserDTO{}
if err = ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
password := dto.Password
@@ -125,20 +153,21 @@ func processLogin(ctx echo.Context) error {
return errorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
addFlash(ctx, "Invalid credentials", "error")
addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
return redirect(ctx, "/login")
}
if ok, err := argon2id.verify(password, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
addFlash(ctx, "Invalid credentials", "error")
addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
return redirect(ctx, "/login")
}
sess.Values["user"] = user.ID
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
saveSession(sess, ctx)
deleteCsrfCookie(ctx)
@@ -148,7 +177,7 @@ func processLogin(ctx echo.Context) error {
func oauthCallback(ctx echo.Context) error {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return errorRes(400, "Cannot complete user auth: "+err.Error(), err)
return errorRes(400, tr(ctx, "error.complete-oauth-login", err.Error()), err)
}
currUser := getUserLogged(ctx)
@@ -157,10 +186,10 @@ func oauthCallback(ctx echo.Context) error {
updateUserProviderInfo(currUser, user.Provider, user)
if err = currUser.Update(); err != nil {
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
return errorRes(500, "Cannot update user "+cases.Title(language.English).String(user.Provider)+" id", err)
}
addFlash(ctx, "Account linked to "+title.String(user.Provider), "success")
addFlash(ctx, tr(ctx, "flash.auth.account-linked-oauth", cases.Title(language.English).String(user.Provider)), "success")
return redirect(ctx, "/settings")
}
@@ -168,7 +197,7 @@ func oauthCallback(ctx echo.Context) error {
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
if err != nil {
if getData(ctx, "DisableSignup") == true {
return errorRes(403, "Signing up is disabled", nil)
return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -186,7 +215,7 @@ func oauthCallback(ctx echo.Context) error {
if err = userDB.Create(); err != nil {
if db.IsUniqueConstraintViolation(err) {
addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error")
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
return redirect(ctx, "/login")
}
@@ -216,7 +245,7 @@ func oauthCallback(ctx echo.Context) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
addFlash(ctx, "Could not get user keys", "error")
addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-retrievable"), "error")
log.Error().Err(err).Msg("Could not get user keys")
}
@@ -232,7 +261,7 @@ func oauthCallback(ctx echo.Context) error {
}
if err = sshKey.Create(); err != nil {
addFlash(ctx, "Could not create ssh key", "error")
addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-created"), "error")
log.Error().Err(err).Msg("Could not create ssh key")
}
}
@@ -327,10 +356,10 @@ func oauth(ctx echo.Context) error {
// Means that the user wants to unlink the account
if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() {
if err := currUser.DeleteProviderID(provider); err != nil {
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
return errorRes(500, "Cannot unlink account from "+cases.Title(language.English).String(provider), err)
}
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
addFlash(ctx, tr(ctx, "flash.auth.account-unlinked-oauth", cases.Title(language.English).String(provider)), "success")
return redirect(ctx, "/settings")
}
}
@@ -338,7 +367,7 @@ func oauth(ctx echo.Context) error {
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
return errorRes(400, "Unsupported provider", nil)
return errorRes(400, tr(ctx, "error.oauth-unsupported"), nil)
}
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
@@ -411,3 +440,15 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
}
return ""
}
type ContextAuthInfo struct {
context echo.Context
}
func (auth ContextAuthInfo) RequireLogin() (bool, error) {
return getData(auth.context, "RequireLogin") == true, nil
}
func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) {
return getData(auth.context, "AllowGistsWithoutLogin") == true, nil
}

View File

@@ -6,10 +6,6 @@ import (
"bytes"
"errors"
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"html/template"
"net/url"
"path/filepath"
@@ -18,6 +14,13 @@ import (
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/utils"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/config"
@@ -73,21 +76,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
}
}
httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
httpProtocol = "https"
}
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
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
}
setData(ctx, "baseHttpUrl", baseHttpUrl)
baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
if config.C.HttpGit {
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
@@ -152,18 +141,18 @@ func allGists(ctx echo.Context) error {
pageInt := getPage(ctx)
sort := "created"
sortText := tr(ctx, "gist.list.sort-by-created")
sortText := trH(ctx, "gist.list.sort-by-created")
order := "desc"
orderText := tr(ctx, "gist.list.order-by-desc")
orderText := trH(ctx, "gist.list.order-by-desc")
if ctx.QueryParam("sort") == "updated" {
sort = "updated"
sortText = tr(ctx, "gist.list.sort-by-updated")
sortText = trH(ctx, "gist.list.sort-by-updated")
}
if ctx.QueryParam("order") == "asc" {
order = "asc"
orderText = tr(ctx, "gist.list.order-by-asc")
orderText = trH(ctx, "gist.list.order-by-asc")
}
setData(ctx, "sort", sortText)
@@ -180,14 +169,14 @@ func allGists(ctx echo.Context) error {
if fromUserStr == "" {
urlctx := ctx.Request().URL.Path
if strings.HasSuffix(urlctx, "search") {
setData(ctx, "htmlTitle", "Search results")
setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
setData(ctx, "mode", "search")
setData(ctx, "searchQuery", ctx.QueryParam("q"))
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
urlPage = "search"
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
} else if strings.HasSuffix(urlctx, "all") {
setData(ctx, "htmlTitle", "All gists")
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all"))
setData(ctx, "mode", "all")
urlPage = "all"
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
@@ -237,17 +226,17 @@ func allGists(ctx echo.Context) error {
if liked {
urlPage = fromUserStr + "/liked"
setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-liked-by", fromUserStr))
setData(ctx, "mode", "liked")
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else if forked {
urlPage = fromUserStr + "/forked"
setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-forked-by", fromUserStr))
setData(ctx, "mode", "forked")
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else {
urlPage = fromUserStr
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr))
setData(ctx, "mode", "fromUser")
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
}
@@ -267,7 +256,7 @@ func allGists(ctx echo.Context) error {
}
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
setData(ctx, "urlPage", urlPage)
@@ -325,11 +314,11 @@ func search(ctx echo.Context) error {
if 10*pageInt < int(nbHits) {
setData(ctx, "nextPage", pageInt+1)
}
setData(ctx, "prevLabel", tr(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
setData(ctx, "urlPage", "search")
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
setData(ctx, "htmlTitle", "Search results")
setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
setData(ctx, "nbHits", nbHits)
setData(ctx, "gists", renderedGists)
setData(ctx, "langs", langs)
@@ -442,8 +431,9 @@ func gistJs(ctx echo.Context) error {
js := `document.write('<link rel="stylesheet" href="%s">')
document.write('%s')
`
js = fmt.Sprintf(js, cssUrl,
strings.Replace(htmlbuf.String(), "\n", `\n`, -1))
content := strings.Replace(htmlbuf.String(), `\n`, `\\n`, -1)
content = strings.Replace(content, "\n", `\n`, -1)
js = fmt.Sprintf(js, cssUrl, content)
ctx.Response().Header().Set("Content-Type", "application/javascript")
return plainText(ctx, 200, js)
}
@@ -461,7 +451,7 @@ func revisions(ctx echo.Context) error {
}
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
emailsSet := map[string]struct{}{}
@@ -480,13 +470,13 @@ func revisions(ctx echo.Context) error {
setData(ctx, "page", "revisions")
setData(ctx, "revision", "HEAD")
setData(ctx, "emails", emailsUsers)
setData(ctx, "htmlTitle", "Revision of "+gist.Title)
setData(ctx, "htmlTitle", trH(ctx, "gist.revision-of", gist.Title))
return html(ctx, "revisions.html")
}
func create(ctx echo.Context) error {
setData(ctx, "htmlTitle", "Create a new gist")
setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
return html(ctx, "create.html")
}
@@ -498,21 +488,21 @@ func processCreate(ctx echo.Context) error {
err := ctx.Request().ParseForm()
if err != nil {
return errorRes(400, "Bad request", err)
return errorRes(400, tr(ctx, "error.bad-request"), err)
}
dto := new(db.GistDTO)
var gist *db.Gist
if isCreate {
setData(ctx, "htmlTitle", "Create a new gist")
setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
} else {
gist = getData(ctx, "gist").(*db.Gist)
setData(ctx, "htmlTitle", "Edit "+gist.Title)
setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
}
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
dto.Files = make([]db.FileDTO, 0)
@@ -528,7 +518,7 @@ func processCreate(ctx echo.Context) error {
escapedValue, err := url.QueryUnescape(content)
if err != nil {
return errorRes(400, "Invalid character unescaped", err)
return errorRes(400, tr(ctx, "error.invalid-character-unescaped"), err)
}
dto.Files = append(dto.Files, db.FileDTO{
@@ -539,7 +529,7 @@ func processCreate(ctx echo.Context) error {
err = ctx.Validate(dto)
if err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
if isCreate {
return html(ctx, "create.html")
} else {
@@ -614,15 +604,20 @@ func processCreate(ctx echo.Context) error {
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
}
func toggleVisibility(ctx echo.Context) error {
func editVisibility(ctx echo.Context) error {
gist := getData(ctx, "gist").(*db.Gist)
gist.Private = (gist.Private + 1) % 3
dto := new(db.VisibilityDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
gist.Private = dto.Private
if err := gist.UpdateNoTimestamps(); err != nil {
return errorRes(500, "Error updating this gist", err)
}
addFlash(ctx, "Gist visibility has been changed", "success")
addFlash(ctx, tr(ctx, "flash.gist.visibility-changed"), "success")
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
}
@@ -634,7 +629,7 @@ func deleteGist(ctx echo.Context) error {
}
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success")
return redirect(ctx, "/")
}
@@ -674,7 +669,7 @@ func fork(ctx echo.Context) error {
}
if gist.User.ID == currentUser.ID {
addFlash(ctx, "Unable to fork own gists", "error")
addFlash(ctx, tr(ctx, "flash.gist.fork-own-gist"), "error")
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
}
@@ -710,7 +705,7 @@ func fork(ctx echo.Context) error {
return errorRes(500, "Error incrementing the fork count", err)
}
addFlash(ctx, "Gist has been forked", "success")
addFlash(ctx, tr(ctx, "flash.gist.forked"), "success")
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
}
@@ -744,7 +739,6 @@ func downloadFile(ctx echo.Context) error {
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
_, err = ctx.Response().Write([]byte(file.Content))
if err != nil {
return errorRes(500, "Error downloading the file", err)
}
@@ -761,7 +755,7 @@ func edit(ctx echo.Context) error {
}
setData(ctx, "files", files)
setData(ctx, "htmlTitle", "Edit "+gist.Title)
setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
return html(ctx, "edit.html")
}
@@ -822,10 +816,10 @@ func likes(ctx echo.Context) error {
}
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
setData(ctx, "htmlTitle", "Like for "+gist.Title)
setData(ctx, "htmlTitle", trH(ctx, "gist.likes.for", gist.Title))
setData(ctx, "revision", "HEAD")
return html(ctx, "likes.html")
}
@@ -846,10 +840,10 @@ func forks(ctx echo.Context) error {
}
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
return errorRes(404, "Page not found", nil)
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
}
setData(ctx, "htmlTitle", "Forks for "+gist.Title)
setData(ctx, "htmlTitle", trH(ctx, "gist.forks.for", gist.Title))
setData(ctx, "revision", "HEAD")
return html(ctx, "forks.html")
}
@@ -860,7 +854,7 @@ func checkbox(ctx echo.Context) error {
i, err := strconv.Atoi(checkboxNb)
if err != nil {
return errorRes(400, "Invalid number", nil)
return errorRes(400, tr(ctx, "error.invalid-number"), nil)
}
gist := getData(ctx, "gist").(*db.Gist)
@@ -889,3 +883,14 @@ func checkbox(ctx echo.Context) error {
return plainText(ctx, 200, "ok")
}
func preview(ctx echo.Context) error {
content := ctx.FormValue("content")
previewStr, err := render.MarkdownString(content)
if err != nil {
return errorRes(500, "Error rendering markdown", err)
}
return plainText(ctx, 200, previewStr)
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/utils"
"net/http"
"os"
"os/exec"
@@ -18,6 +19,7 @@ import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
@@ -69,12 +71,17 @@ func gitHttp(ctx echo.Context) error {
setData(ctx, "repositoryPath", repositoryPath)
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
if err != nil {
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
}
// Shows basic auth if :
// - user wants to push the gist
// - user wants to clone/pull a private gist
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && allow {
return route.handler(ctx)
}
@@ -98,7 +105,14 @@ func gitHttp(ctx echo.Context) error {
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
}
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
var userToCheckPermissions *db.User
if gist.Private != db.PrivateVisibility && isPull {
userToCheckPermissions, _ = db.GetUserByUsername(authUsername)
} else {
userToCheckPermissions = &gist.User
}
if ok, err := utils.Argon2id.Verify(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return errorRes(500, "Cannot verify password", err)
}
@@ -115,7 +129,7 @@ func gitHttp(ctx echo.Context) error {
return errorRes(401, "Invalid credentials", nil)
}
if ok, err := argon2id.verify(authPassword, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
@@ -134,7 +148,7 @@ func gitHttp(ctx echo.Context) error {
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
gist.Title = "gist:" + gist.Uuid
if err = gist.InitRepositoryViaInit(ctx); err != nil {
if err = gist.InitRepository(); err != nil {
return errorRes(500, "Cannot init repository in the file system", err)
}
@@ -193,6 +207,7 @@ func pack(ctx echo.Context, serviceType string) error {
}
repositoryPath := getData(ctx, "repositoryPath").(string)
gist := getData(ctx, "gist").(*db.Gist)
var stderr bytes.Buffer
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
@@ -200,26 +215,14 @@ func pack(ctx echo.Context, serviceType string) error {
cmd.Stdin = reqBody
cmd.Stdout = ctx.Response().Writer
cmd.Stderr = &stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_URL_INTERNAL="+git.RepositoryUrl(ctx, gist.User.Username, gist.Identifier()))
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_ID="+strconv.Itoa(int(gist.ID)))
if err = cmd.Run(); err != nil {
return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
}
// updatedAt is updated only if serviceType is receive-pack
if serviceType == "receive-pack" {
gist := getData(ctx, "gist").(*db.Gist)
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
return err
} else if hasNoCommits {
if err = gist.Delete(); err != nil {
return err
}
}
_ = gist.SetLastActiveNow()
_ = gist.UpdatePreviewAndCount(false)
gist.AddInIndex()
}
return nil
}

View File

@@ -23,3 +23,9 @@ func healthcheck(ctx echo.Context) error {
"time": time.Now().Format(time.RFC3339),
})
}
// metrics is a dummy handler to satisfy the /metrics endpoint (for Prometheus, Openmetrics, etc.)
// until we have a proper metrics endpoint
func metrics(ctx echo.Context) error {
return ctx.String(200, "")
}

View File

@@ -5,36 +5,43 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/index"
htmlpkg "html"
"html/template"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/utils"
"github.com/thomiceli/opengist/templates"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/markbates/goth/gothic"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/public"
"github.com/thomiceli/opengist/templates"
"golang.org/x/text/language"
)
var (
dev bool
store *sessions.CookieStore
re = regexp.MustCompile("[^a-z0-9]+")
fm = template.FuncMap{
dev bool
flashStore *sessions.CookieStore // session store for flash messages
userStore *sessions.FilesystemStore // session store for user sessions
re = regexp.MustCompile("[^a-z0-9]+")
fm = template.FuncMap{
"split": strings.Split,
"indexByte": strings.IndexByte,
"toInt": func(i string) int {
@@ -88,7 +95,8 @@ var (
return defaultAvatar()
},
"asset": asset,
"asset": asset,
"custom": customAsset,
"dev": func() bool {
return dev
},
@@ -133,6 +141,10 @@ var (
},
"addMetadataToSearchQuery": addMetadataToSearchQuery,
"indexEnabled": index.Enabled,
"isUrl": func(s string) bool {
_, err := url.ParseRequestURI(s)
return err == nil
},
}
)
@@ -149,10 +161,15 @@ type Server struct {
dev bool
}
func NewServer(isDev bool) *Server {
func NewServer(isDev bool, sessionsPath string) *Server {
dev = isDev
store = sessions.NewCookieStore([]byte("opengist"))
gothic.Store = store
flashStore = sessions.NewCookieStore([]byte("opengist"))
userStore = sessions.NewFilesystemStore(sessionsPath,
utils.ReadKey(path.Join(sessionsPath, "session-auth.key")),
utils.ReadKey(path.Join(sessionsPath, "session-encrypt.key")),
)
userStore.MaxLength(10 * 1024)
gothic.Store = userStore
e := echo.New()
e.HideBanner = true
@@ -172,8 +189,8 @@ func NewServer(isDev bool) *Server {
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogURI: true, LogStatus: true, LogMethod: true,
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
log.Info().Str("URI", v.URI).Int("status", v.Status).Str("method", v.Method).
Str("ip", ctx.RealIP()).
log.Info().Str("uri", v.URI).Int("status", v.Status).Str("method", v.Method).
Str("ip", ctx.RealIP()).TimeDiff("duration", time.Now(), v.StartTime).
Msg("HTTP")
return nil
},
@@ -181,15 +198,24 @@ func NewServer(isDev bool) *Server {
e.Use(middleware.Recover())
e.Use(middleware.Secure())
e.Renderer = &Template{
templates: template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")),
t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html")
matches, err := filepath.Glob(customPattern)
if err != nil {
log.Fatal().Err(err).Msg("Failed to check for custom templates")
}
if len(matches) > 0 {
t, err = t.ParseGlob(customPattern)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse custom templates")
}
}
e.Renderer = &Template{
templates: t,
}
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
if err, ok := er.(*echo.HTTPError); ok {
if err.Code >= 500 {
log.Error().Int("code", err.Code).Err(err.Internal).Msg("HTTP: " + err.Message.(string))
}
setData(ctx, "error", err)
if errHtml := htmlWithCode(ctx, err.Code, "error.html"); errHtml != nil {
log.Fatal().Err(errHtml).Send()
@@ -201,11 +227,10 @@ func NewServer(isDev bool) *Server {
e.Use(sessionInit)
e.Validator = NewValidator()
e.Validator = utils.NewValidator()
if !dev {
parseManifestEntries()
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.FileServer(http.FS(public.Files)))))
}
// Web based routes
@@ -223,8 +248,10 @@ func NewServer(isDev bool) *Server {
g1.GET("/", create, logged)
g1.POST("/", processCreate, logged)
g1.GET("/preview", preview, logged)
g1.GET("/healthcheck", healthcheck)
g1.GET("/metrics", metrics)
g1.GET("/register", register)
g1.POST("/register", processRegister)
@@ -249,6 +276,9 @@ func NewServer(isDev bool) *Server {
g2.POST("/users/:user/delete", adminUserDelete)
g2.GET("/gists", adminGists)
g2.POST("/gists/:gist/delete", adminGistDelete)
g2.GET("/invitations", adminInvitations)
g2.POST("/invitations", adminInvitationsCreate)
g2.POST("/invitations/:id/delete", adminInvitationsDelete)
g2.POST("/sync-fs", adminSyncReposFromFS)
g2.POST("/sync-db", adminSyncReposFromDB)
g2.POST("/gc-repos", adminGcRepos)
@@ -277,25 +307,45 @@ func NewServer(isDev bool) *Server {
g3 := g1.Group("/:user/:gistname")
{
g3.Use(checkRequireLogin, gistInit)
g3.Use(makeCheckRequireLogin(true), gistInit)
g3.GET("", gistIndex)
g3.GET("/rev/:revision", gistIndex)
g3.GET("/revisions", revisions)
g3.GET("/archive/:revision", downloadZip)
g3.POST("/visibility", toggleVisibility, logged, writePermission)
g3.POST("/visibility", editVisibility, logged, writePermission)
g3.POST("/delete", deleteGist, logged, writePermission)
g3.GET("/raw/:revision/:file", rawFile)
g3.GET("/download/:revision/:file", downloadFile)
g3.GET("/edit", edit, logged, writePermission)
g3.POST("/edit", processCreate, logged, writePermission)
g3.POST("/like", like, logged)
g3.GET("/likes", likes)
g3.GET("/likes", likes, checkRequireLogin)
g3.POST("/fork", fork, logged)
g3.GET("/forks", forks)
g3.GET("/forks", forks, checkRequireLogin)
g3.PUT("/checkbox", checkbox, logged, writePermission)
}
}
customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom"))
e.GET("/assets/*", func(ctx echo.Context) error {
if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil {
ctx.Response().Header().Set("Cache-Control", "public, max-age=31536000")
ctx.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat))
return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx)
}
// if the custom file is an .html template, render it
if strings.HasSuffix(ctx.Param("*"), ".html") {
if err := html(ctx, ctx.Param("*")); err != nil {
return notFound("Page not found")
}
return nil
}
return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx)
})
// Git HTTP routes
if config.C.HttpGit {
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
@@ -342,6 +392,22 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
httpProtocol = "https"
}
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
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
}
setData(ctx, "baseHttpUrl", baseHttpUrl)
return next(ctx)
}
}
@@ -451,26 +517,29 @@ func logged(next echo.HandlerFunc) echo.HandlerFunc {
}
}
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if user := getUserLogged(ctx); user != nil {
func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if user := getUserLogged(ctx); user != nil {
return next(ctx)
}
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
if err != nil {
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
}
if !allow {
addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
return redirect(ctx, "/login")
}
return next(ctx)
}
require := getData(ctx, "RequireLogin")
if require == true {
addFlash(ctx, "You must be logged in to access gists", "error")
return redirect(ctx, "/login")
}
return next(ctx)
}
}
func cacheControl(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
return next(c)
}
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
return makeCheckRequireLogin(false)(next)
}
func noRouteFound(echo.Context) error {
@@ -512,3 +581,11 @@ func asset(file string) string {
}
return config.C.ExternalUrl + "/" + manifestEntries[file].File
}
func customAsset(file string) string {
assetpath, err := url.JoinPath("/", "assets", file)
if err != nil {
log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
}
return config.C.ExternalUrl + assetpath
}

View File

@@ -4,6 +4,9 @@ import (
"crypto/md5"
"fmt"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/utils"
"os"
"path/filepath"
"strconv"
@@ -26,7 +29,8 @@ func userSettings(ctx echo.Context) error {
setData(ctx, "email", user.Email)
setData(ctx, "sshKeys", keys)
setData(ctx, "hasPassword", user.Password != "")
setData(ctx, "htmlTitle", "Settings")
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
setData(ctx, "htmlTitle", trH(ctx, "settings"))
return html(ctx, "settings.html")
}
@@ -49,7 +53,7 @@ func emailProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update email", err)
}
addFlash(ctx, "Email updated", "success")
addFlash(ctx, tr(ctx, "flash.user.email-updated"), "success")
return redirect(ctx, "/settings")
}
@@ -68,11 +72,11 @@ func sshKeysProcess(ctx echo.Context) error {
dto := new(db.SSHKeyDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
return redirect(ctx, "/settings")
}
key := dto.ToSSHKey()
@@ -81,16 +85,24 @@ func sshKeysProcess(ctx echo.Context) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil {
addFlash(ctx, "Invalid SSH key", "error")
addFlash(ctx, tr(ctx, "flash.user.invalid-ssh-key"), "error")
return redirect(ctx, "/settings")
}
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
if exists, err := db.SSHKeyDoesExists(key.Content); exists {
if err != nil {
return errorRes(500, "Cannot check if SSH key exists", err)
}
addFlash(ctx, tr(ctx, "settings.ssh-key-exists"), "error")
return redirect(ctx, "/settings")
}
if err := key.Create(); err != nil {
return errorRes(500, "Cannot add SSH key", err)
}
addFlash(ctx, "SSH key added", "success")
addFlash(ctx, tr(ctx, "flash.user.ssh-key-added"), "success")
return redirect(ctx, "/settings")
}
@@ -111,7 +123,7 @@ func sshKeysDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete SSH key", err)
}
addFlash(ctx, "SSH key deleted", "success")
addFlash(ctx, tr(ctx, "flash.user.ssh-key-deleted"), "success")
return redirect(ctx, "/settings")
}
@@ -120,16 +132,16 @@ func passwordProcess(ctx echo.Context) error {
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
dto.Username = user.Username
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
return html(ctx, "settings.html")
}
password, err := argon2id.hash(dto.Password)
password, err := utils.Argon2id.Hash(dto.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
@@ -139,7 +151,7 @@ func passwordProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update password", err)
}
addFlash(ctx, "Password updated", "success")
addFlash(ctx, tr(ctx, "flash.user.password-updated"), "success")
return redirect(ctx, "/settings")
}
@@ -148,25 +160,28 @@ func usernameProcess(ctx echo.Context) error {
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
dto.Password = user.Password
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
return redirect(ctx, "/settings")
}
if exists, err := db.UserExists(dto.Username); err != nil || exists {
addFlash(ctx, "Username already exists", "error")
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
return redirect(ctx, "/settings")
}
err := os.Rename(
filepath.Join(config.C.OpengistHome, "repos", user.Username),
filepath.Join(config.C.OpengistHome, "repos", dto.Username))
if err != nil {
return errorRes(500, "Cannot rename user directory", err)
sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
err := os.Rename(sourceDir, destinationDir)
if err != nil {
return errorRes(500, "Cannot rename user directory", err)
}
}
user.Username = dto.Username
@@ -175,6 +190,6 @@ func usernameProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update username", err)
}
addFlash(ctx, "Username updated", "success")
addFlash(ctx, tr(ctx, "flash.user.username-updated"), "success")
return redirect(ctx, "/settings")
}

View File

@@ -1,8 +1,13 @@
package test
import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"os"
"os/exec"
"path"
"testing"
)
@@ -89,3 +94,225 @@ func login(t *testing.T, s *testServer, user db.UserDTO) {
err := s.request("POST", "/login", user, 302)
require.NoError(t, err)
}
type settingSet struct {
key string `form:"key"`
value string `form:"value"`
}
func TestAnonymous(t *testing.T) {
setup(t)
s, err := newTestServer()
require.NoError(t, err, "Failed to create test server")
defer teardown(t, s)
user := db.UserDTO{Username: "thomas", Password: "azeaze"}
register(t, s, user)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
require.NoError(t, err)
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
err = s.request("GET", "/all", nil, 200)
require.NoError(t, err)
cookie := s.sessionCookie
s.sessionCookie = ""
err = s.request("GET", "/all", nil, 302)
require.NoError(t, err)
// Should redirect to login if RequireLogin
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 302)
require.NoError(t, err)
s.sessionCookie = cookie
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
require.NoError(t, err)
s.sessionCookie = ""
// Should return results
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
require.NoError(t, err)
}
func TestGitOperations(t *testing.T) {
setup(t)
s, err := newTestServer()
require.NoError(t, err, "Failed to create test server")
defer teardown(t, s)
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, admin)
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})
gist1 := db.GistDTO{
Title: "kaguya-pub-gist",
URL: "kaguya-pub-gist",
Description: "kaguya's first gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PublicVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"yeah",
},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
gist2 := db.GistDTO{
Title: "kaguya-unl-gist",
URL: "kaguya-unl-gist",
Description: "kaguya's second gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.UnlistedVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"cool",
},
}
err = s.request("POST", "/", gist2, 302)
require.NoError(t, err)
gist3 := db.GistDTO{
Title: "kaguya-priv-gist",
URL: "kaguya-priv-gist",
Description: "kaguya's second gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"super",
},
}
err = s.request("POST", "/", gist3, 302)
require.NoError(t, err)
gitOperations := func(credentials, owner, url, filename string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
fmt.Println("Testing", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
err := clientGitClone(credentials, owner, url)
if expectErrorClone {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientCheckRepo(url, filename)
if expectErrorCheck {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientGitPush(url)
if expectErrorPush {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
tests := []struct {
credentials string
user string
url string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", false, false, true},
{":", "kaguya", "kaguya-unl-gist", false, false, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
}
for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
login(t, s, admin)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
require.NoError(t, err)
testsRequireLogin := []struct {
credentials string
user string
url string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", true, true, true},
{":", "kaguya", "kaguya-unl-gist", true, true, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
}
for _, test := range testsRequireLogin {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
login(t, s, admin)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
require.NoError(t, err)
for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
}
func clientGitClone(creds string, user string, url string) error {
return exec.Command("git", "clone", "http://"+creds+"@localhost:6157/"+user+"/"+url, path.Join(config.GetHomeDir(), "tmp", url)).Run()
}
func clientGitPush(url string) error {
f, err := os.Create(path.Join(config.GetHomeDir(), "tmp", url, "newfile.txt"))
if err != nil {
return err
}
f.Close()
_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "add", "newfile.txt").Run()
_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "commit", "-m", "new file").Run()
err = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "push", "origin", "master").Run()
_ = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", url))
return err
}
func clientCheckRepo(url string, file string) error {
_, err := os.ReadFile(path.Join(config.GetHomeDir(), "tmp", url, file))
return err
}

View File

@@ -1,10 +1,11 @@
package test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"testing"
)
func TestGists(t *testing.T) {
@@ -28,9 +29,11 @@ func TestGists(t *testing.T) {
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
Private: 0,
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
@@ -57,9 +60,11 @@ func TestGists(t *testing.T) {
gist2 := db.GistDTO{
Title: "gist2",
Description: "my second gist",
Private: 0,
Name: []string{"", "gist2.txt", "gist3.txt"},
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"", "gist2.txt", "gist3.txt"},
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist2, 200)
require.NoError(t, err)
@@ -67,9 +72,11 @@ func TestGists(t *testing.T) {
gist3 := db.GistDTO{
Title: "gist3",
Description: "my third gist",
Private: 0,
Name: []string{""},
Content: []string{"yeah"},
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{""},
Content: []string{"yeah"},
}
err = s.request("POST", "/", gist3, 302)
require.NoError(t, err)
@@ -110,9 +117,11 @@ func TestVisibility(t *testing.T) {
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
Private: db.UnlistedVisibility,
Name: []string{""},
Content: []string{"yeah"},
VisibilityDTO: db.VisibilityDTO{
Private: db.UnlistedVisibility,
},
Name: []string{""},
Content: []string{"yeah"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
@@ -121,19 +130,19 @@ func TestVisibility(t *testing.T) {
require.NoError(t, err)
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PrivateVisibility}, 302)
require.NoError(t, err)
gist1db, err = db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, db.PrivateVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PublicVisibility}, 302)
require.NoError(t, err)
gist1db, err = db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, db.PublicVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.UnlistedVisibility}, 302)
require.NoError(t, err)
gist1db, err = db.GetGistByID("1")
require.NoError(t, err)
@@ -152,9 +161,11 @@ func TestLikeFork(t *testing.T) {
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
Private: 1,
Name: []string{""},
Content: []string{"yeah"},
VisibilityDTO: db.VisibilityDTO{
Private: 1,
},
Name: []string{""},
Content: []string{"yeah"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
@@ -212,9 +223,11 @@ func TestCustomUrl(t *testing.T) {
Title: "gist1",
URL: "my-gist",
Description: "my first gist",
Private: 0,
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
@@ -241,9 +254,11 @@ func TestCustomUrl(t *testing.T) {
gist2 := db.GistDTO{
Title: "gist2",
Description: "my second gist",
Private: 0,
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist2, 302)
require.NoError(t, err)

View File

@@ -3,13 +3,6 @@ package test
import (
"errors"
"fmt"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/web"
"io"
"net/http"
"net/http/httptest"
@@ -21,6 +14,14 @@ import (
"strconv"
"strings"
"testing"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/web"
)
type testServer struct {
@@ -30,7 +31,7 @@ type testServer struct {
func newTestServer() (*testServer, error) {
s := &testServer{
server: web.NewServer(true),
server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions")),
}
go s.start()
@@ -106,7 +107,7 @@ func structToURLValues(s interface{}) url.Values {
for i := 0; i < rValue.NumField(); i++ {
field := rValue.Type().Field(i)
tag := field.Tag.Get("form")
if tag != "" {
if tag != "" || field.Anonymous {
if field.Type.Kind() == reflect.Int {
fieldValue := rValue.Field(i).Int()
v.Add(tag, strconv.FormatInt(fieldValue, 10))
@@ -115,6 +116,12 @@ func structToURLValues(s interface{}) url.Values {
for _, va := range fieldValue {
v.Add(tag, va)
}
} else if field.Type.Kind() == reflect.Struct {
for key, val := range structToURLValues(rValue.Field(i).Interface()) {
for _, vv := range val {
v.Add(key, vv)
}
}
} else {
fieldValue := rValue.Field(i).String()
v.Add(tag, fieldValue)
@@ -125,7 +132,9 @@ func structToURLValues(s interface{}) url.Values {
}
func setup(t *testing.T) {
err := config.InitConfig("")
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
err := config.InitConfig("", io.Discard)
require.NoError(t, err, "Could not init config")
err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755)
@@ -140,6 +149,9 @@ func setup(t *testing.T) {
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)
err = os.MkdirAll(filepath.Join(homePath, "tmp", "sessions"), 0755)
require.NoError(t, err, "Could not create sessions directory")
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
require.NoError(t, err, "Could not create tmp repos directory")
@@ -159,7 +171,13 @@ func teardown(t *testing.T, s *testServer) {
err := db.Close()
require.NoError(t, err, "Could not close database")
err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
require.NoError(t, err, "Could not remove repos directory")
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
require.NoError(t, err, "Could not remove repos directory")
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "sessions"))
require.NoError(t, err, "Could not remove repos directory")
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))

View File

@@ -2,21 +2,17 @@ package web
import (
"context"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"golang.org/x/crypto/argon2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"net/http"
"regexp"
"strconv"
"strings"
)
@@ -63,6 +59,11 @@ func notFound(message string) error {
}
func errorRes(code int, message string, err error) error {
if code >= 500 {
var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
skipLogger.Error().Err(err).Msg(message)
}
return &echo.HTTPError{Code: code, Message: message, Internal: err}
}
@@ -75,7 +76,7 @@ func getUserLogged(ctx echo.Context) *db.User {
}
func setErrorFlashes(ctx echo.Context) {
sess, _ := store.Get(ctx.Request(), "flash")
sess, _ := flashStore.Get(ctx.Request(), "flash")
setData(ctx, "flashErrors", sess.Flashes("error"))
setData(ctx, "flashSuccess", sess.Flashes("success"))
@@ -84,13 +85,13 @@ func setErrorFlashes(ctx echo.Context) {
}
func addFlash(ctx echo.Context, flashMessage string, flashType string) {
sess, _ := store.Get(ctx.Request(), "flash")
sess, _ := flashStore.Get(ctx.Request(), "flash")
sess.AddFlash(flashMessage, flashType)
_ = sess.Save(ctx.Request(), ctx.Response())
}
func getSession(ctx echo.Context) *sessions.Session {
sess, _ := store.Get(ctx.Request(), "session")
sess, _ := userStore.Get(ctx.Request(), "session")
return sess
}
@@ -123,78 +124,12 @@ func loadSettings(ctx echo.Context) error {
for key, value := range settings {
s := strings.ReplaceAll(key, "-", " ")
s = title.String(s)
s = cases.Title(language.English).String(s)
setData(ctx, strings.ReplaceAll(s, " ", ""), value == "1")
}
return nil
}
type OpengistValidator struct {
v *validator.Validate
}
func NewValidator() *OpengistValidator {
v := validator.New()
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
return &OpengistValidator{v}
}
func (cv *OpengistValidator) Validate(i interface{}) error {
if err := cv.v.Struct(i); err != nil {
return err
}
return nil
}
func validationMessages(err *error) string {
errs := (*err).(validator.ValidationErrors)
messages := make([]string, len(errs))
for i, e := range errs {
switch e.Tag() {
case "max":
messages[i] = e.Field() + " is too long"
case "required":
messages[i] = e.Field() + " should not be empty"
case "excludes":
messages[i] = e.Field() + " should not include a sub directory"
case "alphanum":
messages[i] = e.Field() + " should only contain alphanumeric characters"
case "alphanumdash":
case "alphanumdashorempty":
messages[i] = e.Field() + " should only contain alphanumeric characters and dashes"
case "min":
messages[i] = "Not enough " + e.Field()
case "notreserved":
messages[i] = "Invalid " + e.Field()
}
}
return strings.Join(messages, " ; ")
}
func validateReservedKeywords(fl validator.FieldLevel) bool {
name := fl.Field().String()
restrictedNames := map[string]struct{}{}
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck"} {
restrictedNames[restrictedName] = struct{}{}
}
// if the name is not in the restricted names, it is valid
_, ok := restrictedNames[name]
return !ok
}
func validateAlphaNumDash(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func getPage(ctx echo.Context) int {
page := ctx.QueryParam("page")
if page == "" {
@@ -231,11 +166,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
switch labels {
case 1:
setData(ctx, "prevLabel", tr(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
case 2:
setData(ctx, "prevLabel", tr(ctx, "pagination.newer"))
setData(ctx, "nextLabel", tr(ctx, "pagination.older"))
setData(ctx, "prevLabel", trH(ctx, "pagination.newer"))
setData(ctx, "nextLabel", trH(ctx, "pagination.older"))
}
setData(ctx, "urlPage", urlPage)
@@ -243,9 +178,14 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
return nil
}
func tr(ctx echo.Context, key string) template.HTML {
func trH(ctx echo.Context, key string, args ...any) template.HTML {
l := getData(ctx, "locale").(*i18n.Locale)
return l.Tr(key)
return l.Tr(key, args...)
}
func tr(ctx echo.Context, key string, args ...any) string {
l := getData(ctx, "locale").(*i18n.Locale)
return l.String(key, args...)
}
func parseSearchQueryStr(query string) (string, map[string]string) {
@@ -287,68 +227,3 @@ func addMetadataToSearchQuery(input, key, value string) string {
return strings.TrimSpace(resultBuilder.String())
}
type Argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
}
var argon2id = Argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}
func (a Argon2ID) hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
func (a Argon2ID) verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}
hashParts := strings.Split(hash, "$")
if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
}

View File

@@ -1,79 +1,12 @@
package main
import (
"flag"
"fmt"
"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"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web"
"github.com/thomiceli/opengist/internal/cli"
"os"
"path/filepath"
)
func initialize() {
fmt.Println("Opengist v" + config.OpengistVersion)
configPath := flag.String("config", "", "Path to a config file in YML format")
flag.Parse()
if err := config.InitConfig(*configPath); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
panic(err)
}
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 := 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()
}
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), 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 config.C.IndexEnabled {
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname))
if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil {
log.Fatal().Err(err).Msg("Failed to open index")
}
}
}
func main() {
initialize()
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
go ssh.Start()
select {}
if err := cli.App(); err != nil {
os.Exit(1)
}
}

2257
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,10 @@
"scripts": {
"dev": "node_modules/.bin/vite -c public/vite.config.js",
"build": "node_modules/.bin/vite -c public/vite.config.js build",
"preview": "node_modules/.bin/vite -c public/vite.config.js preview"
"preview": "node_modules/.bin/vite -c public/vite.config.js preview",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"devDependencies": {
"@codemirror/commands": "^6.2.2",
@@ -21,7 +24,7 @@
"dayjs": "^1.11.9",
"github-markdown-css": "^5.5.0",
"nodemon": "^2.0.22",
"postcss": "^8.4.13",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"postcss-cssnext": "^3.1.1",
"postcss-import": "^15.1.0",
@@ -30,6 +33,6 @@
"sass": "^1.62.1",
"sugarss": "^4.0.1",
"tailwindcss": "^3.2.7",
"vite": "^4.2.3"
"vite": "^4.5.3"
}
}

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