Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b039b0703 | ||
|
|
6d31ef9732 | ||
|
|
678fb9938c | ||
|
|
df73b29fb1 | ||
|
|
690a6d55f9 | ||
|
|
0ef35fdb36 | ||
|
|
cf4e0e303c | ||
|
|
ab4bfcbcfb | ||
|
|
6499e3cc63 | ||
|
|
d4e4ae0b43 | ||
|
|
de6578d9e8 | ||
|
|
0950c9ce38 | ||
|
|
f881e1c13c | ||
|
|
069a999297 | ||
|
|
a97f54d92f | ||
|
|
22dbc32f23 | ||
|
|
9043cbcefe | ||
|
|
e969f04084 | ||
|
|
f490f36e56 | ||
|
|
d40eb65086 | ||
|
|
7d113e026e | ||
|
|
38892d8a4a | ||
|
|
77d87aeecd | ||
|
|
22052bd38f | ||
|
|
2fd053a077 | ||
|
|
97636b23f5 | ||
|
|
f705e879a1 | ||
|
|
6836dedda4 | ||
|
|
88f0f6e4c0 | ||
|
|
9b0c06d98b | ||
|
|
0757c4e7fb | ||
|
|
1ec77590e9 | ||
|
|
e439d96e43 | ||
|
|
1aa94292db | ||
|
|
3551fd745a | ||
|
|
785d89d6ab | ||
|
|
6a8759e21e | ||
|
|
a3a3d367ea | ||
|
|
e4bbd756f0 | ||
|
|
2782ced03d | ||
|
|
45a84df5b4 | ||
|
|
57273946c3 | ||
|
|
572e834999 | ||
|
|
f1541368e5 | ||
|
|
9936c6bf1e | ||
|
|
a97d9cdbf4 | ||
|
|
ef004675a5 | ||
|
|
3f5f4e01f1 | ||
|
|
c185cb8933 | ||
|
|
1c1e3a8919 | ||
|
|
fc9a75ce8f | ||
|
|
2bf0e9b7ce | ||
|
|
e1303c95d0 | ||
|
|
915287dc10 | ||
|
|
86590d2990 | ||
|
|
3179762fd3 | ||
|
|
86ad88fb09 | ||
|
|
db6d6a5eba | ||
|
|
7a75c5ecfa | ||
|
|
dfe70dc4cf | ||
|
|
afbecd9a1e | ||
|
|
7f4be43bb4 | ||
|
|
05eccfa8e7 | ||
|
|
a6c4183aac | ||
|
|
7fc8577ce0 | ||
|
|
a1524af7a9 | ||
|
|
10cf7e6e25 | ||
|
|
7ce94eea59 | ||
|
|
8eb8f4e231 | ||
|
|
af19268d6f | ||
|
|
4215d7e43b | ||
|
|
d85917bfb2 | ||
|
|
7c1d6e8bfd | ||
|
|
3a2fd2374a | ||
|
|
87a6113cc7 | ||
|
|
4cb7dc2d30 | ||
|
|
f52310a841 | ||
|
|
97707f7cca | ||
|
|
5058ca8f27 | ||
|
|
b3a856a05e | ||
|
|
f557bd45df | ||
|
|
2f8435892e | ||
|
|
4bba26daf6 | ||
|
|
3c97901995 | ||
|
|
3828022a1c | ||
|
|
85e2da054b | ||
|
|
0753c5cb54 | ||
|
|
845e28dd59 | ||
|
|
eff88711ea | ||
|
|
8466e50cc3 | ||
|
|
c9fd58c904 | ||
|
|
47869a77c9 | ||
|
|
246f12c8cb | ||
|
|
943212e492 | ||
|
|
7a6fb98223 | ||
|
|
3444fb9b75 | ||
|
|
be46304e23 | ||
|
|
5fa55dfbba | ||
|
|
09fb647f03 | ||
|
|
d518a44d32 | ||
|
|
dcacde0959 | ||
|
|
064d4d53f6 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: thomiceli
|
||||||
47
.github/workflows/docs.yml
vendored
Normal file
47
.github/workflows/docs.yml
vendored
Normal 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 }}
|
||||||
17
.github/workflows/go.yml
vendored
17
.github/workflows/go.yml
vendored
@@ -5,6 +5,8 @@ on:
|
|||||||
- master
|
- master
|
||||||
- 'dev-*'
|
- 'dev-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -13,10 +15,10 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go 1.20
|
- name: Set up Go 1.22
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.22"
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
@@ -34,20 +36,23 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go 1.20
|
- name: Set up Go 1.22
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.22"
|
||||||
|
|
||||||
- name: Check
|
- name: Check Go modules
|
||||||
run: make go_mod check_changes
|
run: make go_mod check_changes
|
||||||
|
|
||||||
|
- name: Check translations
|
||||||
|
run: make check-tr
|
||||||
|
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||||
go: ["1.20", "1.21"]
|
go: ["1.22"]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go 1.20
|
- name: Set up Go 1.22
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.22"
|
||||||
|
|
||||||
- name: Cross compile build
|
- name: Cross compile build
|
||||||
run: make all_crosscompile
|
run: make all_crosscompile
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ public/assets/*
|
|||||||
public/manifest.json
|
public/manifest.json
|
||||||
opengist
|
opengist
|
||||||
build/
|
build/
|
||||||
|
docs/.vitepress/dist/
|
||||||
|
docs/.vitepress/cache/
|
||||||
|
|||||||
175
CHANGELOG.md
175
CHANGELOG.md
@@ -1,5 +1,180 @@
|
|||||||
# Changelog
|
# 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.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Embedded gists (#179)
|
||||||
|
- Gist code search (#194)
|
||||||
|
- Custom URLS for gists (#183)
|
||||||
|
- Gist JSON data/metadata (#179)
|
||||||
|
- Keep default visibility when creating a gist on the UI (#155)
|
||||||
|
- Health check endpoint (#170)
|
||||||
|
- GitLab OAuth2 login (#174)
|
||||||
|
- Syntax highlighting for more file types (#176)
|
||||||
|
- Checkable Markdown checkboxes (#182)
|
||||||
|
- Config:
|
||||||
|
- Log output (#172)
|
||||||
|
- Default git branch name (#171)
|
||||||
|
- Change username setting (#190)
|
||||||
|
- Admin actions:
|
||||||
|
- Synchronize all gists previews (#191)
|
||||||
|
- Reset Git server hooks for all repositories (#191)
|
||||||
|
- Index all gists (#194)
|
||||||
|
- Translations:
|
||||||
|
- cs-CZ (#164)
|
||||||
|
- zh-TW (#166, #195)
|
||||||
|
- hu-HU (#185)
|
||||||
|
- pt-BR (#193)
|
||||||
|
- Docs (#198)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated dependencies (#197):
|
||||||
|
- Go `1.20` -> `1.21`
|
||||||
|
- JavaScript packages
|
||||||
|
- NodeJS Docker image `18` -> `20`
|
||||||
|
- Alpine Docker image `3.17` -> `3.19`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix reverse proxy subpath support (#192)
|
||||||
|
- Fix undecoded gist content when going back to editing in the UI (#184)
|
||||||
|
- Fix outputting non-truncated large files for editon/zip download (#184)
|
||||||
|
- Allow dashes in usernames (#184)
|
||||||
|
- Delete SSH keys associated to deleted user (#184)
|
||||||
|
- Better error message when there is no files in gist (#184)
|
||||||
|
- Show if there is no files in gist preview (#184)
|
||||||
|
- Log parsing for the 11th empty commit (#184)
|
||||||
|
- Optimize reading gist files content (#186)
|
||||||
|
|
||||||
|
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
|
||||||
|
### Added
|
||||||
|
- es-ES translation (#139)
|
||||||
|
- Create/change account password (#156)
|
||||||
|
- Display OAuth error messages when HTTP 400 (#159)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Git bare repository branch name creation (#157)
|
||||||
|
- Git file truncated output hanging (#157)
|
||||||
|
- Home user directory detection handling (#145)
|
||||||
|
- UI changes (#158)
|
||||||
|
|
||||||
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
|
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
|
||||||
### Added
|
### Added
|
||||||
- zh-CN translation (#130)
|
- zh-CN translation (#130)
|
||||||
|
|||||||
41
Dockerfile
41
Dockerfile
@@ -1,16 +1,25 @@
|
|||||||
FROM alpine:3.17 AS build
|
FROM alpine:3.19 AS base
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
make \
|
make \
|
||||||
gcc \
|
shadow \
|
||||||
musl-dev \
|
openssl \
|
||||||
libstdc++
|
openssh \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
gnupg \
|
||||||
|
xz \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libstdc++
|
||||||
|
|
||||||
COPY --from=golang:1.20-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 PATH="/usr/local/go/bin:${PATH}"
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
COPY --from=node:18-alpine /usr/local/ /usr/local/
|
COPY --from=node:20-alpine /usr/local/ /usr/local/
|
||||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||||
ENV PATH="/usr/local/bin:${PATH}"
|
ENV PATH="/usr/local/bin:${PATH}"
|
||||||
|
|
||||||
@@ -18,10 +27,23 @@ WORKDIR /opengist
|
|||||||
|
|
||||||
COPY . .
|
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
|
RUN make
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.17 as run
|
FROM alpine:3.19 as prod
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
@@ -49,4 +71,5 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
|||||||
|
|
||||||
EXPOSE 6157 2222
|
EXPOSE 6157 2222
|
||||||
VOLUME /opengist
|
VOLUME /opengist
|
||||||
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
||||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||||
|
|||||||
25
Makefile
25
Makefile
@@ -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
|
# Specify the name of your Go binary output
|
||||||
BINARY_NAME := opengist
|
BINARY_NAME := opengist
|
||||||
|
GIT_TAG := $(shell git describe --tags)
|
||||||
|
VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion
|
||||||
|
|
||||||
all: clean install build
|
all: clean install build
|
||||||
|
|
||||||
@@ -15,11 +17,12 @@ install:
|
|||||||
|
|
||||||
build_frontend:
|
build_frontend:
|
||||||
@echo "Building frontend assets..."
|
@echo "Building frontend assets..."
|
||||||
npx vite build
|
npx vite -c public/vite.config.js build
|
||||||
|
@EMBED=1 npx postcss 'public/assets/embed-*.css' -c public/postcss.config.js --replace # until we can .nest { @tailwind } in Sass
|
||||||
|
|
||||||
build_backend:
|
build_backend:
|
||||||
@echo "Building Opengist binary..."
|
@echo "Building Opengist binary..."
|
||||||
go build -tags fs_embed -o $(BINARY_NAME) .
|
go build -tags fs_embed -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" -o $(BINARY_NAME) .
|
||||||
|
|
||||||
build: build_frontend build_backend
|
build: build_frontend build_backend
|
||||||
|
|
||||||
@@ -30,16 +33,23 @@ build_docker:
|
|||||||
@echo "Building Docker image..."
|
@echo "Building Docker image..."
|
||||||
docker build -t $(BINARY_NAME):latest .
|
docker build -t $(BINARY_NAME):latest .
|
||||||
|
|
||||||
|
build_dev_docker:
|
||||||
|
@echo "Building Docker image..."
|
||||||
|
docker build -t $(BINARY_NAME)-dev:latest --target dev .
|
||||||
|
|
||||||
|
run_dev_docker:
|
||||||
|
docker run -v .:/opengist -p 6157:6157 -p 16157:16157 -p 2222:2222 -v $(HOME)/.opengist-dev:/root/.opengist --rm $(BINARY_NAME)-dev:latest
|
||||||
|
|
||||||
watch_frontend:
|
watch_frontend:
|
||||||
@echo "Building frontend assets..."
|
@echo "Building frontend assets..."
|
||||||
npx vite dev --port 16157
|
npx vite -c public/vite.config.js dev --port 16157 --host
|
||||||
|
|
||||||
watch_backend:
|
watch_backend:
|
||||||
@echo "Building Opengist binary..."
|
@echo "Building Opengist binary..."
|
||||||
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
|
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
@bash ./scripts/watch.sh
|
@sh ./scripts/watch.sh
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning up build artifacts..."
|
@echo "Cleaning up build artifacts..."
|
||||||
@@ -63,3 +73,6 @@ fmt:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
@go test ./... -p 1
|
@go test ./... -p 1
|
||||||
|
|
||||||
|
check-tr:
|
||||||
|
@bash ./scripts/check-translations.sh
|
||||||
26
README.md
26
README.md
@@ -1,12 +1,12 @@
|
|||||||
# Opengist
|
# Opengist
|
||||||
|
|
||||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" />
|
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
|
||||||
|
|
||||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||||
read and/or modified using standard Git commands, or with the web interface.
|
read and/or modified using standard Git commands, or with the web interface.
|
||||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
It is 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)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -19,12 +19,13 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
|
|||||||
|
|
||||||
* Create public, unlisted or private snippets
|
* Create public, unlisted or private snippets
|
||||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||||
* Revisions history
|
|
||||||
* Syntax highlighting ; markdown & CSV support
|
* 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
|
* Like / Fork snippets
|
||||||
* Search for snippets ; browse users snippets, likes and forks
|
|
||||||
* Download raw files or as a ZIP archive
|
* Download raw files or as a ZIP archive
|
||||||
* OAuth2 login with GitHub, Gitea, and OpenID Connect
|
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
|
||||||
* Restrict or unrestrict snippets visibility to anonymous users
|
* Restrict or unrestrict snippets visibility to anonymous users
|
||||||
* Docker support
|
* Docker support
|
||||||
* [More...](/docs/index.md#features)
|
* [More...](/docs/index.md#features)
|
||||||
@@ -36,7 +37,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
|
|||||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull ghcr.io/thomiceli/opengist:1
|
docker pull ghcr.io/thomiceli/opengist:1.7
|
||||||
```
|
```
|
||||||
|
|
||||||
It can be used in a `docker-compose.yml` file :
|
It can be used in a `docker-compose.yml` file :
|
||||||
@@ -50,7 +51,7 @@ version: "3"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
opengist:
|
opengist:
|
||||||
image: ghcr.io/thomiceli/opengist:1
|
image: ghcr.io/thomiceli/opengist:1.7
|
||||||
container_name: opengist
|
container_name: opengist
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -77,9 +78,9 @@ Download the archive for your system from the release page [here](https://github
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# example for linux amd64
|
# example for linux amd64
|
||||||
wget https://github.com/thomiceli/opengist/releases/download/v1.5.2/opengist1.5.2-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.5.2-linux-amd64.tar.gz
|
tar xzvf opengist1.7.5-linux-amd64.tar.gz
|
||||||
cd opengist
|
cd opengist
|
||||||
chmod +x opengist
|
chmod +x opengist
|
||||||
./opengist # with or without `--config config.yml`
|
./opengist # with or without `--config config.yml`
|
||||||
@@ -89,7 +90,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
|
|||||||
|
|
||||||
### From source
|
### From source
|
||||||
|
|
||||||
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.20+), [Node.js](https://nodejs.org/en/download/) (16+)
|
Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.22+), [Node.js](https://nodejs.org/en/download/) (16+), [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/thomiceli/opengist
|
git clone https://github.com/thomiceli/opengist
|
||||||
@@ -100,10 +101,13 @@ make
|
|||||||
|
|
||||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation is available in [/docs](/docs) directory.
|
The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
44
config.yml
44
config.yml
@@ -2,11 +2,13 @@
|
|||||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md
|
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md
|
||||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
|
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
|
||||||
|
|
||||||
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
|
# Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
|
||||||
log-level: warn
|
log-level: warn
|
||||||
|
|
||||||
# Public URL for the Git HTTP/SSH connection.
|
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
|
||||||
# If not set, uses the URL from the request
|
log-output: stdout,file
|
||||||
|
|
||||||
|
# Public URL to access to Opengist
|
||||||
external-url:
|
external-url:
|
||||||
|
|
||||||
# Directory where Opengist will store its data. Default: ~/.opengist/
|
# Directory where Opengist will store its data. Default: ~/.opengist/
|
||||||
@@ -15,6 +17,16 @@ opengist-home:
|
|||||||
# Name of the SQLite database file. Default: opengist.db
|
# Name of the SQLite database file. Default: opengist.db
|
||||||
db-filename: opengist.db
|
db-filename: opengist.db
|
||||||
|
|
||||||
|
# Enable or disable the code search index (either `true` or `false`). Default: true
|
||||||
|
index.enabled: true
|
||||||
|
|
||||||
|
# Name of the directory where the code search index is stored. Default: opengist.index
|
||||||
|
index.dirname: opengist.index
|
||||||
|
|
||||||
|
# Default branch name used by Opengist when initializing Git repositories.
|
||||||
|
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
||||||
|
git.default-branch:
|
||||||
|
|
||||||
# Set the journal mode for SQLite. Default: WAL
|
# Set the journal mode for SQLite. Default: WAL
|
||||||
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||||
sqlite.journal-mode: WAL
|
sqlite.journal-mode: WAL
|
||||||
@@ -55,20 +67,44 @@ ssh.keygen-executable: ssh-keygen
|
|||||||
|
|
||||||
|
|
||||||
# OAuth2 configuration
|
# OAuth2 configuration
|
||||||
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
|
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
|
||||||
|
|
||||||
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
||||||
github.client-key:
|
github.client-key:
|
||||||
github.secret:
|
github.secret:
|
||||||
|
|
||||||
|
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
|
||||||
|
gitlab.client-key:
|
||||||
|
gitlab.secret:
|
||||||
|
# URL of the Gitlab instance. Default: https://gitlab.com/
|
||||||
|
gitlab.url: https://gitlab.com/
|
||||||
|
# The name of the GitLab instance. It is displayed in the OAuth login button. Default: GitLab
|
||||||
|
gitlab.name: GitLab
|
||||||
|
|
||||||
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
||||||
gitea.client-key:
|
gitea.client-key:
|
||||||
gitea.secret:
|
gitea.secret:
|
||||||
# URL of the Gitea instance. Default: https://gitea.com/
|
# URL of the Gitea instance. Default: https://gitea.com/
|
||||||
gitea.url: https://gitea.com/
|
gitea.url: https://gitea.com/
|
||||||
|
# The name of the Gitea instance. It is displayed in the OAuth login button. Default: Gitea
|
||||||
|
gitea.name: Gitea
|
||||||
|
|
||||||
# To create a new OAuth2 application using OpenID Connect:
|
# To create a new OAuth2 application using OpenID Connect:
|
||||||
oidc.client-key:
|
oidc.client-key:
|
||||||
oidc.secret:
|
oidc.secret:
|
||||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||||
oidc.discovery-url:
|
oidc.discovery-url:
|
||||||
|
|
||||||
|
|
||||||
|
# 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
75
deploy/README.md
Normal 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
29
deploy/deployment.yaml
Normal 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
20
deploy/ingress.yaml
Normal 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
11
deploy/kustomization.yaml
Normal 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
15
deploy/pvc.yaml
Normal 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
14
deploy/service.yaml
Normal 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
|
||||||
89
docs/.vitepress/config.mts
Normal file
89
docs/.vitepress/config.mts
Normal 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
37
docs/.vitepress/tailwind.config.js
vendored
Normal 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',
|
||||||
|
}
|
||||||
101
docs/.vitepress/theme/Home.vue
Normal file
101
docs/.vitepress/theme/Home.vue
Normal 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>
|
||||||
16
docs/.vitepress/theme/Layout.vue
Normal file
16
docs/.vitepress/theme/Layout.vue
Normal 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>
|
||||||
12
docs/.vitepress/theme/index.ts
Normal file
12
docs/.vitepress/theme/index.ts
Normal 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
|
||||||
147
docs/.vitepress/theme/style.css
Normal file
147
docs/.vitepress/theme/style.css
Normal 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;
|
||||||
13
docs/administration/healthcheck.md
Normal file
13
docs/administration/healthcheck.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Healthcheck
|
||||||
|
|
||||||
|
A healthcheck is a simple HTTP GET request to the `/healthcheck` endpoint. It returns a `200 OK` response if the server is healthy.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://localhost:6157/healthcheck
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"database":"ok","opengist":"ok","time":"2024-01-04T05:18:33+01:00"}
|
||||||
|
```
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# Use Nginx as a reverse proxy
|
# Use Nginx as a reverse proxy
|
||||||
|
|
||||||
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
|
Configure Nginx to proxy requests to Opengist. Here are example configuration file to use Opengist on a subdomain or on a subpath.
|
||||||
|
|
||||||
|
Make sure you set the base url for Opengist via the [configuration](/docs/configuration/cheat-sheet.md).
|
||||||
|
|
||||||
|
### Subdomain
|
||||||
```
|
```
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@@ -16,7 +20,27 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run :
|
### Subpath
|
||||||
```shell
|
```
|
||||||
service nginx restart
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com;
|
||||||
|
|
||||||
|
location /opengist/ {
|
||||||
|
rewrite ^/opengist(/.*)$ $1 break;
|
||||||
|
proxy_pass http://127.0.0.1:6157;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /opengist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To apply changes:
|
||||||
|
```shell
|
||||||
|
sudo systemctl restart nginx
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# Use OAuth providers
|
|
||||||
|
|
||||||
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
|
|
||||||
|
|
||||||
## Github
|
|
||||||
|
|
||||||
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new)
|
|
||||||
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback`
|
|
||||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
|
||||||
```yaml
|
|
||||||
github.client-key: <key>
|
|
||||||
github.secret: <secret>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Gitea
|
|
||||||
|
|
||||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
|
||||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback`
|
|
||||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
|
||||||
```yaml
|
|
||||||
gitea.client-key: <key>
|
|
||||||
gitea.secret: <secret>
|
|
||||||
# URL of the Gitea instance. Default: https://gitea.com/
|
|
||||||
gitea.url: http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## OpenID Connect
|
|
||||||
|
|
||||||
* Add a new OAuth app in Application settings of your OIDC provider
|
|
||||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback`
|
|
||||||
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
|
||||||
```yaml
|
|
||||||
oidc.client-key: <key>
|
|
||||||
oidc.secret: <secret>
|
|
||||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
|
||||||
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
|
|
||||||
```
|
|
||||||
7
docs/administration/reset-password.md
Normal file
7
docs/administration/reset-password.md
Normal 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>
|
||||||
|
```
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
For non-Docker users, you could run Opengist as a systemd service.
|
For non-Docker users, you could run Opengist as a systemd service.
|
||||||
|
|
||||||
|
## As root
|
||||||
On Unix distributions with systemd, place the Opengist binary like:
|
On Unix distributions with systemd, place the Opengist binary like:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -45,3 +46,47 @@ systemctl daemon-reload
|
|||||||
systemctl enable --now opengist
|
systemctl enable --now opengist
|
||||||
systemctl status opengist
|
systemctl status opengist
|
||||||
```
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## As a normal user
|
||||||
|
**NOTE: This was tested on Ubuntu 20.04 and newer. For other distros, please check the respective documentation**
|
||||||
|
|
||||||
|
#### For the purpose of this documentation, we will assume that:
|
||||||
|
- You've followed the instructions on how to run opengist [from source](https://github.com/thomiceli/opengist?tab=readme-ov-file#from-source)
|
||||||
|
- Your shell user is named `pastebin`
|
||||||
|
- All commands are being executed as the `pastebin` user
|
||||||
|
|
||||||
|
_If none of the above is true, then adapt the commands and paths to fit your needs._
|
||||||
|
|
||||||
|
Enable lingering for the user:
|
||||||
|
```shell
|
||||||
|
loginctl enable-linger
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the user systemd folder:
|
||||||
|
```
|
||||||
|
mkdir -p /home/pastebin/.config/systemd/user
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create a service file at `/home/pastebin/.config/systemd/user/opengist.service`:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=opengist Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/home/pastebin/opengist/opengist --config /home/pastebin/opengist/config.yml
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, start the service:
|
||||||
|
```shell
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now opengist
|
||||||
|
systemctl --user status opengist
|
||||||
|
```
|
||||||
|
|||||||
48
docs/administration/traefik-reverse-proxy.md
Normal file
48
docs/administration/traefik-reverse-proxy.md
Normal 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>
|
||||||
53
docs/configuration/admin-panel.md
Normal file
53
docs/configuration/admin-panel.md
Normal 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.
|
||||||
@@ -1,25 +1,41 @@
|
|||||||
|
---
|
||||||
|
aside: false
|
||||||
|
---
|
||||||
|
|
||||||
# Configuration Cheat Sheet
|
# Configuration Cheat Sheet
|
||||||
|
|
||||||
| YAML Config Key | Environment Variable | Default value | Description |
|
| YAML Config Key | Environment Variable | Default value | Description |
|
||||||
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. |
|
||||||
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
|
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
|
||||||
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
|
||||||
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
|
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
||||||
| 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) |
|
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
|
||||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) |
|
||||||
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
|
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. |
|
||||||
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
||||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
| 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) |
|
||||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
||||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP 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. |
|
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
||||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
| 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. |
|
||||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||||
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
|
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||||
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
|
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
||||||
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
|
| 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). |
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Usage via command line :
|
|||||||
./opengist --config /path/to/config.yml
|
./opengist --config /path/to/config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
You can start by copying and/or modifying the provided [config.yml](/config.yml) file.
|
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
|
||||||
|
|
||||||
|
|
||||||
## Configuration via Environment Variables
|
## Configuration via Environment Variables
|
||||||
31
docs/configuration/custom-assets.md
Normal file
31
docs/configuration/custom-assets.md
Normal 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
|
||||||
|
```
|
||||||
62
docs/configuration/custom-links.md
Normal file
62
docs/configuration/custom-links.md
Normal 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.
|
||||||
77
docs/configuration/oauth-providers.md
Normal file
77
docs/configuration/oauth-providers.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Use OAuth providers
|
||||||
|
|
||||||
|
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
|
||||||
|
|
||||||
|
## GitHub
|
||||||
|
|
||||||
|
* Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
|
||||||
|
* Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
|
||||||
|
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||||
|
```yaml
|
||||||
|
github.client-key: <key>
|
||||||
|
github.secret: <secret>
|
||||||
|
```
|
||||||
|
```shell
|
||||||
|
OG_GITHUB_CLIENT_KEY=<key>
|
||||||
|
OG_GITHUB_SECRET=<secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## GitLab
|
||||||
|
|
||||||
|
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
|
||||||
|
* Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback`
|
||||||
|
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||||
|
```yaml
|
||||||
|
gitlab.client-key: <key>
|
||||||
|
gitlab.secret: <secret>
|
||||||
|
# URL of the GitLab instance. Default: https://gitlab.com/
|
||||||
|
gitlab.url: https://gitlab.com/
|
||||||
|
```
|
||||||
|
```shell
|
||||||
|
OG_GITLAB_CLIENT_KEY=<key>
|
||||||
|
OG_GITLAB_SECRET=<secret>
|
||||||
|
# URL of the GitLab instance. Default: https://gitlab.com/
|
||||||
|
OG_GITLAB_URL=https://gitlab.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||||
|
* Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback`
|
||||||
|
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||||
|
```yaml
|
||||||
|
gitea.client-key: <key>
|
||||||
|
gitea.secret: <secret>
|
||||||
|
# URL of the Gitea instance. Default: https://gitea.com/
|
||||||
|
gitea.url: http://localhost:3000
|
||||||
|
```
|
||||||
|
```shell
|
||||||
|
OG_GITEA_CLIENT_KEY=<key>
|
||||||
|
OG_GITEA_SECRET=<secret>
|
||||||
|
# URL of the Gitea instance. Default: https://gitea.com/
|
||||||
|
OG_GITEA_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## OpenID Connect
|
||||||
|
|
||||||
|
* Add a new OAuth app in Application settings of your OIDC provider
|
||||||
|
* Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback`
|
||||||
|
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](cheat-sheet.md) :
|
||||||
|
```yaml
|
||||||
|
oidc.client-key: <key>
|
||||||
|
oidc.secret: <secret>
|
||||||
|
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||||
|
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
|
||||||
|
```
|
||||||
|
```shell
|
||||||
|
OG_OIDC_CLIENT_KEY=<key>
|
||||||
|
OG_OIDC_SECRET=<secret>
|
||||||
|
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||||
|
OG_OIDC_DISCOVERY_URL=http://auth.example.com/.well-known/openid-configuration
|
||||||
|
```
|
||||||
|
|
||||||
6
docs/contributing/community.md
Normal file
6
docs/contributing/community.md
Normal 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
|
||||||
38
docs/contributing/development.md
Normal file
38
docs/contributing/development.md
Normal 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
|
||||||
@@ -1,52 +1,4 @@
|
|||||||
# Opengist
|
---
|
||||||
|
layout: home
|
||||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
navbar: false
|
||||||
read and/or modified using standard Git commands, or with the web interface.
|
---
|
||||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
|
||||||
|
|
||||||
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Create public, unlisted or private snippets
|
|
||||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
|
||||||
* Revisions history
|
|
||||||
* Syntax highlighting ; markdown & CSV support
|
|
||||||
* Like / Fork snippets
|
|
||||||
* Search for snippets ; browse users snippets, likes and forks
|
|
||||||
* Editor with indentation mode & size ; drag and drop files
|
|
||||||
* Download raw files or as a ZIP archive
|
|
||||||
* OAuth2 login with GitHub, Gitea, and OpenID Connect
|
|
||||||
* Avatars via Gravatar or OAuth2 providers
|
|
||||||
* Light/Dark mode
|
|
||||||
* Responsive UI
|
|
||||||
* Enable or disable signups
|
|
||||||
* Restrict or unrestrict snippets visibility to anonymous users
|
|
||||||
* Admin panel :
|
|
||||||
* delete users/gists;
|
|
||||||
* clean database/filesystem by syncing gists
|
|
||||||
* run `git gc` for all repositories
|
|
||||||
* SQLite database
|
|
||||||
* Logging
|
|
||||||
* Docker support
|
|
||||||
|
|
||||||
|
|
||||||
## System requirements
|
|
||||||
|
|
||||||
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
|
|
||||||
Version **2.20** or later is recommended as the app has not been tested with older Git versions.
|
|
||||||
|
|
||||||
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
|
|
||||||
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
* Backend Web Framework: [Echo](https://echo.labstack.com/)
|
|
||||||
* ORM: [GORM](https://gorm.io/)
|
|
||||||
* Frontend libraries:
|
|
||||||
* [Tailwind CSS](https://tailwindcss.com/)
|
|
||||||
* [CodeMirror](https://codemirror.net/)
|
|
||||||
* [Day.js](https://day.js.org/)
|
|
||||||
* [highlight.js](https://highlightjs.org/)
|
|
||||||
* and [others](/package.json)
|
|
||||||
|
|||||||
@@ -1,73 +1,7 @@
|
|||||||
# Installation
|
# Install Opengist
|
||||||
|
|
||||||
## With Docker
|
There are several ways to install Opengist, depending on your preferences and your environment.
|
||||||
|
|
||||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
- [Docker](installation/docker.md)
|
||||||
|
- [Source](installation/source.md)
|
||||||
```shell
|
- [Binary](installation/binary.md)
|
||||||
docker pull ghcr.io/thomiceli/opengist:1
|
|
||||||
```
|
|
||||||
|
|
||||||
It can be used in a `docker-compose.yml` file :
|
|
||||||
|
|
||||||
1. Create a `docker-compose.yml` file with the following content
|
|
||||||
2. Run `docker compose up -d`
|
|
||||||
3. Opengist is now running on port 6157, you can browse http://localhost:6157
|
|
||||||
|
|
||||||
```yml
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
opengist:
|
|
||||||
image: ghcr.io/thomiceli/opengist:1
|
|
||||||
container_name: opengist
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6157:6157" # HTTP port
|
|
||||||
- "2222:2222" # SSH port, can be removed if you don't use SSH
|
|
||||||
volumes:
|
|
||||||
- "$HOME/.opengist:/opengist"
|
|
||||||
```
|
|
||||||
|
|
||||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
|
|
||||||
variables :
|
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
opengist:
|
|
||||||
# ...
|
|
||||||
environment:
|
|
||||||
UID: 1001
|
|
||||||
GID: 1001
|
|
||||||
```
|
|
||||||
|
|
||||||
## Via binary
|
|
||||||
|
|
||||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# example for linux amd64
|
|
||||||
wget https://github.com/thomiceli/opengist/releases/download/v1.5.1/opengist1.5.1-linux-amd64.tar.gz
|
|
||||||
|
|
||||||
tar xzvf opengist1.5.1-linux-amd64.tar.gz
|
|
||||||
cd opengist
|
|
||||||
chmod +x opengist
|
|
||||||
./opengist # with or without `--config config.yml`
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## From source
|
|
||||||
|
|
||||||
Requirements :
|
|
||||||
* [Git](https://git-scm.com/downloads) (2.20+)
|
|
||||||
* [Go](https://go.dev/doc/install) (1.20+)
|
|
||||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git clone https://github.com/thomiceli/opengist
|
|
||||||
cd opengist
|
|
||||||
make
|
|
||||||
./opengist
|
|
||||||
```
|
|
||||||
|
|
||||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
|
||||||
|
|||||||
14
docs/installation/binary.md
Normal file
14
docs/installation/binary.md
Normal 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`
|
||||||
|
```
|
||||||
|
|
||||||
43
docs/installation/docker.md
Normal file
43
docs/installation/docker.md
Normal 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
|
||||||
|
```
|
||||||
19
docs/installation/source.md
Normal file
19
docs/installation/source.md
Normal 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
55
docs/introduction.md
Normal 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
17
docs/public/favicon.svg
Normal 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 |
BIN
docs/public/opengist-demo.png
Normal file
BIN
docs/public/opengist-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
58
docs/update.md
Normal file
58
docs/update.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Update Opengist
|
||||||
|
|
||||||
|
## Make a backup
|
||||||
|
|
||||||
|
Before updating, always make sure to backup the Opengist home directory, where all the data is stored.
|
||||||
|
|
||||||
|
You can do so by copying the `~/.opengist` directory (default location).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cp -r ~/.opengist ~/.opengist.bak
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install the new version
|
||||||
|
|
||||||
|
### With Docker
|
||||||
|
|
||||||
|
Pull the last version of Opengist
|
||||||
|
```shell
|
||||||
|
docker pull ghcr.io/thomiceli/opengist:1
|
||||||
|
```
|
||||||
|
|
||||||
|
And restart the container, using `docker compose up -d` for example if you use docker compose.
|
||||||
|
|
||||||
|
### Via binary
|
||||||
|
|
||||||
|
Stop the running instance; then like your first installation of Opengist, download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# example for linux amd64
|
||||||
|
wget https://github.com/thomiceli/opengist/releases/download/v1.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`
|
||||||
|
```
|
||||||
|
|
||||||
|
### From source
|
||||||
|
|
||||||
|
Stop the running instance; then pull the last changes from the master branch, and build the new version.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git switch master
|
||||||
|
git pull
|
||||||
|
make
|
||||||
|
./opengist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Restore the backup
|
||||||
|
|
||||||
|
If you have any issue with the new version, you can restore the backup you made before updating.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rm -rf ~/.opengist
|
||||||
|
cp -r ~/.opengist.bak ~/.opengist
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the old version of Opengist again.
|
||||||
11
docs/usage/embed.md
Normal file
11
docs/usage/embed.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Embed a Gist to your webpage
|
||||||
|
|
||||||
|
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="http://opengist.url/user/gist-url.js"></script>
|
||||||
|
|
||||||
|
<!-- Dark mode: -->
|
||||||
|
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||||
|
```
|
||||||
|
|
||||||
37
docs/usage/gist-json.md
Normal file
37
docs/usage/gist-json.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Retrieve Gist as JSON
|
||||||
|
|
||||||
|
To retrieve a Gist as JSON, you can add `.json` to the end of the URL of your gist:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://opengist.url/thomas/my-gist.json | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns a JSON object with the following structure similar to this one:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created_at": "2023-04-12T13:15:20+02:00",
|
||||||
|
"description": "",
|
||||||
|
"embed": {
|
||||||
|
"css": "http://localhost:6157/assets/embed-94abc261.css",
|
||||||
|
"html": "<div class=\"opengist-embed\" id=\"my-gist\">\n <div class=\"html \">\n \n <div class=\"rounded-md border-1 border-gray-100 dark:border-gray-800 overflow-auto mb-4\">\n <div class=\"border-b-1 border-gray-100 dark:border-gray-700 text-xs p-2 pl-4 bg-gray-50 dark:bg-gray-800 text-gray-400\">\n <a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist#file-hello-md\"><span class=\"font-bold text-gray-700 dark:text-gray-200\">hello.md</span> · 21 B · Markdown</a>\n <span class=\"float-right\"><a target=\"_blank\" href=\"http://localhost:6157\">Hosted via Opengist</a> · <span class=\"text-gray-700 dark:text-gray-200 font-bold\"><a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist/raw/HEAD/hello.md\">view raw</a></span></span>\n </div>\n \n \n \n <div class=\"chroma markdown markdown-body p-8\"><h1>Welcome to Opengist</h1>\n</div>\n \n\n </div>\n \n </div>\n</div>\n",
|
||||||
|
"js": "http://localhost:6157/thomas/my-gist.js",
|
||||||
|
"js_dark": "http://localhost:6157/thomas/my-gist.js?dark"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"filename": "hello.md",
|
||||||
|
"size": 21,
|
||||||
|
"human_size": "21 B",
|
||||||
|
"content": "# Welcome to Opengist",
|
||||||
|
"truncated": false,
|
||||||
|
"type": "Markdown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "my-gist",
|
||||||
|
"owner": "thomas",
|
||||||
|
"title": "hello.md",
|
||||||
|
"uuid": "8622b297bce54b408e36d546cef8019d",
|
||||||
|
"visibility": "public"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
26
docs/usage/git-push-options.md
Normal file
26
docs/usage/git-push-options.md
Normal 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
|
||||||
|
```
|
||||||
23
docs/usage/import-from-github-gist.md
Normal file
23
docs/usage/import-from-github-gist.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Import Gists from GitHub
|
||||||
|
|
||||||
|
After running Opengist at least once, you can import your Gists from GitHub using this script:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
github_user=user # replace with your GitHub username
|
||||||
|
opengist_url="http://user:password@opengist.url/init" # replace user, password and Opengist url
|
||||||
|
|
||||||
|
curl -s https://api.github.com/users/"$github_user"/gists?per_page=100 | jq '.[] | .git_pull_url' -r | while read url; do
|
||||||
|
git clone "$url"
|
||||||
|
repo_dir=$(basename "$url" .git)
|
||||||
|
|
||||||
|
# Add remote, push, and remove the directory
|
||||||
|
if [ -d "$repo_dir" ]; then
|
||||||
|
cd "$repo_dir"
|
||||||
|
git remote add gist "$opengist_url"
|
||||||
|
git push -u gist --all
|
||||||
|
cd ..
|
||||||
|
rm -rf "$repo_dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
@@ -39,4 +39,4 @@ To http://localhost:6157/init
|
|||||||
* [new branch] master -> master
|
* [new branch] master -> master
|
||||||
```
|
```
|
||||||
|
|
||||||
https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066
|
<video controls="controls" src="https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066" />
|
||||||
101
go.mod
101
go.mod
@@ -1,54 +1,95 @@
|
|||||||
module github.com/thomiceli/opengist
|
module github.com/thomiceli/opengist
|
||||||
|
|
||||||
go 1.20
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/glebarez/go-sqlite v1.21.2
|
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||||
github.com/glebarez/sqlite v1.9.0
|
github.com/alecthomas/chroma/v2 v2.14.0
|
||||||
github.com/go-playground/validator/v10 v10.15.4
|
github.com/blevesearch/bleve/v2 v2.4.0
|
||||||
github.com/google/uuid v1.3.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/glebarez/go-sqlite v1.22.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/hashicorp/go-memdb v1.3.4
|
||||||
github.com/labstack/echo/v4 v4.11.1
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
github.com/markbates/goth v1.78.0
|
github.com/markbates/goth v1.80.0
|
||||||
github.com/rs/zerolog v1.30.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/crypto v0.13.0
|
github.com/urfave/cli/v2 v2.27.2
|
||||||
golang.org/x/text v0.13.0
|
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.23.0
|
||||||
|
golang.org/x/text v0.15.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/gorm v1.25.4
|
gorm.io/gorm v1.25.10
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
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.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.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
|
||||||
|
github.com/blevesearch/vellum v1.0.10 // indirect
|
||||||
|
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
|
||||||
|
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
|
||||||
|
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
|
||||||
|
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
||||||
|
github.com/blevesearch/zapx/v15 v15.3.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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/net v0.15.0 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
golang.org/x/oauth2 v0.12.0 // indirect
|
go.etcd.io/bbolt v1.3.10 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/oauth2 v0.20.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
modernc.org/libc v1.24.1 // 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/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.7.1 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.25.0 // indirect
|
modernc.org/sqlite v1.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
685
go.sum
685
go.sum
@@ -1,144 +1,117 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
|
github.com/blevesearch/bleve/v2 v2.4.0 h1:2xyg+Wv60CFHYccXc+moGxbL+8QKT/dZK09AewHgKsg=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
github.com/blevesearch/bleve/v2 v2.4.0/go.mod h1:IhQHoFAbHgWKYavb9rQgQEJJVMuY99cKdQ0wPpst2aY=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
github.com/blevesearch/bleve_index_api v1.1.8 h1:rJUccYfWqRY2/BGowlsv1lwrLKYK/zPE6hgNn1pTGdk=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
github.com/blevesearch/bleve_index_api v1.1.8/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
github.com/blevesearch/go-faiss v1.0.16 h1:lfzXzzjO1mAf15MRiRY5yz6KVGr02CyRrr7m0z70Ih8=
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
github.com/blevesearch/go-faiss v1.0.16/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 h1:UfbyRpIMdcaNsgciGYS9Pib7N3xd3EEw8KKbd/aDBlA=
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
github.com/blevesearch/scorch_segment_api/v2 v2.2.13/go.mod h1:osG1bAUONZB2r/ozUJwjbuOzPvdrULWaLOm+vsMANsk=
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
|
||||||
|
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
|
||||||
|
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
|
||||||
|
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
|
||||||
|
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
|
||||||
|
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
||||||
|
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
||||||
|
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
||||||
|
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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
github.com/dlclark/regexp2 v1.7.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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
|
||||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
|
||||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
|
github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0=
|
||||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||||
|
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
|
||||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
|
||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
@@ -147,387 +120,145 @@ 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 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
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.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 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 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
||||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
||||||
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
|
||||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
|
||||||
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
|
|
||||||
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
|
||||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
|
||||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
|
||||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
|
||||||
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
|
||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
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 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
modernc.org/sqlite v1.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=
|
||||||
|
|||||||
177
internal/actions/actions.go
Normal file
177
internal/actions/actions.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"github.com/thomiceli/opengist/internal/index"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionStatus struct {
|
||||||
|
Running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SyncReposFromFS = iota
|
||||||
|
SyncReposFromDB
|
||||||
|
GitGcRepos
|
||||||
|
SyncGistPreviews
|
||||||
|
ResetHooks
|
||||||
|
IndexGists
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mutex sync.Mutex
|
||||||
|
actions = make(map[int]ActionStatus)
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateActionStatus(actionType int, running bool) {
|
||||||
|
actions[actionType] = ActionStatus{
|
||||||
|
Running: running,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRunning(actionType int) bool {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
return actions[actionType].Running
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(actionType int) {
|
||||||
|
mutex.Lock()
|
||||||
|
|
||||||
|
if actions[actionType].Running {
|
||||||
|
mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActionStatus(actionType, true)
|
||||||
|
mutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
mutex.Lock()
|
||||||
|
updateActionStatus(actionType, false)
|
||||||
|
mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var functionToRun func()
|
||||||
|
switch actionType {
|
||||||
|
case SyncReposFromFS:
|
||||||
|
functionToRun = syncReposFromFS
|
||||||
|
case SyncReposFromDB:
|
||||||
|
functionToRun = syncReposFromDB
|
||||||
|
case GitGcRepos:
|
||||||
|
functionToRun = gitGcRepos
|
||||||
|
case SyncGistPreviews:
|
||||||
|
functionToRun = syncGistPreviews
|
||||||
|
case ResetHooks:
|
||||||
|
functionToRun = resetHooks
|
||||||
|
case IndexGists:
|
||||||
|
functionToRun = indexGists
|
||||||
|
default:
|
||||||
|
log.Error().Msg("Unknown action type")
|
||||||
|
}
|
||||||
|
|
||||||
|
functionToRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncReposFromFS() {
|
||||||
|
log.Info().Msg("Syncing repositories from filesystem...")
|
||||||
|
gists, err := db.GetAllGistsRows()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot get gists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, gist := range gists {
|
||||||
|
// if repository does not exist, delete gist from database
|
||||||
|
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
|
||||||
|
if err2 := gist.Delete(); err2 != nil {
|
||||||
|
log.Error().Err(err2).Msgf("Cannot delete gist %d", gist.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncReposFromDB() {
|
||||||
|
log.Info().Msg("Syncing repositories from database...")
|
||||||
|
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
path := strings.Split(e, string(os.PathSeparator))
|
||||||
|
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
|
||||||
|
|
||||||
|
if gist.ID == 0 {
|
||||||
|
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot delete repository %s/%s", path[len(path)-2], path[len(path)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitGcRepos() {
|
||||||
|
log.Info().Msg("Garbage collecting all repositories...")
|
||||||
|
if err := git.GcRepos(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error garbage collecting repositories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGistPreviews() {
|
||||||
|
log.Info().Msg("Syncing all Gist previews...")
|
||||||
|
|
||||||
|
gists, err := db.GetAllGistsRows()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot get gists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, gist := range gists {
|
||||||
|
if err = gist.UpdatePreviewAndCount(false); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetHooks() {
|
||||||
|
log.Info().Msg("Resetting Git server hooks for all repositories...")
|
||||||
|
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
path := strings.Split(e, string(os.PathSeparator))
|
||||||
|
if err := git.CreateDotGitFiles(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", path[len(path)-2], path[len(path)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexGists() {
|
||||||
|
log.Info().Msg("Indexing all Gists...")
|
||||||
|
gists, err := db.GetAllGistsRows()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot get gists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gist := range gists {
|
||||||
|
log.Info().Msgf("Indexing gist %d", gist.ID)
|
||||||
|
indexedGist, err := gist.ToIndexedGist()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = index.AddInIndex(indexedGist); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot index gist %d", gist.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
internal/auth/auth.go
Normal file
18
internal/auth/auth.go
Normal 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
50
internal/cli/admin.go
Normal 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
56
internal/cli/hook.go
Normal 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
177
internal/cli/main.go
Normal 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
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -15,7 +18,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var OpengistVersion = "1.5.2"
|
var OpengistVersion = ""
|
||||||
|
|
||||||
var C *config
|
var C *config
|
||||||
|
|
||||||
@@ -23,9 +26,14 @@ var C *config
|
|||||||
// doesn't support dot notation in this case sadly
|
// doesn't support dot notation in this case sadly
|
||||||
type config struct {
|
type config struct {
|
||||||
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
||||||
|
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
|
||||||
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
||||||
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
||||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
|
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
|
||||||
|
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"`
|
||||||
|
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"`
|
||||||
|
|
||||||
|
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||||
|
|
||||||
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
||||||
|
|
||||||
@@ -42,25 +50,39 @@ type config struct {
|
|||||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
||||||
|
|
||||||
|
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
|
||||||
|
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
|
||||||
|
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
|
||||||
|
GitlabName string `yaml:"gitlab.name" env:"OG_GITLAB_NAME"`
|
||||||
|
|
||||||
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
||||||
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
||||||
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
||||||
|
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
|
||||||
|
|
||||||
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
|
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
|
||||||
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
|
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
|
||||||
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
|
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
|
||||||
|
|
||||||
|
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
|
||||||
|
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
|
||||||
|
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaticLink struct {
|
||||||
|
Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"`
|
||||||
|
Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func configWithDefaults() (*config, error) {
|
func configWithDefaults() (*config, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
c := &config{}
|
c := &config{}
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.LogLevel = "warn"
|
c.LogLevel = "warn"
|
||||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
c.LogOutput = "stdout,file"
|
||||||
|
c.OpengistHome = ""
|
||||||
c.DBFilename = "opengist.db"
|
c.DBFilename = "opengist.db"
|
||||||
|
c.IndexEnabled = true
|
||||||
|
c.IndexDirname = "opengist.index"
|
||||||
|
|
||||||
c.SqliteJournalMode = "WAL"
|
c.SqliteJournalMode = "WAL"
|
||||||
|
|
||||||
@@ -73,32 +95,47 @@ func configWithDefaults() (*config, error) {
|
|||||||
c.SshPort = "2222"
|
c.SshPort = "2222"
|
||||||
c.SshKeygen = "ssh-keygen"
|
c.SshKeygen = "ssh-keygen"
|
||||||
|
|
||||||
c.GiteaUrl = "http://gitea.com"
|
c.GitlabName = "GitLab"
|
||||||
|
|
||||||
|
c.GiteaUrl = "https://gitea.com"
|
||||||
|
c.GiteaName = "Gitea"
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig(configPath string) error {
|
func InitConfig(configPath string, out io.Writer) error {
|
||||||
// Default values
|
// Default values
|
||||||
c, err := configWithDefaults()
|
c, err := configWithDefaults()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = loadConfigFromYaml(c, configPath); err != nil {
|
if err = loadConfigFromYaml(c, configPath, out); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = loadConfigFromEnv(c); err != nil {
|
if err = loadConfigFromEnv(c, out); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.OpengistHome == "" {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||||
|
}
|
||||||
|
|
||||||
if err = checks(c); err != nil {
|
if err = checks(c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
C = c
|
C = c
|
||||||
|
|
||||||
|
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,21 +143,61 @@ func InitLog() {
|
|||||||
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var level zerolog.Level
|
var level zerolog.Level
|
||||||
level, err = zerolog.ParseLevel(C.LogLevel)
|
level, err := zerolog.ParseLevel(C.LogLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
level = zerolog.InfoLevel
|
level = zerolog.InfoLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
|
var logWriters []io.Writer
|
||||||
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
|
logOutputTypes := utils.RemoveDuplicates[string](
|
||||||
|
strings.Split(strings.ToLower(C.LogOutput), ","),
|
||||||
|
)
|
||||||
|
|
||||||
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
|
consoleWriter := zerolog.NewConsoleWriter(
|
||||||
|
func(w *zerolog.ConsoleWriter) {
|
||||||
|
w.TimeFormat = time.TimeOnly
|
||||||
|
w.FormatCaller = func(i interface{}) string {
|
||||||
|
file := i.(string)
|
||||||
|
index := strings.Index(file, "internal")
|
||||||
|
if index == -1 {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
return file[index:]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, logOutputType := range logOutputTypes {
|
||||||
|
logOutputType = strings.TrimSpace(logOutputType)
|
||||||
|
if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
|
||||||
|
defer func() { log.Warn().Msg("Invalid log output type: " + logOutputType) }()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch logOutputType {
|
||||||
|
case "stdout":
|
||||||
|
logWriters = append(logWriters, consoleWriter)
|
||||||
|
defer func() { log.Debug().Msg("Logging to stdout") }()
|
||||||
|
case "file":
|
||||||
|
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
logWriters = append(logWriters, file)
|
||||||
|
defer func() { log.Debug().Msg("Logging to file: " + file.Name()) }()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(logWriters) == 0 {
|
||||||
|
logWriters = append(logWriters, consoleWriter)
|
||||||
|
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
|
||||||
|
}
|
||||||
|
|
||||||
|
multi := zerolog.MultiLevelWriter(logWriters...)
|
||||||
|
log.Logger = zerolog.New(multi).Level(level).With().Caller().Timestamp().Logger()
|
||||||
|
|
||||||
|
if !slices.Contains([]string{"debug", "info", "warn", "error", "fatal"}, strings.ToLower(C.LogLevel)) {
|
||||||
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,8 +216,8 @@ func CheckGitVersion(version string) (bool, error) {
|
|||||||
return false, fmt.Errorf("invalid minor version number")
|
return false, fmt.Errorf("invalid minor version number")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if version is prior to 2.20
|
// Check if version is prior to 2.28
|
||||||
if major < 2 || (major == 2 && minor < 20) {
|
if major < 2 || (major == 2 && minor < 28) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -151,7 +228,7 @@ func GetHomeDir() string {
|
|||||||
return filepath.Clean(absolutePath)
|
return filepath.Clean(absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFromYaml(c *config, configPath string) error {
|
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
absolutePath, _ := filepath.Abs(configPath)
|
absolutePath, _ := filepath.Abs(configPath)
|
||||||
absolutePath = filepath.Clean(absolutePath)
|
absolutePath = filepath.Clean(absolutePath)
|
||||||
@@ -160,9 +237,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("No YAML config file found at " + absolutePath)
|
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Using YAML config file: " + absolutePath)
|
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
|
||||||
|
|
||||||
// Override default values with values from config.yml
|
// Override default values with values from config.yml
|
||||||
d := yaml.NewDecoder(file)
|
d := yaml.NewDecoder(file)
|
||||||
@@ -172,13 +249,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("No YAML config file specified.")
|
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFromEnv(c *config) error {
|
func loadConfigFromEnv(c *config, out io.Writer) error {
|
||||||
v := reflect.ValueOf(c).Elem()
|
v := reflect.ValueOf(c).Elem()
|
||||||
var envVars []string
|
var envVars []string
|
||||||
|
|
||||||
@@ -190,28 +267,69 @@ func loadConfigFromEnv(c *config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
envValue := os.Getenv(strings.ToUpper(tag))
|
envValue := os.Getenv(strings.ToUpper(tag))
|
||||||
if envValue == "" {
|
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v.Field(i).Kind() {
|
switch v.Field(i).Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
v.Field(i).SetString(envValue)
|
v.Field(i).SetString(envValue)
|
||||||
|
envVars = append(envVars, tag)
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
boolVal, err := strconv.ParseBool(envValue)
|
boolVal, err := strconv.ParseBool(envValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Field(i).SetBool(boolVal)
|
v.Field(i).SetBool(boolVal)
|
||||||
|
envVars = append(envVars, tag)
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
|
||||||
|
prefix := strings.ToUpper(tag) + "_"
|
||||||
|
var sliceValue reflect.Value
|
||||||
|
elemType := v.Type().Field(i).Type.Elem()
|
||||||
|
|
||||||
|
for index := 0; ; index++ {
|
||||||
|
allFieldsPresent := true
|
||||||
|
elemValue := reflect.New(elemType).Elem()
|
||||||
|
|
||||||
|
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
|
||||||
|
elemField := elemValue.Type().Field(j)
|
||||||
|
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
|
||||||
|
envValue, present := os.LookupEnv(envName)
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
allFieldsPresent = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
envVars = append(envVars, envName)
|
||||||
|
elemValue.Field(j).SetString(envValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allFieldsPresent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if sliceValue.Kind() != reflect.Slice {
|
||||||
|
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
|
||||||
|
}
|
||||||
|
sliceValue = reflect.Append(sliceValue, elemValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sliceValue.IsValid() {
|
||||||
|
v.Field(i).Set(sliceValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
envVars = append(envVars, tag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(envVars) > 0 {
|
if len(envVars) > 0 {
|
||||||
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
|
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("No environment variables config specified.")
|
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ type AdminSetting struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SettingDisableSignup = "disable-signup"
|
SettingDisableSignup = "disable-signup"
|
||||||
SettingRequireLogin = "require-login"
|
SettingRequireLogin = "require-login"
|
||||||
SettingDisableLoginForm = "disable-login-form"
|
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
|
||||||
SettingDisableGravatar = "disable-gravatar"
|
SettingDisableLoginForm = "disable-login-form"
|
||||||
|
SettingDisableGravatar = "disable-gravatar"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetSetting(key string) (string, error) {
|
func GetSetting(key string) (string, error) {
|
||||||
@@ -62,3 +63,21 @@ func initAdminSettings(settings map[string]string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
msqlite "github.com/glebarez/go-sqlite"
|
msqlite "github.com/glebarez/go-sqlite"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,7 @@ func Setup(dbPath string, sharedCache bool) error {
|
|||||||
var err error
|
var err error
|
||||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
||||||
|
|
||||||
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||||
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ func Setup(dbPath string, sharedCache bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
|
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,10 +52,11 @@ func Setup(dbPath string, sharedCache bool) error {
|
|||||||
|
|
||||||
// Default admin setting values
|
// Default admin setting values
|
||||||
return initAdminSettings(map[string]string{
|
return initAdminSettings(map[string]string{
|
||||||
SettingDisableSignup: "0",
|
SettingDisableSignup: "0",
|
||||||
SettingRequireLogin: "0",
|
SettingRequireLogin: "0",
|
||||||
SettingDisableLoginForm: "0",
|
SettingAllowGistsWithoutLogin: "0",
|
||||||
SettingDisableGravatar: "0",
|
SettingDisableLoginForm: "0",
|
||||||
|
SettingDisableGravatar: "0",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,3 +81,12 @@ func IsUniqueConstraintViolation(err error) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Ping() error {
|
||||||
|
sql, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.Ping()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,75 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
"fmt"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"github.com/thomiceli/opengist/internal/index"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Visibility int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PublicVisibility Visibility = iota
|
||||||
|
UnlistedVisibility
|
||||||
|
PrivateVisibility
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v Visibility) String() string {
|
||||||
|
switch v {
|
||||||
|
case PublicVisibility:
|
||||||
|
return "public"
|
||||||
|
case UnlistedVisibility:
|
||||||
|
return "unlisted"
|
||||||
|
case PrivateVisibility:
|
||||||
|
return "private"
|
||||||
|
default:
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Visibility) Next() Visibility {
|
||||||
|
switch v {
|
||||||
|
case PublicVisibility:
|
||||||
|
return UnlistedVisibility
|
||||||
|
case UnlistedVisibility:
|
||||||
|
return PrivateVisibility
|
||||||
|
default:
|
||||||
|
return PublicVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVisibility[T string | int](v T) (Visibility, error) {
|
||||||
|
switch s := fmt.Sprint(v); s {
|
||||||
|
case "0", "public":
|
||||||
|
return PublicVisibility, nil
|
||||||
|
case "1", "unlisted":
|
||||||
|
return UnlistedVisibility, nil
|
||||||
|
case "2", "private":
|
||||||
|
return PrivateVisibility, nil
|
||||||
|
default:
|
||||||
|
return -1, fmt.Errorf("unknown visibility %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Gist struct {
|
type Gist struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
Uuid string
|
Uuid string
|
||||||
Title string
|
Title string
|
||||||
|
URL string
|
||||||
Preview string
|
Preview string
|
||||||
PreviewFilename string
|
PreviewFilename string
|
||||||
Description string
|
Description string
|
||||||
Private int // 0: public, 1: unlisted, 2: private
|
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||||
UserID uint
|
UserID uint
|
||||||
User User
|
User User
|
||||||
NbFiles int
|
NbFiles int
|
||||||
@@ -48,7 +101,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
|||||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||||
gist := new(Gist)
|
gist := new(Gist)
|
||||||
err := db.Preload("User").Preload("Forked.User").
|
err := db.Preload("User").Preload("Forked.User").
|
||||||
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
|
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user).
|
||||||
Joins("join users on gists.user_id = users.id").
|
Joins("join users on gists.user_id = users.id").
|
||||||
First(&gist).Error
|
First(&gist).Error
|
||||||
|
|
||||||
@@ -177,6 +230,25 @@ func GetAllGistsRows() ([]*Gist, error) {
|
|||||||
return gists, err
|
return gists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
|
||||||
|
var gists []uint
|
||||||
|
|
||||||
|
err := db.Table("gists").
|
||||||
|
Where("gists.private = 0 or gists.user_id = ?", userId).
|
||||||
|
Pluck("gists.id", &gists).Error
|
||||||
|
|
||||||
|
return gists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
|
||||||
|
var gists []*Gist
|
||||||
|
err := db.Preload("User").Preload("Forked.User").
|
||||||
|
Where("id in ?", ids).
|
||||||
|
Find(&gists).Error
|
||||||
|
|
||||||
|
return gists, err
|
||||||
|
}
|
||||||
|
|
||||||
func (gist *Gist) Create() error {
|
func (gist *Gist) Create() error {
|
||||||
// avoids foreign key constraint error because the default value in the struct is 0
|
// avoids foreign key constraint error because the default value in the struct is 0
|
||||||
return db.Omit("forked_id").Create(&gist).Error
|
return db.Omit("forked_id").Create(&gist).Error
|
||||||
@@ -190,6 +262,10 @@ func (gist *Gist) Update() error {
|
|||||||
return db.Omit("forked_id").Save(&gist).Error
|
return db.Omit("forked_id").Save(&gist).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) UpdateNoTimestamps() error {
|
||||||
|
return db.Omit("forked_id", "updated_at").Save(&gist).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (gist *Gist) Delete() error {
|
func (gist *Gist) Delete() error {
|
||||||
err := gist.DeleteRepository()
|
err := gist.DeleteRepository()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,33 +342,29 @@ func (gist *Gist) InitRepository() error {
|
|||||||
return git.InitRepository(gist.User.Username, gist.Uuid)
|
return git.InitRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
|
|
||||||
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gist *Gist) DeleteRepository() error {
|
func (gist *Gist) DeleteRepository() error {
|
||||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) Files(revision string) ([]*git.File, error) {
|
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||||
var files []*git.File
|
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if the revision or the file do not exist
|
// if the revision or the file do not exist
|
||||||
|
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||||
return nil, nil
|
return nil, &git.RevisionNotFoundError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileStr := range filesStr {
|
var files []*git.File
|
||||||
file, err := gist.File(revision, fileStr, true)
|
for _, fileCat := range filesCat {
|
||||||
if err != nil {
|
files = append(files, &git.File{
|
||||||
return nil, err
|
Filename: fileCat.Name,
|
||||||
}
|
Size: fileCat.Size,
|
||||||
files = append(files, file)
|
HumanSize: humanize.IBytes(fileCat.Size),
|
||||||
|
Content: fileCat.Content,
|
||||||
|
Truncated: fileCat.Truncated,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
@@ -305,13 +377,26 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var size uint64
|
||||||
|
|
||||||
|
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &git.File{
|
return &git.File{
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
|
Size: size,
|
||||||
|
HumanSize: humanize.IBytes(size),
|
||||||
Content: content,
|
Content: content,
|
||||||
Truncated: truncated,
|
Truncated: truncated,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) FileNames(revision string) ([]string, error) {
|
||||||
|
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||||
|
}
|
||||||
|
|
||||||
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||||
}
|
}
|
||||||
@@ -321,7 +406,7 @@ func (gist *Gist) NbCommits() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
||||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email); err != nil {
|
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +427,26 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
|||||||
return git.Push(gist.Uuid)
|
return git.Push(gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) AddAndCommitFile(file *FileDTO) error {
|
||||||
|
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.AddAll(gist.Uuid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return git.Push(gist.Uuid)
|
||||||
|
}
|
||||||
|
|
||||||
func (gist *Gist) ForkClone(username string, uuid string) error {
|
func (gist *Gist) ForkClone(username string, uuid string) error {
|
||||||
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
||||||
}
|
}
|
||||||
@@ -354,7 +459,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
|
|||||||
return git.RPC(gist.User.Username, gist.Uuid, service)
|
return git.RPC(gist.User.Username, gist.Uuid, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) UpdatePreviewAndCount() error {
|
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
|
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -380,7 +485,54 @@ func (gist *Gist) UpdatePreviewAndCount() error {
|
|||||||
gist.PreviewFilename = file.Filename
|
gist.PreviewFilename = file.Filename
|
||||||
}
|
}
|
||||||
|
|
||||||
return gist.Update()
|
if withTimestampUpdate {
|
||||||
|
return gist.Update()
|
||||||
|
}
|
||||||
|
return gist.UpdateNoTimestamps()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) VisibilityStr() string {
|
||||||
|
switch gist.Private {
|
||||||
|
case PublicVisibility:
|
||||||
|
return "public"
|
||||||
|
case UnlistedVisibility:
|
||||||
|
return "unlisted"
|
||||||
|
case PrivateVisibility:
|
||||||
|
return "private"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) Identifier() string {
|
||||||
|
if gist.URL != "" {
|
||||||
|
return gist.URL
|
||||||
|
}
|
||||||
|
return gist.Uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
|
||||||
|
files, err := gist.Files("HEAD", true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
languages := make([]string, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
var lexer chroma.Lexer
|
||||||
|
if lexer = lexers.Get(file.Filename); lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
fileType := lexer.Config().Name
|
||||||
|
if lexer.Config().Name == "fallback" || lexer.Config().Name == "plaintext" {
|
||||||
|
fileType = "Text"
|
||||||
|
}
|
||||||
|
|
||||||
|
languages = append(languages, fileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return languages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- DTO -- //
|
// -- DTO -- //
|
||||||
@@ -388,10 +540,15 @@ func (gist *Gist) UpdatePreviewAndCount() error {
|
|||||||
type GistDTO struct {
|
type GistDTO struct {
|
||||||
Title string `validate:"max=250" form:"title"`
|
Title string `validate:"max=250" form:"title"`
|
||||||
Description string `validate:"max=1000" form:"description"`
|
Description string `validate:"max=1000" form:"description"`
|
||||||
Private int `validate:"number,min=0,max=2" form:"private"`
|
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||||
Files []FileDTO `validate:"min=1,dive"`
|
Files []FileDTO `validate:"min=1,dive"`
|
||||||
Name []string `form:"name"`
|
Name []string `form:"name"`
|
||||||
Content []string `form:"content"`
|
Content []string `form:"content"`
|
||||||
|
VisibilityDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type VisibilityDTO struct {
|
||||||
|
Private Visibility `validate:"number,min=0,max=2" form:"private"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileDTO struct {
|
type FileDTO struct {
|
||||||
@@ -404,11 +561,84 @@ func (dto *GistDTO) ToGist() *Gist {
|
|||||||
Title: dto.Title,
|
Title: dto.Title,
|
||||||
Description: dto.Description,
|
Description: dto.Description,
|
||||||
Private: dto.Private,
|
Private: dto.Private,
|
||||||
|
URL: dto.URL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||||
gist.Title = dto.Title
|
gist.Title = dto.Title
|
||||||
gist.Description = dto.Description
|
gist.Description = dto.Description
|
||||||
|
gist.URL = dto.URL
|
||||||
return gist
|
return gist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Index -- //
|
||||||
|
|
||||||
|
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||||
|
files, err := gist.Files("HEAD", true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exts := make([]string, 0, len(files))
|
||||||
|
wholeContent := ""
|
||||||
|
for _, file := range files {
|
||||||
|
wholeContent += file.Content
|
||||||
|
exts = append(exts, filepath.Ext(file.Filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNames, err := gist.FileNames("HEAD")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
langs, err := gist.GetLanguagesFromFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexedGist := &index.Gist{
|
||||||
|
GistID: gist.ID,
|
||||||
|
Username: gist.User.Username,
|
||||||
|
Title: gist.Title,
|
||||||
|
Content: wholeContent,
|
||||||
|
Filenames: fileNames,
|
||||||
|
Extensions: exts,
|
||||||
|
Languages: langs,
|
||||||
|
CreatedAt: gist.CreatedAt,
|
||||||
|
UpdatedAt: gist.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexedGist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) AddInIndex() {
|
||||||
|
if !index.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
indexedGist, err := gist.ToIndexedGist()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = index.AddInIndex(indexedGist)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error adding gist %d to index", gist.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) RemoveFromIndex() {
|
||||||
|
if !index.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := index.RemoveFromIndex(gist.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error remove gist %d from index", gist.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
87
internal/db/invitation.go
Normal file
87
internal/db/invitation.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ type SSHKey struct {
|
|||||||
User User `validate:"-" `
|
User User `validate:"-" `
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
|
func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
|
||||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
|
|||||||
return sshKey, err
|
return sshKey, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
|
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
|
||||||
sshKey := new(SSHKey)
|
var count int64
|
||||||
err := db.
|
err := db.Model(&SSHKey{}).
|
||||||
Where("content like ?", sshKeyContent+"%").
|
Where("content = ?", sshKeyContent).
|
||||||
First(&sshKey).Error
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
return sshKey, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sshKey *SSHKey) Create() error {
|
func (sshKey *SSHKey) Create() error {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type User struct {
|
|||||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||||
AvatarURL string
|
AvatarURL string
|
||||||
GithubID string
|
GithubID string
|
||||||
|
GitlabID string
|
||||||
GiteaID string
|
GiteaID string
|
||||||
OIDCID string `gorm:"column:oidc_id"`
|
OIDCID string `gorm:"column:oidc_id"`
|
||||||
|
|
||||||
@@ -52,6 +53,11 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = tx.Where("user_id = ?", user.ID).Delete(&SSHKey{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Delete all gists created by this user
|
// Delete all gists created by this user
|
||||||
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
|
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
|
||||||
}
|
}
|
||||||
@@ -100,7 +106,6 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
|||||||
err := db.
|
err := db.
|
||||||
Where("email IN ?", emails).
|
Where("email IN ?", emails).
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -113,6 +118,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
|||||||
return userMap, nil
|
return userMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserFromSSHKey(sshKey string) (*User, error) {
|
||||||
|
user := new(User)
|
||||||
|
err := db.
|
||||||
|
Joins("JOIN ssh_keys ON users.id = ssh_keys.user_id").
|
||||||
|
Where("ssh_keys.content = ?", sshKey).
|
||||||
|
First(&user).Error
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
|
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
|
||||||
key := new(SSHKey)
|
key := new(SSHKey)
|
||||||
err := db.
|
err := db.
|
||||||
@@ -129,6 +143,8 @@ func GetUserByProvider(id string, provider string) (*User, error) {
|
|||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case "github":
|
||||||
err = db.Where("github_id = ?", id).First(&user).Error
|
err = db.Where("github_id = ?", id).First(&user).Error
|
||||||
|
case "gitlab":
|
||||||
|
err = db.Where("gitlab_id = ?", id).First(&user).Error
|
||||||
case "gitea":
|
case "gitea":
|
||||||
err = db.Where("gitea_id = ?", id).First(&user).Error
|
err = db.Where("gitea_id = ?", id).First(&user).Error
|
||||||
case "openid-connect":
|
case "openid-connect":
|
||||||
@@ -167,20 +183,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) DeleteProviderID(provider string) error {
|
func (user *User) DeleteProviderID(provider string) error {
|
||||||
switch provider {
|
providerIDFields := map[string]string{
|
||||||
case "github":
|
"github": "github_id",
|
||||||
|
"gitlab": "gitlab_id",
|
||||||
|
"gitea": "gitea_id",
|
||||||
|
"openid-connect": "oidc_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
if providerIDField, ok := providerIDFields[provider]; ok {
|
||||||
return db.Model(&user).
|
return db.Model(&user).
|
||||||
Update("github_id", nil).
|
Update(providerIDField, nil).
|
||||||
Update("avatar_url", nil).
|
|
||||||
Error
|
|
||||||
case "gitea":
|
|
||||||
return db.Model(&user).
|
|
||||||
Update("gitea_id", nil).
|
|
||||||
Update("avatar_url", nil).
|
|
||||||
Error
|
|
||||||
case "openid-connect":
|
|
||||||
return db.Model(&user).
|
|
||||||
Update("oidc_id", nil).
|
|
||||||
Update("avatar_url", nil).
|
Update("avatar_url", nil).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
@@ -191,7 +203,7 @@ func (user *User) DeleteProviderID(provider string) error {
|
|||||||
// -- DTO -- //
|
// -- DTO -- //
|
||||||
|
|
||||||
type UserDTO struct {
|
type UserDTO struct {
|
||||||
Username string `form:"username" validate:"required,max=24,alphanum,notreserved"`
|
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
|
||||||
Password string `form:"password" validate:"required"`
|
Password string `form:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"io"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,6 +24,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const truncateLimit = 2 << 18
|
const truncateLimit = 2 << 18
|
||||||
|
const diffSize = 2 << 12
|
||||||
|
const maxFilesPerDiffCommit = 10
|
||||||
|
|
||||||
|
type RevisionNotFoundError struct{}
|
||||||
|
|
||||||
|
func (m *RevisionNotFoundError) Error() string {
|
||||||
|
return "revision not found"
|
||||||
|
}
|
||||||
|
|
||||||
func RepositoryPath(user string, gist string) string {
|
func RepositoryPath(user string, gist string) string {
|
||||||
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
|
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
|
||||||
@@ -53,28 +66,20 @@ func TmpRepositoriesPath() string {
|
|||||||
func InitRepository(user string, gist string) error {
|
func InitRepository(user string, gist string) error {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
cmd := exec.Command(
|
var args []string
|
||||||
"git",
|
args = append(args, "init")
|
||||||
"init",
|
if config.C.GitDefaultBranch != "" {
|
||||||
"--bare",
|
args = append(args, "--initial-branch", config.C.GitDefaultBranch)
|
||||||
repositoryPath,
|
}
|
||||||
)
|
args = append(args, "--bare", repositoryPath)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return createDotGitFiles(repositoryPath)
|
return CreateDotGitFiles(user, gist)
|
||||||
}
|
|
||||||
|
|
||||||
func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
|
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
|
||||||
|
|
||||||
if err := InitRepository(user, gist); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
repositoryUrl := RepositoryUrl(ctx, user, gist)
|
|
||||||
return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountCommits(user string, gist string) (string, error) {
|
func CountCommits(user string, gist string) (string, error) {
|
||||||
@@ -113,6 +118,120 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
|
|||||||
return slice[:len(slice)-1], nil
|
return slice[:len(slice)-1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type catFileBatch struct {
|
||||||
|
Name, Hash, Content string
|
||||||
|
Size uint64
|
||||||
|
Truncated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
|
||||||
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
|
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
|
||||||
|
lsTreeCmd.Dir = repositoryPath
|
||||||
|
lsTreeOutput, err := lsTreeCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMap := make([]*catFileBatch, 0)
|
||||||
|
|
||||||
|
lines := strings.Split(string(lsTreeOutput), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
continue // Skip lines that don't have enough fields
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := fields[2]
|
||||||
|
size, err := strconv.ParseUint(fields[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue // Skip lines with invalid size field
|
||||||
|
}
|
||||||
|
name := strings.Join(fields[4:], " ") // File name may contain spaces
|
||||||
|
|
||||||
|
fileMap = append(fileMap, &catFileBatch{
|
||||||
|
Hash: hash,
|
||||||
|
Size: size,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
catFileCmd := exec.Command("git", "cat-file", "--batch")
|
||||||
|
catFileCmd.Dir = repositoryPath
|
||||||
|
stdin, err := catFileCmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stdout, err := catFileCmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = catFileCmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(stdout)
|
||||||
|
|
||||||
|
for _, file := range fileMap {
|
||||||
|
_, err = stdin.Write([]byte(file.Hash + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(header)
|
||||||
|
if len(parts) > 3 {
|
||||||
|
continue // Not a valid header, skip this entry
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := strconv.ParseUint(parts[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeToRead := size
|
||||||
|
if truncate && sizeToRead > truncateLimit {
|
||||||
|
sizeToRead = truncateLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read exactly size bytes from header, or the max allowed if truncated
|
||||||
|
content := make([]byte, sizeToRead)
|
||||||
|
if _, err = io.ReadFull(reader, content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Content = string(content)
|
||||||
|
|
||||||
|
if truncate && size > truncateLimit {
|
||||||
|
// skip other bytes if truncated
|
||||||
|
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
file.Truncated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the blank line following the content
|
||||||
|
if _, err := reader.ReadByte(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = stdin.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = catFileCmd.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
|
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
@@ -121,7 +240,12 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
|||||||
maxBytes = truncateLimit
|
maxBytes = truncateLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(
|
// Set up a context with a timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(
|
||||||
|
ctx,
|
||||||
"git",
|
"git",
|
||||||
"--no-pager",
|
"--no-pager",
|
||||||
"show",
|
"show",
|
||||||
@@ -129,22 +253,36 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
|||||||
)
|
)
|
||||||
cmd.Dir = repositoryPath
|
cmd.Dir = repositoryPath
|
||||||
|
|
||||||
stdout, _ := cmd.StdoutPipe()
|
output, err := cmd.Output()
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
output, truncated, err := truncateCommandOutput(stdout, maxBytes)
|
content, truncated, err := truncateCommandOutput(bytes.NewReader(output), maxBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
return content, truncated, nil
|
||||||
return "", false, err
|
}
|
||||||
|
|
||||||
|
func GetFileSize(user string, gist string, revision string, filename string) (uint64, error) {
|
||||||
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
|
cmd := exec.Command(
|
||||||
|
"git",
|
||||||
|
"cat-file",
|
||||||
|
"-s",
|
||||||
|
revision+":"+filename,
|
||||||
|
)
|
||||||
|
cmd.Dir = repositoryPath
|
||||||
|
|
||||||
|
stdout, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, truncated, nil
|
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||||
@@ -177,10 +315,10 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
|||||||
}
|
}
|
||||||
}(cmd)
|
}(cmd)
|
||||||
|
|
||||||
return parseLog(stdout, truncateLimit), err
|
return parseLog(stdout, maxFilesPerDiffCommit, diffSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
tmpPath := TmpRepositoriesPath()
|
tmpPath := TmpRepositoriesPath()
|
||||||
@@ -198,11 +336,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove every file (and not the .git directory!)
|
// remove every file (keep the .git directory)
|
||||||
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
|
// useful when user wants to edit multiple files from an existing gist
|
||||||
return err
|
if remove {
|
||||||
|
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = exec.Command("git", "config", "--local", "user.name", user)
|
cmd = exec.Command("git", "config", "--local", "user.name", user)
|
||||||
cmd.Dir = tmpRepositoryPath
|
cmd.Dir = tmpRepositoryPath
|
||||||
if err = cmd.Run(); err != nil {
|
if err = cmd.Run(); err != nil {
|
||||||
@@ -223,7 +363,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return createDotGitFiles(repositoryPathDst)
|
return CreateDotGitFiles(userDst, gistDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFileContent(gistTmpId string, filename string, content string) error {
|
func SetFileContent(gistTmpId string, filename string, content string) error {
|
||||||
@@ -276,7 +416,6 @@ func Push(gistTmpId string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.RemoveAll(tmpRepositoryPath)
|
return os.RemoveAll(tmpRepositoryPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,22 +516,28 @@ func GetGitVersion() (string, error) {
|
|||||||
return versionFields[2], nil
|
return versionFields[2], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDotGitFiles(repositoryPath string) error {
|
func CreateDotGitFiles(user string, gist string) error {
|
||||||
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
|
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f1.Close()
|
defer f1.Close()
|
||||||
|
|
||||||
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil {
|
if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
|
||||||
return err
|
for _, hook := range []string{"pre-receive", "post-receive"} {
|
||||||
|
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
|
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
|
||||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
|
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -420,51 +565,6 @@ func removeFilesExceptGit(dir string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const preReceive = `#!/bin/sh
|
const hookTemplate = `#!/bin/sh
|
||||||
|
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,42 +1,18 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setup(t *testing.T) {
|
|
||||||
err := config.InitConfig("")
|
|
||||||
require.NoError(t, err, "Could not init config")
|
|
||||||
|
|
||||||
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
|
|
||||||
ReposDirectory = path.Join("tests")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = InitRepository("thomas", "gist1")
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func teardown(t *testing.T) {
|
|
||||||
err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
|
|
||||||
require.NoError(t, err, "Could not remove repos directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitDeleteRepository(t *testing.T) {
|
func TestInitDeleteRepository(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
|
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
|
||||||
cmd.Dir = RepositoryPath("thomas", "gist1")
|
cmd.Dir = RepositoryPath("thomas", "gist1")
|
||||||
@@ -44,9 +20,6 @@ func TestInitDeleteRepository(t *testing.T) {
|
|||||||
require.NoError(t, err, "Could not run git command")
|
require.NoError(t, err, "Could not run git command")
|
||||||
require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare")
|
require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare")
|
||||||
|
|
||||||
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "hooks", "pre-receive"))
|
|
||||||
require.NoError(t, err, "pre-receive hook not found")
|
|
||||||
|
|
||||||
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok"))
|
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok"))
|
||||||
require.NoError(t, err, "git-daemon-export-ok file not found")
|
require.NoError(t, err, "git-daemon-export-ok file not found")
|
||||||
|
|
||||||
@@ -56,14 +29,14 @@ func TestInitDeleteRepository(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommits(t *testing.T) {
|
func TestCommits(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
hasNoCommits, err := HasNoCommits("thomas", "gist1")
|
hasNoCommits, err := HasNoCommits("thomas", "gist1")
|
||||||
require.NoError(t, err, "Could not check if repository has no commits")
|
require.NoError(t, err, "Could not check if repository has no commits")
|
||||||
require.True(t, hasNoCommits, "Repository should have no commits")
|
require.True(t, hasNoCommits, "Repository should have no commits")
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", nil)
|
CommitToBare(t, "thomas", "gist1", nil)
|
||||||
|
|
||||||
hasNoCommits, err = HasNoCommits("thomas", "gist1")
|
hasNoCommits, err = HasNoCommits("thomas", "gist1")
|
||||||
require.NoError(t, err, "Could not check if repository has no commits")
|
require.NoError(t, err, "Could not check if repository has no commits")
|
||||||
@@ -73,17 +46,17 @@ func TestCommits(t *testing.T) {
|
|||||||
require.NoError(t, err, "Could not count commits")
|
require.NoError(t, err, "Could not count commits")
|
||||||
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
|
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", nil)
|
CommitToBare(t, "thomas", "gist1", nil)
|
||||||
nbCommits, err = CountCommits("thomas", "gist1")
|
nbCommits, err = CountCommits("thomas", "gist1")
|
||||||
require.NoError(t, err, "Could not count commits")
|
require.NoError(t, err, "Could not count commits")
|
||||||
require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
|
require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContent(t *testing.T) {
|
func TestContent(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_file.txt": "I love Opengist\n",
|
"my_file.txt": "I love Opengist\n",
|
||||||
"my_other_file.txt": `I really
|
"my_other_file.txt": `I really
|
||||||
hate Opengist`,
|
hate Opengist`,
|
||||||
@@ -104,7 +77,7 @@ hate Opengist`,
|
|||||||
require.False(t, truncated, "Content should not be truncated")
|
require.False(t, truncated, "Content should not be truncated")
|
||||||
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
|
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_renamed_file.txt": "I love Opengist\n",
|
"my_renamed_file.txt": "I love Opengist\n",
|
||||||
"my_other_file.txt": `I really
|
"my_other_file.txt": `I really
|
||||||
like Opengist actually`,
|
like Opengist actually`,
|
||||||
@@ -152,7 +125,7 @@ like Opengist actually`,
|
|||||||
|
|
||||||
require.Contains(t, commits[0].Files, File{
|
require.Contains(t, commits[0].Files, File{
|
||||||
Filename: "my_other_file.txt",
|
Filename: "my_other_file.txt",
|
||||||
OldFilename: "",
|
OldFilename: "my_other_file.txt",
|
||||||
Content: `@@ -1,2 +1,2 @@
|
Content: `@@ -1,2 +1,2 @@
|
||||||
I really
|
I really
|
||||||
-hate Opengist
|
-hate Opengist
|
||||||
@@ -183,18 +156,18 @@ like Opengist actually`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGitGc(t *testing.T) {
|
func TestGitGc(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
err := GcRepos()
|
err := GcRepos()
|
||||||
require.NoError(t, err, "Could not run git gc")
|
require.NoError(t, err, "Could not run git gc")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFork(t *testing.T) {
|
func TestFork(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_file.txt": "I love Opengist\n",
|
"my_file.txt": "I love Opengist\n",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -211,10 +184,10 @@ func TestFork(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTruncate(t *testing.T) {
|
func TestTruncate(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_file.txt": "A",
|
"my_file.txt": "A",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -228,7 +201,7 @@ func TestTruncate(t *testing.T) {
|
|||||||
builder.WriteString("A")
|
builder.WriteString("A")
|
||||||
}
|
}
|
||||||
str := builder.String()
|
str := builder.String()
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_file.txt": str,
|
"my_file.txt": str,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -237,7 +210,7 @@ func TestTruncate(t *testing.T) {
|
|||||||
require.True(t, truncated, "Content should be truncated")
|
require.True(t, truncated, "Content should be truncated")
|
||||||
require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit")
|
require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit")
|
||||||
|
|
||||||
commitToBare(t, "thomas", "gist1", map[string]string{
|
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||||
"my_file.txt": "AA\n" + str,
|
"my_file.txt": "AA\n" + str,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,52 +220,23 @@ func TestTruncate(t *testing.T) {
|
|||||||
require.Equal(t, 2, len(content), "Content size is not correct")
|
require.Equal(t, 2, len(content), "Content size is not correct")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitViaGitInit(t *testing.T) {
|
func TestGitInitBranchNames(t *testing.T) {
|
||||||
setup(t)
|
SetupTest(t)
|
||||||
defer teardown(t)
|
defer TeardownTest(t)
|
||||||
|
|
||||||
e := echo.New()
|
cmd := exec.Command("git", "symbolic-ref", "HEAD")
|
||||||
|
cmd.Dir = RepositoryPath("thomas", "gist1")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
require.NoError(t, err, "Could not run git command")
|
||||||
|
require.Equal(t, "refs/heads/master", strings.TrimSpace(string(out)), "Repository should have master branch as default")
|
||||||
|
|
||||||
// Create a mock HTTP request
|
config.C.GitDefaultBranch = "main"
|
||||||
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)
|
|
||||||
|
|
||||||
|
err = InitRepository("thomas", "gist2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
cmd = exec.Command("git", "symbolic-ref", "HEAD")
|
||||||
|
cmd.Dir = RepositoryPath("thomas", "gist2")
|
||||||
func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
|
out, err = cmd.Output()
|
||||||
err := CloneTmp(user, gist, gist, "thomas@mail.com")
|
require.NoError(t, err, "Could not run git command")
|
||||||
require.NoError(t, err, "Could not commit to repository")
|
require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default")
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Filename string
|
Filename string `json:"filename"`
|
||||||
OldFilename string
|
Size uint64 `json:"size"`
|
||||||
Content string
|
HumanSize string `json:"human_size"`
|
||||||
Truncated bool
|
OldFilename string `json:"-"`
|
||||||
IsCreated bool
|
Content string `json:"content"`
|
||||||
IsDeleted bool
|
Truncated bool `json:"truncated"`
|
||||||
|
IsCreated bool `json:"-"`
|
||||||
|
IsDeleted bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CsvFile struct {
|
type CsvFile struct {
|
||||||
@@ -61,124 +62,287 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
|
|||||||
return string(buf), truncated, nil
|
return string(buf), truncated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLog(out io.Reader, maxBytes int) []*Commit {
|
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
|
||||||
scanner := bufio.NewScanner(out)
|
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
|
||||||
|
|
||||||
var commits []*Commit
|
var commits []*Commit
|
||||||
var currentCommit *Commit
|
var currentCommit *Commit
|
||||||
var currentFile *File
|
var currentFile *File
|
||||||
var isContent bool
|
var headerParsed = false
|
||||||
var bytesRead = 0
|
var skipped = false
|
||||||
scanNext := true
|
var line string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
input := bufio.NewReaderSize(out, maxBytes)
|
||||||
|
|
||||||
|
// Loop Commits
|
||||||
|
loopLog:
|
||||||
|
for {
|
||||||
|
// If a commit was skipped, do not read a new line
|
||||||
|
if !skipped {
|
||||||
|
line, err = input.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break loopLog
|
||||||
|
}
|
||||||
|
return commits, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing newline characters
|
||||||
|
if len(line) > 0 && (line[len(line)-1] == '\n' || line[len(line)-1] == '\r') {
|
||||||
|
line = line[:len(line)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse commit header (hash, author, mail, timestamp) or a diff
|
||||||
|
switch line[0] {
|
||||||
|
// Commit hash
|
||||||
|
case 'c':
|
||||||
|
if headerParsed {
|
||||||
|
commits = append(commits, currentCommit)
|
||||||
|
}
|
||||||
|
skipped = false
|
||||||
|
currentCommit = &Commit{Hash: line[2:], Files: []File{}}
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Author name
|
||||||
|
case 'a':
|
||||||
|
headerParsed = true
|
||||||
|
currentCommit.AuthorName = line[2:]
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Author email
|
||||||
|
case 'm':
|
||||||
|
currentCommit.AuthorEmail = line[2:]
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Commit timestamp
|
||||||
|
case 't':
|
||||||
|
currentCommit.Timestamp = line[2:]
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Commit shortstat
|
||||||
|
case ' ':
|
||||||
|
changed := []byte(line)[1:]
|
||||||
|
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
||||||
|
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
||||||
|
currentCommit.Changed = string(changed)
|
||||||
|
|
||||||
|
// shortstat is followed by an empty line
|
||||||
|
line, err = input.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break loopLog
|
||||||
|
}
|
||||||
|
return commits, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Commit diff
|
||||||
|
default:
|
||||||
|
// Loop files in diff
|
||||||
|
loopCommit:
|
||||||
|
for {
|
||||||
|
// If we have reached the maximum number of files to show for a single commit, skip to the next commit
|
||||||
|
if len(currentCommit.Files) >= maxFiles {
|
||||||
|
line, err = skipToNextCommit(input)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break loopLog
|
||||||
|
}
|
||||||
|
return commits, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to the next commit
|
||||||
|
headerParsed = false
|
||||||
|
skipped = true
|
||||||
|
break loopCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else create a new file and parse it
|
||||||
|
currentFile = &File{}
|
||||||
|
parseRename := true
|
||||||
|
|
||||||
|
loopFileDiff:
|
||||||
|
for {
|
||||||
|
line, err = input.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
return commits, err
|
||||||
|
}
|
||||||
|
headerParsed = false
|
||||||
|
break loopCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the line is a newline character, the commit is finished
|
||||||
|
if line == "\n" {
|
||||||
|
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||||
|
headerParsed = false
|
||||||
|
break loopCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse the file header
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "diff --git"):
|
||||||
|
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||||
|
headerParsed = false
|
||||||
|
break loopFileDiff
|
||||||
|
case strings.HasPrefix(line, "old mode"):
|
||||||
|
case strings.HasPrefix(line, "new mode"):
|
||||||
|
case strings.HasPrefix(line, "index"):
|
||||||
|
case strings.HasPrefix(line, "similarity index"):
|
||||||
|
case strings.HasPrefix(line, "dissimilarity index"):
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(line, "rename from "):
|
||||||
|
currentFile.OldFilename = 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 {
|
for {
|
||||||
if scanNext && !scanner.Scan() {
|
for isFragment {
|
||||||
break
|
currentFile.Truncated = true
|
||||||
|
|
||||||
|
// Read the next line
|
||||||
|
_, isFragment, err = input.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scanNext = true
|
|
||||||
|
|
||||||
// new commit found
|
sb.Reset()
|
||||||
currentFile = nil
|
|
||||||
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}}
|
|
||||||
|
|
||||||
scanner.Scan()
|
// Read the next line
|
||||||
currentCommit.AuthorName = string(scanner.Bytes()[2:])
|
lineBytes, isFragment, err = input.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return lineBytes, isFragment, err
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
scanner.Scan()
|
// End of file
|
||||||
currentCommit.AuthorEmail = string(scanner.Bytes()[2:])
|
if len(lineBytes) == 0 {
|
||||||
|
return lineBytes, false, err
|
||||||
|
}
|
||||||
|
if lineBytes[0] == 'd' {
|
||||||
|
return lineBytes, false, err
|
||||||
|
}
|
||||||
|
|
||||||
scanner.Scan()
|
if currFileLineCount >= maxBytes {
|
||||||
currentCommit.Timestamp = string(scanner.Bytes()[2:])
|
currentFile.Truncated = true
|
||||||
|
|
||||||
scanner.Scan()
|
|
||||||
|
|
||||||
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one
|
|
||||||
if scanner.Bytes()[0] != ' ' {
|
|
||||||
commits = append(commits, currentCommit)
|
|
||||||
|
|
||||||
// avoid scanning the next line, as we already did it
|
|
||||||
scanNext = false
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := scanner.Bytes()[1:]
|
line := string(lineBytes)
|
||||||
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
if isFragment {
|
||||||
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
currentFile.Truncated = true
|
||||||
currentCommit.Changed = string(changed)
|
for isFragment {
|
||||||
|
lineBytes, isFragment, err = input.ReadLine()
|
||||||
// twice because --shortstat adds a new line
|
if err != nil {
|
||||||
scanner.Scan()
|
return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err)
|
||||||
scanner.Scan()
|
|
||||||
// commit header parsed
|
|
||||||
|
|
||||||
// files changes inside the commit
|
|
||||||
for {
|
|
||||||
line := scanner.Bytes()
|
|
||||||
|
|
||||||
// end of content of file
|
|
||||||
if len(line) == 0 {
|
|
||||||
isContent = false
|
|
||||||
if currentFile != nil {
|
|
||||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// new file found
|
|
||||||
if bytes.HasPrefix(line, []byte("diff --git")) {
|
|
||||||
// current file is finished, we can add it to the commit
|
|
||||||
if currentFile != nil {
|
|
||||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new file
|
|
||||||
isContent = false
|
|
||||||
bytesRead = 0
|
|
||||||
currentFile = &File{}
|
|
||||||
filenameRegex := regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
|
|
||||||
matches := filenameRegex.FindStringSubmatch(string(line))
|
|
||||||
if len(matches) == 3 {
|
|
||||||
currentFile.Filename = matches[2]
|
|
||||||
if matches[1] != matches[2] {
|
|
||||||
currentFile.OldFilename = matches[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scanner.Scan()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.HasPrefix(line, []byte("new")) {
|
|
||||||
currentFile.IsCreated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.HasPrefix(line, []byte("deleted")) {
|
|
||||||
currentFile.IsDeleted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// file content found
|
|
||||||
if line[0] == '@' {
|
|
||||||
isContent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if isContent {
|
|
||||||
currentFile.Content += string(line) + "\n"
|
|
||||||
|
|
||||||
bytesRead += len(line)
|
|
||||||
if bytesRead > maxBytes {
|
|
||||||
currentFile.Truncated = true
|
|
||||||
currentFile.Content = ""
|
|
||||||
isContent = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner.Scan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commits = append(commits, currentCommit)
|
if len(line) > maxBytes {
|
||||||
|
currentFile.Truncated = true
|
||||||
|
line = line[:maxBytes]
|
||||||
|
}
|
||||||
|
currentFile.Content += line + "\n"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return commits
|
func skipToNextCommit(input *bufio.Reader) (line string, err error) {
|
||||||
|
// need to skip until the next cmdDiffHead
|
||||||
|
var isFragment, wasFragment bool
|
||||||
|
var lineBytes []byte
|
||||||
|
for {
|
||||||
|
lineBytes, isFragment, err = input.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if wasFragment {
|
||||||
|
wasFragment = isFragment
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(lineBytes, []byte("c")) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
wasFragment = isFragment
|
||||||
|
}
|
||||||
|
line = string(lineBytes)
|
||||||
|
if isFragment {
|
||||||
|
var tail string
|
||||||
|
tail, err = input.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
line += tail
|
||||||
|
}
|
||||||
|
return line, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCsv(file *File) (*CsvFile, error) {
|
func ParseCsv(file *File) (*CsvFile, error) {
|
||||||
|
|||||||
71
internal/git/test_funcs.go
Normal file
71
internal/git/test_funcs.go
Normal 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
24
internal/hooks/hook.go
Normal 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
|
||||||
|
}
|
||||||
107
internal/hooks/post_receive.go
Normal file
107
internal/hooks/post_receive.go
Normal 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()
|
||||||
|
}
|
||||||
78
internal/hooks/pre_receive.go
Normal file
78
internal/hooks/pre_receive.go
Normal 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
|
||||||
|
}
|
||||||
54
internal/hooks/pre_receive_test.go
Normal file
54
internal/hooks/pre_receive_test.go
Normal 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
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var title = cases.Title(language.English)
|
|
||||||
var Locales = NewLocaleStore()
|
var Locales = NewLocaleStore()
|
||||||
|
|
||||||
type LocaleStore struct {
|
type LocaleStore struct {
|
||||||
@@ -53,11 +52,13 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
|
|||||||
name := display.Self.Name(tag)
|
name := display.Self.Name(tag)
|
||||||
if tag == language.AmericanEnglish {
|
if tag == language.AmericanEnglish {
|
||||||
name = "English"
|
name = "English"
|
||||||
|
} else if tag == language.EuropeanSpanish {
|
||||||
|
name = "Español"
|
||||||
}
|
}
|
||||||
|
|
||||||
locale := &Locale{
|
locale := &Locale{
|
||||||
Code: localeCode,
|
Code: localeCode,
|
||||||
Name: title.String(name),
|
Name: cases.Title(language.English).String(name),
|
||||||
Messages: make(map[string]string),
|
Messages: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +111,20 @@ func (store *LocaleStore) MatchTag(langs []language.Tag) string {
|
|||||||
return "en-US"
|
return "en-US"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Locale) String(key string, args ...any) string {
|
||||||
|
message := l.Messages[key]
|
||||||
|
|
||||||
|
if message == "" {
|
||||||
|
return Locales.Locales["en-US"].String(key, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Locale) Tr(key string, args ...any) template.HTML {
|
func (l *Locale) Tr(key string, args ...any) template.HTML {
|
||||||
message := l.Messages[key]
|
message := l.Messages[key]
|
||||||
|
|
||||||
|
|||||||
261
internal/i18n/locales/cs-CZ.yml
Normal file
261
internal/i18n/locales/cs-CZ.yml
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
gist.public: Veřejný
|
||||||
|
gist.unlisted: Neveřejný
|
||||||
|
gist.private: Privátní
|
||||||
|
|
||||||
|
gist.header.like: To se mi líbí
|
||||||
|
gist.header.unlike: Už se mi nelíbí
|
||||||
|
gist.header.fork: Fork
|
||||||
|
gist.header.edit: Upravit
|
||||||
|
gist.header.delete: Smazat
|
||||||
|
gist.header.forked-from: Forkováno z
|
||||||
|
gist.header.last-active: Naposledy aktivní
|
||||||
|
gist.header.select-tab: Vyberte záložku
|
||||||
|
gist.header.code: Kód
|
||||||
|
gist.header.revisions: Revize
|
||||||
|
gist.header.revision: Revize
|
||||||
|
gist.header.clone-http: Klonovat pomocí %s
|
||||||
|
gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP.
|
||||||
|
gist.header.clone-ssh: Klonovat pomocí SSH
|
||||||
|
gist.header.clone-ssh-help: Klonovat s pomocí Git pomocí klíče SSH.
|
||||||
|
gist.header.embed: ''
|
||||||
|
gist.header.embed-help: ''
|
||||||
|
gist.header.download-zip: Stáhnout ZIP
|
||||||
|
|
||||||
|
gist.raw: Raw
|
||||||
|
gist.file-truncated: Tento soubor byl zkrácen.
|
||||||
|
gist.watch-full-file: Zobrazit celý soubor.
|
||||||
|
gist.file-not-valid: Tento soubor není validní CSV.
|
||||||
|
gist.no-content: Žádný obsah
|
||||||
|
|
||||||
|
gist.new.new_gist: Nový gist
|
||||||
|
gist.new.title: Titulek
|
||||||
|
gist.new.description: Popis
|
||||||
|
gist.new.filename-with-extension: Název s příponou
|
||||||
|
gist.new.indent-mode: Režim odsazení
|
||||||
|
gist.new.indent-mode-space: Mezery
|
||||||
|
gist.new.indent-mode-tab: Tabulátory
|
||||||
|
gist.new.indent-size: Velikost odsazení
|
||||||
|
gist.new.wrap-mode: Režim zalamování
|
||||||
|
gist.new.wrap-mode-no: Bez zalamování
|
||||||
|
gist.new.wrap-mode-soft: Měkké zalamování
|
||||||
|
gist.new.add-file: Přidat soubor
|
||||||
|
gist.new.create-public-button: Vytvořit veřejný gist
|
||||||
|
gist.new.create-unlisted-button: Vytvořit neveřejný gist
|
||||||
|
gist.new.create-private-button: Vytvořit soukromý gist
|
||||||
|
|
||||||
|
gist.edit.editing: Úprava
|
||||||
|
gist.edit.change-visibility: Změnit viditelnost
|
||||||
|
gist.edit.delete: Smazat
|
||||||
|
gist.edit.cancel: Zrušit
|
||||||
|
gist.edit.save: Uložit
|
||||||
|
|
||||||
|
gist.list.joined: Připojeno
|
||||||
|
gist.list.all: Všechny gisty
|
||||||
|
gist.list.search-results: Výsledky hledání
|
||||||
|
gist.list.sort: Seřadit
|
||||||
|
gist.list.sort-by-created: Vytvořeno
|
||||||
|
gist.list.sort-by-updated: Aktualizováno
|
||||||
|
gist.list.order-by-asc: Nejméně nedávno
|
||||||
|
gist.list.order-by-desc: Nedávno
|
||||||
|
gist.list.select-tab: Vyberte záložku
|
||||||
|
gist.list.liked: Líbí se
|
||||||
|
gist.list.likes: Lajky
|
||||||
|
gist.list.forked: Forkováno
|
||||||
|
gist.list.forked-from: Forkováno z
|
||||||
|
gist.list.forks: Forky
|
||||||
|
gist.list.files: Soubory
|
||||||
|
gist.list.last-active: Naposledy aktivní
|
||||||
|
gist.list.no-gists: Žádné gisty
|
||||||
|
|
||||||
|
gist.forks: Forky
|
||||||
|
gist.forks.view: Zobrazit forky
|
||||||
|
gist.forks.no: Žádné veřejné forky
|
||||||
|
|
||||||
|
|
||||||
|
gist.likes: Lajky
|
||||||
|
gist.likes.no: Zatím žádné lajky
|
||||||
|
|
||||||
|
gist.revisions: Revize
|
||||||
|
gist.revision.revised: revidoval tento gist
|
||||||
|
gist.revision.go-to-revision: Přejít na revizi
|
||||||
|
gist.revision.file-created: vytvořil soubor
|
||||||
|
gist.revision.file-deleted: smazal soubor
|
||||||
|
gist.revision.file-renamed: přejmenováno na
|
||||||
|
gist.revision.diff-truncated: Diff je příliš velký na zobrazení
|
||||||
|
gist.revision.file-renamed-no-changes: Soubor přejmenován beze změn
|
||||||
|
gist.revision.empty-file: Prázdný soubor
|
||||||
|
gist.revision.no-changes: Žádné změny
|
||||||
|
gist.revision.no-revisions: Žádné revize k zobrazení
|
||||||
|
|
||||||
|
|
||||||
|
settings: Nastavení
|
||||||
|
settings.email: Email
|
||||||
|
settings.email-help: Používá se pro commity a Gravatary
|
||||||
|
settings.email-set: Nastavit email
|
||||||
|
settings.link-accounts: Propojit účty
|
||||||
|
settings.link-github-account: Propojit účet na GitHubu
|
||||||
|
settings.link-gitea-account: Propojit účet na Gitea
|
||||||
|
settings.unlink-github-account: Odpojit účet na GitHubu
|
||||||
|
settings.unlink-gitea-account: Odpojit účet na Gitea
|
||||||
|
settings.delete-account: Smazat účet
|
||||||
|
settings.delete-account-confirm: Opravdu chcete smazat svůj účet?
|
||||||
|
settings.add-ssh-key: Přidat SSH klíč
|
||||||
|
settings.add-ssh-key-help: Používá se pouze k tahání/pushování gistů pomocí Gitu přes SSH
|
||||||
|
settings.add-ssh-key-title: Titulek
|
||||||
|
settings.add-ssh-key-content: Obsah
|
||||||
|
settings.delete-ssh-key: Smazat
|
||||||
|
settings.delete-ssh-key-confirm: Potvrdit smazání SSH klíče
|
||||||
|
settings.ssh-key-added-at: Přidáno
|
||||||
|
settings.ssh-key-never-used: Nikdy nepoužito
|
||||||
|
settings.ssh-key-last-used: Naposledy použito
|
||||||
|
settings.create-password: Vytvořit heslo
|
||||||
|
settings.create-password-help: Vytvořte si heslo pro přihlášení do Opengist pomocí HTTP
|
||||||
|
settings.change-password: Změnit heslo
|
||||||
|
settings.change-password-help: Změňte své heslo pro přihlášení do Opengist pomocí HTTP
|
||||||
|
settings.password-label-title: Heslo
|
||||||
|
|
||||||
|
auth.signup-disabled: Správce zakázal registraci
|
||||||
|
auth.login: Přihlásit se
|
||||||
|
auth.signup: Registrovat
|
||||||
|
auth.new-account: Nový účet
|
||||||
|
auth.username: Uživatelské jméno
|
||||||
|
auth.password: Heslo
|
||||||
|
auth.register-instead: Raději se zaregistrovat
|
||||||
|
auth.login-instead: Raději se přihlásit
|
||||||
|
auth.oauth: Pokračovat s účtem na %s
|
||||||
|
|
||||||
|
error: Chyba
|
||||||
|
|
||||||
|
header.menu.all: Všechno
|
||||||
|
header.menu.new: Nové
|
||||||
|
header.menu.search: Hledat
|
||||||
|
header.menu.my-gists: Moje gisty
|
||||||
|
header.menu.liked: Lajknuté
|
||||||
|
header.menu.admin: Administrace
|
||||||
|
header.menu.settings: Nastavení
|
||||||
|
header.menu.logout: Odhlásit se
|
||||||
|
header.menu.register: Registrovat
|
||||||
|
header.menu.login: Přihlásit se
|
||||||
|
header.menu.light: Světlý
|
||||||
|
header.menu.dark: Tmavý
|
||||||
|
header.menu.system: Systém
|
||||||
|
footer.powered-by: Vytvořeno pomocí %s
|
||||||
|
|
||||||
|
pagination.older: Starší
|
||||||
|
pagination.newer: Novější
|
||||||
|
pagination.previous: Předchozí
|
||||||
|
pagination.next: Další
|
||||||
|
|
||||||
|
admin.admin_panel: Administrační panel
|
||||||
|
admin.general: Obecné
|
||||||
|
admin.users: Uživatelé
|
||||||
|
admin.gists: Gisty
|
||||||
|
admin.configuration: Konfigurace
|
||||||
|
admin.versions: Verze
|
||||||
|
admin.ssh_keys: SSH klíče
|
||||||
|
admin.stats: Statistiky
|
||||||
|
admin.actions: Akce
|
||||||
|
admin.actions.sync-fs: Synchronizovat gisty ze souborového systému
|
||||||
|
admin.actions.sync-db: Synchronizovat gisty z databáze
|
||||||
|
admin.actions.git-gc: Garbage collect git repozitářů
|
||||||
|
admin.id: ID
|
||||||
|
admin.user: Uživatel
|
||||||
|
admin.delete: Smazat
|
||||||
|
admin.created_at: Vytvořeno
|
||||||
|
|
||||||
|
admin.config-link: Tato konfigurace může být %s pomocí YAML konfiguračního souboru a/nebo prostřednictvím proměnných prostředí.
|
||||||
|
admin.config-link-overriden: přepsána
|
||||||
|
admin.disable-signup: Zakázat registraci
|
||||||
|
admin.disable-signup_help: Zakázat vytváření nových účtů.
|
||||||
|
admin.require-login: Vyžadovat přihlášení
|
||||||
|
admin.require-login_help: Vynutit, aby uživatelé byli přihlášeni k zobrazení gistů.
|
||||||
|
admin.disable-login: Zakázat přihlášení
|
||||||
|
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
|
||||||
|
admin.disable-gravatar: Zakázat Gravatar
|
||||||
|
admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara.
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
|
admin.users.delete_confirm: Opravdu chcete smazat tohoto uživatele?
|
||||||
|
|
||||||
|
admin.gists.title: Titulek
|
||||||
|
admin.gists.private: Soukromé?
|
||||||
|
admin.gists.nb-files: Počet souborů
|
||||||
|
admin.gists.nb-likes: Počet lajků
|
||||||
|
admin.gists.delete_confirm: Opravdu chcete smazat tento gist?
|
||||||
|
gist.forks.for: ''
|
||||||
|
gist.likes.for: ''
|
||||||
|
gist.revision-of: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
error.bad-request: ''
|
||||||
|
error.signup-disabled: ''
|
||||||
|
error.signup-disabled-form: ''
|
||||||
|
error.login-disabled-form: ''
|
||||||
|
error.complete-oauth-login: ''
|
||||||
|
error.oauth-unsupported: ''
|
||||||
|
error.cannot-bind-data: ''
|
||||||
|
error.invalid-number: ''
|
||||||
|
error.invalid-character-unescaped: ''
|
||||||
|
admin.actions.reset-hooks: ''
|
||||||
|
admin.invitations.expired: ''
|
||||||
|
flash.admin.user-deleted: ''
|
||||||
|
flash.admin.gist-deleted: ''
|
||||||
|
flash.admin.invitation-created: ''
|
||||||
|
flash.admin.invitation-deleted: ''
|
||||||
|
flash.admin.sync-fs: ''
|
||||||
|
flash.admin.sync-db: ''
|
||||||
|
flash.admin.git-gc: ''
|
||||||
|
flash.admin.sync-previews: ''
|
||||||
|
gist.new.create-a-new-gist: ''
|
||||||
|
gist.edit.edit-gist: ''
|
||||||
|
flash.admin.reset-hooks: ''
|
||||||
|
flash.admin.index-gists: ''
|
||||||
|
flash.auth.username-exists: ''
|
||||||
|
flash.auth.invalid-credentials: ''
|
||||||
|
flash.auth.account-linked-oauth: ''
|
||||||
|
flash.auth.account-unlinked-oauth: ''
|
||||||
|
flash.auth.user-sshkeys-not-retrievable: ''
|
||||||
|
flash.auth.user-sshkeys-not-created: ''
|
||||||
|
flash.auth.must-be-logged-in: ''
|
||||||
|
flash.gist.visibility-changed: ''
|
||||||
|
flash.gist.fork-own-gist: ''
|
||||||
|
flash.gist.forked: ''
|
||||||
|
flash.user.email-updated: ''
|
||||||
|
flash.user.invalid-ssh-key: ''
|
||||||
|
flash.user.ssh-key-added: ''
|
||||||
|
flash.user.ssh-key-deleted: ''
|
||||||
|
flash.user.password-updated: ''
|
||||||
|
flash.user.username-updated: ''
|
||||||
|
validation.is-too-long: ''
|
||||||
|
validation.should-not-be-empty: ''
|
||||||
|
validation.should-not-include-sub-directory: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters: ''
|
||||||
|
gist.list.all-liked-by: ''
|
||||||
|
gist.list.all-forked-by: ''
|
||||||
|
gist.list.all-from: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||||
|
validation.not-enough: ''
|
||||||
|
validation.invalid: ''
|
||||||
|
html.title.admin-panel: ''
|
||||||
|
flash.gist.deleted: ''
|
||||||
|
gist.new.url: ''
|
||||||
|
gist.search.found: ''
|
||||||
|
gist.search.no-results: ''
|
||||||
|
gist.search.help.user: ''
|
||||||
|
gist.search.help.title: ''
|
||||||
|
gist.search.help.filename: ''
|
||||||
|
gist.search.help.extension: ''
|
||||||
|
gist.search.help.language: ''
|
||||||
|
settings.change-username: ''
|
||||||
|
admin.invitations: ''
|
||||||
|
admin.invitations.create: ''
|
||||||
|
admin.actions.sync-previews: ''
|
||||||
|
admin.actions.index-gists: ''
|
||||||
|
admin.invitations.code: ''
|
||||||
|
admin.invitations.copy_link: ''
|
||||||
|
admin.invitations.uses: ''
|
||||||
|
gist.new.preview: ''
|
||||||
|
settings.link-gitlab-account: ''
|
||||||
|
settings.unlink-gitlab-account: ''
|
||||||
|
admin.invitations.help: ''
|
||||||
|
admin.invitations.max_uses: ''
|
||||||
|
admin.invitations.expires_at: ''
|
||||||
269
internal/i18n/locales/de-DE.yml
Normal file
269
internal/i18n/locales/de-DE.yml
Normal 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'
|
||||||
@@ -17,19 +17,20 @@ gist.header.clone-http: Clone via %s
|
|||||||
gist.header.clone-http-help: Clone with Git using HTTP basic authentication.
|
gist.header.clone-http-help: Clone with Git using HTTP basic authentication.
|
||||||
gist.header.clone-ssh: Clone via SSH
|
gist.header.clone-ssh: Clone via SSH
|
||||||
gist.header.clone-ssh-help: Clone with Git using an SSH key.
|
gist.header.clone-ssh-help: Clone with Git using an SSH key.
|
||||||
gist.header.share: Share
|
gist.header.embed: Embed
|
||||||
gist.header.share-help: Copy shareable link for this gist.
|
gist.header.embed-help: Embed this gist to your website.
|
||||||
gist.header.download-zip: Download ZIP
|
gist.header.download-zip: Download ZIP
|
||||||
|
|
||||||
gist.raw: Raw
|
gist.raw: Raw
|
||||||
gist.file-truncated: This file has been truncated.
|
gist.file-truncated: This file has been truncated.
|
||||||
gist.watch-full-file: View the full file.
|
gist.watch-full-file: View the full file.
|
||||||
gist.file-not-valid: This file is not a valid CSV file.
|
gist.file-not-valid: This file is not a valid CSV file.
|
||||||
gist.no-content: No content
|
gist.no-content: No files found
|
||||||
|
|
||||||
gist.new.new_gist: New gist
|
gist.new.new_gist: New gist
|
||||||
gist.new.title: Title
|
gist.new.title: Title
|
||||||
gist.new.description: Description
|
gist.new.description: Description
|
||||||
|
gist.new.url: URL
|
||||||
gist.new.filename-with-extension: Filename with extension
|
gist.new.filename-with-extension: Filename with extension
|
||||||
gist.new.indent-mode: Indent mode
|
gist.new.indent-mode: Indent mode
|
||||||
gist.new.indent-mode-space: Space
|
gist.new.indent-mode-space: Space
|
||||||
@@ -42,8 +43,11 @@ gist.new.add-file: Add file
|
|||||||
gist.new.create-public-button: Create public gist
|
gist.new.create-public-button: Create public gist
|
||||||
gist.new.create-unlisted-button: Create unlisted gist
|
gist.new.create-unlisted-button: Create unlisted gist
|
||||||
gist.new.create-private-button: Create private gist
|
gist.new.create-private-button: Create private gist
|
||||||
|
gist.new.preview: Preview
|
||||||
|
gist.new.create-a-new-gist: Create a new gist
|
||||||
|
|
||||||
gist.edit.editing: Editing
|
gist.edit.editing: Editing
|
||||||
|
gist.edit.edit-gist: Edit %s
|
||||||
gist.edit.change-visibility: Make
|
gist.edit.change-visibility: Make
|
||||||
gist.edit.delete: Delete
|
gist.edit.delete: Delete
|
||||||
gist.edit.cancel: Cancel
|
gist.edit.cancel: Cancel
|
||||||
@@ -66,13 +70,26 @@ gist.list.forks: forks
|
|||||||
gist.list.files: files
|
gist.list.files: files
|
||||||
gist.list.last-active: Last active
|
gist.list.last-active: Last active
|
||||||
gist.list.no-gists: No gists
|
gist.list.no-gists: No gists
|
||||||
|
gist.list.all-liked-by: All gists liked by %s
|
||||||
|
gist.list.all-forked-by: All gists forked by %s
|
||||||
|
gist.list.all-from: All gists from %s
|
||||||
|
|
||||||
|
gist.search.found: gists found
|
||||||
|
gist.search.no-results: No gists found
|
||||||
|
gist.search.help.user: gists created by user
|
||||||
|
gist.search.help.title: gists with given title
|
||||||
|
gist.search.help.filename: gists having files with given name
|
||||||
|
gist.search.help.extension: gists having files with given extension
|
||||||
|
gist.search.help.language: gists having files with given language
|
||||||
|
|
||||||
gist.forks: Forks
|
gist.forks: Forks
|
||||||
gist.forks.view: View fork
|
gist.forks.view: View fork
|
||||||
gist.forks.no: No public forks
|
gist.forks.no: No public forks
|
||||||
|
gist.forks.for: Forks for %s
|
||||||
|
|
||||||
gist.likes: Likes
|
gist.likes: Likes
|
||||||
gist.likes.no: No likes yet
|
gist.likes.no: No likes yet
|
||||||
|
gist.likes.for: Likes for %s
|
||||||
|
|
||||||
gist.revisions: Revisions
|
gist.revisions: Revisions
|
||||||
gist.revision.revised: revised this gist
|
gist.revision.revised: revised this gist
|
||||||
@@ -80,11 +97,12 @@ gist.revision.go-to-revision: Go to revision
|
|||||||
gist.revision.file-created: file created
|
gist.revision.file-created: file created
|
||||||
gist.revision.file-deleted: file deleted
|
gist.revision.file-deleted: file deleted
|
||||||
gist.revision.file-renamed: renamed to
|
gist.revision.file-renamed: renamed to
|
||||||
gist.revision.diff-truncated: Diff truncated because it's too large to be shown
|
gist.revision.diff-truncated: Diff is too large to be shown
|
||||||
gist.revision.file-renamed-no-changes: File renamed without changes
|
gist.revision.file-renamed-no-changes: File renamed without changes
|
||||||
gist.revision.empty-file: Empty file
|
gist.revision.empty-file: Empty file
|
||||||
gist.revision.no-changes: No changes
|
gist.revision.no-changes: No changes
|
||||||
gist.revision.no-revisions: No revisions to show
|
gist.revision.no-revisions: No revisions to show
|
||||||
|
gist.revision-of: Revision of %s
|
||||||
|
|
||||||
settings: Settings
|
settings: Settings
|
||||||
settings.email: Email
|
settings.email: Email
|
||||||
@@ -92,8 +110,10 @@ settings.email-help: Used for commits and Gravatar
|
|||||||
settings.email-set: Set email
|
settings.email-set: Set email
|
||||||
settings.link-accounts: Link accounts
|
settings.link-accounts: Link accounts
|
||||||
settings.link-github-account: Link GitHub account
|
settings.link-github-account: Link GitHub account
|
||||||
|
settings.link-gitlab-account: Link GitLab account
|
||||||
settings.link-gitea-account: Link Gitea account
|
settings.link-gitea-account: Link Gitea account
|
||||||
settings.unlink-github-account: Unlink GitHub account
|
settings.unlink-github-account: Unlink GitHub account
|
||||||
|
settings.unlink-gitlab-account: Unlink GitLab account
|
||||||
settings.unlink-gitea-account: Unlink Gitea account
|
settings.unlink-gitea-account: Unlink Gitea account
|
||||||
settings.delete-account: Delete account
|
settings.delete-account: Delete account
|
||||||
settings.delete-account-confirm: Are you sure you want to delete your account ?
|
settings.delete-account-confirm: Are you sure you want to delete your account ?
|
||||||
@@ -106,6 +126,13 @@ settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
|||||||
settings.ssh-key-added-at: Added
|
settings.ssh-key-added-at: Added
|
||||||
settings.ssh-key-never-used: Never used
|
settings.ssh-key-never-used: Never used
|
||||||
settings.ssh-key-last-used: Last used
|
settings.ssh-key-last-used: Last used
|
||||||
|
settings.ssh-key-exists: SSH key already exists
|
||||||
|
settings.change-username: Change username
|
||||||
|
settings.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.signup-disabled: Administrator has disabled signing up
|
||||||
auth.login: Login
|
auth.login: Login
|
||||||
@@ -115,10 +142,19 @@ auth.username: Username
|
|||||||
auth.password: Password
|
auth.password: Password
|
||||||
auth.register-instead: Register instead
|
auth.register-instead: Register instead
|
||||||
auth.login-instead: Login instead
|
auth.login-instead: Login instead
|
||||||
auth.github-oauth: Continue with GitHub account
|
auth.oauth: Continue with %s account
|
||||||
auth.gitea-oauth: Continue with Gitea account
|
|
||||||
|
|
||||||
error: Error
|
error: Error
|
||||||
|
error.page-not-found: Page not found
|
||||||
|
error.bad-request: Bad request
|
||||||
|
error.signup-disabled: Signing up is disabled
|
||||||
|
error.signup-disabled-form: Signing up via registration form is disabled
|
||||||
|
error.login-disabled-form: Logging in via login form is disabled
|
||||||
|
error.complete-oauth-login: "Cannot complete user auth: %s"
|
||||||
|
error.oauth-unsupported: Unsupported provider
|
||||||
|
error.cannot-bind-data: Cannot bind data
|
||||||
|
error.invalid-number: Invalid number
|
||||||
|
error.invalid-character-unescaped: Invalid character unescaped
|
||||||
|
|
||||||
header.menu.all: All
|
header.menu.all: All
|
||||||
header.menu.new: New
|
header.menu.new: New
|
||||||
@@ -145,13 +181,18 @@ admin.general: General
|
|||||||
admin.users: Users
|
admin.users: Users
|
||||||
admin.gists: Gists
|
admin.gists: Gists
|
||||||
admin.configuration: Configuration
|
admin.configuration: Configuration
|
||||||
|
admin.invitations: Invitations
|
||||||
|
admin.invitations.create: Create invitation
|
||||||
admin.versions: Versions
|
admin.versions: Versions
|
||||||
admin.ssh_keys: SSH keys
|
admin.ssh_keys: SSH keys
|
||||||
admin.stats: Stats
|
admin.stats: Stats
|
||||||
admin.actions: Actions
|
admin.actions: Actions
|
||||||
admin.actions.sync-fs: Synchronize gists from filesystem
|
admin.actions.sync-fs: Synchronize gists from filesystem
|
||||||
admin.actions.sync-db: Synchronize gists from database
|
admin.actions.sync-db: Synchronize gists from database
|
||||||
admin.actions.git-gc: Garbage collect git repositories
|
admin.actions.git-gc: Garbage collect all git repositories
|
||||||
|
admin.actions.sync-previews: Synchronize all gists previews
|
||||||
|
admin.actions.reset-hooks: Reset Git server hooks for all repositories
|
||||||
|
admin.actions.index-gists: Index all gists
|
||||||
admin.id: ID
|
admin.id: ID
|
||||||
admin.user: User
|
admin.user: User
|
||||||
admin.delete: Delete
|
admin.delete: Delete
|
||||||
@@ -163,6 +204,8 @@ admin.disable-signup: Disable signup
|
|||||||
admin.disable-signup_help: Forbid the creation of new accounts.
|
admin.disable-signup_help: Forbid the creation of new accounts.
|
||||||
admin.require-login: Require login
|
admin.require-login: Require login
|
||||||
admin.require-login_help: Enforce users to be logged in to see gists.
|
admin.require-login_help: Enforce users to be logged in to see gists.
|
||||||
|
admin.allow-gists-without-login: Allow individual gists without login
|
||||||
|
admin.allow-gists-without-login_help: Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
|
||||||
admin.disable-login: Disable login form
|
admin.disable-login: Disable login form
|
||||||
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
||||||
admin.disable-gravatar: Disable Gravatar
|
admin.disable-gravatar: Disable Gravatar
|
||||||
@@ -175,3 +218,52 @@ admin.gists.private: Private ?
|
|||||||
admin.gists.nb-files: Nb. files
|
admin.gists.nb-files: Nb. files
|
||||||
admin.gists.nb-likes: Nb. likes
|
admin.gists.nb-likes: Nb. likes
|
||||||
admin.gists.delete_confirm: Do you want to delete this gist ?
|
admin.gists.delete_confirm: Do you want to delete this gist ?
|
||||||
|
|
||||||
|
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
|
||||||
|
admin.invitations.max_uses: Max uses
|
||||||
|
admin.invitations.expires_at: Expires at
|
||||||
|
admin.invitations.code: Code
|
||||||
|
admin.invitations.copy_link: Copy link
|
||||||
|
admin.invitations.uses: Uses
|
||||||
|
admin.invitations.expired: Expired
|
||||||
|
|
||||||
|
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
|
||||||
259
internal/i18n/locales/es-ES.yml
Normal file
259
internal/i18n/locales/es-ES.yml
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
gist.public: Público
|
||||||
|
gist.unlisted: No listado
|
||||||
|
gist.private: Privado
|
||||||
|
|
||||||
|
gist.header.like: Me gusta
|
||||||
|
gist.header.unlike: No me gusta
|
||||||
|
gist.header.fork: Bifurcar
|
||||||
|
gist.header.edit: Editar
|
||||||
|
gist.header.delete: Eliminar
|
||||||
|
gist.header.forked-from: Bifurcado desde
|
||||||
|
gist.header.last-active: Última actividad
|
||||||
|
gist.header.select-tab: Seleccionar pestaña
|
||||||
|
gist.header.code: Código
|
||||||
|
gist.header.revisions: Revisiones
|
||||||
|
gist.header.revision: Revisión
|
||||||
|
gist.header.clone-http: Clonar via %s
|
||||||
|
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
|
||||||
|
gist.header.clone-ssh: Clonar via SSH
|
||||||
|
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
|
||||||
|
gist.header.embed: ''
|
||||||
|
gist.header.embed-help: ''
|
||||||
|
gist.header.download-zip: Descargar ZIP
|
||||||
|
|
||||||
|
gist.raw: Sin formato
|
||||||
|
gist.file-truncated: Este archivo ha sido truncado.
|
||||||
|
gist.watch-full-file: Ver el archivo completo.
|
||||||
|
gist.file-not-valid: Este archivo no es un archivo CSV válido.
|
||||||
|
gist.no-content: Sin contenido
|
||||||
|
|
||||||
|
gist.new.new_gist: Nuevo gist
|
||||||
|
gist.new.title: Título
|
||||||
|
gist.new.description: Descripción
|
||||||
|
gist.new.filename-with-extension: Nombre de archivo con extensión
|
||||||
|
gist.new.indent-mode: Modo de sangrado
|
||||||
|
gist.new.indent-mode-space: Espacio
|
||||||
|
gist.new.indent-mode-tab: Tabulación
|
||||||
|
gist.new.indent-size: Tamaño de sangrado
|
||||||
|
gist.new.wrap-mode: Modo de ajuste
|
||||||
|
gist.new.wrap-mode-no: Sin ajuste
|
||||||
|
gist.new.wrap-mode-soft: Ajuste suave
|
||||||
|
gist.new.add-file: Agregar archivo
|
||||||
|
gist.new.create-public-button: Crear gist público
|
||||||
|
gist.new.create-unlisted-button: Crear gist no listado
|
||||||
|
gist.new.create-private-button: Crear gist privado
|
||||||
|
|
||||||
|
gist.edit.editing: Editando
|
||||||
|
gist.edit.change-visibility: Hacer
|
||||||
|
gist.edit.delete: Eliminar
|
||||||
|
gist.edit.cancel: Cancelar
|
||||||
|
gist.edit.save: Guardar
|
||||||
|
|
||||||
|
gist.list.joined: Unido
|
||||||
|
gist.list.all: Todos los gists
|
||||||
|
gist.list.search-results: Resultados de búsqueda
|
||||||
|
gist.list.sort: Ordenar
|
||||||
|
gist.list.sort-by-created: creado
|
||||||
|
gist.list.sort-by-updated: actualizado
|
||||||
|
gist.list.order-by-asc: Menos reciente
|
||||||
|
gist.list.order-by-desc: Recientemente
|
||||||
|
gist.list.select-tab: Seleccionar pestaña
|
||||||
|
gist.list.liked: Gustado
|
||||||
|
gist.list.likes: gustos
|
||||||
|
gist.list.forked: Bifurcado
|
||||||
|
gist.list.forked-from: Bifurcado desde
|
||||||
|
gist.list.forks: bifurcaciones
|
||||||
|
gist.list.files: archivos
|
||||||
|
gist.list.last-active: Última actividad
|
||||||
|
gist.list.no-gists: Sin gists
|
||||||
|
|
||||||
|
gist.forks: Bifurcaciones
|
||||||
|
gist.forks.view: Ver bifurcación
|
||||||
|
gist.forks.no: No hay bifurcaciones públicas
|
||||||
|
|
||||||
|
gist.likes: Gustos
|
||||||
|
gist.likes.no: Aún no hay gustos
|
||||||
|
|
||||||
|
gist.revisions: Revisiones
|
||||||
|
gist.revision.revised: revisó este gist
|
||||||
|
gist.revision.go-to-revision: Ir a la revisión
|
||||||
|
gist.revision.file-created: archivo creado
|
||||||
|
gist.revision.file-deleted: archivo eliminado
|
||||||
|
gist.revision.file-renamed: renombrado a
|
||||||
|
gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse.
|
||||||
|
gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios
|
||||||
|
gist.revision.empty-file: Archivo vacío
|
||||||
|
gist.revision.no-changes: Sin cambios
|
||||||
|
gist.revision.no-revisions: No hay revisiones para mostrar
|
||||||
|
|
||||||
|
settings: Configuración
|
||||||
|
settings.email: Correo electrónico
|
||||||
|
settings.email-help: Usado para confirmaciones y Gravatar
|
||||||
|
settings.email-set: Establecer correo electrónico
|
||||||
|
settings.link-accounts: Enlazar cuentas
|
||||||
|
settings.link-github-account: Enlazar cuenta de GitHub
|
||||||
|
settings.link-gitea-account: Enlazar cuenta de Gitea
|
||||||
|
settings.unlink-github-account: Desenlazar cuenta de GitHub
|
||||||
|
settings.unlink-gitea-account: Desenlazar cuenta de Gitea
|
||||||
|
settings.delete-account: Eliminar cuenta
|
||||||
|
settings.delete-account-confirm: ¿Estás seguro de que quieres eliminar tu cuenta?
|
||||||
|
settings.add-ssh-key: Agregar clave SSH
|
||||||
|
settings.add-ssh-key-help: Usado solo para extraer/push gists usando Git a través de SSH
|
||||||
|
settings.add-ssh-key-title: Título
|
||||||
|
settings.add-ssh-key-content: Clave
|
||||||
|
settings.delete-ssh-key: Eliminar
|
||||||
|
settings.delete-ssh-key-confirm: Confirmar eliminación de clave SSH
|
||||||
|
settings.ssh-key-added-at: Añadido
|
||||||
|
settings.ssh-key-never-used: Nunca usado
|
||||||
|
settings.ssh-key-last-used: Último uso
|
||||||
|
|
||||||
|
auth.signup-disabled: El administrador ha deshabilitado el registro
|
||||||
|
auth.login: Iniciar sesión
|
||||||
|
auth.signup: Registrarse
|
||||||
|
auth.new-account: Nueva cuenta
|
||||||
|
auth.username: Nombre de usuario
|
||||||
|
auth.password: Contraseña
|
||||||
|
auth.register-instead: Registrarse en su lugar
|
||||||
|
auth.login-instead: Iniciar sesión en su lugar
|
||||||
|
auth.oauth: Continuar con cuenta de %s
|
||||||
|
|
||||||
|
error: Error
|
||||||
|
|
||||||
|
header.menu.all: Todos
|
||||||
|
header.menu.new: Nuevo
|
||||||
|
header.menu.search: Buscar
|
||||||
|
header.menu.my-gists: Mis gists
|
||||||
|
header.menu.liked: Gustados
|
||||||
|
header.menu.admin: Administrador
|
||||||
|
header.menu.settings: Configuración
|
||||||
|
header.menu.logout: Cerrar sesión
|
||||||
|
header.menu.register: Registrarse
|
||||||
|
header.menu.login: Iniciar sesión
|
||||||
|
header.menu.light: Claro
|
||||||
|
header.menu.dark: Oscuro
|
||||||
|
header.menu.system: Sistema
|
||||||
|
footer.powered-by: Desarrollado por %s
|
||||||
|
|
||||||
|
pagination.older: Anterior
|
||||||
|
pagination.newer: Siguiente
|
||||||
|
pagination.previous: Anterior
|
||||||
|
pagination.next: Siguiente
|
||||||
|
|
||||||
|
admin.admin_panel: Panel de administración
|
||||||
|
admin.general: General
|
||||||
|
admin.users: Usuarios
|
||||||
|
admin.gists: Gists
|
||||||
|
admin.configuration: Configuración
|
||||||
|
admin.versions: Versiones
|
||||||
|
admin.ssh_keys: Claves SSH
|
||||||
|
admin.stats: Estadísticas
|
||||||
|
admin.actions: Acciones
|
||||||
|
admin.actions.sync-fs: Sincronizar gists desde el sistema de archivos
|
||||||
|
admin.actions.sync-db: Sincronizar gists desde la base de datos
|
||||||
|
admin.actions.git-gc: Recolectar basura en los repositorios Git
|
||||||
|
admin.id: ID
|
||||||
|
admin.user: Usuario
|
||||||
|
admin.delete: Eliminar
|
||||||
|
admin.created_at: Creado
|
||||||
|
|
||||||
|
admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno.
|
||||||
|
admin.disable-signup: Deshabilitar registro
|
||||||
|
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
|
||||||
|
admin.require-login: Requerir inicio de sesión
|
||||||
|
admin.require-login_help: Obligar a los usuarios a iniciar sesión para ver gists.
|
||||||
|
admin.disable-login: Deshabilitar formulario de inicio de sesión
|
||||||
|
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
|
||||||
|
admin.disable-gravatar: Deshabilitar Gravatar
|
||||||
|
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
|
admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
|
||||||
|
|
||||||
|
admin.gists.title: Título
|
||||||
|
admin.gists.private: ¿Privado?
|
||||||
|
admin.gists.nb-files: Núm. de archivos
|
||||||
|
admin.gists.nb-likes: Núm. de gustos
|
||||||
|
admin.gists.delete_confirm: ¿Quieres eliminar este gist?
|
||||||
|
gist.new.url: ''
|
||||||
|
gist.new.preview: ''
|
||||||
|
gist.new.create-a-new-gist: ''
|
||||||
|
gist.edit.edit-gist: ''
|
||||||
|
gist.list.all-liked-by: ''
|
||||||
|
gist.list.all-forked-by: ''
|
||||||
|
gist.list.all-from: ''
|
||||||
|
gist.search.found: ''
|
||||||
|
gist.search.no-results: ''
|
||||||
|
gist.search.help.user: ''
|
||||||
|
gist.search.help.title: ''
|
||||||
|
gist.search.help.filename: ''
|
||||||
|
gist.search.help.extension: ''
|
||||||
|
gist.search.help.language: ''
|
||||||
|
gist.forks.for: ''
|
||||||
|
gist.likes.for: ''
|
||||||
|
gist.revision-of: ''
|
||||||
|
settings.link-gitlab-account: ''
|
||||||
|
settings.unlink-gitlab-account: ''
|
||||||
|
settings.change-username: ''
|
||||||
|
settings.create-password: ''
|
||||||
|
settings.create-password-help: ''
|
||||||
|
settings.change-password: ''
|
||||||
|
settings.change-password-help: ''
|
||||||
|
settings.password-label-title: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
error.bad-request: ''
|
||||||
|
error.signup-disabled: ''
|
||||||
|
error.signup-disabled-form: ''
|
||||||
|
error.login-disabled-form: ''
|
||||||
|
error.complete-oauth-login: ''
|
||||||
|
error.oauth-unsupported: ''
|
||||||
|
error.cannot-bind-data: ''
|
||||||
|
error.invalid-number: ''
|
||||||
|
error.invalid-character-unescaped: ''
|
||||||
|
admin.invitations: ''
|
||||||
|
admin.invitations.create: ''
|
||||||
|
admin.actions.sync-previews: ''
|
||||||
|
admin.actions.reset-hooks: ''
|
||||||
|
admin.actions.index-gists: ''
|
||||||
|
admin.config-link-overriden: ''
|
||||||
|
admin.invitations.help: ''
|
||||||
|
admin.invitations.max_uses: ''
|
||||||
|
admin.invitations.expires_at: ''
|
||||||
|
admin.invitations.code: ''
|
||||||
|
admin.invitations.copy_link: ''
|
||||||
|
admin.invitations.uses: ''
|
||||||
|
admin.invitations.expired: ''
|
||||||
|
flash.admin.user-deleted: ''
|
||||||
|
flash.admin.gist-deleted: ''
|
||||||
|
flash.admin.invitation-created: ''
|
||||||
|
flash.admin.invitation-deleted: ''
|
||||||
|
flash.admin.sync-fs: ''
|
||||||
|
flash.admin.sync-db: ''
|
||||||
|
flash.admin.git-gc: ''
|
||||||
|
flash.admin.sync-previews: ''
|
||||||
|
flash.admin.reset-hooks: ''
|
||||||
|
flash.admin.index-gists: ''
|
||||||
|
flash.auth.username-exists: ''
|
||||||
|
flash.auth.invalid-credentials: ''
|
||||||
|
flash.auth.account-linked-oauth: ''
|
||||||
|
flash.auth.account-unlinked-oauth: ''
|
||||||
|
flash.auth.user-sshkeys-not-retrievable: ''
|
||||||
|
flash.auth.user-sshkeys-not-created: ''
|
||||||
|
flash.auth.must-be-logged-in: ''
|
||||||
|
flash.gist.visibility-changed: ''
|
||||||
|
flash.gist.deleted: ''
|
||||||
|
flash.gist.fork-own-gist: ''
|
||||||
|
flash.gist.forked: ''
|
||||||
|
flash.user.email-updated: ''
|
||||||
|
flash.user.invalid-ssh-key: ''
|
||||||
|
flash.user.ssh-key-added: ''
|
||||||
|
flash.user.ssh-key-deleted: ''
|
||||||
|
flash.user.password-updated: ''
|
||||||
|
flash.user.username-updated: ''
|
||||||
|
validation.is-too-long: ''
|
||||||
|
validation.should-not-be-empty: ''
|
||||||
|
validation.should-not-include-sub-directory: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||||
|
validation.not-enough: ''
|
||||||
|
validation.invalid: ''
|
||||||
|
html.title.admin-panel: ''
|
||||||
@@ -17,15 +17,15 @@ gist.header.clone-http: Cloner via %s
|
|||||||
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic.
|
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic.
|
||||||
gist.header.clone-ssh: Cloner via SSH
|
gist.header.clone-ssh: Cloner via SSH
|
||||||
gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH.
|
gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH.
|
||||||
gist.header.share: Partager
|
gist.header.embed: Intégrer
|
||||||
gist.header.share-help: Copier le lien partageable de ce gist.
|
gist.header.embed-help: Intégrer ce gist dans une page web.
|
||||||
gist.header.download-zip: Télécharger en ZIP
|
gist.header.download-zip: Télécharger en ZIP
|
||||||
|
|
||||||
gist.raw: Brut
|
gist.raw: Brut
|
||||||
gist.file-truncated: Ce fichier a été tronqué.
|
gist.file-truncated: Ce fichier a été tronqué.
|
||||||
gist.watch-full-file: Voir le fichier complet.
|
gist.watch-full-file: Voir le fichier complet.
|
||||||
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
|
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
|
||||||
gist.no-content: Pas de contenu
|
gist.no-content: Aucun fichier
|
||||||
|
|
||||||
gist.new.new_gist: Nouveau gist
|
gist.new.new_gist: Nouveau gist
|
||||||
gist.new.title: Titre
|
gist.new.title: Titre
|
||||||
@@ -80,7 +80,7 @@ gist.revision.go-to-revision: Aller à la révision
|
|||||||
gist.revision.file-created: fichier créé
|
gist.revision.file-created: fichier créé
|
||||||
gist.revision.file-deleted: fichier supprimé
|
gist.revision.file-deleted: fichier supprimé
|
||||||
gist.revision.file-renamed: renommé en
|
gist.revision.file-renamed: renommé en
|
||||||
gist.revision.diff-truncated: Révision tronquée car trop volumineuse pour être affichée
|
gist.revision.diff-truncated: Révision trop volumineuse pour être affichée
|
||||||
gist.revision.file-renamed-no-changes: Fichier renommé sans modifications
|
gist.revision.file-renamed-no-changes: Fichier renommé sans modifications
|
||||||
gist.revision.empty-file: Fichier vide
|
gist.revision.empty-file: Fichier vide
|
||||||
gist.revision.no-changes: Aucun changement
|
gist.revision.no-changes: Aucun changement
|
||||||
@@ -115,8 +115,7 @@ auth.username: Nom d'utilisateur
|
|||||||
auth.password: Mot de passe
|
auth.password: Mot de passe
|
||||||
auth.register-instead: Je préfère m'inscrire
|
auth.register-instead: Je préfère m'inscrire
|
||||||
auth.login-instead: Je préfère me connecter
|
auth.login-instead: Je préfère me connecter
|
||||||
auth.github-oauth: Continuer avec un compte GitHub
|
auth.oauth: Continuer avec un compte %s
|
||||||
auth.gitea-oauth: Continuer avec un compte Gitea
|
|
||||||
|
|
||||||
error: Erreur
|
error: Erreur
|
||||||
|
|
||||||
@@ -167,7 +166,8 @@ admin.disable-login: Désactiver le formulaire de connexion
|
|||||||
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
|
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
|
||||||
admin.disable-gravatar: Désactiver Gravatar
|
admin.disable-gravatar: Désactiver Gravatar
|
||||||
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
|
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
|
||||||
|
admin.allow-gists-without-login: Autoriser les gists individuelles sans login
|
||||||
|
admin.allow-gists-without-login_help: Autoriser la visualisation et le téléchargement de gists individuels sans connexion, tout en exigeant une connexion pour la découverte de gists.
|
||||||
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
|
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
|
||||||
|
|
||||||
admin.gists.title: Titre
|
admin.gists.title: Titre
|
||||||
@@ -175,3 +175,86 @@ admin.gists.private: Privé ?
|
|||||||
admin.gists.nb-files: Nb. de fichiers
|
admin.gists.nb-files: Nb. de fichiers
|
||||||
admin.gists.nb-likes: Nb. de j'aime
|
admin.gists.nb-likes: Nb. de j'aime
|
||||||
admin.gists.delete_confirm: Voulez-vous supprimer ce gist ?
|
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à
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ gist.header.clone-http: "Clone-ozás ezzel: %s"
|
|||||||
gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel.
|
gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel.
|
||||||
gist.header.clone-ssh: Clone-ozás SSH-n keresztül
|
gist.header.clone-ssh: Clone-ozás SSH-n keresztül
|
||||||
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
|
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
|
||||||
gist.header.share: Megosztás
|
gist.header.embed: ''
|
||||||
gist.header.share-help: Másold ki ennek a gistnek a megosztható linkjét
|
gist.header.embed-help: ''
|
||||||
gist.header.download-zip: ZIP archívum letöltése
|
gist.header.download-zip: ZIP archívum letöltése
|
||||||
|
|
||||||
gist.raw: Eredeti
|
gist.raw: Eredeti
|
||||||
@@ -30,6 +30,7 @@ gist.no-content: Nincs tartalom
|
|||||||
gist.new.new_gist: Új gist
|
gist.new.new_gist: Új gist
|
||||||
gist.new.title: Cím
|
gist.new.title: Cím
|
||||||
gist.new.description: Leírás
|
gist.new.description: Leírás
|
||||||
|
gist.new.url: URL
|
||||||
gist.new.filename-with-extension: Fájlnév kiterjesztéssel
|
gist.new.filename-with-extension: Fájlnév kiterjesztéssel
|
||||||
gist.new.indent-mode: Indentáció típusa
|
gist.new.indent-mode: Indentáció típusa
|
||||||
gist.new.indent-mode-space: Szóköz
|
gist.new.indent-mode-space: Szóköz
|
||||||
@@ -67,6 +68,14 @@ gist.list.files: fájlok
|
|||||||
gist.list.last-active: Utoljára aktív
|
gist.list.last-active: Utoljára aktív
|
||||||
gist.list.no-gists: Nincsenek gistek
|
gist.list.no-gists: Nincsenek gistek
|
||||||
|
|
||||||
|
gist.search.found: találat
|
||||||
|
gist.search.no-results: Nincsenek találatok
|
||||||
|
gist.search.help.user: létrehozva e felhasználó által
|
||||||
|
gist.search.help.title: gistek egyező címmel
|
||||||
|
gist.search.help.filename: gistek melyek tartalmaznak fájlt egyező névvel
|
||||||
|
gist.search.help.extension: gistek melyek tartalmaznak fájlt egyező kiterjesztéssel
|
||||||
|
gist.search.help.language: gistek melyek tartalmaznak fájlt egyező nyelvvel
|
||||||
|
|
||||||
gist.forks: Forkok
|
gist.forks: Forkok
|
||||||
gist.forks.view: Fork megtekintése
|
gist.forks.view: Fork megtekintése
|
||||||
gist.forks.no: Nincsenek nyilvános forkok
|
gist.forks.no: Nincsenek nyilvános forkok
|
||||||
@@ -92,8 +101,10 @@ settings.email-help: Commitoknál és Gravatarnál van használva
|
|||||||
settings.email-set: Email beállítása
|
settings.email-set: Email beállítása
|
||||||
settings.link-accounts: Fiókok összekötése
|
settings.link-accounts: Fiókok összekötése
|
||||||
settings.link-github-account: GitHub fiók hozzáadása
|
settings.link-github-account: GitHub fiók hozzáadása
|
||||||
|
settings.link-gitlab-account: GitLab fiók hozzáadása
|
||||||
settings.link-gitea-account: Gitea fiók hozzáadása
|
settings.link-gitea-account: Gitea fiók hozzáadása
|
||||||
settings.unlink-github-account: GitHub fiók eltávolítása
|
settings.unlink-github-account: GitHub fiók eltávolítása
|
||||||
|
settings.unlink-gitlab-account: GitLab fiók eltávolítása
|
||||||
settings.unlink-gitea-account: Gitea fiók eltávolítása
|
settings.unlink-gitea-account: Gitea fiók eltávolítása
|
||||||
settings.delete-account: Fiók törlése
|
settings.delete-account: Fiók törlése
|
||||||
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
|
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
|
||||||
@@ -106,6 +117,12 @@ settings.delete-ssh-key-confirm: Erősítsd meg az SSH kulcs törlését
|
|||||||
settings.ssh-key-added-at: "Hozzáadva:"
|
settings.ssh-key-added-at: "Hozzáadva:"
|
||||||
settings.ssh-key-never-used: Sosem használt
|
settings.ssh-key-never-used: Sosem használt
|
||||||
settings.ssh-key-last-used: "Utoljára használva:"
|
settings.ssh-key-last-used: "Utoljára használva:"
|
||||||
|
settings.change-username: Felhasználónév megváltoztatása
|
||||||
|
settings.create-password: Jelszó létrehozása
|
||||||
|
settings.create-password-help: Hozz létre egy jelszót, hogy bejelentkezhess az OpenGist-be HTTP-n keresztül
|
||||||
|
settings.change-password: Jelszó megváltoztatása
|
||||||
|
settings.change-password-help: Változtasd meg a jelszót, amivel bejelentkezel az OpenGist-be HTTP-n keresztül
|
||||||
|
settings.password-label-title: Jelszó
|
||||||
|
|
||||||
auth.signup-disabled: Az adminisztrátor kikapcsolta a regisztrációkat
|
auth.signup-disabled: Az adminisztrátor kikapcsolta a regisztrációkat
|
||||||
auth.login: Bejelentkezés
|
auth.login: Bejelentkezés
|
||||||
@@ -115,8 +132,7 @@ auth.username: Felhasználónév
|
|||||||
auth.password: Jelszó
|
auth.password: Jelszó
|
||||||
auth.register-instead: Vagy regisztrálj
|
auth.register-instead: Vagy regisztrálj
|
||||||
auth.login-instead: Vagy jelentkezz be
|
auth.login-instead: Vagy jelentkezz be
|
||||||
auth.github-oauth: Folytatás GitHub fiókkal
|
auth.oauth: Folytatás %s fiókkal
|
||||||
auth.gitea-oauth: Folytatás Gitea fiókkal
|
|
||||||
|
|
||||||
error: Hiba
|
error: Hiba
|
||||||
|
|
||||||
@@ -152,6 +168,9 @@ admin.actions: Műveletek
|
|||||||
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
|
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
|
||||||
admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
|
admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
|
||||||
admin.actions.git-gc: Használatlan git repository-k eltávolítása
|
admin.actions.git-gc: Használatlan git repository-k eltávolítása
|
||||||
|
admin.actions.sync-previews: Gist előnézetek szinkronizálása
|
||||||
|
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
|
||||||
|
admin.actions.index-gists: Gistek indexelése
|
||||||
admin.id: Azonosító
|
admin.id: Azonosító
|
||||||
admin.user: Felhasználó
|
admin.user: Felhasználó
|
||||||
admin.delete: Törlés
|
admin.delete: Törlés
|
||||||
@@ -167,7 +186,8 @@ admin.disable-login: Bejelentkezés űrlap letiltása
|
|||||||
admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette.
|
admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette.
|
||||||
admin.disable-gravatar: Gravatar kikapcsolása
|
admin.disable-gravatar: Gravatar kikapcsolása
|
||||||
admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató.
|
admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató.
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót?
|
admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót?
|
||||||
|
|
||||||
admin.gists.title: Cím
|
admin.gists.title: Cím
|
||||||
@@ -175,3 +195,66 @@ admin.gists.private: Privát ?
|
|||||||
admin.gists.nb-files: Fájlok száma
|
admin.gists.nb-files: Fájlok száma
|
||||||
admin.gists.nb-likes: Kedv. száma
|
admin.gists.nb-likes: Kedv. száma
|
||||||
admin.gists.delete_confirm: Biztosan törlöd a gistet?
|
admin.gists.delete_confirm: Biztosan törlöd a gistet?
|
||||||
|
gist.new.preview: ''
|
||||||
|
gist.new.create-a-new-gist: ''
|
||||||
|
gist.edit.edit-gist: ''
|
||||||
|
gist.list.all-liked-by: ''
|
||||||
|
gist.list.all-forked-by: ''
|
||||||
|
gist.list.all-from: ''
|
||||||
|
gist.forks.for: ''
|
||||||
|
gist.likes.for: ''
|
||||||
|
gist.revision-of: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
error.bad-request: ''
|
||||||
|
error.signup-disabled: ''
|
||||||
|
error.signup-disabled-form: ''
|
||||||
|
error.login-disabled-form: ''
|
||||||
|
error.complete-oauth-login: ''
|
||||||
|
error.oauth-unsupported: ''
|
||||||
|
error.cannot-bind-data: ''
|
||||||
|
error.invalid-number: ''
|
||||||
|
error.invalid-character-unescaped: ''
|
||||||
|
admin.invitations: ''
|
||||||
|
admin.invitations.create: ''
|
||||||
|
admin.invitations.help: ''
|
||||||
|
admin.invitations.max_uses: ''
|
||||||
|
admin.invitations.expires_at: ''
|
||||||
|
admin.invitations.code: ''
|
||||||
|
admin.invitations.copy_link: ''
|
||||||
|
admin.invitations.uses: ''
|
||||||
|
admin.invitations.expired: ''
|
||||||
|
flash.admin.user-deleted: ''
|
||||||
|
flash.admin.gist-deleted: ''
|
||||||
|
flash.admin.invitation-created: ''
|
||||||
|
flash.admin.invitation-deleted: ''
|
||||||
|
flash.admin.sync-fs: ''
|
||||||
|
flash.admin.sync-db: ''
|
||||||
|
flash.admin.git-gc: ''
|
||||||
|
flash.admin.sync-previews: ''
|
||||||
|
flash.admin.reset-hooks: ''
|
||||||
|
flash.admin.index-gists: ''
|
||||||
|
flash.auth.username-exists: ''
|
||||||
|
flash.auth.invalid-credentials: ''
|
||||||
|
flash.auth.account-linked-oauth: ''
|
||||||
|
flash.auth.account-unlinked-oauth: ''
|
||||||
|
flash.auth.user-sshkeys-not-retrievable: ''
|
||||||
|
flash.auth.user-sshkeys-not-created: ''
|
||||||
|
flash.auth.must-be-logged-in: ''
|
||||||
|
flash.gist.visibility-changed: ''
|
||||||
|
flash.gist.deleted: ''
|
||||||
|
flash.gist.fork-own-gist: ''
|
||||||
|
flash.gist.forked: ''
|
||||||
|
flash.user.email-updated: ''
|
||||||
|
flash.user.invalid-ssh-key: ''
|
||||||
|
flash.user.ssh-key-added: ''
|
||||||
|
flash.user.ssh-key-deleted: ''
|
||||||
|
flash.user.password-updated: ''
|
||||||
|
flash.user.username-updated: ''
|
||||||
|
validation.is-too-long: ''
|
||||||
|
validation.should-not-be-empty: ''
|
||||||
|
validation.should-not-include-sub-directory: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||||
|
validation.not-enough: ''
|
||||||
|
validation.invalid: ''
|
||||||
|
html.title.admin-panel: ''
|
||||||
|
|||||||
269
internal/i18n/locales/it_IT.yml
Normal file
269
internal/i18n/locales/it_IT.yml
Normal 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à
|
||||||
259
internal/i18n/locales/pt-BR.yml
Normal file
259
internal/i18n/locales/pt-BR.yml
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
gist.public: Público
|
||||||
|
gist.unlisted: Não listado
|
||||||
|
gist.private: Privado
|
||||||
|
|
||||||
|
gist.header.like: Curtir
|
||||||
|
gist.header.unlike: Não curtir
|
||||||
|
gist.header.fork: Bifurcar
|
||||||
|
gist.header.edit: Editar
|
||||||
|
gist.header.delete: Excluir
|
||||||
|
gist.header.forked-from: Bifurcado de
|
||||||
|
gist.header.last-active: Última atividade
|
||||||
|
gist.header.select-tab: Selecionar aba
|
||||||
|
gist.header.code: Código
|
||||||
|
gist.header.revisions: Revisões
|
||||||
|
gist.header.revision: Revisão
|
||||||
|
gist.header.clone-http: Clonar via %s
|
||||||
|
gist.header.clone-http-help: Clonar com Git usando autenticação básica HTTP.
|
||||||
|
gist.header.clone-ssh: Clonar via SSH
|
||||||
|
gist.header.clone-ssh-help: Clonar com Git usando uma chave SSH.
|
||||||
|
gist.header.download-zip: Baixar ZIP
|
||||||
|
|
||||||
|
gist.raw: Bruto
|
||||||
|
gist.file-truncated: Este arquivo foi truncado.
|
||||||
|
gist.watch-full-file: Ver arquivo completo.
|
||||||
|
gist.file-not-valid: Este arquivo não é um arquivo CSV válido.
|
||||||
|
gist.no-content: Sem conteúdo
|
||||||
|
|
||||||
|
gist.new.new_gist: Novo gist
|
||||||
|
gist.new.title: Título
|
||||||
|
gist.new.description: Descrição
|
||||||
|
gist.new.filename-with-extension: Nome do arquivo com extensão
|
||||||
|
gist.new.indent-mode: Modo de indentação
|
||||||
|
gist.new.indent-mode-space: Espaço
|
||||||
|
gist.new.indent-mode-tab: Tabulação
|
||||||
|
gist.new.indent-size: Tamanho da indentação
|
||||||
|
gist.new.wrap-mode: Modo de quebra
|
||||||
|
gist.new.wrap-mode-no: Sem quebra
|
||||||
|
gist.new.wrap-mode-soft: Quebra suave
|
||||||
|
gist.new.add-file: Adicionar arquivo
|
||||||
|
gist.new.create-public-button: Criar gist público
|
||||||
|
gist.new.create-unlisted-button: Criar gist não listado
|
||||||
|
gist.new.create-private-button: Criar gist privado
|
||||||
|
|
||||||
|
gist.edit.editing: Editando
|
||||||
|
gist.edit.change-visibility: Alterar visibilidade
|
||||||
|
gist.edit.delete: Excluir
|
||||||
|
gist.edit.cancel: Cancelar
|
||||||
|
gist.edit.save: Salvar
|
||||||
|
|
||||||
|
gist.list.joined: Juntou-se
|
||||||
|
gist.list.all: Todos os gists
|
||||||
|
gist.list.search-results: Resultados da busca
|
||||||
|
gist.list.sort: Ordenar
|
||||||
|
gist.list.sort-by-created: criado
|
||||||
|
gist.list.sort-by-updated: atualizado
|
||||||
|
gist.list.order-by-asc: Menos recente
|
||||||
|
gist.list.order-by-desc: Mais recente
|
||||||
|
gist.list.select-tab: Selecionar aba
|
||||||
|
gist.list.liked: Curtido
|
||||||
|
gist.list.likes: curtidas
|
||||||
|
gist.list.forked: Bifurcado
|
||||||
|
gist.list.forked-from: Bifurcado de
|
||||||
|
gist.list.forks: bifurcações
|
||||||
|
gist.list.files: arquivos
|
||||||
|
gist.list.last-active: Última atividade
|
||||||
|
gist.list.no-gists: Sem gists
|
||||||
|
|
||||||
|
gist.forks: Bifurcações
|
||||||
|
gist.forks.view: Ver bifurcação
|
||||||
|
gist.forks.no: Não há bifurcações públicas
|
||||||
|
|
||||||
|
gist.likes: Curtidas
|
||||||
|
gist.likes.no: Ainda não há curtidas
|
||||||
|
|
||||||
|
gist.revisions: Revisões
|
||||||
|
gist.revision.revised: revisou este gist
|
||||||
|
gist.revision.go-to-revision: Ir para a revisão
|
||||||
|
gist.revision.file-created: arquivo criado
|
||||||
|
gist.revision.file-deleted: arquivo excluído
|
||||||
|
gist.revision.file-renamed: renomeado para
|
||||||
|
gist.revision.diff-truncated: Diferença truncada porque é muito grande para ser exibida.
|
||||||
|
gist.revision.file-renamed-no-changes: Arquivo renomeado sem alterações
|
||||||
|
gist.revision.empty-file: Arquivo vazio
|
||||||
|
gist.revision.no-changes: Sem alterações
|
||||||
|
gist.revision.no-revisions: Não há revisões para mostrar
|
||||||
|
|
||||||
|
settings: Configurações
|
||||||
|
settings.email: E-mail
|
||||||
|
settings.email-help: Usado para confirmações e Gravatar
|
||||||
|
settings.email-set: Configurar e-mail
|
||||||
|
settings.link-accounts: Vincular contas
|
||||||
|
settings.link-github-account: Vincular conta do GitHub
|
||||||
|
settings.link-gitea-account: Vincular conta do Gitea
|
||||||
|
settings.unlink-github-account: Desvincular conta do GitHub
|
||||||
|
settings.unlink-gitea-account: Desvincular conta do Gitea
|
||||||
|
settings.delete-account: Excluir conta
|
||||||
|
settings.delete-account-confirm: Tem certeza de que deseja excluir sua conta?
|
||||||
|
settings.add-ssh-key: Adicionar chave SSH
|
||||||
|
settings.add-ssh-key-help: Usado apenas para extrair/puxar gists usando Git via SSH
|
||||||
|
settings.add-ssh-key-title: Título
|
||||||
|
settings.add-ssh-key-content: Chave
|
||||||
|
settings.delete-ssh-key: Excluir
|
||||||
|
settings.delete-ssh-key-confirm: Confirmar exclusão da chave SSH
|
||||||
|
settings.ssh-key-added-at: Adicionado
|
||||||
|
settings.ssh-key-never-used: Nunca usado
|
||||||
|
settings.ssh-key-last-used: Último uso
|
||||||
|
|
||||||
|
auth.signup-disabled: O administrador desabilitou o registro
|
||||||
|
auth.login: Entrar
|
||||||
|
auth.signup: Cadastrar-se
|
||||||
|
auth.new-account: Nova conta
|
||||||
|
auth.username: Nome de usuário
|
||||||
|
auth.password: Senha
|
||||||
|
auth.register-instead: Registrar-se no lugar
|
||||||
|
auth.login-instead: Entrar no lugar
|
||||||
|
auth.oauth: Continuar com conta do %s
|
||||||
|
|
||||||
|
error: Erro
|
||||||
|
|
||||||
|
header.menu.all: Todos
|
||||||
|
header.menu.new: Novo
|
||||||
|
header.menu.search: Buscar
|
||||||
|
header.menu.my-gists: Meus gists
|
||||||
|
header.menu.liked: Curtidos
|
||||||
|
header.menu.admin: Administrador
|
||||||
|
header.menu.settings: Configurações
|
||||||
|
header.menu.logout: Sair
|
||||||
|
header.menu.register: Registrar-se
|
||||||
|
header.menu.login: Entrar
|
||||||
|
header.menu.light: Claro
|
||||||
|
header.menu.dark: Escuro
|
||||||
|
header.menu.system: Sistema
|
||||||
|
footer.powered-by: Desenvolvido por %s
|
||||||
|
|
||||||
|
pagination.older: Anterior
|
||||||
|
pagination.newer: Próximo
|
||||||
|
pagination.previous: Anterior
|
||||||
|
pagination.next: Próximo
|
||||||
|
|
||||||
|
admin.admin_panel: Painel de administração
|
||||||
|
admin.general: Geral
|
||||||
|
admin.users: Usuários
|
||||||
|
admin.gists: Gists
|
||||||
|
admin.configuration: Configuração
|
||||||
|
admin.versions: Versões
|
||||||
|
admin.ssh_keys: Chaves SSH
|
||||||
|
admin.stats: Estatísticas
|
||||||
|
admin.actions: Ações
|
||||||
|
admin.actions.sync-fs: Sincronizar gists do sistema de arquivos
|
||||||
|
admin.actions.sync-db: Sincronizar gists do banco de dados
|
||||||
|
admin.actions.git-gc: Coletar lixo nos repositórios Git
|
||||||
|
admin.id: ID
|
||||||
|
admin.user: Usuário
|
||||||
|
admin.delete: Excluir
|
||||||
|
admin.created_at: Criado
|
||||||
|
|
||||||
|
admin.config-link: Esta configuração pode ser %s por um arquivo de configuração YAML e/ou variáveis de ambiente.
|
||||||
|
admin.disable-signup: Desabilitar registro
|
||||||
|
admin.disable-signup_help: Proibir a criação de novas contas.
|
||||||
|
admin.require-login: Exigir login
|
||||||
|
admin.require-login_help: Obrigar os usuários a fazerem login para ver gists.
|
||||||
|
admin.disable-login: Desabilitar formulário de login
|
||||||
|
admin.disable-login_help: Proibir o login através do formulário de login para forçar o uso de provedores de OAuth no lugar.
|
||||||
|
admin.disable-gravatar: Desabilitar Gravatar
|
||||||
|
admin.disable-gravatar_help: Desabilitar o uso do Gravatar como provedor de avatar.
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
|
admin.users.delete_confirm: Quer excluir este usuário?
|
||||||
|
|
||||||
|
admin.gists.title: Título
|
||||||
|
admin.gists.private: Privado
|
||||||
|
admin.gists.nb-files: Núm. de arquivos
|
||||||
|
admin.gists.nb-likes: Núm. de curtidas
|
||||||
|
admin.gists.delete_confirm: Quer excluir este gist?
|
||||||
|
flash.admin.index-gists: ''
|
||||||
|
gist.header.embed: ''
|
||||||
|
gist.header.embed-help: ''
|
||||||
|
gist.new.url: ''
|
||||||
|
gist.list.all-liked-by: ''
|
||||||
|
gist.new.preview: ''
|
||||||
|
gist.new.create-a-new-gist: ''
|
||||||
|
gist.edit.edit-gist: ''
|
||||||
|
gist.list.all-forked-by: ''
|
||||||
|
gist.list.all-from: ''
|
||||||
|
gist.search.found: ''
|
||||||
|
gist.search.no-results: ''
|
||||||
|
gist.search.help.user: ''
|
||||||
|
gist.search.help.title: ''
|
||||||
|
gist.search.help.filename: ''
|
||||||
|
gist.search.help.extension: ''
|
||||||
|
gist.search.help.language: ''
|
||||||
|
gist.forks.for: ''
|
||||||
|
gist.likes.for: ''
|
||||||
|
gist.revision-of: ''
|
||||||
|
settings.link-gitlab-account: ''
|
||||||
|
settings.unlink-gitlab-account: ''
|
||||||
|
settings.change-username: ''
|
||||||
|
settings.create-password: ''
|
||||||
|
settings.create-password-help: ''
|
||||||
|
settings.change-password: ''
|
||||||
|
settings.change-password-help: ''
|
||||||
|
settings.password-label-title: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
error.bad-request: ''
|
||||||
|
error.signup-disabled: ''
|
||||||
|
error.signup-disabled-form: ''
|
||||||
|
error.login-disabled-form: ''
|
||||||
|
error.complete-oauth-login: ''
|
||||||
|
error.oauth-unsupported: ''
|
||||||
|
error.cannot-bind-data: ''
|
||||||
|
error.invalid-number: ''
|
||||||
|
error.invalid-character-unescaped: ''
|
||||||
|
admin.invitations: ''
|
||||||
|
admin.invitations.create: ''
|
||||||
|
admin.actions.sync-previews: ''
|
||||||
|
admin.actions.reset-hooks: ''
|
||||||
|
admin.actions.index-gists: ''
|
||||||
|
admin.config-link-overriden: ''
|
||||||
|
validation.invalid: ''
|
||||||
|
admin.invitations.help: ''
|
||||||
|
admin.invitations.max_uses: ''
|
||||||
|
admin.invitations.expires_at: ''
|
||||||
|
admin.invitations.code: ''
|
||||||
|
admin.invitations.copy_link: ''
|
||||||
|
admin.invitations.uses: ''
|
||||||
|
admin.invitations.expired: ''
|
||||||
|
flash.admin.user-deleted: ''
|
||||||
|
flash.admin.gist-deleted: ''
|
||||||
|
flash.admin.invitation-created: ''
|
||||||
|
flash.admin.invitation-deleted: ''
|
||||||
|
flash.admin.sync-fs: ''
|
||||||
|
flash.admin.sync-db: ''
|
||||||
|
flash.admin.git-gc: ''
|
||||||
|
flash.admin.sync-previews: ''
|
||||||
|
flash.admin.reset-hooks: ''
|
||||||
|
flash.auth.username-exists: ''
|
||||||
|
flash.auth.invalid-credentials: ''
|
||||||
|
flash.auth.account-linked-oauth: ''
|
||||||
|
flash.auth.account-unlinked-oauth: ''
|
||||||
|
flash.auth.user-sshkeys-not-retrievable: ''
|
||||||
|
flash.auth.user-sshkeys-not-created: ''
|
||||||
|
flash.auth.must-be-logged-in: ''
|
||||||
|
flash.gist.visibility-changed: ''
|
||||||
|
flash.gist.deleted: ''
|
||||||
|
flash.gist.fork-own-gist: ''
|
||||||
|
flash.gist.forked: ''
|
||||||
|
flash.user.email-updated: ''
|
||||||
|
flash.user.invalid-ssh-key: ''
|
||||||
|
flash.user.ssh-key-added: ''
|
||||||
|
flash.user.ssh-key-deleted: ''
|
||||||
|
flash.user.password-updated: ''
|
||||||
|
flash.user.username-updated: ''
|
||||||
|
validation.is-too-long: ''
|
||||||
|
validation.should-not-be-empty: ''
|
||||||
|
validation.should-not-include-sub-directory: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||||
|
validation.not-enough: ''
|
||||||
|
html.title.admin-panel: ''
|
||||||
@@ -17,8 +17,8 @@ gist.header.clone-http: Клонировать с помощью %s
|
|||||||
gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP.
|
gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP.
|
||||||
gist.header.clone-ssh: Клонировать c помощью SSH
|
gist.header.clone-ssh: Клонировать c помощью SSH
|
||||||
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
|
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
|
||||||
gist.header.share: Поделиться
|
gist.header.embed: 'Встроить'
|
||||||
gist.header.share-help: Скопировать ссылку на фрагмент.
|
gist.header.embed-help: 'Встроить этот фрагмент в ваш веб-сайт.'
|
||||||
gist.header.download-zip: Скачать ZIP-архив
|
gist.header.download-zip: Скачать ZIP-архив
|
||||||
|
|
||||||
gist.raw: Исходник
|
gist.raw: Исходник
|
||||||
@@ -115,8 +115,7 @@ auth.username: Имя пользователя
|
|||||||
auth.password: Пароль
|
auth.password: Пароль
|
||||||
auth.register-instead: Зарегистрироваться
|
auth.register-instead: Зарегистрироваться
|
||||||
auth.login-instead: Войти
|
auth.login-instead: Войти
|
||||||
auth.github-oauth: Войти с помощью доступа GitHub
|
auth.oauth: Войти с помощью доступа %s
|
||||||
auth.gitea-oauth: Войти с помощью доступа Gitea
|
|
||||||
|
|
||||||
error: Ошибка
|
error: Ошибка
|
||||||
|
|
||||||
@@ -167,7 +166,8 @@ admin.disable-login: Запретить авторизацию по паролю
|
|||||||
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
|
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
|
||||||
admin.disable-gravatar: Запретить Gravatar
|
admin.disable-gravatar: Запретить Gravatar
|
||||||
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
|
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
||||||
|
|
||||||
admin.gists.title: Название
|
admin.gists.title: Название
|
||||||
@@ -175,3 +175,85 @@ admin.gists.private: Приватный
|
|||||||
admin.gists.nb-files: Файлов
|
admin.gists.nb-files: Файлов
|
||||||
admin.gists.nb-likes: Понравилось
|
admin.gists.nb-likes: Понравилось
|
||||||
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
||||||
|
gist.new.url: 'URL'
|
||||||
|
gist.new.preview: 'Предпросмотр'
|
||||||
|
gist.new.create-a-new-gist: 'Создать новый фрагмент'
|
||||||
|
gist.edit.edit-gist: 'Редактировать %s'
|
||||||
|
gist.list.all-liked-by: 'Все фрагменты, понравившиеся %s'
|
||||||
|
gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s'
|
||||||
|
gist.list.all-from: 'Все фрагменты от %s'
|
||||||
|
gist.search.found: 'фрагментов найдено'
|
||||||
|
gist.search.no-results: 'Не найден ни один фрагмент'
|
||||||
|
gist.search.help.user: 'фрагментов создано пользователем'
|
||||||
|
gist.search.help.title: ''
|
||||||
|
gist.search.help.filename: ''
|
||||||
|
gist.search.help.extension: ''
|
||||||
|
gist.search.help.language: ''
|
||||||
|
gist.forks.for: ''
|
||||||
|
gist.likes.for: ''
|
||||||
|
gist.revision-of: ''
|
||||||
|
settings.link-gitlab-account: ''
|
||||||
|
settings.unlink-gitlab-account: ''
|
||||||
|
settings.change-username: ''
|
||||||
|
settings.create-password: ''
|
||||||
|
settings.create-password-help: ''
|
||||||
|
settings.change-password: ''
|
||||||
|
settings.change-password-help: ''
|
||||||
|
settings.password-label-title: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
error.bad-request: ''
|
||||||
|
error.signup-disabled: ''
|
||||||
|
error.signup-disabled-form: ''
|
||||||
|
error.login-disabled-form: ''
|
||||||
|
error.complete-oauth-login: ''
|
||||||
|
error.oauth-unsupported: ''
|
||||||
|
error.cannot-bind-data: ''
|
||||||
|
error.invalid-number: ''
|
||||||
|
error.invalid-character-unescaped: ''
|
||||||
|
admin.invitations: ''
|
||||||
|
admin.invitations.create: ''
|
||||||
|
admin.actions.sync-previews: ''
|
||||||
|
admin.actions.reset-hooks: ''
|
||||||
|
admin.actions.index-gists: ''
|
||||||
|
validation.should-not-be-empty: ''
|
||||||
|
admin.invitations.help: ''
|
||||||
|
admin.invitations.max_uses: ''
|
||||||
|
admin.invitations.expires_at: ''
|
||||||
|
admin.invitations.code: ''
|
||||||
|
admin.invitations.copy_link: ''
|
||||||
|
admin.invitations.uses: ''
|
||||||
|
admin.invitations.expired: ''
|
||||||
|
flash.admin.user-deleted: ''
|
||||||
|
flash.admin.gist-deleted: ''
|
||||||
|
flash.admin.invitation-created: ''
|
||||||
|
flash.admin.invitation-deleted: ''
|
||||||
|
flash.admin.sync-fs: ''
|
||||||
|
flash.admin.sync-db: ''
|
||||||
|
flash.admin.git-gc: ''
|
||||||
|
flash.admin.sync-previews: ''
|
||||||
|
flash.admin.reset-hooks: ''
|
||||||
|
flash.admin.index-gists: ''
|
||||||
|
flash.auth.username-exists: ''
|
||||||
|
flash.auth.invalid-credentials: ''
|
||||||
|
flash.auth.account-linked-oauth: ''
|
||||||
|
flash.auth.account-unlinked-oauth: ''
|
||||||
|
flash.auth.user-sshkeys-not-retrievable: ''
|
||||||
|
flash.auth.user-sshkeys-not-created: ''
|
||||||
|
flash.auth.must-be-logged-in: ''
|
||||||
|
flash.gist.visibility-changed: ''
|
||||||
|
flash.gist.deleted: ''
|
||||||
|
flash.gist.fork-own-gist: ''
|
||||||
|
flash.gist.forked: ''
|
||||||
|
flash.user.email-updated: ''
|
||||||
|
flash.user.invalid-ssh-key: ''
|
||||||
|
flash.user.ssh-key-added: ''
|
||||||
|
flash.user.ssh-key-deleted: ''
|
||||||
|
flash.user.password-updated: ''
|
||||||
|
flash.user.username-updated: ''
|
||||||
|
validation.is-too-long: ''
|
||||||
|
validation.should-not-include-sub-directory: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters: ''
|
||||||
|
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||||
|
validation.not-enough: ''
|
||||||
|
validation.invalid: ''
|
||||||
|
html.title.admin-panel: ''
|
||||||
|
|||||||
267
internal/i18n/locales/tr-TR.yml
Normal file
267
internal/i18n/locales/tr-TR.yml
Normal 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
|
||||||
269
internal/i18n/locales/uk-UK.yml
Normal file
269
internal/i18n/locales/uk-UK.yml
Normal 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: Панель адміністратора
|
||||||
@@ -17,8 +17,8 @@ gist.header.clone-http: 通过 %s 克隆
|
|||||||
gist.header.clone-http-help: 使用 Git 通过 HTTP 基础认证克隆。
|
gist.header.clone-http-help: 使用 Git 通过 HTTP 基础认证克隆。
|
||||||
gist.header.clone-ssh: 通过 SSH 克隆
|
gist.header.clone-ssh: 通过 SSH 克隆
|
||||||
gist.header.clone-ssh-help: 使用 Git 通过 SSH 密钥克隆。
|
gist.header.clone-ssh-help: 使用 Git 通过 SSH 密钥克隆。
|
||||||
gist.header.share: 分享
|
gist.header.embed: ''
|
||||||
gist.header.share-help: 为此 Gist 复制可供分享的链接。
|
gist.header.embed-help: '在你的网页中嵌入此gist。'
|
||||||
gist.header.download-zip: 下载 ZIP
|
gist.header.download-zip: 下载 ZIP
|
||||||
|
|
||||||
gist.raw: 原始文件
|
gist.raw: 原始文件
|
||||||
@@ -57,7 +57,7 @@ gist.list.sort-by-created: 创建
|
|||||||
gist.list.sort-by-updated: 更新
|
gist.list.sort-by-updated: 更新
|
||||||
gist.list.order-by-asc: 最早
|
gist.list.order-by-asc: 最早
|
||||||
gist.list.order-by-desc: 最近
|
gist.list.order-by-desc: 最近
|
||||||
gist.list.select-tab: Select a tab
|
gist.list.select-tab: 选择一个标签
|
||||||
gist.list.liked: 已喜欢
|
gist.list.liked: 已喜欢
|
||||||
gist.list.likes: 喜欢
|
gist.list.likes: 喜欢
|
||||||
gist.list.forked: 已派生
|
gist.list.forked: 已派生
|
||||||
@@ -115,8 +115,7 @@ auth.username: 用户名
|
|||||||
auth.password: 密码
|
auth.password: 密码
|
||||||
auth.register-instead: 转到注册
|
auth.register-instead: 转到注册
|
||||||
auth.login-instead: 转到登录
|
auth.login-instead: 转到登录
|
||||||
auth.github-oauth: 使用 GitHub 账号继续
|
auth.oauth: 使用 %s 账号继续
|
||||||
auth.gitea-oauth: 使用 Gitea 账号继续
|
|
||||||
|
|
||||||
error: 错误
|
error: 错误
|
||||||
|
|
||||||
@@ -167,7 +166,8 @@ admin.disable-login: 禁用登录表单
|
|||||||
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
|
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
|
||||||
admin.disable-gravatar: 禁用 Gravatar
|
admin.disable-gravatar: 禁用 Gravatar
|
||||||
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
|
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
admin.users.delete_confirm: 你想要删除此用户吗?
|
admin.users.delete_confirm: 你想要删除此用户吗?
|
||||||
|
|
||||||
admin.gists.title: 标题
|
admin.gists.title: 标题
|
||||||
@@ -175,3 +175,85 @@ admin.gists.private: 私有?
|
|||||||
admin.gists.nb-files: 文件数
|
admin.gists.nb-files: 文件数
|
||||||
admin.gists.nb-likes: 喜欢数
|
admin.gists.nb-likes: 喜欢数
|
||||||
admin.gists.delete_confirm: 你想要删除此 Gist 吗?
|
admin.gists.delete_confirm: 你想要删除此 Gist 吗?
|
||||||
|
gist.new.url: 'URL'
|
||||||
|
gist.new.preview: ''
|
||||||
|
error.page-not-found: ''
|
||||||
|
gist.new.create-a-new-gist: '创建一个新的gist'
|
||||||
|
gist.edit.edit-gist: ''
|
||||||
|
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: ''
|
||||||
|
|||||||
259
internal/i18n/locales/zh-TW.yml
Normal file
259
internal/i18n/locales/zh-TW.yml
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
gist.public: 公開
|
||||||
|
gist.unlisted: 非公開
|
||||||
|
gist.private: 私人
|
||||||
|
|
||||||
|
gist.header.like: 喜歡
|
||||||
|
gist.header.unlike: 不喜歡
|
||||||
|
gist.header.fork: 分支
|
||||||
|
gist.header.edit: 編輯
|
||||||
|
gist.header.delete: 刪除
|
||||||
|
gist.header.forked-from: 分支自
|
||||||
|
gist.header.last-active: 最後活躍
|
||||||
|
gist.header.select-tab: 選擇分頁
|
||||||
|
gist.header.code: 程式碼
|
||||||
|
gist.header.revisions: 修訂記錄
|
||||||
|
gist.header.revision: 修訂
|
||||||
|
gist.header.clone-http: 透過 %s 複製
|
||||||
|
gist.header.clone-http-help: 使用 HTTP 基本認證透過 Git 複製。
|
||||||
|
gist.header.clone-ssh: 透過 SSH 複製
|
||||||
|
gist.header.clone-ssh-help: 使用 SSH 金鑰透過 Git 複製。
|
||||||
|
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: 自定義 Gist 網址
|
||||||
|
gist.new.filename-with-extension: 含副檔名的檔案名稱
|
||||||
|
gist.new.indent-mode: 縮排模式
|
||||||
|
gist.new.indent-mode-space: 空格
|
||||||
|
gist.new.indent-mode-tab: 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.edit.editing: 編輯中
|
||||||
|
gist.edit.change-visibility: 更改可見性
|
||||||
|
gist.edit.delete: 刪除
|
||||||
|
gist.edit.cancel: 取消
|
||||||
|
gist.edit.save: 保存
|
||||||
|
|
||||||
|
gist.list.joined: 加入
|
||||||
|
gist.list.all: 所有 Gists
|
||||||
|
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: 喜歡的 Gists
|
||||||
|
gist.list.likes: 喜歡
|
||||||
|
gist.list.forked: 分支
|
||||||
|
gist.list.forked-from: 分支自
|
||||||
|
gist.list.forks: 分支
|
||||||
|
gist.list.files: 檔案
|
||||||
|
gist.list.last-active: 最後活躍
|
||||||
|
gist.list.no-gists: 沒有任何的 Gist
|
||||||
|
|
||||||
|
gist.forks: 分支
|
||||||
|
gist.forks.view: 查看分支
|
||||||
|
gist.forks.no: 沒有任何公開的分支
|
||||||
|
|
||||||
|
gist.likes: 喜歡
|
||||||
|
gist.likes.no: 目前還沒有任何人喜歡
|
||||||
|
|
||||||
|
gist.revisions: 修訂版本
|
||||||
|
gist.revision.revised: 已修改
|
||||||
|
gist.revision.go-to-revision: 還原成這個修訂版本
|
||||||
|
gist.revision.file-created: 檔案已創建
|
||||||
|
gist.revision.file-deleted: 檔案已刪除
|
||||||
|
gist.revision.file-renamed: 重命名為
|
||||||
|
gist.revision.diff-truncated: 差異太大無法顯示
|
||||||
|
gist.revision.file-renamed-no-changes: 檔案名稱與重新命名前相同
|
||||||
|
gist.revision.empty-file: 檔案為空
|
||||||
|
gist.revision.no-changes: 沒有任何變更
|
||||||
|
gist.revision.no-revisions: 沒有任何修訂版可顯示
|
||||||
|
|
||||||
|
settings: 設定
|
||||||
|
settings.email: 電子郵件
|
||||||
|
settings.email-help: 用於提交和 Gravatar
|
||||||
|
settings.email-set: 設定電子郵件
|
||||||
|
settings.link-accounts: 連結帳號
|
||||||
|
settings.link-github-account: 連結 GitHub 帳號
|
||||||
|
settings.link-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: 僅用於藉由 SSH 使用 Git 拉取/推送 Gist
|
||||||
|
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.change-username: 變更使用者名稱
|
||||||
|
settings.create-password: 創建密碼
|
||||||
|
settings.create-password-help: 創建您的密碼以通過 HTTP 登錄到 Opengist
|
||||||
|
settings.change-password: 更改密碼
|
||||||
|
settings.change-password-help: 更改您的密碼以通過 HTTP 登錄到 Opengist
|
||||||
|
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: 錯誤
|
||||||
|
|
||||||
|
header.menu.all: 全部
|
||||||
|
header.menu.new: 新建
|
||||||
|
header.menu.search: 搜索
|
||||||
|
header.menu.my-gists: 我的 Gists
|
||||||
|
header.menu.liked: 喜歡的 Gists
|
||||||
|
header.menu.admin: 管理
|
||||||
|
header.menu.settings: 設定
|
||||||
|
header.menu.logout: 登出
|
||||||
|
header.menu.register: 註冊
|
||||||
|
header.menu.login: 登錄
|
||||||
|
header.menu.light: 亮色
|
||||||
|
header.menu.dark: 暗色
|
||||||
|
header.menu.system: 系統
|
||||||
|
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.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 伺服器所有儲存庫的 Git hooks
|
||||||
|
admin.id: ID
|
||||||
|
admin.user: 使用者
|
||||||
|
admin.delete: 刪除
|
||||||
|
admin.created_at: 創建時間
|
||||||
|
|
||||||
|
admin.config-link: 這裡的設定可以通過 YAML 配置檔案或是環境變數 %s。
|
||||||
|
admin.config-link-overriden: 覆蓋
|
||||||
|
admin.disable-signup: 關閉註冊
|
||||||
|
admin.disable-signup_help: 禁止創建新帳號。
|
||||||
|
admin.require-login: 登錄後瀏覽
|
||||||
|
admin.require-login_help: 強制使用者登錄以查看 Gist。
|
||||||
|
admin.disable-login: 關閉登錄頁面
|
||||||
|
admin.disable-login_help: 關閉通過登錄頁面登錄,強制使用 OAuth 提供者。
|
||||||
|
admin.disable-gravatar: 禁用 Gravatar
|
||||||
|
admin.disable-gravatar_help: 禁止使用 Gravatar 作為頭像提供者。
|
||||||
|
admin.allow-gists-without-login:
|
||||||
|
admin.allow-gists-without-login_help:
|
||||||
|
admin.users.delete_confirm: 您要刪除這個使用者嗎?
|
||||||
|
|
||||||
|
admin.gists.title: 標題
|
||||||
|
admin.gists.private: 是否為私人
|
||||||
|
admin.gists.nb-files: 檔案數
|
||||||
|
admin.gists.nb-likes: 喜歡
|
||||||
|
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?
|
||||||
|
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: ''
|
||||||
202
internal/index/bleve.go
Normal file
202
internal/index/bleve.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/blevesearch/bleve/v2"
|
||||||
|
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||||
|
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||||
|
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||||
|
"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 atomicIndexer atomic.Pointer[Indexer]
|
||||||
|
|
||||||
|
type Indexer struct {
|
||||||
|
Index bleve.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enabled() bool {
|
||||||
|
return config.C.IndexEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bleveIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, bleve.ErrorIndexPathDoesNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docMapping := bleve.NewDocumentMapping()
|
||||||
|
docMapping.AddFieldMappingsAt("GistID", bleve.NewNumericFieldMapping())
|
||||||
|
docMapping.AddFieldMappingsAt("Content", bleve.NewTextFieldMapping())
|
||||||
|
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
|
||||||
|
if err = mapping.AddCustomTokenFilter("unicodeNormalize", map[string]any{
|
||||||
|
"type": unicodenorm.Name,
|
||||||
|
"form": unicodenorm.NFC,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = mapping.AddCustomAnalyzer("gistAnalyser", map[string]interface{}{
|
||||||
|
"type": custom.Name,
|
||||||
|
"char_filters": []string{},
|
||||||
|
"tokenizer": unicode.Name,
|
||||||
|
"token_filters": []string{"unicodeNormalize", camelcase.Name, lowercase.Name},
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docMapping.DefaultAnalyzer = "gistAnalyser"
|
||||||
|
|
||||||
|
return bleve.New(indexFilename, mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (*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 (*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
|
||||||
|
if queryStr != "" {
|
||||||
|
contentQuery := bleve.NewMatchPhraseQuery(queryStr)
|
||||||
|
contentQuery.FieldVal = "Content"
|
||||||
|
indexerQuery = contentQuery
|
||||||
|
} else {
|
||||||
|
contentQuery := bleve.NewMatchAllQuery()
|
||||||
|
indexerQuery = contentQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
addQuery := func(field, value string) {
|
||||||
|
if value != "" && value != "." {
|
||||||
|
q := bleve.NewMatchPhraseQuery(value)
|
||||||
|
q.FieldVal = field
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addQuery("Username", queryMetadata.Username)
|
||||||
|
addQuery("Title", queryMetadata.Title)
|
||||||
|
addQuery("Extensions", "."+queryMetadata.Extension)
|
||||||
|
addQuery("Filenames", queryMetadata.Filename)
|
||||||
|
addQuery("Languages", queryMetadata.Language)
|
||||||
|
|
||||||
|
languageFacet := bleve.NewFacetRequest("Languages", 10)
|
||||||
|
|
||||||
|
perPage := 10
|
||||||
|
offset := (page - 1) * perPage
|
||||||
|
|
||||||
|
s := bleve.NewSearchRequestOptions(indexerQuery, perPage, offset, false)
|
||||||
|
s.AddFacet("languageFacet", languageFacet)
|
||||||
|
s.Fields = []string{"GistID"}
|
||||||
|
s.IncludeLocations = false
|
||||||
|
|
||||||
|
results, err := (*atomicIndexer.Load()).Index.Search(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gistIds := make([]uint, 0, len(results.Hits))
|
||||||
|
for _, hit := range results.Hits {
|
||||||
|
gistIds = append(gistIds, uint(hit.Fields["GistID"].(float64)))
|
||||||
|
}
|
||||||
|
|
||||||
|
languageCounts := make(map[string]int)
|
||||||
|
if facets, found := results.Facets["languageFacet"]; found {
|
||||||
|
for _, term := range facets.Terms.Terms() {
|
||||||
|
languageCounts[term.Term] = term.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gistIds, results.Total, languageCounts, nil
|
||||||
|
}
|
||||||
21
internal/index/gist.go
Normal file
21
internal/index/gist.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package index
|
||||||
|
|
||||||
|
type Gist struct {
|
||||||
|
GistID uint
|
||||||
|
Username string
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
Filenames []string
|
||||||
|
Extensions []string
|
||||||
|
Languages []string
|
||||||
|
CreatedAt int64
|
||||||
|
UpdatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchGistMetadata struct {
|
||||||
|
Username string
|
||||||
|
Title string
|
||||||
|
Filename string
|
||||||
|
Extension string
|
||||||
|
Language string
|
||||||
|
}
|
||||||
168
internal/render/highlight.go
Normal file
168
internal/render/highlight.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RenderedFile struct {
|
||||||
|
*git.File
|
||||||
|
Type string `json:"type"`
|
||||||
|
Lines []string `json:"-"`
|
||||||
|
HTML string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedGist struct {
|
||||||
|
*db.Gist
|
||||||
|
Lines []string
|
||||||
|
HTML string
|
||||||
|
}
|
||||||
|
|
||||||
|
func HighlightFile(file *git.File) (RenderedFile, error) {
|
||||||
|
rendered := RenderedFile{
|
||||||
|
File: file,
|
||||||
|
}
|
||||||
|
|
||||||
|
style := newStyle()
|
||||||
|
lexer := newLexer(file.Filename)
|
||||||
|
if lexer.Config().Name == "markdown" {
|
||||||
|
return MarkdownFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||||
|
|
||||||
|
iterator, err := lexer.Tokenise(nil, file.Content+"\n")
|
||||||
|
if err != nil {
|
||||||
|
return rendered, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlbuf := bytes.Buffer{}
|
||||||
|
w := bufio.NewWriter(&htmlbuf)
|
||||||
|
|
||||||
|
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||||
|
lines := make([]string, 0, len(tokensLines))
|
||||||
|
for _, tokens := range tokensLines {
|
||||||
|
iterator = chroma.Literator(tokens...)
|
||||||
|
err = formatter.Format(&htmlbuf, style, iterator)
|
||||||
|
if err != nil {
|
||||||
|
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||||
|
}
|
||||||
|
lines = append(lines, htmlbuf.String())
|
||||||
|
htmlbuf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
rendered.Lines = lines
|
||||||
|
rendered.Type = parseFileTypeName(*lexer.Config())
|
||||||
|
|
||||||
|
return rendered, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func HighlightFiles(files []*git.File) []RenderedFile {
|
||||||
|
const numWorkers = 10
|
||||||
|
jobs := make(chan int, numWorkers)
|
||||||
|
renderedFiles := make([]RenderedFile, len(files))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
worker := func() {
|
||||||
|
for idx := range jobs {
|
||||||
|
rendered, err := HighlightFile(files[idx])
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error rendering gist preview for " + files[idx].Filename)
|
||||||
|
}
|
||||||
|
renderedFiles[idx] = rendered
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go worker()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range files {
|
||||||
|
jobs <- i
|
||||||
|
}
|
||||||
|
close(jobs)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return renderedFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||||
|
rendered := RenderedGist{
|
||||||
|
Gist: gist,
|
||||||
|
}
|
||||||
|
|
||||||
|
style := newStyle()
|
||||||
|
lexer := newLexer(gist.PreviewFilename)
|
||||||
|
if lexer.Config().Name == "markdown" {
|
||||||
|
return MarkdownGistPreview(gist)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||||
|
|
||||||
|
iterator, err := lexer.Tokenise(nil, gist.Preview)
|
||||||
|
if err != nil {
|
||||||
|
return rendered, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlbuf := bytes.Buffer{}
|
||||||
|
w := bufio.NewWriter(&htmlbuf)
|
||||||
|
|
||||||
|
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||||
|
lines := make([]string, 0, len(tokensLines))
|
||||||
|
for _, tokens := range tokensLines {
|
||||||
|
iterator = chroma.Literator(tokens...)
|
||||||
|
err = formatter.Format(&htmlbuf, style, iterator)
|
||||||
|
if err != nil {
|
||||||
|
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||||
|
}
|
||||||
|
lines = append(lines, htmlbuf.String())
|
||||||
|
htmlbuf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
rendered.Lines = lines
|
||||||
|
|
||||||
|
return rendered, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFileTypeName(config chroma.Config) string {
|
||||||
|
fileType := config.Name
|
||||||
|
if fileType == "fallback" || fileType == "plaintext" {
|
||||||
|
return "Text"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileType
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLexer(filename string) chroma.Lexer {
|
||||||
|
var lexer chroma.Lexer
|
||||||
|
if lexer = lexers.Get(filename); lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStyle() *chroma.Style {
|
||||||
|
var style *chroma.Style
|
||||||
|
if style = styles.Get("catppuccin-latte"); style == nil {
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
}
|
||||||
119
internal/render/markdown.go
Normal file
119
internal/render/markdown.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"github.com/Kunde21/markdownfmt/v3"
|
||||||
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
emoji "github.com/yuin/goldmark-emoji"
|
||||||
|
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
astex "github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"go.abhg.dev/goldmark/mermaid"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := newMarkdown().Convert([]byte(gist.Preview), &buf)
|
||||||
|
|
||||||
|
return RenderedGist{
|
||||||
|
Gist: gist,
|
||||||
|
HTML: buf.String(),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarkdownFile(file *git.File) (RenderedFile, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := newMarkdown().Convert([]byte(file.Content), &buf)
|
||||||
|
|
||||||
|
return RenderedFile{
|
||||||
|
File: file,
|
||||||
|
HTML: buf.String(),
|
||||||
|
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(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM,
|
||||||
|
highlighting.NewHighlighting(
|
||||||
|
highlighting.WithStyle("catppuccin-latte"),
|
||||||
|
highlighting.WithFormatOptions(html.WithClasses(true))),
|
||||||
|
emoji.Emoji,
|
||||||
|
&mermaid.Extender{},
|
||||||
|
),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithASTTransformers(
|
||||||
|
util.Prioritized(&CheckboxTransformer{}, 10000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckboxTransformer struct{}
|
||||||
|
|
||||||
|
func (t *CheckboxTransformer) Transform(node *ast.Document, _ text.Reader, _ parser.Context) {
|
||||||
|
i := 1
|
||||||
|
err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if entering {
|
||||||
|
if _, ok := n.(*astex.TaskCheckBox); ok {
|
||||||
|
listitem := n.Parent().Parent()
|
||||||
|
listitem.SetAttribute([]byte("data-checkbox-nb"), []byte(strconv.Itoa(i)))
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Checkbox(content string, checkboxNb int) (string, error) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
w := bufio.NewWriter(&buf)
|
||||||
|
|
||||||
|
source := []byte(content)
|
||||||
|
markdown := markdownfmt.NewGoldmark()
|
||||||
|
reader := text.NewReader(source)
|
||||||
|
document := markdown.Parser().Parse(reader)
|
||||||
|
|
||||||
|
i := 1
|
||||||
|
err := ast.Walk(document, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if entering {
|
||||||
|
if listItem, ok := n.(*astex.TaskCheckBox); ok {
|
||||||
|
if i == checkboxNb {
|
||||||
|
listItem.IsChecked = !listItem.IsChecked
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = markdown.Renderer().Render(w, source, document); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
@@ -2,14 +2,16 @@ package ssh
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/auth"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
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")
|
return errors.New("gist not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
requireLogin, err := db.GetSetting(db.SettingRequireLogin)
|
allowUnauthenticated, err := auth.ShouldAllowUnauthenticatedGistAccess(db.DBAuthInfo{}, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("internal server error")
|
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)
|
// - gist is not found (obfuscation)
|
||||||
// - admin setting to require login is set to true
|
// - admin setting to require login is set to true
|
||||||
if verb == "receive-pack" ||
|
if verb == "receive-pack" ||
|
||||||
gist.Private == 2 ||
|
gist.Private == db.PrivateVisibility ||
|
||||||
gist.ID == 0 ||
|
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
||||||
@@ -94,7 +103,8 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
|||||||
// updatedAt is updated only if serviceType is receive-pack
|
// updatedAt is updated only if serviceType is receive-pack
|
||||||
if verb == "receive-pack" {
|
if verb == "receive-pack" {
|
||||||
_ = gist.SetLastActiveNow()
|
_ = gist.SetLastActiveNow()
|
||||||
_ = gist.UpdatePreviewAndCount()
|
_ = gist.UpdatePreviewAndCount(false)
|
||||||
|
gist.AddInIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ func Start() {
|
|||||||
sshConfig := &ssh.ServerConfig{
|
sshConfig := &ssh.ServerConfig{
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
||||||
_, err := db.SSHKeyDoesExists(strKey)
|
exists, err := db.SSHKeyDoesExists(strKey)
|
||||||
if err != nil {
|
if !exists || err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func listen(serverConfig *ssh.ServerConfig) {
|
|||||||
go func() {
|
go func() {
|
||||||
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !(err != io.EOF && !errors.Is(err, syscall.ECONNRESET)) {
|
if err != io.EOF && !errors.Is(err, syscall.ECONNRESET) {
|
||||||
errorSsh("Failed to handshake", err)
|
errorSsh("Failed to handshake", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
76
internal/utils/argon2id.go
Normal file
76
internal/utils/argon2id.go
Normal 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
26
internal/utils/session.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
func SliceContains(slice []string, item string) bool {
|
func RemoveDuplicates[T string | int](sliceList []T) []T {
|
||||||
for _, s := range slice {
|
allKeys := make(map[T]bool)
|
||||||
if s == item {
|
list := []T{}
|
||||||
return true
|
for _, item := range sliceList {
|
||||||
|
if _, value := allKeys[item]; !value {
|
||||||
|
allKeys[item] = true
|
||||||
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
75
internal/utils/validator.go
Normal file
75
internal/utils/validator.go
Normal 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())
|
||||||
|
}
|
||||||
@@ -2,26 +2,17 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/thomiceli/opengist/internal/actions"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
syncReposFromFS = false
|
|
||||||
syncReposFromDB = false
|
|
||||||
gitGcRepos = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func adminIndex(ctx echo.Context) error {
|
func adminIndex(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "Admin panel")
|
setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel"))
|
||||||
setData(ctx, "htmlTitle", "Admin panel")
|
|
||||||
setData(ctx, "adminHeaderPage", "index")
|
setData(ctx, "adminHeaderPage", "index")
|
||||||
|
|
||||||
setData(ctx, "opengistVersion", config.OpengistVersion)
|
setData(ctx, "opengistVersion", config.OpengistVersion)
|
||||||
@@ -50,15 +41,17 @@ func adminIndex(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
setData(ctx, "countKeys", countKeys)
|
setData(ctx, "countKeys", countKeys)
|
||||||
|
|
||||||
setData(ctx, "syncReposFromFS", syncReposFromFS)
|
setData(ctx, "syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
|
||||||
setData(ctx, "syncReposFromDB", syncReposFromDB)
|
setData(ctx, "syncReposFromDB", actions.IsRunning(actions.SyncReposFromDB))
|
||||||
setData(ctx, "gitGcRepos", gitGcRepos)
|
setData(ctx, "gitGcRepos", actions.IsRunning(actions.GitGcRepos))
|
||||||
|
setData(ctx, "syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
|
||||||
|
setData(ctx, "resetHooks", actions.IsRunning(actions.ResetHooks))
|
||||||
|
setData(ctx, "indexGists", actions.IsRunning(actions.IndexGists))
|
||||||
return html(ctx, "admin_index.html")
|
return html(ctx, "admin_index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminUsers(ctx echo.Context) error {
|
func adminUsers(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "Users")
|
setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel"))
|
||||||
setData(ctx, "htmlTitle", "Users - Admin panel")
|
|
||||||
setData(ctx, "adminHeaderPage", "users")
|
setData(ctx, "adminHeaderPage", "users")
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -69,15 +62,14 @@ func adminUsers(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
|
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")
|
return html(ctx, "admin_users.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminGists(ctx echo.Context) error {
|
func adminGists(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "Gists")
|
setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel"))
|
||||||
setData(ctx, "htmlTitle", "Gists - Admin panel")
|
|
||||||
setData(ctx, "adminHeaderPage", "gists")
|
setData(ctx, "adminHeaderPage", "gists")
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -88,7 +80,7 @@ func adminGists(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
|
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")
|
return html(ctx, "admin_gists.html")
|
||||||
@@ -105,7 +97,7 @@ func adminUserDelete(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Cannot delete this user", err)
|
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")
|
return redirect(ctx, "/admin-panel/users")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,90 +115,50 @@ func adminGistDelete(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Cannot delete this gist", err)
|
return errorRes(500, "Cannot delete this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFlash(ctx, "Gist has been deleted", "success")
|
gist.RemoveFromIndex()
|
||||||
|
|
||||||
|
addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success")
|
||||||
return redirect(ctx, "/admin-panel/gists")
|
return redirect(ctx, "/admin-panel/gists")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminSyncReposFromFS(ctx echo.Context) error {
|
func adminSyncReposFromFS(ctx echo.Context) error {
|
||||||
addFlash(ctx, "Syncing repositories from filesystem...", "success")
|
addFlash(ctx, tr(ctx, "flash.admin.sync-fs"), "success")
|
||||||
go func() {
|
go actions.Run(actions.SyncReposFromFS)
|
||||||
if syncReposFromFS {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncReposFromFS = true
|
|
||||||
|
|
||||||
gists, err := db.GetAllGistsRows()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Cannot get gists")
|
|
||||||
syncReposFromFS = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, gist := range gists {
|
|
||||||
// if repository does not exist, delete gist from database
|
|
||||||
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
|
|
||||||
if err2 := gist.Delete(); err2 != nil {
|
|
||||||
log.Error().Err(err2).Msg("Cannot delete gist")
|
|
||||||
syncReposFromFS = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
syncReposFromFS = false
|
|
||||||
}()
|
|
||||||
return redirect(ctx, "/admin-panel")
|
return redirect(ctx, "/admin-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminSyncReposFromDB(ctx echo.Context) error {
|
func adminSyncReposFromDB(ctx echo.Context) error {
|
||||||
addFlash(ctx, "Syncing repositories from database...", "success")
|
addFlash(ctx, tr(ctx, "flash.admin.sync-db"), "success")
|
||||||
go func() {
|
go actions.Run(actions.SyncReposFromDB)
|
||||||
if syncReposFromDB {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncReposFromDB = true
|
|
||||||
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Cannot read repos directories")
|
|
||||||
syncReposFromDB = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range entries {
|
|
||||||
path := strings.Split(e, string(os.PathSeparator))
|
|
||||||
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
|
|
||||||
|
|
||||||
if gist.ID == 0 {
|
|
||||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Cannot delete repository")
|
|
||||||
syncReposFromDB = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
syncReposFromDB = false
|
|
||||||
}()
|
|
||||||
return redirect(ctx, "/admin-panel")
|
return redirect(ctx, "/admin-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminGcRepos(ctx echo.Context) error {
|
func adminGcRepos(ctx echo.Context) error {
|
||||||
addFlash(ctx, "Garbage collecting repositories...", "success")
|
addFlash(ctx, tr(ctx, "flash.admin.git-gc"), "success")
|
||||||
go func() {
|
go actions.Run(actions.GitGcRepos)
|
||||||
if gitGcRepos {
|
return redirect(ctx, "/admin-panel")
|
||||||
return
|
}
|
||||||
}
|
|
||||||
gitGcRepos = true
|
func adminSyncGistPreviews(ctx echo.Context) error {
|
||||||
if err := git.GcRepos(); err != nil {
|
addFlash(ctx, tr(ctx, "flash.admin.sync-previews"), "success")
|
||||||
log.Error().Err(err).Msg("Error garbage collecting repositories")
|
go actions.Run(actions.SyncGistPreviews)
|
||||||
gitGcRepos = false
|
return redirect(ctx, "/admin-panel")
|
||||||
return
|
}
|
||||||
}
|
|
||||||
gitGcRepos = false
|
func adminResetHooks(ctx echo.Context) error {
|
||||||
}()
|
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, tr(ctx, "flash.admin.index-gists"), "success")
|
||||||
|
go actions.Run(actions.IndexGists)
|
||||||
return redirect(ctx, "/admin-panel")
|
return redirect(ctx, "/admin-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminConfig(ctx echo.Context) error {
|
func adminConfig(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "Configuration")
|
setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel"))
|
||||||
setData(ctx, "htmlTitle", "Configuration - Admin panel")
|
|
||||||
setData(ctx, "adminHeaderPage", "config")
|
setData(ctx, "adminHeaderPage", "config")
|
||||||
|
|
||||||
return html(ctx, "admin_config.html")
|
return html(ctx, "admin_config.html")
|
||||||
@@ -224,3 +176,58 @@ func adminSetConfig(ctx echo.Context) error {
|
|||||||
"success": true,
|
"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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,67 +6,97 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
"github.com/markbates/goth/providers/gitea"
|
"github.com/markbates/goth/providers/gitea"
|
||||||
"github.com/markbates/goth/providers/github"
|
"github.com/markbates/goth/providers/github"
|
||||||
|
"github.com/markbates/goth/providers/gitlab"
|
||||||
"github.com/markbates/goth/providers/openidConnect"
|
"github.com/markbates/goth/providers/openidConnect"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"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/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var title = cases.Title(language.English)
|
const (
|
||||||
|
GitHubProvider = "github"
|
||||||
|
GitLabProvider = "gitlab"
|
||||||
|
GiteaProvider = "gitea"
|
||||||
|
OpenIDConnect = "openid-connect"
|
||||||
|
)
|
||||||
|
|
||||||
func register(ctx echo.Context) error {
|
func register(ctx echo.Context) error {
|
||||||
setData(ctx, "title", tr(ctx, "auth.new-account"))
|
disableSignup := getData(ctx, "DisableSignup")
|
||||||
setData(ctx, "htmlTitle", "New account")
|
disableForm := getData(ctx, "DisableLoginForm")
|
||||||
setData(ctx, "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)
|
setData(ctx, "isLoginPage", false)
|
||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processRegister(ctx echo.Context) error {
|
func processRegister(ctx echo.Context) error {
|
||||||
if getData(ctx, "DisableSignup") == true {
|
disableSignup := getData(ctx, "DisableSignup")
|
||||||
return errorRes(403, "Signing up is disabled", nil)
|
|
||||||
|
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 {
|
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, "title", trH(ctx, "auth.new-account"))
|
||||||
setData(ctx, "htmlTitle", "New account")
|
setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
|
||||||
|
|
||||||
sess := getSession(ctx)
|
sess := getSession(ctx)
|
||||||
|
|
||||||
dto := new(db.UserDTO)
|
dto := new(db.UserDTO)
|
||||||
if err := ctx.Bind(dto); err != nil {
|
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 {
|
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")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
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")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
user := dto.ToUser()
|
user := dto.ToUser()
|
||||||
|
|
||||||
password, err := argon2id.hash(user.Password)
|
password, err := utils.Argon2id.Hash(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot hash password", err)
|
return errorRes(500, "Cannot hash password", err)
|
||||||
}
|
}
|
||||||
@@ -82,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
|
sess.Values["user"] = user.ID
|
||||||
saveSession(sess, ctx)
|
saveSession(sess, ctx)
|
||||||
|
|
||||||
@@ -89,8 +125,8 @@ func processRegister(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func login(ctx echo.Context) error {
|
func login(ctx echo.Context) error {
|
||||||
setData(ctx, "title", tr(ctx, "auth.login"))
|
setData(ctx, "title", trH(ctx, "auth.login"))
|
||||||
setData(ctx, "htmlTitle", "Login")
|
setData(ctx, "htmlTitle", trH(ctx, "auth.login"))
|
||||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||||
setData(ctx, "isLoginPage", true)
|
setData(ctx, "isLoginPage", true)
|
||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
@@ -98,7 +134,7 @@ func login(ctx echo.Context) error {
|
|||||||
|
|
||||||
func processLogin(ctx echo.Context) error {
|
func processLogin(ctx echo.Context) error {
|
||||||
if getData(ctx, "DisableLoginForm") == true {
|
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
|
var err error
|
||||||
@@ -106,7 +142,7 @@ func processLogin(ctx echo.Context) error {
|
|||||||
|
|
||||||
dto := &db.UserDTO{}
|
dto := &db.UserDTO{}
|
||||||
if err = ctx.Bind(dto); err != nil {
|
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
|
password := dto.Password
|
||||||
|
|
||||||
@@ -117,20 +153,21 @@ func processLogin(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Cannot get user", err)
|
return errorRes(500, "Cannot get user", err)
|
||||||
}
|
}
|
||||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
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")
|
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot check for password", err)
|
return errorRes(500, "Cannot check for password", err)
|
||||||
}
|
}
|
||||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
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")
|
return redirect(ctx, "/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
sess.Values["user"] = user.ID
|
sess.Values["user"] = user.ID
|
||||||
|
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||||
saveSession(sess, ctx)
|
saveSession(sess, ctx)
|
||||||
deleteCsrfCookie(ctx)
|
deleteCsrfCookie(ctx)
|
||||||
|
|
||||||
@@ -140,29 +177,19 @@ func processLogin(ctx echo.Context) error {
|
|||||||
func oauthCallback(ctx echo.Context) error {
|
func oauthCallback(ctx echo.Context) error {
|
||||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(400, "Cannot complete user auth", err)
|
return errorRes(400, tr(ctx, "error.complete-oauth-login", err.Error()), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currUser := getUserLogged(ctx)
|
currUser := getUserLogged(ctx)
|
||||||
if currUser != nil {
|
if currUser != nil {
|
||||||
// if user is logged in, link account to user and update its avatar URL
|
// if user is logged in, link account to user and update its avatar URL
|
||||||
switch user.Provider {
|
updateUserProviderInfo(currUser, user.Provider, user)
|
||||||
case "github":
|
|
||||||
currUser.GithubID = user.UserID
|
|
||||||
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
|
||||||
case "gitea":
|
|
||||||
currUser.GiteaID = user.UserID
|
|
||||||
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
|
||||||
case "openid-connect":
|
|
||||||
currUser.OIDCID = user.UserID
|
|
||||||
currUser.AvatarURL = user.AvatarURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = currUser.Update(); err != nil {
|
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")
|
return redirect(ctx, "/settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +197,7 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
|
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if getData(ctx, "DisableSignup") == true {
|
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) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -184,21 +211,11 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set provider id and avatar URL
|
// set provider id and avatar URL
|
||||||
switch user.Provider {
|
updateUserProviderInfo(userDB, user.Provider, user)
|
||||||
case "github":
|
|
||||||
userDB.GithubID = user.UserID
|
|
||||||
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
|
||||||
case "gitea":
|
|
||||||
userDB.GiteaID = user.UserID
|
|
||||||
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
|
||||||
case "openid-connect":
|
|
||||||
userDB.OIDCID = user.UserID
|
|
||||||
userDB.AvatarURL = user.AvatarURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = userDB.Create(); err != nil {
|
if err = userDB.Create(); err != nil {
|
||||||
if db.IsUniqueConstraintViolation(err) {
|
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")
|
return redirect(ctx, "/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +230,13 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
switch user.Provider {
|
switch user.Provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
||||||
case "gitea":
|
case GitLabProvider:
|
||||||
|
resp, err = http.Get(urlJoin(config.C.GitlabUrl, user.NickName+".keys"))
|
||||||
|
case GiteaProvider:
|
||||||
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
|
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
|
||||||
case "openid-connect":
|
case OpenIDConnect:
|
||||||
err = errors.New("cannot get keys from OIDC provider")
|
err = errors.New("cannot get keys from OIDC provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +245,7 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
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")
|
log.Error().Err(err).Msg("Could not get user keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +261,7 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = sshKey.Create(); err != nil {
|
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")
|
log.Error().Err(err).Msg("Could not create ssh key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +292,7 @@ func oauth(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
goth.UseProviders(
|
goth.UseProviders(
|
||||||
github.New(
|
github.New(
|
||||||
config.C.GithubClientKey,
|
config.C.GithubClientKey,
|
||||||
@@ -282,7 +301,19 @@ func oauth(ctx echo.Context) error {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
case "gitea":
|
case GitLabProvider:
|
||||||
|
goth.UseProviders(
|
||||||
|
gitlab.NewCustomisedURL(
|
||||||
|
config.C.GitlabClientKey,
|
||||||
|
config.C.GitlabSecret,
|
||||||
|
urlJoin(opengistUrl, "/oauth/gitlab/callback"),
|
||||||
|
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
|
||||||
|
urlJoin(config.C.GitlabUrl, "/oauth/token"),
|
||||||
|
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case GiteaProvider:
|
||||||
goth.UseProviders(
|
goth.UseProviders(
|
||||||
gitea.NewCustomisedURL(
|
gitea.NewCustomisedURL(
|
||||||
config.C.GiteaClientKey,
|
config.C.GiteaClientKey,
|
||||||
@@ -293,7 +324,7 @@ func oauth(ctx echo.Context) error {
|
|||||||
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
|
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
case "openid-connect":
|
case OpenIDConnect:
|
||||||
oidcProvider, err := openidConnect.New(
|
oidcProvider, err := openidConnect.New(
|
||||||
config.C.OIDCClientKey,
|
config.C.OIDCClientKey,
|
||||||
config.C.OIDCSecret,
|
config.C.OIDCSecret,
|
||||||
@@ -313,40 +344,30 @@ func oauth(ctx echo.Context) error {
|
|||||||
|
|
||||||
currUser := getUserLogged(ctx)
|
currUser := getUserLogged(ctx)
|
||||||
if currUser != nil {
|
if currUser != nil {
|
||||||
isDelete := false
|
// Map each provider to a function that checks the relevant ID in currUser
|
||||||
var err error
|
providerIDCheckMap := map[string]func() bool{
|
||||||
switch provider {
|
GitHubProvider: func() bool { return currUser.GithubID != "" },
|
||||||
case "github":
|
GitLabProvider: func() bool { return currUser.GitlabID != "" },
|
||||||
if currUser.GithubID != "" {
|
GiteaProvider: func() bool { return currUser.GiteaID != "" },
|
||||||
isDelete = true
|
OpenIDConnect: func() bool { return currUser.OIDCID != "" },
|
||||||
err = currUser.DeleteProviderID(provider)
|
|
||||||
}
|
|
||||||
case "gitea":
|
|
||||||
if currUser.GiteaID != "" {
|
|
||||||
isDelete = true
|
|
||||||
err = currUser.DeleteProviderID(provider)
|
|
||||||
}
|
|
||||||
case "openid-connect":
|
|
||||||
if currUser.OIDCID != "" {
|
|
||||||
isDelete = true
|
|
||||||
err = currUser.DeleteProviderID(provider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
// Check if the provider is valid and if the user has a linked ID
|
||||||
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
|
// 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 "+cases.Title(language.English).String(provider), err)
|
||||||
|
}
|
||||||
|
|
||||||
if isDelete {
|
addFlash(ctx, tr(ctx, "flash.auth.account-unlinked-oauth", cases.Title(language.English).String(provider)), "success")
|
||||||
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
|
|
||||||
return redirect(ctx, "/settings")
|
return redirect(ctx, "/settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
||||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||||
if provider != "github" && provider != "gitea" && provider != "openid-connect" {
|
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())
|
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
|
||||||
@@ -368,11 +389,28 @@ func urlJoin(base string, elem ...string) string {
|
|||||||
return joined
|
return joined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUserProviderInfo(userDB *db.User, provider string, user goth.User) {
|
||||||
|
userDB.AvatarURL = getAvatarUrlFromProvider(provider, user.UserID)
|
||||||
|
switch provider {
|
||||||
|
case GitHubProvider:
|
||||||
|
userDB.GithubID = user.UserID
|
||||||
|
case GitLabProvider:
|
||||||
|
userDB.GitlabID = user.UserID
|
||||||
|
case GiteaProvider:
|
||||||
|
userDB.GiteaID = user.UserID
|
||||||
|
case OpenIDConnect:
|
||||||
|
userDB.OIDCID = user.UserID
|
||||||
|
userDB.AvatarURL = user.AvatarURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getAvatarUrlFromProvider(provider string, identifier string) string {
|
func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
|
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
|
||||||
case "gitea":
|
case GitLabProvider:
|
||||||
|
return urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", identifier, "/avatar.png") + "?width=400"
|
||||||
|
case GiteaProvider:
|
||||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
|
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||||
@@ -402,3 +440,15 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,18 +2,30 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"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/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"html/template"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
@@ -23,14 +35,24 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
userName := ctx.Param("user")
|
userName := ctx.Param("user")
|
||||||
gistName := ctx.Param("gistname")
|
gistName := ctx.Param("gistname")
|
||||||
|
|
||||||
gistName = strings.TrimSuffix(gistName, ".git")
|
switch filepath.Ext(gistName) {
|
||||||
|
case ".js":
|
||||||
|
setData(ctx, "gistpage", "js")
|
||||||
|
gistName = strings.TrimSuffix(gistName, ".js")
|
||||||
|
case ".json":
|
||||||
|
setData(ctx, "gistpage", "json")
|
||||||
|
gistName = strings.TrimSuffix(gistName, ".json")
|
||||||
|
case ".git":
|
||||||
|
setData(ctx, "gistpage", "git")
|
||||||
|
gistName = strings.TrimSuffix(gistName, ".git")
|
||||||
|
}
|
||||||
|
|
||||||
gist, err := db.GetGist(userName, gistName)
|
gist, err := db.GetGist(userName, gistName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return notFound("Gist not found")
|
return notFound("Gist not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if gist.Private == 2 {
|
if gist.Private == db.PrivateVisibility {
|
||||||
if currUser == nil || currUser.ID != gist.UserID {
|
if currUser == nil || currUser.ID != gist.UserID {
|
||||||
return notFound("Gist not found")
|
return notFound("Gist not found")
|
||||||
}
|
}
|
||||||
@@ -54,19 +76,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
httpProtocol := "http"
|
baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.C.HttpGit {
|
if config.C.HttpGit {
|
||||||
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||||
@@ -74,6 +84,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
|
|
||||||
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
|
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
|
||||||
setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
|
setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
|
||||||
|
setData(ctx, "embedScript", fmt.Sprintf(`<script src="%s"></script>`, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
|
||||||
|
|
||||||
nbCommits, err := gist.NbCommits()
|
nbCommits, err := gist.NbCommits()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,18 +141,18 @@ func allGists(ctx echo.Context) error {
|
|||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
sort := "created"
|
sort := "created"
|
||||||
sortText := tr(ctx, "gist.list.sort-by-created")
|
sortText := trH(ctx, "gist.list.sort-by-created")
|
||||||
order := "desc"
|
order := "desc"
|
||||||
orderText := tr(ctx, "gist.list.order-by-desc")
|
orderText := trH(ctx, "gist.list.order-by-desc")
|
||||||
|
|
||||||
if ctx.QueryParam("sort") == "updated" {
|
if ctx.QueryParam("sort") == "updated" {
|
||||||
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" {
|
if ctx.QueryParam("order") == "asc" {
|
||||||
order = "asc"
|
order = "asc"
|
||||||
orderText = tr(ctx, "gist.list.order-by-asc")
|
orderText = trH(ctx, "gist.list.order-by-asc")
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "sort", sortText)
|
setData(ctx, "sort", sortText)
|
||||||
@@ -158,14 +169,14 @@ func allGists(ctx echo.Context) error {
|
|||||||
if fromUserStr == "" {
|
if fromUserStr == "" {
|
||||||
urlctx := ctx.Request().URL.Path
|
urlctx := ctx.Request().URL.Path
|
||||||
if strings.HasSuffix(urlctx, "search") {
|
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, "mode", "search")
|
||||||
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||||
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
||||||
urlPage = "search"
|
urlPage = "search"
|
||||||
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
|
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
|
||||||
} else if strings.HasSuffix(urlctx, "all") {
|
} else if strings.HasSuffix(urlctx, "all") {
|
||||||
setData(ctx, "htmlTitle", "All gists")
|
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all"))
|
||||||
setData(ctx, "mode", "all")
|
setData(ctx, "mode", "all")
|
||||||
urlPage = "all"
|
urlPage = "all"
|
||||||
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||||
@@ -215,35 +226,113 @@ func allGists(ctx echo.Context) error {
|
|||||||
|
|
||||||
if liked {
|
if liked {
|
||||||
urlPage = fromUserStr + "/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")
|
setData(ctx, "mode", "liked")
|
||||||
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||||
} else if forked {
|
} else if forked {
|
||||||
urlPage = fromUserStr + "/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")
|
setData(ctx, "mode", "forked")
|
||||||
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||||
} else {
|
} else {
|
||||||
urlPage = fromUserStr
|
urlPage = fromUserStr
|
||||||
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr))
|
||||||
setData(ctx, "mode", "fromUser")
|
setData(ctx, "mode", "fromUser")
|
||||||
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderedGists := make([]*render.RenderedGist, 0, len(gists))
|
||||||
|
for _, gist := range gists {
|
||||||
|
rendered, err := render.HighlightGistPreview(gist)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
|
||||||
|
}
|
||||||
|
renderedGists = append(renderedGists, &rendered)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching gists", err)
|
return errorRes(500, "Error fetching gists", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
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)
|
setData(ctx, "urlPage", urlPage)
|
||||||
return html(ctx, "all.html")
|
return html(ctx, "all.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func search(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
content, meta := parseSearchQueryStr(ctx.QueryParam("q"))
|
||||||
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
|
var currentUserId uint
|
||||||
|
userLogged := getUserLogged(ctx)
|
||||||
|
if userLogged != nil {
|
||||||
|
currentUserId = userLogged.ID
|
||||||
|
} else {
|
||||||
|
currentUserId = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var visibleGistsIds []uint
|
||||||
|
visibleGistsIds, err = db.GetAllGistsVisibleByUser(currentUserId)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error fetching gists", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
|
||||||
|
Username: meta["user"],
|
||||||
|
Title: meta["title"],
|
||||||
|
Filename: meta["filename"],
|
||||||
|
Extension: meta["extension"],
|
||||||
|
Language: meta["language"],
|
||||||
|
}, visibleGistsIds, pageInt)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error searching gists", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gists, err := db.GetAllGistsByIds(gistsIds)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error fetching gists", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedGists := make([]*render.RenderedGist, 0, len(gists))
|
||||||
|
for _, gist := range gists {
|
||||||
|
rendered, err := render.HighlightGistPreview(gist)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
|
||||||
|
}
|
||||||
|
renderedGists = append(renderedGists, &rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageInt > 1 && len(renderedGists) != 0 {
|
||||||
|
setData(ctx, "prevPage", pageInt-1)
|
||||||
|
}
|
||||||
|
if 10*pageInt < int(nbHits) {
|
||||||
|
setData(ctx, "nextPage", pageInt+1)
|
||||||
|
}
|
||||||
|
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", trH(ctx, "gist.list.search-results"))
|
||||||
|
setData(ctx, "nbHits", nbHits)
|
||||||
|
setData(ctx, "gists", renderedGists)
|
||||||
|
setData(ctx, "langs", langs)
|
||||||
|
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||||
|
return html(ctx, "search.html")
|
||||||
|
}
|
||||||
|
|
||||||
func gistIndex(ctx echo.Context) error {
|
func gistIndex(ctx echo.Context) error {
|
||||||
|
if getData(ctx, "gistpage") == "js" {
|
||||||
|
return gistJs(ctx)
|
||||||
|
} else if getData(ctx, "gistpage") == "json" {
|
||||||
|
return gistJson(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
gist := getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
revision := ctx.Param("revision")
|
revision := ctx.Param("revision")
|
||||||
|
|
||||||
@@ -251,27 +340,108 @@ func gistIndex(ctx echo.Context) error {
|
|||||||
revision = "HEAD"
|
revision = "HEAD"
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := gist.Files(revision)
|
files, err := gist.Files(revision, true)
|
||||||
if err != nil {
|
if _, ok := err.(*git.RevisionNotFoundError); ok {
|
||||||
|
return notFound("Revision not found")
|
||||||
|
} else if err != nil {
|
||||||
return errorRes(500, "Error fetching files", err)
|
return errorRes(500, "Error fetching files", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) == 0 {
|
renderedFiles := render.HighlightFiles(files)
|
||||||
return notFound("Revision not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(ctx, "page", "code")
|
setData(ctx, "page", "code")
|
||||||
setData(ctx, "commit", revision)
|
setData(ctx, "commit", revision)
|
||||||
setData(ctx, "files", files)
|
setData(ctx, "files", renderedFiles)
|
||||||
setData(ctx, "revision", revision)
|
setData(ctx, "revision", revision)
|
||||||
setData(ctx, "htmlTitle", gist.Title)
|
setData(ctx, "htmlTitle", gist.Title)
|
||||||
return html(ctx, "gist.html")
|
return html(ctx, "gist.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gistJson(ctx echo.Context) error {
|
||||||
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
files, err := gist.Files("HEAD", true)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error fetching files", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedFiles := render.HighlightFiles(files)
|
||||||
|
setData(ctx, "files", renderedFiles)
|
||||||
|
|
||||||
|
htmlbuf := bytes.Buffer{}
|
||||||
|
w := bufio.NewWriter(&htmlbuf)
|
||||||
|
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error joining js url", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error joining css url", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(200, map[string]interface{}{
|
||||||
|
"owner": gist.User.Username,
|
||||||
|
"id": gist.Identifier(),
|
||||||
|
"uuid": gist.Uuid,
|
||||||
|
"title": gist.Title,
|
||||||
|
"description": gist.Description,
|
||||||
|
"created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339),
|
||||||
|
"visibility": gist.VisibilityStr(),
|
||||||
|
"files": renderedFiles,
|
||||||
|
"embed": map[string]string{
|
||||||
|
"html": htmlbuf.String(),
|
||||||
|
"css": cssUrl,
|
||||||
|
"js": jsUrl,
|
||||||
|
"js_dark": jsUrl + "?dark",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func gistJs(ctx echo.Context) error {
|
||||||
|
if _, exists := ctx.QueryParams()["dark"]; exists {
|
||||||
|
setData(ctx, "dark", "dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
files, err := gist.Files("HEAD", true)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error fetching files", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedFiles := render.HighlightFiles(files)
|
||||||
|
setData(ctx, "files", renderedFiles)
|
||||||
|
|
||||||
|
htmlbuf := bytes.Buffer{}
|
||||||
|
w := bufio.NewWriter(&htmlbuf)
|
||||||
|
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error joining css url", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
js := `document.write('<link rel="stylesheet" href="%s">')
|
||||||
|
document.write('%s')
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
func revisions(ctx echo.Context) error {
|
func revisions(ctx echo.Context) error {
|
||||||
gist := getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
userName := gist.User.Username
|
userName := gist.User.Username
|
||||||
gistName := gist.Uuid
|
gistName := gist.Identifier()
|
||||||
|
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -281,7 +451,7 @@ func revisions(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
|
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{}{}
|
emailsSet := map[string]struct{}{}
|
||||||
@@ -300,13 +470,13 @@ func revisions(ctx echo.Context) error {
|
|||||||
setData(ctx, "page", "revisions")
|
setData(ctx, "page", "revisions")
|
||||||
setData(ctx, "revision", "HEAD")
|
setData(ctx, "revision", "HEAD")
|
||||||
setData(ctx, "emails", emailsUsers)
|
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")
|
return html(ctx, "revisions.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(ctx echo.Context) error {
|
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")
|
return html(ctx, "create.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,21 +488,21 @@ func processCreate(ctx echo.Context) error {
|
|||||||
|
|
||||||
err := ctx.Request().ParseForm()
|
err := ctx.Request().ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(400, "Bad request", err)
|
return errorRes(400, tr(ctx, "error.bad-request"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := new(db.GistDTO)
|
dto := new(db.GistDTO)
|
||||||
var gist *db.Gist
|
var gist *db.Gist
|
||||||
|
|
||||||
if isCreate {
|
if isCreate {
|
||||||
setData(ctx, "htmlTitle", "Create a new gist")
|
setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
|
||||||
} else {
|
} else {
|
||||||
gist = getData(ctx, "gist").(*db.Gist)
|
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 {
|
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)
|
dto.Files = make([]db.FileDTO, 0)
|
||||||
@@ -348,7 +518,7 @@ func processCreate(ctx echo.Context) error {
|
|||||||
|
|
||||||
escapedValue, err := url.QueryUnescape(content)
|
escapedValue, err := url.QueryUnescape(content)
|
||||||
if err != nil {
|
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{
|
dto.Files = append(dto.Files, db.FileDTO{
|
||||||
@@ -359,11 +529,11 @@ func processCreate(ctx echo.Context) error {
|
|||||||
|
|
||||||
err = ctx.Validate(dto)
|
err = ctx.Validate(dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
addFlash(ctx, validationMessages(&err), "error")
|
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||||
if isCreate {
|
if isCreate {
|
||||||
return html(ctx, "create.html")
|
return html(ctx, "create.html")
|
||||||
} else {
|
} else {
|
||||||
files, err := gist.Files("HEAD")
|
files, err := gist.Files("HEAD", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files", err)
|
return errorRes(500, "Error fetching files", err)
|
||||||
}
|
}
|
||||||
@@ -429,34 +599,42 @@ func processCreate(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(ctx, "/"+user.Username+"/"+gist.Uuid)
|
gist.AddInIndex()
|
||||||
|
|
||||||
|
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleVisibility(ctx echo.Context) error {
|
func editVisibility(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
gist.Private = (gist.Private + 1) % 3
|
dto := new(db.VisibilityDTO)
|
||||||
if err := gist.Update(); err != nil {
|
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)
|
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.Uuid)
|
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteGist(ctx echo.Context) error {
|
func deleteGist(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
if err := gist.Delete(); err != nil {
|
if err := gist.Delete(); err != nil {
|
||||||
return errorRes(500, "Error deleting this gist", err)
|
return errorRes(500, "Error deleting this gist", err)
|
||||||
}
|
}
|
||||||
|
gist.RemoveFromIndex()
|
||||||
|
|
||||||
addFlash(ctx, "Gist has been deleted", "success")
|
addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success")
|
||||||
return redirect(ctx, "/")
|
return redirect(ctx, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func like(ctx echo.Context) error {
|
func like(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
currentUser := getUserLogged(ctx)
|
currentUser := getUserLogged(ctx)
|
||||||
|
|
||||||
hasLiked, err := currentUser.HasLiked(gist)
|
hasLiked, err := currentUser.HasLiked(gist)
|
||||||
@@ -474,7 +652,7 @@ func like(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error liking/dislking this gist", err)
|
return errorRes(500, "Error liking/dislking this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo := "/" + gist.User.Username + "/" + gist.Uuid
|
redirectTo := "/" + gist.User.Username + "/" + gist.Identifier()
|
||||||
if r := ctx.QueryParam("redirecturl"); r != "" {
|
if r := ctx.QueryParam("redirecturl"); r != "" {
|
||||||
redirectTo = r
|
redirectTo = r
|
||||||
}
|
}
|
||||||
@@ -482,7 +660,7 @@ func like(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fork(ctx echo.Context) error {
|
func fork(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
currentUser := getUserLogged(ctx)
|
currentUser := getUserLogged(ctx)
|
||||||
|
|
||||||
alreadyForked, err := gist.GetForkParent(currentUser)
|
alreadyForked, err := gist.GetForkParent(currentUser)
|
||||||
@@ -491,12 +669,12 @@ func fork(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gist.User.ID == currentUser.ID {
|
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.Uuid)
|
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
if alreadyForked.ID != 0 {
|
if alreadyForked.ID != 0 {
|
||||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
|
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
uuidGist, err := uuid.NewRandom()
|
uuidGist, err := uuid.NewRandom()
|
||||||
@@ -527,15 +705,14 @@ func fork(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error incrementing the fork count", err)
|
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.Uuid)
|
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawFile(ctx echo.Context) error {
|
func rawFile(ctx echo.Context) error {
|
||||||
gist := getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error getting file content", err)
|
return errorRes(500, "Error getting file content", err)
|
||||||
}
|
}
|
||||||
@@ -550,7 +727,6 @@ func rawFile(ctx echo.Context) error {
|
|||||||
func downloadFile(ctx echo.Context) error {
|
func downloadFile(ctx echo.Context) error {
|
||||||
gist := getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error getting file content", err)
|
return errorRes(500, "Error getting file content", err)
|
||||||
}
|
}
|
||||||
@@ -563,7 +739,6 @@ func downloadFile(ctx echo.Context) error {
|
|||||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
||||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
|
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
|
||||||
_, err = ctx.Response().Write([]byte(file.Content))
|
_, err = ctx.Response().Write([]byte(file.Content))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error downloading the file", err)
|
return errorRes(500, "Error downloading the file", err)
|
||||||
}
|
}
|
||||||
@@ -572,24 +747,24 @@ func downloadFile(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func edit(ctx echo.Context) error {
|
func edit(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
files, err := gist.Files("HEAD")
|
files, err := gist.Files("HEAD", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files from repository", err)
|
return errorRes(500, "Error fetching files from repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "files", files)
|
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")
|
return html(ctx, "edit.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadZip(ctx echo.Context) error {
|
func downloadZip(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
var revision = ctx.Param("revision")
|
revision := ctx.Param("revision")
|
||||||
|
|
||||||
files, err := gist.Files(revision)
|
files, err := gist.Files(revision, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files from repository", err)
|
return errorRes(500, "Error fetching files from repository", err)
|
||||||
}
|
}
|
||||||
@@ -621,7 +796,7 @@ func downloadZip(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Response().Header().Set("Content-Type", "application/zip")
|
ctx.Response().Header().Set("Content-Type", "application/zip")
|
||||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip")
|
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Identifier()+".zip")
|
||||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
|
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
|
||||||
_, err = ctx.Response().Write(zipFile.Bytes())
|
_, err = ctx.Response().Write(zipFile.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -631,7 +806,7 @@ func downloadZip(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func likes(ctx echo.Context) error {
|
func likes(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -640,17 +815,17 @@ func likes(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error getting users who liked this gist", err)
|
return errorRes(500, "Error getting users who liked this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes", 1); err != nil {
|
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")
|
setData(ctx, "revision", "HEAD")
|
||||||
return html(ctx, "likes.html")
|
return html(ctx, "likes.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
func forks(ctx echo.Context) error {
|
func forks(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*db.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
currentUser := getUserLogged(ctx)
|
currentUser := getUserLogged(ctx)
|
||||||
@@ -664,11 +839,58 @@ func forks(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error getting users who liked this gist", err)
|
return errorRes(500, "Error getting users who liked this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks", 2); err != nil {
|
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")
|
setData(ctx, "revision", "HEAD")
|
||||||
return html(ctx, "forks.html")
|
return html(ctx, "forks.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkbox(ctx echo.Context) error {
|
||||||
|
filename := ctx.FormValue("file")
|
||||||
|
checkboxNb := ctx.FormValue("checkbox")
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(checkboxNb)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(400, tr(ctx, "error.invalid-number"), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
file, err := gist.File("HEAD", filename, false)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error getting file content", err)
|
||||||
|
} else if file == nil {
|
||||||
|
return notFound("File not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown, err := render.Checkbox(file.Content, i)
|
||||||
|
if err != nil {
|
||||||
|
return errorRes(500, "Error checking checkbox", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = gist.AddAndCommitFile(&db.FileDTO{
|
||||||
|
Filename: filename,
|
||||||
|
Content: markdown,
|
||||||
|
}); err != nil {
|
||||||
|
return errorRes(500, "Error adding and committing files", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = gist.UpdatePreviewAndCount(true); err != nil {
|
||||||
|
return errorRes(500, "Error updating the gist", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/thomiceli/opengist/internal/utils"
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
|
||||||
"github.com/thomiceli/opengist/internal/memdb"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -21,6 +15,15 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var routes = []struct {
|
var routes = []struct {
|
||||||
@@ -68,12 +71,17 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
|
|
||||||
setData(ctx, "repositoryPath", repositoryPath)
|
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 :
|
// Shows basic auth if :
|
||||||
// - user wants to push the gist
|
// - user wants to push the gist
|
||||||
// - user wants to clone/pull a private gist
|
// - user wants to clone/pull a private gist
|
||||||
// - gist is not found (obfuscation)
|
// - gist is not found (obfuscation)
|
||||||
// - admin setting to require login is set to true
|
// - admin setting to require login is set to true
|
||||||
if isPull && gist.Private != 2 && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
|
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && allow {
|
||||||
return route.handler(ctx)
|
return route.handler(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,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")
|
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot verify password", err)
|
return errorRes(500, "Cannot verify password", err)
|
||||||
}
|
}
|
||||||
@@ -114,7 +129,7 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
return errorRes(401, "Invalid credentials", nil)
|
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot check for password", err)
|
return errorRes(500, "Cannot check for password", err)
|
||||||
}
|
}
|
||||||
@@ -133,7 +148,7 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
||||||
gist.Title = "gist:" + gist.Uuid
|
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)
|
return errorRes(500, "Cannot init repository in the file system", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +207,7 @@ func pack(ctx echo.Context, serviceType string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositoryPath := getData(ctx, "repositoryPath").(string)
|
repositoryPath := getData(ctx, "repositoryPath").(string)
|
||||||
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
||||||
@@ -199,25 +215,14 @@ func pack(ctx echo.Context, serviceType string) error {
|
|||||||
cmd.Stdin = reqBody
|
cmd.Stdin = reqBody
|
||||||
cmd.Stdout = ctx.Response().Writer
|
cmd.Stdout = ctx.Response().Writer
|
||||||
cmd.Stderr = &stderr
|
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 {
|
if err = cmd.Run(); err != nil {
|
||||||
return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
|
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()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user