Compare commits

...

42 Commits

Author SHA1 Message Date
Thomas Miceli
8eb8f4e231 v1.6.0 2024-01-04 18:06:19 +01:00
Thomas Miceli
af19268d6f Add some docs (#198) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4215d7e43b Update dependencies (#197)
Go 1.20 -> 1.21
JS package-lock
Nodejs Docker 18 -> 20
Alpine Docker 3.17 -> 3.19
2024-01-04 18:06:19 +01:00
Thomas Miceli
d85917bfb2 Small fixes (#196) 2024-01-04 18:06:19 +01:00
Chiawei Chen
7c1d6e8bfd chore: update taiwan translation (#195) 2024-01-04 18:06:19 +01:00
Matheus C. França
3a2fd2374a Add pt-BR translation (#193)
new translation
2024-01-04 18:06:19 +01:00
Thomas Miceli
87a6113cc7 Add Gist code search (#194) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4cb7dc2d30 Fix reverse proxy subpath support (#192) 2024-01-04 18:06:19 +01:00
Thomas Miceli
f52310a841 Add 2 new admin actions (#191)
* Synchronize all gists previews
* Reset Git server hooks for all repositories
2024-01-04 18:06:19 +01:00
Thomas Miceli
97707f7cca Change username setting (#190) 2024-01-04 18:06:19 +01:00
Thomas Miceli
5058ca8f27 Optimize multiple file rendering (#189) 2024-01-04 18:06:19 +01:00
Thomas Miceli
b3a856a05e Optimize reading gist files content (#186) 2024-01-04 18:06:19 +01:00
WilliamNT
f557bd45df Updated the hungarian translation file (#185) 2024-01-04 18:06:19 +01:00
Jacob Hands
2f8435892e Add config for default branch name (#171)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Jacob Hands
4bba26daf6 Add log output config option (#172)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Thomas Miceli
3c97901995 Bug fixes (#184)
* Fix gist content when going back to editing

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

* Allow dashes in usernames

* Delete keys associated to deleted user

* Fix error message when there is no files in gist

* Show if there is not files in gist preview

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

Added Mermaidjs

More languages supported

Add default values for gist links input

Added copy code from markdown blocks
2024-01-04 18:06:19 +01:00
Chiawei Chen
eff88711ea Trivial Typo: Change 'Gitlab' to 'GitLab' (#177) 2024-01-04 18:06:19 +01:00
Thomas Miceli
8466e50cc3 Add GitLab OAuth provider (#174) 2024-01-04 18:06:19 +01:00
Thomas Miceli
c9fd58c904 Update JS dependencies versions (#175) 2024-01-04 18:06:19 +01:00
Thomas Miceli
47869a77c9 Add healthcheck endpoint (#170) 2024-01-04 18:06:19 +01:00
John Olheiser
246f12c8cb feat: default visibility (#155)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2024-01-04 18:06:19 +01:00
Chiawei Chen
943212e492 feat: add traditional chinese translation (#166) 2024-01-04 18:06:19 +01:00
Pavel Vácha
7a6fb98223 Add Czech translation (#164) 2024-01-04 18:06:19 +01:00
Thomas Miceli
3444fb9b75 v1.5.3 2023-11-20 18:49:46 +01:00
Thomas Miceli
be46304e23 Display OAuth errors (#159) 2023-11-20 18:41:01 +01:00
Thomas Miceli
5fa55dfbba Tiny UI fixes (#158) 2023-11-20 18:28:13 +01:00
Thomas Miceli
09fb647f03 Fix: bare first branch name, truncated output hanging (#157) 2023-11-20 18:03:59 +01:00
Thomas Miceli
d518a44d32 Create/change account password (#156) 2023-11-20 18:03:28 +01:00
Thomas Miceli
dcacde0959 Fix home user directory detection handling (#145) 2023-10-31 15:23:15 +09:00
Manuel Vergara
064d4d53f6 Add spanish translation (#139) 2023-10-31 15:22:58 +09:00
Thomas Miceli
aec7ee2708 v1.5.2 2023-10-16 12:26:05 +02:00
Thomas Miceli
10fd170833 Fix markdown render dark background (#137) 2023-10-16 12:20:09 +02:00
Slava Krampetz
ba03b8df38 Add ru-RU translation (#135) 2023-10-15 18:09:54 +02:00
Thomas Miceli
ef45f3d0ca config.yml with Docker (#131) 2023-10-15 08:14:34 +02:00
Thomas Miceli
b1acea9f1c Better password hashes error handling (#132) 2023-10-13 05:36:00 +02:00
Gary Wang
7059d5c834 Add zh-CN translation and minor UI fix (#130) 2023-10-12 14:13:39 +02:00
Thomas Miceli
1539499294 Longer title and description (#129) 2023-10-04 18:48:02 +02:00
Thomas Miceli
6f587f4757 Fix private gist visibility (#128) 2023-10-04 18:47:50 +02:00
100 changed files with 6261 additions and 5326 deletions

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

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

View File

@@ -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.21
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.21"
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
@@ -34,10 +34,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.21
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.21"
- name: Check - name: Check
run: make go_mod check_changes run: make go_mod check_changes
@@ -47,7 +47,7 @@ jobs:
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.21"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -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.21
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.21"
- name: Cross compile build - name: Cross compile build
run: make all_crosscompile run: make all_crosscompile

View File

@@ -1,5 +1,75 @@
# Changelog # Changelog
## [1.6.0](https://github.com/thomiceli/opengist/compare/v1.5.3...v1.6.0) - 2024-01-04
See here how to [update](/docs/update.md) Opengist.
### Added
- Embedded gists (#179)
- Gist code search (#194)
- Custom URLS for gists (#183)
- Gist JSON data/metadata (#179)
- Keep default visibility when creating a gist on the UI (#155)
- Health check endpoint (#170)
- GitLab OAuth2 login (#174)
- Syntax highlighting for more file types (#176)
- Checkable Markdown checkboxes (#182)
- Config:
- Log output (#172)
- Default git branch name (#171)
- Change username setting (#190)
- Admin actions:
- Synchronize all gists previews (#191)
- Reset Git server hooks for all repositories (#191)
- Index all gists (#194)
- Translations:
- cs-CZ (#164)
- zh-TW (#166, #195)
- hu-HU (#185)
- pt-BR (#193)
- Docs (#198)
### Changed
- Updated dependencies (#197):
- Go `1.20` -> `1.21`
- JavaScript packages
- NodeJS Docker image `18` -> `20`
- Alpine Docker image `3.17` -> `3.19`
### Fixed
- Fix reverse proxy subpath support (#192)
- Fix undecoded gist content when going back to editing in the UI (#184)
- Fix outputting non-truncated large files for editon/zip download (#184)
- Allow dashes in usernames (#184)
- Delete SSH keys associated to deleted user (#184)
- Better error message when there is no files in gist (#184)
- Show if there is no files in gist preview (#184)
- Log parsing for the 11th empty commit (#184)
- Optimize reading gist files content (#186)
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
### Added
- es-ES translation (#139)
- Create/change account password (#156)
- Display OAuth error messages when HTTP 400 (#159)
### Fixed
- Git bare repository branch name creation (#157)
- Git file truncated output hanging (#157)
- Home user directory detection handling (#145)
- UI changes (#158)
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
### Added
- zh-CN translation (#130)
- ru-RU translation (#135)
- config.yml usage in the Docker container (#131)
- Longer title and description (#129)
### Fixed
- Private gist visibility (#128)
- Dark background color in Markdown rendering (#137)
- Error handling for password hashes (#132)
## [1.5.1](https://github.com/thomiceli/opengist/compare/v1.5.0...v1.5.1) - 2023-09-29 ## [1.5.1](https://github.com/thomiceli/opengist/compare/v1.5.0...v1.5.1) - 2023-09-29
### Added ### Added
- Hungarian translations (#123) - Hungarian translations (#123)

View File

@@ -1,4 +1,4 @@
FROM alpine:3.17 AS build FROM alpine:3.19 AS build
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
@@ -7,10 +7,10 @@ RUN apk update && \
musl-dev \ musl-dev \
libstdc++ libstdc++
COPY --from=golang:1.20-alpine /usr/local/go/ /usr/local/go/ COPY --from=golang:1.21-alpine /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="/usr/local/go/bin:${PATH}"
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}"
@@ -21,7 +21,7 @@ COPY . .
RUN make RUN make
FROM alpine:3.17 as run FROM alpine:3.19 as run
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
@@ -40,6 +40,8 @@ RUN apk update && \
RUN addgroup -S opengist && \ RUN addgroup -S opengist && \
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist
COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml
WORKDIR /app/opengist WORKDIR /app/opengist
COPY --from=build --chown=opengist:opengist /opengist/opengist . COPY --from=build --chown=opengist:opengist /opengist/opengist .

View File

@@ -15,7 +15,8 @@ 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..."
@@ -32,7 +33,7 @@ build_docker:
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
watch_backend: watch_backend:
@echo "Building Opengist binary..." @echo "Building Opengist binary..."

View File

@@ -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)
@@ -77,17 +78,19 @@ 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.1/opengist1.5.1-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.6.0/opengist1.6.0-linux-amd64.tar.gz
tar xzvf opengist1.5.1-linux-amd64.tar.gz tar xzvf opengist1.6.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`
``` ```
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.21+), [Node.js](https://nodejs.org/en/download/) (16+)
```shell ```shell
git clone https://github.com/thomiceli/opengist git clone https://github.com/thomiceli/opengist

View File

@@ -5,8 +5,10 @@
# 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: trace, debug, info, warn, error, fatal, panic. 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,12 +67,18 @@ 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/
# 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:

View File

@@ -7,5 +7,6 @@ groupmod -o -g "$GID" $USER
usermod -o -u "$UID" $USER usermod -o -u "$UID" $USER
chown -R "$USER:$USER" /opengist chown -R "$USER:$USER" /opengist
chown -R "$USER:$USER" /config.yml
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist" exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"

View File

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

View File

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

View File

@@ -4,8 +4,8 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
## Github ## Github
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new) * Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback` * Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml ```yaml
github.client-key: <key> github.client-key: <key>
@@ -13,10 +13,23 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
``` ```
## GitLab
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
* Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
gitlab.client-key: <key>
gitlab.secret: <secret>
# URL of the GitLab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/
```
## Gitea ## Gitea
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications) * Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml ```yaml
gitea.client-key: <key> gitea.client-key: <key>
@@ -29,7 +42,7 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
## OpenID Connect ## OpenID Connect
* Add a new OAuth app in Application settings of your OIDC provider * Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback`
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) : * Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml ```yaml
oidc.client-key: <key> oidc.client-key: <key>

View File

@@ -1,25 +1,32 @@
# 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: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. | | 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. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |

View File

@@ -12,6 +12,17 @@ The [configuration cheat sheet](cheat-sheet.md) lists all available configuratio
The configuration file must be specified when launching the application, using the `--config` flag followed by the path The configuration file must be specified when launching the application, using the `--config` flag followed by the path
to your YAML file. to your YAML file.
Usage with Docker Compose :
```yml
services:
opengist:
# ...
volumes:
# ...
- "/path/to/config.yml:/config.yml"
```
Usage via command line :
```shell ```shell
./opengist --config /path/to/config.yml ./opengist --config /path/to/config.yml
``` ```
@@ -22,7 +33,6 @@ You can start by copying and/or modifying the provided [config.yml](/config.yml)
## Configuration via Environment Variables ## Configuration via Environment Variables
Usage with Docker Compose : Usage with Docker Compose :
```yml ```yml
services: services:
opengist: opengist:
@@ -31,8 +41,8 @@ services:
OG_LOG_LEVEL: "info" OG_LOG_LEVEL: "info"
# etc. # etc.
``` ```
Usage via command line :
Usage via command line :
```shell ```shell
OG_LOG_LEVEL=info ./opengist OG_LOG_LEVEL=info ./opengist
``` ```

View File

@@ -11,13 +11,15 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
* 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
* Editor with indentation mode & size ; drag and drop files * Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive * Download raw files or as a ZIP archive
* OAuth2 login with GitHub, Gitea, and OpenID Connect * Retrieve snippet data/metadata via a JSON API
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Avatars via Gravatar or OAuth2 providers * Avatars via Gravatar or OAuth2 providers
* Light/Dark mode * Light/Dark mode
* Responsive UI * Responsive UI
@@ -35,7 +37,7 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
## System requirements ## System requirements
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app. [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. 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. [OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.

View File

@@ -47,9 +47,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.1/opengist1.5.1-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.6.0/opengist1.6.0-linux-amd64.tar.gz
tar xzvf opengist1.5.1-linux-amd64.tar.gz tar xzvf opengist1.6.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`
@@ -59,8 +59,8 @@ chmod +x opengist
## From source ## From source
Requirements : Requirements :
* [Git](https://git-scm.com/downloads) (2.20+) * [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.20+) * [Go](https://go.dev/doc/install) (1.21+)
* [Node.js](https://nodejs.org/en/download/) (16+) * [Node.js](https://nodejs.org/en/download/) (16+)
```shell ```shell

57
docs/update.md Normal file
View File

@@ -0,0 +1,57 @@
# Update
## 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.6.0/opengist1.6.0-linux-amd64.tar.gz
tar xzvf opengist1.6.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```
### From source
Stop the running instance; then pull the last changes from the master branch, and build the new version.
```shell
git pull
make
./opengist
```
## Restore the backup
If you have any issue with the new version, you can restore the backup you made before updating.
```shell
rm -rf ~/.opengist
cp -r ~/.opengist.bak ~/.opengist
```
Then run the old version of Opengist again.

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

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

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

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

View File

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

84
go.mod
View File

@@ -1,54 +1,88 @@
module github.com/thomiceli/opengist module github.com/thomiceli/opengist
go 1.20 go 1.21
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.12.0
github.com/go-playground/validator/v10 v10.15.4 github.com/blevesearch/bleve/v2 v2.3.10
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.10.0
github.com/go-playground/validator/v10 v10.16.0
github.com/google/uuid v1.5.0
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.11.4
github.com/markbates/goth v1.78.0 github.com/markbates/goth v1.78.0
github.com/rs/zerolog v1.30.0 github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.13.0 github.com/yuin/goldmark v1.6.0
golang.org/x/text v0.13.0 github.com/yuin/goldmark-emoji v1.0.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.5.0
golang.org/x/crypto v0.17.0
golang.org/x/text v0.14.0
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.5
) )
require ( require (
github.com/RoaringBitmap/roaring v1.7.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/blevesearch/bleve_index_api v1.1.4 // indirect
github.com/blevesearch/geo v0.1.18 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.5 // indirect
github.com/blevesearch/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/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.10.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // 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/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/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/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // 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/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/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 go.etcd.io/bbolt v1.3.8 // indirect
golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/libc v1.38.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.7.2 // indirect
modernc.org/sqlite v1.25.0 // indirect modernc.org/sqlite v1.28.0 // indirect
) )

206
go.sum
View File

@@ -34,7 +34,60 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
github.com/blevesearch/bleve_index_api v1.1.4 h1:n9Ilxlb80g9DAhchR95IcVrzohamDSri0wPnkKnva50=
github.com/blevesearch/bleve_index_api v1.1.4/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.2.5 h1:5SsNQmR8v1bojtGQ1zFhZravcMg5rdiX8AVu6LwlVtc=
github.com/blevesearch/scorch_segment_api/v2 v2.2.5/go.mod h1:8N2ytOlBCdurlxDgbqsfeR1oTKRN0ZVIKdUUP1VFZNc=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -46,33 +99,46 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw 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-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-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.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -101,6 +167,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -113,6 +181,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/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.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/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -124,21 +196,23 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 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/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.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY= github.com/gorilla/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/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.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=
@@ -151,12 +225,18 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -165,10 +245,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
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.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
@@ -177,31 +257,44 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4= github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= 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 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc= 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/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/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/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/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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -213,14 +306,25 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -233,8 +337,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -296,16 +400,16 @@ golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-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-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-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-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-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.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.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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-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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -344,18 +448,17 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-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/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -364,13 +467,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-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-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.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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-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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -500,19 +603,18 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/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.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.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -520,14 +622,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
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.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

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

@@ -0,0 +1,177 @@
package actions
import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"os"
"path/filepath"
"strings"
"sync"
)
type ActionStatus struct {
Running bool
}
const (
SyncReposFromFS = iota
SyncReposFromDB = iota
GitGcRepos = iota
SyncGistPreviews = iota
ResetHooks = iota
IndexGists = iota
)
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:
panic("unhandled default case")
}
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)
}
}
}

View File

@@ -2,10 +2,12 @@ package config
import ( import (
"fmt" "fmt"
"io"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -15,7 +17,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
var OpengistVersion = "1.5.1" var OpengistVersion = "1.6.0"
var C *config var C *config
@@ -23,9 +25,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,6 +49,10 @@ 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"`
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"`
@@ -52,15 +63,14 @@ type config struct {
} }
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,7 +83,7 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222" c.SshPort = "2222"
c.SshKeygen = "ssh-keygen" c.SshKeygen = "ssh-keygen"
c.GiteaUrl = "http://gitea.com" c.GiteaUrl = "https://gitea.com"
return c, nil return c, nil
} }
@@ -93,6 +103,15 @@ func InitConfig(configPath string) error {
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
} }
@@ -106,21 +125,46 @@ 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
logOutputTypes := utils.RemoveDuplicates[string](
strings.Split(strings.ToLower(C.LogOutput), ","),
)
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, zerolog.NewConsoleWriter())
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, zerolog.NewConsoleWriter())
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
}
multi := zerolog.MultiLevelWriter(logWriters...)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger() log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) { if !slices.Contains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel) log.Warn().Msg("Invalid log level: " + C.LogLevel)
} }
} }
@@ -139,8 +183,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

View File

@@ -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)
} }
@@ -80,3 +80,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()
}

View File

@@ -1,22 +1,63 @@
package db package db
import ( import (
"fmt"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/git" "github.com/rs/zerolog/log"
"gorm.io/gorm" "github.com/thomiceli/opengist/internal/index"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
) )
type Visibility int
const (
PublicVisibility Visibility = iota
UnlistedVisibility
PrivateVisibility
)
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":
return PublicVisibility, nil
case "1":
return UnlistedVisibility, nil
case "2":
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 +89,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 +218,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 +250,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 {
@@ -274,25 +338,25 @@ 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 +369,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 +398,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 +419,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 +451,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,22 +477,70 @@ 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 -- //
type GistDTO struct { type GistDTO struct {
Title string `validate:"max=50" form:"title"` Title string `validate:"max=250" form:"title"`
Description string `validate:"max=150" 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"` Private Visibility `validate:"number,min=0,max=2" form:"private"`
Name []string `form:"name"` Files []FileDTO `validate:"min=1,dive"`
Content []string `form:"content"` Name []string `form:"name"`
Content []string `form:"content"`
} }
type FileDTO struct { type FileDTO struct {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"` Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string `validate:"required"` Content string `validate:"required"`
} }
@@ -404,11 +549,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)
}
}()
}

View File

@@ -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
} }
@@ -129,6 +134,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 +174,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 +194,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"`
} }

View File

@@ -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 (
@@ -20,6 +25,12 @@ var (
const truncateLimit = 2 << 18 const truncateLimit = 2 << 18
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,18 +64,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 { func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
@@ -113,6 +126,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 +248,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 +261,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) {
@@ -180,7 +326,7 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
return parseLog(stdout, truncateLimit), err return parseLog(stdout, truncateLimit), err
} }
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 +344,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 +371,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 {
@@ -377,7 +525,9 @@ 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
@@ -392,7 +542,7 @@ func createDotGitFiles(repositoryPath string) error {
} }
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
} }
@@ -459,6 +609,12 @@ fi
const postReceive = `#!/bin/sh const postReceive = `#!/bin/sh
while read oldrev newrev refname; do
if ! git rev-parse --verify --quiet HEAD &>/dev/null; then
git symbolic-ref HEAD "$refname"
fi
done
echo "" echo ""
echo "Your new repository has been created here: %s" echo "Your new repository has been created here: %s"
echo "" echo ""

View File

@@ -271,8 +271,29 @@ func TestInitViaGitInit(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func TestGitInitBranchNames(t *testing.T) {
setup(t)
defer teardown(t)
cmd := exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = RepositoryPath("thomas", "gist1")
out, err := cmd.Output()
require.NoError(t, err, "Could not run git command")
require.Equal(t, "refs/heads/master", strings.TrimSpace(string(out)), "Repository should have master branch as default")
config.C.GitDefaultBranch = "main"
err = InitRepository("thomas", "gist2")
require.NoError(t, err)
cmd = exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = RepositoryPath("thomas", "gist2")
out, err = cmd.Output()
require.NoError(t, err, "Could not run git command")
require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default")
}
func commitToBare(t *testing.T, user string, gist string, files map[string]string) { func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com") err := CloneTmp(user, gist, gist, "thomas@mail.com", true)
require.NoError(t, err, "Could not commit to repository") require.NoError(t, err, "Could not commit to repository")
if len(files) > 0 { if len(files) > 0 {

View File

@@ -11,12 +11,14 @@ import (
) )
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 {
@@ -92,6 +94,11 @@ func parseLog(out io.Reader, maxBytes int) []*Commit {
scanner.Scan() scanner.Scan()
if len(scanner.Bytes()) == 0 {
commits = append(commits, currentCommit)
break
}
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one // 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] != ' ' { if scanner.Bytes()[0] != ' ' {
commits = append(commits, currentCommit) commits = append(commits, currentCommit)

View File

@@ -53,6 +53,8 @@ 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{

View File

@@ -0,0 +1,184 @@
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.github-oauth: Pokračovat s účtem na GitHubu
auth.gitea-oauth: Pokračovat s účtem na Gitea
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.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?

View File

@@ -17,19 +17,20 @@ gist.header.clone-http: Clone via %s
gist.header.clone-http-help: Clone with Git using HTTP basic authentication. gist.header.clone-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
@@ -67,6 +68,14 @@ 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.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
@@ -80,7 +89,7 @@ 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
@@ -92,8 +101,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 +117,12 @@ 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.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
@@ -116,6 +133,7 @@ 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.github-oauth: Continue with GitHub account
auth.gitlab-oauth: Continue with GitLab account
auth.gitea-oauth: Continue with Gitea account auth.gitea-oauth: Continue with Gitea account
error: Error error: Error
@@ -151,7 +169,10 @@ 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

View File

@@ -0,0 +1,177 @@
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.github-oauth: Continuar con cuenta de GitHub
auth.gitea-oauth: Continuar con cuenta de Gitea
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.config-link-overridden: sobrescrito
admin.disable-signup: Deshabilitar registro
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
admin.require-login: Requerir inicio de sesión
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.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?

View File

@@ -17,15 +17,15 @@ gist.header.clone-http: Cloner via %s
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic. gist.header.clone-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:
gist.header.share-help: Copier le lien partageable de ce gist. gist.header.embed-help:
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

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: "Clone-ozás ezzel: %s"
gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel. gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel.
gist.header.clone-ssh: Clone-ozás SSH-n keresztül gist.header.clone-ssh: Clone-ozás SSH-n keresztül
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
gist.header.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
@@ -106,6 +106,11 @@ 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.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

View File

@@ -0,0 +1,177 @@
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.share: Compartilhar
gist.header.share-help: Copiar link para compartilhar este gist.
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.github-oauth: Continuar com conta do GitHub
auth.gitea-oauth: Continuar com conta do Gitea
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.config-link-overridden: sobrescrito
admin.disable-signup: Desabilitar registro
admin.disable-signup_help: Proibir a criação de novas contas.
admin.require-login: Exigir login
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.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?

View File

@@ -0,0 +1,177 @@
gist.public: Публичный
gist.unlisted: Скрытый
gist.private: Приватный
gist.header.like: Нравится
gist.header.unlike: Не нравится
gist.header.fork: Создать форк
gist.header.edit: Редактировать
gist.header.delete: Удалить
gist.header.forked-from: Форк с
gist.header.last-active: Последняя активность
gist.header.select-tab: Перейти
gist.header.code: Код
gist.header.revisions: Версии
gist.header.revision: Версия
gist.header.clone-http: Клонировать с помощью %s
gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP.
gist.header.clone-ssh: Клонировать c помощью SSH
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: Скачать ZIP-архив
gist.raw: Исходник
gist.file-truncated: Файл был обрезан.
gist.watch-full-file: Просмотр всего файла.
gist.file-not-valid: Невалидный CSV.
gist.no-content: Нет данных
gist.new.new_gist: Новый фрагмент
gist.new.title: Название
gist.new.description: Описание
gist.new.filename-with-extension: Имя файла с расширением
gist.new.indent-mode: Отступы
gist.new.indent-mode-space: Пробелы
gist.new.indent-mode-tab: Табуляция
gist.new.indent-size: Размер отступа
gist.new.wrap-mode: Переносы строк
gist.new.wrap-mode-no: Без переносов
gist.new.wrap-mode-soft: Мягкие переносы
gist.new.add-file: Добавить файл
gist.new.create-public-button: Создать публичный фрагмент
gist.new.create-unlisted-button: Создать скрытый фрагмент
gist.new.create-private-button: Создать приватный фрагмент
gist.edit.editing: Редактирование
gist.edit.change-visibility: Применить
gist.edit.delete: Удалить
gist.edit.cancel: Отмена
gist.edit.save: Сохранить
gist.list.joined: Зарегистрирован
gist.list.all: Все фрагменты
gist.list.search-results: Результаты поиска
gist.list.sort: Сортировка
gist.list.sort-by-created: по дате создания
gist.list.sort-by-updated: по дате обновления
gist.list.order-by-asc: Свежие снизу
gist.list.order-by-desc: Свежие сверху
gist.list.select-tab: Перейти
gist.list.liked: Понравившиеся
gist.list.likes: лайк(-ов)
gist.list.forked: Форки
gist.list.forked-from: Форки с
gist.list.forks: форк(-ов)
gist.list.files: файл(-ов)
gist.list.last-active: Последняя активность
gist.list.no-gists: Нет фрагментов
gist.forks: Форки
gist.forks.view: Посмотреть форк
gist.forks.no: Нет форков
gist.likes: Нравятся
gist.likes.no: Нет
gist.revisions: Ревизии
gist.revision.revised: ревизий этого фрагмента
gist.revision.go-to-revision: К ревизии
gist.revision.file-created: файл создан
gist.revision.file-deleted: файл удалён
gist.revision.file-renamed: переименован в
gist.revision.diff-truncated: Разница (diff) обрезана, так как результат слишком большой для показа
gist.revision.file-renamed-no-changes: Файл переименован без изменений
gist.revision.empty-file: Пустой файл
gist.revision.no-changes: Без изменений
gist.revision.no-revisions: Нет ревизий
settings: Настройки
settings.email: Адрес эл. почты
settings.email-help: Нужен для коммитов и Gravatar
settings.email-set: Сохранить адрес
settings.link-accounts: Привязка доступов
settings.link-github-account: Привязать доступ GitHub
settings.link-gitea-account: Привязать доступ Gitea
settings.unlink-github-account: Отвязать доступ GitHub
settings.unlink-gitea-account: Отвязать доступ Gitea
settings.delete-account: Удалить аккаунт
settings.delete-account-confirm: Вы уверены что хотите удалить свой аккаунт?
settings.add-ssh-key: Добавить ключ SSH
settings.add-ssh-key-help: Нужен только для работы с фрагментами через Git+SSH
settings.add-ssh-key-title: Название
settings.add-ssh-key-content: Ключ
settings.delete-ssh-key: Удалить
settings.delete-ssh-key-confirm: Подтвердите удаления ключа SSH
settings.ssh-key-added-at: Дата добавления
settings.ssh-key-never-used: Не был использован
settings.ssh-key-last-used: Последнее использование
auth.signup-disabled: Регистрация запрещена Администратором сервиса
auth.login: Вход
auth.signup: Регистрация
auth.new-account: Новый аккаунт
auth.username: Имя пользователя
auth.password: Пароль
auth.register-instead: Зарегистрироваться
auth.login-instead: Войти
auth.github-oauth: Войти с помощью доступа GitHub
auth.gitea-oauth: Войти с помощью доступа Gitea
error: Ошибка
header.menu.all: Все
header.menu.new: Новый
header.menu.search: Поиск
header.menu.my-gists: Мои фрагменты
header.menu.liked: Понравившиеся
header.menu.admin: Администрирование
header.menu.settings: Настройки
header.menu.logout: Выйти
header.menu.register: Регистрация
header.menu.login: Войти
header.menu.light: Светлая
header.menu.dark: Тёмная
header.menu.system: Системная
footer.powered-by: Работает на %s
pagination.older: Позже
pagination.newer: Новее
pagination.previous: Предыдущий
pagination.next: Следующий
admin.admin_panel: Панель управления
admin.general: Общее
admin.users: Пользователи
admin.gists: Фрагменты
admin.configuration: Настройки
admin.versions: Версии
admin.ssh_keys: Ключи SSH
admin.stats: Статистика
admin.actions: Действия
admin.actions.sync-fs: Синхронизировать фрагменты из файловой системы
admin.actions.sync-db: Синхронизировать фрагменты с базой данных
admin.actions.git-gc: Сборка мусора в репозиториях Git
admin.id: ID
admin.user: Пользователь
admin.delete: Удалить
admin.created_at: Создан
admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения.
admin.config-link-overriden: перекрыты
admin.disable-signup: Запретить регистрацию
admin.disable-signup_help: Запретить создание новых доступов
admin.require-login: Требовать авторизацию
admin.require-login_help: Запретить просмотр фрагментов без авторизации.
admin.disable-login: Запретить авторизацию по паролю
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
admin.disable-gravatar: Запретить Gravatar
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
admin.gists.title: Название
admin.gists.private: Приватный
admin.gists.nb-files: Файлов
admin.gists.nb-likes: Понравилось
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?

View File

@@ -0,0 +1,177 @@
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: Select a 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.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.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.edit.editing: 编辑
gist.edit.change-visibility: 设为
gist.edit.delete: 删除
gist.edit.cancel: 取消
gist.edit.save: 保存
gist.list.joined: 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: Select a tab
gist.list.liked: 已喜欢
gist.list.likes: 喜欢
gist.list.forked: 已派生
gist.list.forked-from: 派生自
gist.list.forks: 派生
gist.list.files: 文件
gist.list.last-active: 最后活跃于
gist.list.no-gists: 没有 Gist
gist.forks: 派生
gist.forks.view: 查看派生
gist.forks.no: 无公开派生
gist.likes: 喜欢
gist.likes.no: 还没有喜欢
gist.revisions: 修订
gist.revision.revised: 修订了这个 Gist
gist.revision.go-to-revision: 跳至此修订
gist.revision.file-created: file created
gist.revision.file-deleted: file deleted
gist.revision.file-renamed: 重命名为
gist.revision.diff-truncated: 由于变更差异过大,显示内容已被截断
gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: 空文件
gist.revision.no-changes: 没有变更
gist.revision.no-revisions: 无可供显示的修订
settings: 设置
settings.email: 邮箱
settings.email-help: 用于提交与 Gravatar
settings.email-set: 设置邮箱地址
settings.link-accounts: 关联账号
settings.link-github-account: 关联 GitHub 账号
settings.link-gitea-account: 关联 Gitea 账号
settings.unlink-github-account: 解除关联 GitHub 账号
settings.unlink-gitea-account: 解除关联 Gitea 账号
settings.delete-account: 删除账号
settings.delete-account-confirm: 您确认要删除您的账号吗?
settings.add-ssh-key: 添加 SSH 密钥
settings.add-ssh-key-help: 用于使用 Git 通过 SSH 拉取与推送 Gist
settings.add-ssh-key-title: 标题
settings.add-ssh-key-content: 密钥
settings.delete-ssh-key: 删除
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
settings.ssh-key-added-at: 添加
settings.ssh-key-never-used: 从未使用过
settings.ssh-key-last-used: 最后使用于
auth.signup-disabled: 管理员已禁用了注册
auth.login: 登录
auth.signup: 注册
auth.new-account: 新建账号
auth.username: 用户名
auth.password: 密码
auth.register-instead: 转到注册
auth.login-instead: 转到登录
auth.github-oauth: 使用 GitHub 账号继续
auth.gitea-oauth: 使用 Gitea 账号继续
error: 错误
header.menu.all: 全部
header.menu.new: 创建
header.menu.search: 搜索
header.menu.my-gists: 我的 Gists
header.menu.liked: 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.versions: 版本
admin.ssh_keys: SSH 密钥
admin.stats: 状态
admin.actions: 动作
admin.actions.sync-fs: 从文件系统同步 Gist
admin.actions.sync-db: 从数据库同步 Gist
admin.actions.git-gc: 对 Git 仓库执行垃圾回收
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.users.delete_confirm: 你想要删除此用户吗?
admin.gists.title: 标题
admin.gists.private: 私有?
admin.gists.nb-files: 文件数
admin.gists.nb-likes: 喜欢数
admin.gists.delete_confirm: 你想要删除此 Gist 吗?

View File

@@ -0,0 +1,189 @@
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.github-oauth: 用 GitHub 帳號繼續
auth.gitlab-oauth: 用 GitLab 帳號繼續
auth.gitea-oauth: 用 Gitea 帳號繼續
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.users.delete_confirm: 您要刪除這個使用者嗎?
admin.gists.title: 標題
admin.gists.private: 是否為私人
admin.gists.nb-files: 檔案數
admin.gists.nb-likes: 喜歡
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?

157
internal/index/bleve.go Normal file
View File

@@ -0,0 +1,157 @@
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/thomiceli/opengist/internal/config"
"strconv"
)
var bleveIndex bleve.Index
func Enabled() bool {
return config.C.IndexEnabled
}
func Open(indexFilename string) error {
var err error
bleveIndex, err = bleve.Open(indexFilename)
if err == nil {
return nil
}
if !errors.Is(err, bleve.ErrorIndexPathDoesNotExist) {
return 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 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 err
}
docMapping.DefaultAnalyzer = "gistAnalyser"
bleveIndex, err = bleve.New(indexFilename, mapping)
return err
}
func Close() error {
return bleveIndex.Close()
}
func AddInIndex(gist *Gist) error {
if !Enabled() {
return nil
}
if gist == nil {
return errors.New("failed to add nil gist to index")
}
return bleveIndex.Index(strconv.Itoa(int(gist.GistID)), gist)
}
func RemoveFromIndex(gistID uint) error {
if !Enabled() {
return nil
}
return bleveIndex.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
}
var err error
var indexerQuery query.Query
if queryStr != "" {
contentQuery := bleve.NewMatchPhraseQuery(queryStr)
contentQuery.FieldVal = "Content"
indexerQuery = contentQuery
} else {
contentQuery := bleve.NewMatchAllQuery()
indexerQuery = contentQuery
}
if len(gistsIds) > 0 {
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 := bleveIndex.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
View 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
}

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

113
internal/render/markdown.go Normal file
View File

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

View File

@@ -94,7 +94,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

View File

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

View File

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

View File

@@ -2,21 +2,12 @@ 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"
)
var (
syncReposFromFS = false
syncReposFromDB = false
gitGcRepos = false
) )
func adminIndex(ctx echo.Context) error { func adminIndex(ctx echo.Context) error {
@@ -50,9 +41,12 @@ 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")
} }
@@ -123,84 +117,45 @@ func adminGistDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this gist", err) return errorRes(500, "Cannot delete this gist", err)
} }
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success") addFlash(ctx, "Gist has been 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, "Syncing repositories from filesystem...", "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, "Syncing repositories from database...", "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, "Garbage collecting repositories...", "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, "Syncing Gist 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, "Resetting Git server hooks for all repositories...", "success")
go actions.Run(actions.ResetHooks)
return redirect(ctx, "/admin-panel")
}
func adminIndexGists(ctx echo.Context) error {
addFlash(ctx, "Indexing all gists...", "success")
go actions.Run(actions.IndexGists)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }

View File

@@ -16,6 +16,7 @@ import (
"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"
@@ -25,6 +26,13 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
GitHubProvider = "github"
GitLabProvider = "gitlab"
GiteaProvider = "gitea"
OpenIDConnect = "openid-connect"
)
var title = cases.Title(language.English) var title = cases.Title(language.English)
func register(ctx echo.Context) error { func register(ctx echo.Context) error {
@@ -140,23 +148,13 @@ 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, "Cannot complete user auth: "+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 "+title.String(user.Provider)+" id", err)
@@ -184,17 +182,7 @@ 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) {
@@ -213,11 +201,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")
} }
@@ -273,7 +263,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 +272,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 +295,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,31 +315,21 @@ 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 "+title.String(provider), err)
}
if isDelete {
addFlash(ctx, "Account unlinked from "+title.String(provider), "success") addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
return redirect(ctx, "/settings") return redirect(ctx, "/settings")
} }
@@ -345,7 +337,7 @@ func oauth(ctx echo.Context) error {
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, "Unsupported provider", nil)
} }
@@ -368,11 +360,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")

View File

@@ -2,31 +2,59 @@ package web
import ( import (
"archive/zip" "archive/zip"
"bufio"
"bytes" "bytes"
"errors" "errors"
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"html/template"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"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 {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
currUser := getUserLogged(ctx)
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 == db.PrivateVisibility {
if currUser == nil || currUser.ID != gist.UserID {
return notFound("Gist not found")
}
}
setData(ctx, "gist", gist) setData(ctx, "gist", gist)
if config.C.SshGit { if config.C.SshGit {
@@ -59,12 +87,15 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
} }
setData(ctx, "baseHttpUrl", baseHttpUrl)
if config.C.HttpGit { if config.C.HttpGit {
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git") setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
} }
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 {
@@ -72,7 +103,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
} }
setData(ctx, "nbCommits", nbCommits) setData(ctx, "nbCommits", nbCommits)
if currUser := getUserLogged(ctx); currUser != nil { if currUser != nil {
hasLiked, err := currUser.HasLiked(gist) hasLiked, err := currUser.HasLiked(gist)
if err != nil { if err != nil {
return errorRes(500, "Cannot get user like status", err) return errorRes(500, "Cannot get user like status", err)
@@ -222,11 +253,20 @@ func allGists(ctx echo.Context) error {
} }
} }
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, "Page not found", nil)
} }
@@ -234,7 +274,76 @@ func allGists(ctx echo.Context) error {
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", tr(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
setData(ctx, "urlPage", "search")
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
setData(ctx, "htmlTitle", "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")
@@ -242,27 +351,107 @@ 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')
`
js = fmt.Sprintf(js, cssUrl,
strings.Replace(htmlbuf.String(), "\n", `\n`, -1))
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)
@@ -354,7 +543,7 @@ func processCreate(ctx echo.Context) 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)
} }
@@ -420,34 +609,37 @@ 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 toggleVisibility(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist) gist := getData(ctx, "gist").(*db.Gist)
gist.Private = (gist.Private + 1) % 3 gist.Private = (gist.Private + 1) % 3
if err := gist.Update(); err != nil { 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, "Gist visibility has been 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, "Gist has been 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)
@@ -465,7 +657,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
} }
@@ -473,7 +665,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)
@@ -483,11 +675,11 @@ 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, "Unable to fork own gists", "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()
@@ -520,13 +712,12 @@ func fork(ctx echo.Context) error {
addFlash(ctx, "Gist has been forked", "success") addFlash(ctx, "Gist has been 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)
} }
@@ -541,7 +732,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,9 +753,9 @@ 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)
} }
@@ -577,10 +767,10 @@ func edit(ctx echo.Context) error {
} }
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)
} }
@@ -612,7 +802,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 {
@@ -622,7 +812,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)
@@ -631,7 +821,7 @@ 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, "Page not found", nil)
} }
@@ -641,7 +831,7 @@ func likes(ctx echo.Context) error {
} }
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)
@@ -655,7 +845,7 @@ 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, "Page not found", nil)
} }
@@ -663,3 +853,39 @@ func forks(ctx echo.Context) error {
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, "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")
}

View File

@@ -6,13 +6,6 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
"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 +14,14 @@ 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/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"gorm.io/gorm"
) )
var routes = []struct { var routes = []struct {
@@ -73,7 +74,7 @@ func gitHttp(ctx echo.Context) error {
// - 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 && !getData(ctx, "RequireLogin").(bool) {
return route.handler(ctx) return route.handler(ctx)
} }
@@ -216,7 +217,8 @@ func pack(ctx echo.Context, serviceType string) error {
} }
_ = gist.SetLastActiveNow() _ = gist.SetLastActiveNow()
_ = gist.UpdatePreviewAndCount() _ = gist.UpdatePreviewAndCount(false)
gist.AddInIndex()
} }
return nil return nil
} }

View File

@@ -0,0 +1,25 @@
package web
import (
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/db"
"time"
)
func healthcheck(ctx echo.Context) error {
// Check database connection
dbOk := "ok"
httpStatus := 200
err := db.Ping()
if err != nil {
dbOk = "ko"
httpStatus = 503
}
return ctx.JSON(httpStatus, map[string]interface{}{
"opengist": "ok",
"database": dbOk,
"time": time.Now().Format(time.RFC3339),
})
}

View File

@@ -3,7 +3,19 @@ package web
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/index"
htmlpkg "html"
"html/template"
"io"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
@@ -16,106 +28,113 @@ import (
"github.com/thomiceli/opengist/public" "github.com/thomiceli/opengist/public"
"github.com/thomiceli/opengist/templates" "github.com/thomiceli/opengist/templates"
"golang.org/x/text/language" "golang.org/x/text/language"
htmlpkg "html"
"html/template"
"io"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
) )
var dev bool var (
var store *sessions.CookieStore dev bool
var re = regexp.MustCompile("[^a-z0-9]+") store *sessions.CookieStore
var fm = template.FuncMap{ re = regexp.MustCompile("[^a-z0-9]+")
"split": strings.Split, fm = template.FuncMap{
"indexByte": strings.IndexByte, "split": strings.Split,
"toInt": func(i string) int { "indexByte": strings.IndexByte,
val, _ := strconv.Atoi(i) "toInt": func(i string) int {
return val val, _ := strconv.Atoi(i)
}, return val
"inc": func(i int) int { },
return i + 1 "inc": func(i int) int {
}, return i + 1
"splitGit": func(i string) []string { },
return strings.FieldsFunc(i, func(r rune) bool { "splitGit": func(i string) []string {
return r == ',' || r == ' ' return strings.FieldsFunc(i, func(r rune) bool {
}) return r == ',' || r == ' '
}, })
"lines": func(i string) []string { },
return strings.Split(i, "\n") "lines": func(i string) []string {
}, return strings.Split(i, "\n")
"isMarkdown": func(i string) bool { },
return strings.ToLower(filepath.Ext(i)) == ".md" "isMarkdown": func(i string) bool {
}, return strings.ToLower(filepath.Ext(i)) == ".md"
"isCsv": func(i string) bool { },
return strings.ToLower(filepath.Ext(i)) == ".csv" "isCsv": func(i string) bool {
}, return strings.ToLower(filepath.Ext(i)) == ".csv"
"csvFile": func(file *git.File) *git.CsvFile { },
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" { "csvFile": func(file *git.File) *git.CsvFile {
return nil if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
} return nil
}
csvFile, err := git.ParseCsv(file) csvFile, err := git.ParseCsv(file)
if err != nil { if err != nil {
return nil return nil
} }
return csvFile return csvFile
}, },
"httpStatusText": http.StatusText, "httpStatusText": http.StatusText,
"loadedTime": func(startTime time.Time) string { "loadedTime": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
}, },
"slug": func(s string) string { "slug": func(s string) string {
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-") return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
}, },
"avatarUrl": func(user *db.User, noGravatar bool) string { "avatarUrl": func(user *db.User, noGravatar bool) string {
if user.AvatarURL != "" { if user.AvatarURL != "" {
return user.AvatarURL return user.AvatarURL
} }
if user.MD5Hash != "" && !noGravatar { if user.MD5Hash != "" && !noGravatar {
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200" return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
} }
return defaultAvatar() return defaultAvatar()
}, },
"asset": func(file string) string { "asset": asset,
if dev { "dev": func() bool {
return "http://localhost:16157/" + file return dev
} },
return config.C.ExternalUrl + "/" + manifestEntries[file].File "defaultAvatar": defaultAvatar,
}, "visibilityStr": func(visibility db.Visibility, lowercase bool) string {
"dev": func() bool { s := "Public"
return dev switch visibility {
}, case 1:
"defaultAvatar": defaultAvatar, s = "Unlisted"
"visibilityStr": func(visibility int, lowercase bool) string { case 2:
s := "Public" s = "Private"
switch visibility { }
case 1:
s = "Unlisted"
case 2:
s = "Private"
}
if lowercase { if lowercase {
return strings.ToLower(s) return strings.ToLower(s)
} }
return s return s
}, },
"unescape": htmlpkg.UnescapeString, "unescape": htmlpkg.UnescapeString,
"join": func(s ...string) string { "join": func(s ...string) string {
return strings.Join(s, "") return strings.Join(s, "")
}, },
"toStr": func(i interface{}) string { "toStr": func(i interface{}) string {
return fmt.Sprint(i) return fmt.Sprint(i)
}, },
} "safe": func(s string) template.HTML {
return template.HTML(s)
},
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"addMetadataToSearchQuery": addMetadataToSearchQuery,
"indexEnabled": index.Enabled,
}
)
type Template struct { type Template struct {
templates *template.Template templates *template.Template
@@ -159,7 +178,7 @@ func NewServer(isDev bool) *Server {
return nil return nil
}, },
})) }))
//e.Use(middleware.Recover()) e.Use(middleware.Recover())
e.Use(middleware.Secure()) e.Use(middleware.Secure())
e.Renderer = &Template{ e.Renderer = &Template{
@@ -205,6 +224,8 @@ func NewServer(isDev bool) *Server {
g1.GET("/", create, logged) g1.GET("/", create, logged)
g1.POST("/", processCreate, logged) g1.POST("/", processCreate, logged)
g1.GET("/healthcheck", healthcheck)
g1.GET("/register", register) g1.GET("/register", register)
g1.POST("/register", processRegister) g1.POST("/register", processRegister)
g1.GET("/login", login) g1.GET("/login", login)
@@ -218,7 +239,8 @@ func NewServer(isDev bool) *Server {
g1.DELETE("/settings/account", accountDeleteProcess, logged) g1.DELETE("/settings/account", accountDeleteProcess, logged)
g1.POST("/settings/ssh-keys", sshKeysProcess, logged) g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged) g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
g1.PUT("/settings/password", passwordProcess, logged)
g1.PUT("/settings/username", usernameProcess, logged)
g2 := g1.Group("/admin-panel") g2 := g1.Group("/admin-panel")
{ {
g2.Use(adminPermission) g2.Use(adminPermission)
@@ -230,6 +252,9 @@ func NewServer(isDev bool) *Server {
g2.POST("/sync-fs", adminSyncReposFromFS) g2.POST("/sync-fs", adminSyncReposFromFS)
g2.POST("/sync-db", adminSyncReposFromDB) g2.POST("/sync-db", adminSyncReposFromDB)
g2.POST("/gc-repos", adminGcRepos) g2.POST("/gc-repos", adminGcRepos)
g2.POST("/sync-previews", adminSyncGistPreviews)
g2.POST("/reset-hooks", adminResetHooks)
g2.POST("/index-gists", adminIndexGists)
g2.GET("/configuration", adminConfig) g2.GET("/configuration", adminConfig)
g2.PUT("/set-config", adminSetConfig) g2.PUT("/set-config", adminSetConfig)
} }
@@ -239,7 +264,13 @@ func NewServer(isDev bool) *Server {
} }
g1.GET("/all", allGists, checkRequireLogin) g1.GET("/all", allGists, checkRequireLogin)
g1.GET("/search", allGists, checkRequireLogin)
if index.Enabled() {
g1.GET("/search", search, checkRequireLogin)
} else {
g1.GET("/search", allGists, checkRequireLogin)
}
g1.GET("/:user", allGists, checkRequireLogin) g1.GET("/:user", allGists, checkRequireLogin)
g1.GET("/:user/liked", allGists, checkRequireLogin) g1.GET("/:user/liked", allGists, checkRequireLogin)
g1.GET("/:user/forked", allGists, checkRequireLogin) g1.GET("/:user/forked", allGists, checkRequireLogin)
@@ -261,6 +292,7 @@ func NewServer(isDev bool) *Server {
g3.GET("/likes", likes) g3.GET("/likes", likes)
g3.POST("/fork", fork, logged) g3.POST("/fork", fork, logged)
g3.GET("/forks", forks) g3.GET("/forks", forks)
g3.PUT("/checkbox", checkbox, logged, writePermission)
} }
} }
@@ -306,6 +338,7 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "c", config.C) setData(ctx, "c", config.C)
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "") setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "") setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "") setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
@@ -315,7 +348,6 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
func locale(next echo.HandlerFunc) echo.HandlerFunc { func locale(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
// Check URL arguments // Check URL arguments
lang := ctx.Request().URL.Query().Get("lang") lang := ctx.Request().URL.Query().Get("lang")
changeLang := lang != "" changeLang := lang != ""
@@ -334,7 +366,7 @@ func locale(next echo.HandlerFunc) echo.HandlerFunc {
changeLang = false changeLang = false
} }
//3.Then check from 'Accept-Language' header. // 3.Then check from 'Accept-Language' header.
if len(lang) == 0 { if len(lang) == 0 {
tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language")) tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language"))
lang = i18n.Locales.MatchTag(tags) lang = i18n.Locales.MatchTag(tags)
@@ -393,7 +425,7 @@ func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
gist := getData(ctx, "gist") gist := getData(ctx, "gist")
user := getUserLogged(ctx) user := getUserLogged(ctx)
if !gist.(*db.Gist).CanWrite(user) { if !gist.(*db.Gist).CanWrite(user) {
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Uuid) return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
} }
return next(ctx) return next(ctx)
} }
@@ -473,3 +505,10 @@ func defaultAvatar() string {
} }
return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
} }
func asset(file string) string {
if dev {
return "http://localhost:16157/" + file
}
return config.C.ExternalUrl + "/" + manifestEntries[file].File
}

View File

@@ -3,12 +3,16 @@ package web
import ( import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"github.com/labstack/echo/v4" "github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db" "os"
"golang.org/x/crypto/ssh" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/db"
"golang.org/x/crypto/ssh"
) )
func userSettings(ctx echo.Context) error { func userSettings(ctx echo.Context) error {
@@ -21,6 +25,7 @@ func userSettings(ctx echo.Context) error {
setData(ctx, "email", user.Email) setData(ctx, "email", user.Email)
setData(ctx, "sshKeys", keys) setData(ctx, "sshKeys", keys)
setData(ctx, "hasPassword", user.Password != "")
setData(ctx, "htmlTitle", "Settings") setData(ctx, "htmlTitle", "Settings")
return html(ctx, "settings.html") return html(ctx, "settings.html")
} }
@@ -61,7 +66,7 @@ func accountDeleteProcess(ctx echo.Context) error {
func sshKeysProcess(ctx echo.Context) error { func sshKeysProcess(ctx echo.Context) error {
user := getUserLogged(ctx) user := getUserLogged(ctx)
var dto = new(db.SSHKeyDTO) dto := new(db.SSHKeyDTO)
if err := ctx.Bind(dto); err != nil { if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err) return errorRes(400, "Cannot bind data", err)
} }
@@ -92,7 +97,6 @@ func sshKeysProcess(ctx echo.Context) error {
func sshKeysDelete(ctx echo.Context) error { func sshKeysDelete(ctx echo.Context) error {
user := getUserLogged(ctx) user := getUserLogged(ctx)
keyId, err := strconv.Atoi(ctx.Param("id")) keyId, err := strconv.Atoi(ctx.Param("id"))
if err != nil { if err != nil {
return redirect(ctx, "/settings") return redirect(ctx, "/settings")
} }
@@ -110,3 +114,67 @@ func sshKeysDelete(ctx echo.Context) error {
addFlash(ctx, "SSH key deleted", "success") addFlash(ctx, "SSH key deleted", "success")
return redirect(ctx, "/settings") return redirect(ctx, "/settings")
} }
func passwordProcess(ctx echo.Context) error {
user := getUserLogged(ctx)
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
}
dto.Username = user.Username
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
return html(ctx, "settings.html")
}
password, err := argon2id.hash(dto.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
user.Password = password
if err = user.Update(); err != nil {
return errorRes(500, "Cannot update password", err)
}
addFlash(ctx, "Password updated", "success")
return redirect(ctx, "/settings")
}
func usernameProcess(ctx echo.Context) error {
user := getUserLogged(ctx)
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
}
dto.Password = user.Password
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
return redirect(ctx, "/settings")
}
if exists, err := db.UserExists(dto.Username); err != nil || exists {
addFlash(ctx, "Username already exists", "error")
return redirect(ctx, "/settings")
}
err := os.Rename(
filepath.Join(config.C.OpengistHome, "repos", user.Username),
filepath.Join(config.C.OpengistHome, "repos", dto.Username))
if err != nil {
return errorRes(500, "Cannot rename user directory", err)
}
user.Username = dto.Username
if err := user.Update(); err != nil {
return errorRes(500, "Cannot update username", err)
}
addFlash(ctx, "Username updated", "success")
return redirect(ctx, "/settings")
}

View File

@@ -110,7 +110,7 @@ func TestVisibility(t *testing.T) {
gist1 := db.GistDTO{ gist1 := db.GistDTO{
Title: "gist1", Title: "gist1",
Description: "my first gist", Description: "my first gist",
Private: 1, Private: db.UnlistedVisibility,
Name: []string{""}, Name: []string{""},
Content: []string{"yeah"}, Content: []string{"yeah"},
} }
@@ -119,25 +119,25 @@ func TestVisibility(t *testing.T) {
gist1db, err := db.GetGistByID("1") gist1db, err := db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, gist1db.Private) require.Equal(t, db.UnlistedVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, gist1db.Private) require.Equal(t, db.PrivateVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 0, gist1db.Private) require.Equal(t, db.PublicVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, gist1db.Private) require.Equal(t, db.UnlistedVisibility, gist1db.Private)
} }
func TestLikeFork(t *testing.T) { func TestLikeFork(t *testing.T) {
@@ -198,3 +198,59 @@ func TestLikeFork(t *testing.T) {
require.Equal(t, gist1db.Private, gist2db.Private) require.Equal(t, gist1db.Private, gist2db.Private)
require.Equal(t, user2.Username, gist2db.User.Username) require.Equal(t, user2.Username, gist2db.User.Username)
} }
func TestCustomUrl(t *testing.T) {
setup(t)
s, err := newTestServer()
require.NoError(t, err, "Failed to create test server")
defer teardown(t, s)
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "gist1",
URL: "my-gist",
Description: "my first gist",
Private: 0,
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, uint(1), gist1db.ID)
require.Equal(t, gist1.Title, gist1db.Title)
require.Equal(t, gist1.Description, gist1db.Description)
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
require.Equal(t, gist1.URL, gist1db.URL)
require.Equal(t, user1.Username, gist1db.User.Username)
gist1dbUuid, err := db.GetGist(user1.Username, gist1db.Uuid)
require.NoError(t, err)
require.Equal(t, gist1db, gist1dbUuid)
gist1dbUrl, err := db.GetGist(user1.Username, gist1.URL)
require.NoError(t, err)
require.Equal(t, gist1db, gist1dbUrl)
require.Equal(t, gist1.URL, gist1db.Identifier())
gist2 := db.GistDTO{
Title: "gist2",
Description: "my second gist",
Private: 0,
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist2, 302)
require.NoError(t, err)
gist2db, err := db.GetGistByID("2")
require.NoError(t, err)
require.Equal(t, gist2db.Uuid, gist2db.Identifier())
require.NotEqual(t, gist2db.URL, gist2db.Identifier())
}

View File

@@ -133,14 +133,13 @@ func setup(t *testing.T) {
git.ReposDirectory = path.Join("tests") git.ReposDirectory = path.Join("tests")
config.C.IndexEnabled = false
config.C.LogLevel = "debug"
config.InitLog() config.InitLog()
homePath := config.GetHomeDir() homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath) log.Info().Msg("Data directory: " + homePath)
err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755)
require.NoError(t, err, "Could not create repos directory")
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755) err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
require.NoError(t, err, "Could not create tmp repos directory") require.NoError(t, err, "Could not create tmp repos directory")
@@ -149,6 +148,9 @@ func setup(t *testing.T) {
err = memdb.Setup() err = memdb.Setup()
require.NoError(t, err, "Could not initialize in memory database") require.NoError(t, err, "Could not initialize in memory database")
// err = index.Open(filepath.Join(homePath, "testsindex", "opengist.index"))
// require.NoError(t, err, "Could not open index")
} }
func teardown(t *testing.T, s *testServer) { func teardown(t *testing.T, s *testServer) {
@@ -159,4 +161,10 @@ func teardown(t *testing.T, s *testServer) {
err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests")) err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
require.NoError(t, err, "Could not remove repos directory") require.NoError(t, err, "Could not remove repos directory")
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
// require.NoError(t, err, "Could not remove repos directory")
// err = index.Close()
// require.NoError(t, err, "Could not close index")
} }

View File

@@ -16,6 +16,7 @@ import (
"golang.org/x/crypto/argon2" "golang.org/x/crypto/argon2"
"html/template" "html/template"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
) )
@@ -36,6 +37,10 @@ func getData(ctx echo.Context, key string) any {
return data[key] return data[key]
} }
func dataMap(ctx echo.Context) echo.Map {
return ctx.Request().Context().Value(dataKey).(echo.Map)
}
func html(ctx echo.Context, template string) error { func html(ctx echo.Context, template string) error {
return htmlWithCode(ctx, 200, template) return htmlWithCode(ctx, 200, template)
} }
@@ -131,6 +136,8 @@ type OpengistValidator struct {
func NewValidator() *OpengistValidator { func NewValidator() *OpengistValidator {
v := validator.New() v := validator.New()
_ = v.RegisterValidation("notreserved", validateReservedKeywords) _ = v.RegisterValidation("notreserved", validateReservedKeywords)
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
return &OpengistValidator{v} return &OpengistValidator{v}
} }
@@ -154,6 +161,9 @@ func validationMessages(err *error) string {
messages[i] = e.Field() + " should not include a sub directory" messages[i] = e.Field() + " should not include a sub directory"
case "alphanum": case "alphanum":
messages[i] = e.Field() + " should only contain alphanumeric characters" messages[i] = e.Field() + " should only contain alphanumeric characters"
case "alphanumdash":
case "alphanumdashorempty":
messages[i] = e.Field() + " should only contain alphanumeric characters and dashes"
case "min": case "min":
messages[i] = "Not enough " + e.Field() messages[i] = "Not enough " + e.Field()
case "notreserved": case "notreserved":
@@ -168,7 +178,7 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
name := fl.Field().String() name := fl.Field().String()
restrictedNames := map[string]struct{}{} restrictedNames := map[string]struct{}{}
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init"} { for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck"} {
restrictedNames[restrictedName] = struct{}{} restrictedNames[restrictedName] = struct{}{}
} }
@@ -177,6 +187,14 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
return !ok return !ok
} }
func validateAlphaNumDash(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func getPage(ctx echo.Context) int { func getPage(ctx echo.Context) int {
page := ctx.QueryParam("page") page := ctx.QueryParam("page")
if page == "" { if page == "" {
@@ -230,6 +248,46 @@ func tr(ctx echo.Context, key string) template.HTML {
return l.Tr(key) return l.Tr(key)
} }
func parseSearchQueryStr(query string) (string, map[string]string) {
words := strings.Fields(query)
metadata := make(map[string]string)
var contentBuilder strings.Builder
for _, word := range words {
if strings.Contains(word, ":") {
keyValue := strings.SplitN(word, ":", 2)
if len(keyValue) == 2 {
key := keyValue[0]
value := keyValue[1]
metadata[key] = value
}
} else {
contentBuilder.WriteString(word + " ")
}
}
content := strings.TrimSpace(contentBuilder.String())
return content, metadata
}
func addMetadataToSearchQuery(input, key, value string) string {
content, metadata := parseSearchQueryStr(input)
metadata[key] = value
var resultBuilder strings.Builder
resultBuilder.WriteString(content)
for k, v := range metadata {
resultBuilder.WriteString(" ")
resultBuilder.WriteString(k)
resultBuilder.WriteString(":")
resultBuilder.WriteString(v)
}
return strings.TrimSpace(resultBuilder.String())
}
type Argon2ID struct { type Argon2ID struct {
format string format string
version int version int
@@ -265,8 +323,16 @@ func (a Argon2ID) hash(plain string) (string, error) {
} }
func (a Argon2ID) verify(plain, hash string) (bool, error) { func (a Argon2ID) verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}
hashParts := strings.Split(hash, "$") 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) _, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -7,6 +7,7 @@ import (
"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"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/memdb" "github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/ssh" "github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web" "github.com/thomiceli/opengist/internal/web"
@@ -37,7 +38,7 @@ func initialize() {
if ok, err := config.CheckGitVersion(gitVersion); err != nil { if ok, err := config.CheckGitVersion(gitVersion); err != nil {
log.Fatal().Err(err).Send() log.Fatal().Err(err).Send()
} else if !ok { } else if !ok {
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.20. " + 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) "Current git version: " + gitVersion)
} }
@@ -59,6 +60,13 @@ func initialize() {
if err := memdb.Setup(); err != nil { if err := memdb.Setup(); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize in memory database") log.Fatal().Err(err).Msg("Failed to initialize in memory database")
} }
if config.C.IndexEnabled {
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname))
if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil {
log.Fatal().Err(err).Msg("Failed to open index")
}
}
} }
func main() { func main() {

6153
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
{ {
"name": "opengist", "name": "opengist",
"private": true, "private": true,
"version": "1.0.0",
"scripts": { "scripts": {
"dev": "node_modules/.bin/vite", "dev": "node_modules/.bin/vite -c public/vite.config.js",
"build": "node_modules/.bin/vite build", "build": "node_modules/.bin/vite -c public/vite.config.js build",
"preview": "node_modules/.bin/vite preview" "preview": "node_modules/.bin/vite -c public/vite.config.js preview"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.2.2", "@codemirror/commands": "^6.2.2",
@@ -20,14 +19,14 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"cssnano": "^5.1.15", "cssnano": "^5.1.15",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"github-markdown-css": "^5.2.0", "github-markdown-css": "^5.5.0",
"highlight.js": "^11.7.0",
"markdown-it": "^13.0.1",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4.13", "postcss": "^8.4.13",
"postcss-cli": "^11.0.0",
"postcss-cssnext": "^3.1.1", "postcss-cssnext": "^3.1.1",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
"postcss-loader": "^7.1.0", "postcss-loader": "^7.1.0",
"postcss-selector-namespace": "^3.0.1",
"sass": "^1.62.1", "sass": "^1.62.1",
"sugarss": "^4.0.1", "sugarss": "^4.0.1",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",

7
postcss.config.js vendored
View File

@@ -1,7 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
}

View File

@@ -8,13 +8,15 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const setSetting = (key: string, value: string) => { const setSetting = (key: string, value: string) => {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
const data = new URLSearchParams(); const data = new URLSearchParams();
data.append('key', key); data.append('key', key);
data.append('value', value); data.append('value', value);
if (document.getElementsByName('_csrf').length !== 0) { if (document.getElementsByName('_csrf').length !== 0) {
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value)); data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
} }
return fetch('/admin-panel/set-config', { return fetch(`${baseUrl}/admin-panel/set-config`, {
method: 'PUT', method: 'PUT',
credentials: 'same-origin', credentials: 'same-origin',
body: data, body: data,

74
public/catppuccin-latte.css vendored Normal file
View File

@@ -0,0 +1,74 @@
.chroma:not(.markdown) { color: #4c4f69 }
/* Error */ .chroma .err { color: #d20f39 }
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ .chroma .hl { color: #bcc0cc }
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
/* Line */ .chroma .line { display: flex; }
/* Keyword */ .chroma .k { color: #8839ef }
/* KeywordConstant */ .chroma .kc { color: #fe640b }
/* KeywordDeclaration */ .chroma .kd { color: #d20f39 }
/* KeywordNamespace */ .chroma .kn { color: #179299 }
/* KeywordPseudo */ .chroma .kp { color: #8839ef }
/* KeywordReserved */ .chroma .kr { color: #8839ef }
/* KeywordType */ .chroma .kt { color: #d20f39 }
/* NameAttribute */ .chroma .na { color: #1e66f5 }
/* NameBuiltin */ .chroma .nb { color: #04a5e5 }
/* NameBuiltinPseudo */ .chroma .bp { color: #04a5e5 }
/* NameClass */ .chroma .nc { color: #df8e1d }
/* NameConstant */ .chroma .no { color: #df8e1d }
/* NameDecorator */ .chroma .nd { color: #1e66f5; font-weight: bold }
/* NameEntity */ .chroma .ni { color: #179299 }
/* NameException */ .chroma .ne { color: #fe640b }
/* NameFunction */ .chroma .nf { color: #1e66f5 }
/* NameFunctionMagic */ .chroma .fm { color: #1e66f5 }
/* NameLabel */ .chroma .nl { color: #04a5e5 }
/* NameNamespace */ .chroma .nn { color: #fe640b }
/* NameProperty */ .chroma .py { color: #fe640b }
/* NameTag */ .chroma .nt { color: #8839ef }
/* NameVariable */ .chroma .nv { color: #dc8a78 }
/* NameVariableClass */ .chroma .vc { color: #dc8a78 }
/* NameVariableGlobal */ .chroma .vg { color: #dc8a78 }
/* NameVariableInstance */ .chroma .vi { color: #dc8a78 }
/* NameVariableMagic */ .chroma .vm { color: #dc8a78 }
/* LiteralString */ .chroma .s { color: #40a02b }
/* LiteralStringAffix */ .chroma .sa { color: #d20f39 }
/* LiteralStringBacktick */ .chroma .sb { color: #40a02b }
/* LiteralStringChar */ .chroma .sc { color: #40a02b }
/* LiteralStringDelimiter */ .chroma .dl { color: #1e66f5 }
/* LiteralStringDoc */ .chroma .sd { color: #9ca0b0 }
/* LiteralStringDouble */ .chroma .s2 { color: #40a02b }
/* LiteralStringEscape */ .chroma .se { color: #1e66f5 }
/* LiteralStringHeredoc */ .chroma .sh { color: #9ca0b0 }
/* LiteralStringInterpol */ .chroma .si { color: #40a02b }
/* LiteralStringOther */ .chroma .sx { color: #40a02b }
/* LiteralStringRegex */ .chroma .sr { color: #179299 }
/* LiteralStringSingle */ .chroma .s1 { color: #40a02b }
/* LiteralStringSymbol */ .chroma .ss { color: #40a02b }
/* LiteralNumber */ .chroma .m { color: #fe640b }
/* LiteralNumberBin */ .chroma .mb { color: #fe640b }
/* LiteralNumberFloat */ .chroma .mf { color: #fe640b }
/* LiteralNumberHex */ .chroma .mh { color: #fe640b }
/* LiteralNumberInteger */ .chroma .mi { color: #fe640b }
/* LiteralNumberIntegerLong */ .chroma .il { color: #fe640b }
/* LiteralNumberOct */ .chroma .mo { color: #fe640b }
/* Operator */ .chroma .o { color: #04a5e5; font-weight: bold }
/* OperatorWord */ .chroma .ow { color: #04a5e5; font-weight: bold }
/* Comment */ .chroma .c { color: #9ca0b0; font-style: italic }
/* CommentHashbang */ .chroma .ch { color: #9ca0b0; font-style: italic }
/* CommentMultiline */ .chroma .cm { color: #9ca0b0; font-style: italic }
/* CommentSingle */ .chroma .c1 { color: #9ca0b0; font-style: italic }
/* CommentSpecial */ .chroma .cs { color: #9ca0b0; font-style: italic }
/* CommentPreproc */ .chroma .cp { color: #9ca0b0; font-style: italic }
/* CommentPreprocFile */ .chroma .cpf { color: #9ca0b0; font-weight: bold; font-style: italic }
/* GenericDeleted */ .chroma .gd { color: #d20f39; background-color: #ccd0da }
/* GenericEmph */ .chroma .ge { font-style: italic }
/* GenericError */ .chroma .gr { color: #d20f39 }
/* GenericHeading */ .chroma .gh { color: #fe640b; font-weight: bold }
/* GenericInserted */ .chroma .gi { color: #40a02b; background-color: #ccd0da }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { color: #fe640b; font-weight: bold }
/* GenericTraceback */ .chroma .gt { color: #d20f39 }
/* GenericUnderline */ .chroma .gl { text-decoration: underline }

74
public/catppuccin-macchiato.css vendored Normal file
View File

@@ -0,0 +1,74 @@
.chroma:not(.markdown) { color: #cad3f5 }
/* Error */ .chroma .err { color: #f38ba8 }
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ .chroma .hl { color: #45475a }
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
/* Line */ .chroma .line { display: flex; }
/* Keyword */ .chroma .k { color: #cba6f7 }
/* KeywordConstant */ .chroma .kc { color: #fab387 }
/* KeywordDeclaration */ .chroma .kd { color: #f38ba8 }
/* KeywordNamespace */ .chroma .kn { color: #94e2d5 }
/* KeywordPseudo */ .chroma .kp { color: #cba6f7 }
/* KeywordReserved */ .chroma .kr { color: #cba6f7 }
/* KeywordType */ .chroma .kt { color: #f38ba8 }
/* NameAttribute */ .chroma .na { color: #89b4fa }
/* NameBuiltin */ .chroma .nb { color: #89dceb }
/* NameBuiltinPseudo */ .chroma .bp { color: #89dceb }
/* NameClass */ .chroma .nc { color: #f9e2af }
/* NameConstant */ .chroma .no { color: #f9e2af }
/* NameDecorator */ .chroma .nd { color: #89b4fa; font-weight: bold }
/* NameEntity */ .chroma .ni { color: #94e2d5 }
/* NameException */ .chroma .ne { color: #fab387 }
/* NameFunction */ .chroma .nf { color: #89b4fa }
/* NameFunctionMagic */ .chroma .fm { color: #89b4fa }
/* NameLabel */ .chroma .nl { color: #89dceb }
/* NameNamespace */ .chroma .nn { color: #fab387 }
/* NameProperty */ .chroma .py { color: #fab387 }
/* NameTag */ .chroma .nt { color: #cba6f7 }
/* NameVariable */ .chroma .nv { color: #f5e0dc }
/* NameVariableClass */ .chroma .vc { color: #f5e0dc }
/* NameVariableGlobal */ .chroma .vg { color: #f5e0dc }
/* NameVariableInstance */ .chroma .vi { color: #f5e0dc }
/* NameVariableMagic */ .chroma .vm { color: #f5e0dc }
/* LiteralString */ .chroma .s { color: #a6e3a1 }
/* LiteralStringAffix */ .chroma .sa { color: #f38ba8 }
/* LiteralStringBacktick */ .chroma .sb { color: #a6e3a1 }
/* LiteralStringChar */ .chroma .sc { color: #a6e3a1 }
/* LiteralStringDelimiter */ .chroma .dl { color: #89b4fa }
/* LiteralStringDoc */ .chroma .sd { color: #6c7086 }
/* LiteralStringDouble */ .chroma .s2 { color: #a6e3a1 }
/* LiteralStringEscape */ .chroma .se { color: #89b4fa }
/* LiteralStringHeredoc */ .chroma .sh { color: #6c7086 }
/* LiteralStringInterpol */ .chroma .si { color: #a6e3a1 }
/* LiteralStringOther */ .chroma .sx { color: #a6e3a1 }
/* LiteralStringRegex */ .chroma .sr { color: #94e2d5 }
/* LiteralStringSingle */ .chroma .s1 { color: #a6e3a1 }
/* LiteralStringSymbol */ .chroma .ss { color: #a6e3a1 }
/* LiteralNumber */ .chroma .m { color: #fab387 }
/* LiteralNumberBin */ .chroma .mb { color: #fab387 }
/* LiteralNumberFloat */ .chroma .mf { color: #fab387 }
/* LiteralNumberHex */ .chroma .mh { color: #fab387 }
/* LiteralNumberInteger */ .chroma .mi { color: #fab387 }
/* LiteralNumberIntegerLong */ .chroma .il { color: #fab387 }
/* LiteralNumberOct */ .chroma .mo { color: #fab387 }
/* Operator */ .chroma .o { color: #89dceb; font-weight: bold }
/* OperatorWord */ .chroma .ow { color: #89dceb; font-weight: bold }
/* Comment */ .chroma .c { color: #6c7086; font-style: italic }
/* CommentHashbang */ .chroma .ch { color: #6c7086; font-style: italic }
/* CommentMultiline */ .chroma .cm { color: #6c7086; font-style: italic }
/* CommentSingle */ .chroma .c1 { color: #6c7086; font-style: italic }
/* CommentSpecial */ .chroma .cs { color: #6c7086; font-style: italic }
/* CommentPreproc */ .chroma .cp { color: #6c7086; font-style: italic }
/* CommentPreprocFile */ .chroma .cpf { color: #6c7086; font-weight: bold; font-style: italic }
/* GenericDeleted */ .chroma .gd { color: #f38ba8; background-color: #313244 }
/* GenericEmph */ .chroma .ge { font-style: italic }
/* GenericError */ .chroma .gr { color: #f38ba8 }
/* GenericHeading */ .chroma .gh { color: #fab387; font-weight: bold }
/* GenericInserted */ .chroma .gi { color: #a6e3a1; background-color: #313244 }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { color: #fab387; font-weight: bold }
/* GenericTraceback */ .chroma .gt { color: #f38ba8 }
/* GenericUnderline */ .chroma .gl { text-decoration: underline }

View File

@@ -165,6 +165,19 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}; };
document.getElementById('gist-metadata-btn')!.onclick = (el) => {
let metadata = document.getElementById('gist-metadata')!;
metadata.classList.toggle('hidden');
let btn = el.target as HTMLButtonElement;
if (btn.innerText.endsWith('▼')) {
btn.innerText = btn.innerText.replace('▼', '▲');
} else {
btn.innerText = btn.innerText.replace('▲', '▼');
}
}
document.onsubmit = () => { document.onsubmit = () => {
window.onbeforeunload = null; window.onbeforeunload = null;
}; };

112
public/embed.scss vendored Normal file
View File

@@ -0,0 +1,112 @@
@import "github-markdown-css/github-markdown-light";
@import './catppuccin-latte';
.dark {
@import "github-markdown-css/github-markdown-dark";
@import './catppuccin-macchiato';
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "./tailwind-embed.config.js";
.html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-variation-settings: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
ol, ul {
list-style: revert;
}
.code {
font-family: Menlo, Consolas, Liberation Mono, monospace;
}
.code .line-num {
width: 4%;
text-align: right;
}
.code td {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.code tbody {
line-height: 18.2px;
}
.line-code {
@apply pl-2;
background: none !important;
}
.line-num {
@apply cursor-pointer text-slate-600 dark:text-slate-400 hover:text-black dark:hover:text-white;
}
table.csv-table {
@apply w-full whitespace-pre text-xs text-slate-300;
}
table.csv-table thead {
text-align: left;
}
table.csv-table thead tr {
@apply bg-slate-100 dark:bg-slate-800;
}
table.csv-table tbody tr {
@apply bg-gray-500 dark:bg-gray-900;
}
table.csv-table thead tr th {
@apply border py-2 px-1 border-slate-300 dark:border-slate-700;
}
table.csv-table tbody td {
@apply border py-1.5 px-1 border-slate-200 dark:border-slate-800;
}
dl.dl-config {
@apply grid grid-cols-3 text-sm;
}
dl.dl-config dt {
@apply col-span-1 text-gray-700 dark:text-slate-300 font-bold;
}
dl.dl-config dd {
@apply ml-1 col-span-2 break-words;
}
.markdown-body {
@apply dark:bg-gray-900;
}
.markdown-body pre {
@apply flex relative items-start p-0;
}
.markdown-body .code-div {
@apply p-4 max-w-full overflow-x-auto;
}
.markdown-body code {
@apply overflow-auto whitespace-pre;
}
.chroma.preview.markdown pre code {
@apply p-4;
}

1
public/embed.ts Normal file
View File

@@ -0,0 +1 @@
import "./embed.scss"

76
public/gist.ts Normal file
View File

@@ -0,0 +1,76 @@
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
el.addEventListener('click', event => {
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
const nextSibling = (event.target as HTMLElement).nextSibling;
if (nextSibling instanceof HTMLElement) {
nextSibling.classList.add('selected');
}
const filename = el.dataset.filenameSlug;
const line = (event.target as HTMLElement).textContent;
const url = location.protocol + '//' + location.host + location.pathname;
const hash = '#file-' + filename + '-' + line;
window.history.pushState(null, null, url + hash);
location.hash = hash;
}
});
});
let copybtnhtml = `<button type="button" style="top: 1em !important; right: 1em !important;" class="md-code-copy-btn absolute focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>`;
document.querySelectorAll<HTMLElement>('.markdown-body pre').forEach((el) => {
el.innerHTML = copybtnhtml + `<span class="code-div">` + el.innerHTML + `</span>`;
});
document.querySelectorAll('.md-code-copy-btn').forEach(button => {
button.addEventListener('click', function() {
let code = this.nextElementSibling.textContent;
navigator.clipboard.writeText(code).catch((err) => {
console.error('Could not copy text: ', err);
});
});
});
let checkboxes = document.querySelectorAll('li[data-checkbox-nb] input[type=checkbox]');
if (document.getElementById('gist').dataset.own) {
document.querySelectorAll<HTMLElement>('li[data-checkbox-nb]').forEach((el) => {
let input: HTMLButtonElement = el.querySelector('input[type=checkbox]');
input.disabled = false;
let checkboxNb = (el as HTMLElement).dataset.checkboxNb;
let filename = input.closest<HTMLElement>('div[data-file]').dataset.file;
input.addEventListener('change', function () {
const data = new URLSearchParams();
data.append('checkbox', checkboxNb);
data.append('file', filename);
if (document.getElementsByName('_csrf').length !== 0) {
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
}
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = true;
el.classList.add('text-gray-400')
});
fetch(window.location.href.split('#')[0] + '/checkbox', {
method: 'PUT',
credentials: 'same-origin',
body: data,
}).then((response) => {
if (response.status === 200) {
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = false;
el.classList.remove('text-gray-400')
});
}
});
});
});
} else {
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = true;
});
}

View File

@@ -1,49 +0,0 @@
import hljs from 'highlight.js';
import md from 'markdown-it';
document.querySelectorAll('.markdown').forEach((e: HTMLElement) => {
e.innerHTML = md({
html: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(str, {language: lang, ignoreIllegals: true}).value +
'</code></pre>';
} catch (__) {
}
}
return '<pre class="hljs"><code>' + md().utils.escapeHtml(str) + '</code></pre>';
}
}).render(e.textContent);
});
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
const ext = el.dataset.filename?.split('.').pop() || '';
if (hljs.getLanguage(ext) && ext !== 'txt') {
el.querySelectorAll<HTMLElement>('td.line-code').forEach((ell) => {
ell.classList.add('language-' + ext);
hljs.highlightElement(ell);
});
}
el.addEventListener('click', event => {
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
const nextSibling = (event.target as HTMLElement).nextSibling;
if (nextSibling instanceof HTMLElement) {
nextSibling.classList.add('selected');
}
const filename = el.dataset.filenameSlug;
const line = (event.target as HTMLElement).textContent;
const url = location.protocol + '//' + location.host + location.pathname;
const hash = '#file-' + filename + '-' + line;
window.history.pushState(null, null, url + hash);
location.hash = hash;
}
});
});

View File

@@ -1,4 +1,3 @@
import './style.css';
import './style.scss'; import './style.scss';
import './favicon-32.png'; import './favicon-32.png';
import './opengist.svg'; import './opengist.svg';
@@ -154,12 +153,27 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('gist-visibility-menu-button')!.onclick = () => { document.getElementById('gist-visibility-menu-button')!.onclick = () => {
gistmenuvisibility!.classList.toggle('hidden'); gistmenuvisibility!.classList.toggle('hidden');
} }
const lastVisibility = localStorage.getItem('visibility');
Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => { Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => {
const visibility = (el as HTMLElement).dataset.visibility || '0';
(el as HTMLElement).onclick = () => { (el as HTMLElement).onclick = () => {
submitgistbutton.textContent = (el as HTMLElement).dataset.btntext; submitgistbutton.textContent = (el as HTMLElement).dataset.btntext;
submitgistbutton!.value = (el as HTMLElement).dataset.visibility || '0'; submitgistbutton!.value = visibility;
localStorage.setItem('visibility', visibility);
gistmenuvisibility!.classList.add('hidden'); gistmenuvisibility!.classList.add('hidden');
} }
if (lastVisibility === visibility) {
(el as HTMLElement).click();
}
}); });
} }
const searchinput = document.getElementById('search') as HTMLInputElement;
searchinput.addEventListener('focusin', () => {
document.getElementById('search-help').classList.remove('hidden');
})
searchinput.addEventListener('focusout', (e) => {
document.getElementById('search-help').classList.add('hidden');
})
}); });

12
public/postcss.config.js vendored Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {
config: "./public/tailwind.config.js",
},
autoprefixer: {},
'postcss-selector-namespace': {namespace() {return (process.env.EMBED) ? '.opengist-embed' : '';}},
cssnano: {},
},
}

30
public/style.css vendored
View File

@@ -2,6 +2,8 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@config "./tailwind.config.js";
@layer base { @layer base {
ul, ol { ul, ol {
list-style: revert; list-style: revert;
@@ -98,10 +100,6 @@ pre {
max-height: 337px; max-height: 337px;
} }
.hljs {
color: #c9d1d9;
}
.line-code.selected { .line-code.selected {
background-color: rgb(255, 247, 190) !important; background-color: rgb(255, 247, 190) !important;
box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important; box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important;
@@ -152,3 +150,27 @@ dl.dl-config dt {
dl.dl-config dd { dl.dl-config dd {
@apply ml-1 col-span-2 break-words; @apply ml-1 col-span-2 break-words;
} }
.markdown-body {
@apply dark:bg-gray-900 !important;
}
.markdown-body pre {
@apply flex relative items-start p-0 !important;
}
.markdown-body .code-div {
@apply p-4 max-w-full overflow-x-auto !important;
}
.markdown-body code {
@apply overflow-auto whitespace-pre !important;
}
.chroma.preview.markdown pre code {
@apply p-4 !important;
}
.mermaid {
background: #f6f8fa !important;
}

6
public/style.scss vendored
View File

@@ -1,9 +1,11 @@
:root { :root {
@import "github-markdown-css/github-markdown-light"; @import "github-markdown-css/github-markdown-light";
@import 'highlight.js/scss/base16/one-light.scss'; @import './catppuccin-latte';
} }
.dark { .dark {
@import "github-markdown-css/github-markdown-dark"; @import "github-markdown-css/github-markdown-dark";
@import 'highlight.js/scss/base16/onedark.scss'; @import './catppuccin-macchiato';
} }
@import "style.css";

48
public/tailwind-embed.config.js vendored Normal file
View File

@@ -0,0 +1,48 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./templates/pages/gist_embed.html",
],
theme: {
colors: {
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"
},
primary: {
50: '#d6e1ff',
100: '#d1dfff',
200: '#b9d2fe',
300: '#84b1fb',
400: '#74a4f6',
500: '#588fee',
600: '#3c79e2',
700: '#356fc0',
800: '#2d6195',
900: '#2a5574',
950: '#173040',
},
slate: colors.slate
},
extend: {
borderWidth: {
'1': '1px',
}
},
},
plugins: [],
darkMode: 'class',
}

View File

@@ -1,5 +1,6 @@
const colors = require('tailwindcss/colors') const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: [
"./templates/**/*.html", "./templates/**/*.html",

12
public/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"esModuleInterop": true
},
"files": [
"main.ts",
"editor.ts",
"admin.ts",
"gist.ts",
"embed.ts",
],
}

View File

@@ -9,7 +9,13 @@ export default defineConfig({
assetsDir: 'assets', assetsDir: 'assets',
manifest: true, manifest: true,
rollupOptions: { rollupOptions: {
input: ['./public/main.ts', './public/editor.ts', './public/admin.ts', './public/hljs.ts'] input: [
'./public/main.ts',
'./public/editor.ts',
'./public/admin.ts',
'./public/gist.ts',
'./public/embed.ts'
]
}, },
assetsInlineLimit: 0, assetsInlineLimit: 0,
} }

View File

@@ -5,7 +5,7 @@
{{ define "admin_footer" }} {{ define "admin_footer" }}
{{ if .urlPage }} {{ if .urlPage }}
<div class="flex mt-4 justify-center space-x-2"> <div class="flex mt-4 justify-center space-x-2">
{{ template "pagination" . }} {{ template "_pagination" . }}
</div> </div>
{{ end }} {{ end }}
</main> </main>

View File

@@ -23,16 +23,13 @@
<div id="language-list" class="hidden absolute bottom-0 z-10 mb-10 mt-2 origin-bottom-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-800 dark:ring-gray-700" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1"> <div id="language-list" class="hidden absolute bottom-0 z-10 mb-10 mt-2 origin-bottom-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-800 dark:ring-gray-700" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
<div class="py-1" role="none"> <div class="py-1" role="none">
{{ range .allLocales }} {{ range .allLocales }}
<a href="?lang={{ .Code }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-4 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1" id="menu-item-0">{{ .Name }}</a> <a href="?lang={{ .Code }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-4 py-1.5 text-sm w-max hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1" id="menu-item-0">{{ .Name }}</a>
{{ end }} {{ end }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="{{ asset "hljs.ts" }}"></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -7,7 +7,10 @@
<meta name="robots" content="noindex, follow"> <meta name="robots" content="noindex, follow">
{{ end }} {{ end }}
<base href="{{ $.c.ExternalUrl }}" />
<script> <script>
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
const checkTheme = () => { const checkTheme = () => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
@@ -30,7 +33,7 @@
{{ if dev }} {{ if dev }}
<script type="module" src="{{ asset "@vite/client" }}"></script> <script type="module" src="{{ asset "@vite/client" }}"></script>
<link rel="stylesheet" href="{{ asset "style.css" }}" /> <link rel="stylesheet" href="{{ asset "style.scss" }}" />
<script type="module" src="{{ asset "main.ts" }}"></script> <script type="module" src="{{ asset "main.ts" }}"></script>
{{ else }} {{ else }}
<link rel="stylesheet" href="{{ asset "main.css" }}" /> <link rel="stylesheet" href="{{ asset "main.css" }}" />
@@ -85,10 +88,23 @@
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
<form action="/search" method="GET"> <form action="{{ $.c.ExternalUrl }}/search" method="GET">
<input id="search" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{ .searchQuery }}"> <input id="search" name="q" autocomplete="off" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="{{if indexEnabled}}Code search{{else}}Search{{end}}" type="search" value="{{ .searchQuery }}">
<input type="submit" hidden="hidden"> <input type="submit" hidden="hidden">
</form> </form>
{{if indexEnabled}}
<div id="search-help" class="hidden absolute left-1/2 z-10 mt-5 w-screen max-w-max -translate-x-1/2 px-4">
<div class="flex-auto overflow-hidden rounded-md bg-white dark:bg-gray-800 text-sm leading-6 border-1 border-gray-100 dark:border-gray-700 ring-1 ring-gray-900/5">
<div class="p-4 text-xs space-y-1">
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">user:thomas</code> {{ .locale.Tr "gist.search.help.user" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">title:mygist</code> {{ .locale.Tr "gist.search.help.title" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
</div>
</div>
</div>
{{end}}
</div> </div>
</div> </div>
</div> </div>
@@ -105,15 +121,15 @@
</svg> </svg>
</div> </div>
<div class="hidden relative sm:inline-block text-left"> <div class="hidden relative sm:inline-block text-left">
<div id="user-menu" class="hidden w-32 font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none"> <div id="user-menu" class="hidden w-max font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none">
<div class="py-1" role="none"> <div class="py-1" role="none">
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1"> <a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg> </svg>
{{ .locale.Tr "header.menu.my-gists" }} {{ .locale.Tr "header.menu.my-gists" }}
</a> </a>
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}/liked" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1"> <a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}/liked" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" /> <path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
</svg> </svg>
@@ -122,7 +138,7 @@
</div> </div>
{{ if .userLogged.IsAdmin }} {{ if .userLogged.IsAdmin }}
<div class="py-1" role="none"> <div class="py-1" role="none">
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1"> <a href="{{ $.c.ExternalUrl }}/admin-panel" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
</svg> </svg>
@@ -131,14 +147,14 @@
</div> </div>
{{ end }} {{ end }}
<div class="py-1" role="none"> <div class="py-1" role="none">
<a href="{{ $.c.ExternalUrl }}/settings" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1"> <a href="{{ $.c.ExternalUrl }}/settings" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
{{ .locale.Tr "header.menu.settings" }} {{ .locale.Tr "header.menu.settings" }}
</a> </a>
<a href="{{ $.c.ExternalUrl }}/logout" class="dark:text-rose-400 text-rose-500 group flex items-center px-3 py-1.5 text-sm w-full hover:text-rose-600 dark:hover:text-rose-500" role="menuitem" tabindex="-1"> <a href="{{ $.c.ExternalUrl }}/logout" class="dark:text-rose-400 text-rose-500 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-rose-600 dark:hover:text-rose-500" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 dark:text-rose-400 text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 dark:text-rose-400 text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg> </svg>
@@ -186,7 +202,7 @@
</svg> </svg>
{{ .locale.Tr "header.menu.dark" }} {{ .locale.Tr "header.menu.dark" }}
</button> </button>
<button id="system-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1"> <button id="system-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-max hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg> </svg>
@@ -211,7 +227,7 @@
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
<form action="/search" method="GET"> <form action="{{ $.c.ExternalUrl }}/search" method="GET">
<input id="searchmobile" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{.searchQuery}}"> <input id="searchmobile" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{.searchQuery}}">
<input type="submit" hidden="hidden"> <input type="submit" hidden="hidden">
</form> </form>

View File

@@ -4,6 +4,7 @@
{{ end }} {{ end }}
{{ define "gist_footer" }} {{ define "gist_footer" }}
</main> </main>
</div> </div>
{{ end }} {{ end }}

View File

@@ -1,15 +1,15 @@
{{ define "gist_header" }} {{ define "gist_header" }}
<div class="py-10"> <div class="py-10" id="gist" data-own="{{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }}true{{ end }}{{ end }}">
<header> <header>
<div class="flex flex-col lg:flex-row"> <div class="flex flex-col lg:flex-row">
<div> <div>
<h1 class="text-2xl font-bold leading-tight break-all"> <h1 class="text-2xl font-bold leading-tight break-all">
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}">{{ .gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}">{{ .gist.Title }}</a> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}">{{ .gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}">{{ .gist.Title }}</a>
</h1> </h1>
</div> </div>
<div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto"> <div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto">
{{ if .userLogged }} {{ if .userLogged }}
<form id="like" class="flex items-center" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/like?redirecturl={{ .currentUrl }}"> <form id="like" class="flex items-center" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/like?redirecturl={{ .currentUrl }}">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <button type="submit" class="focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
{{ if not .hasLiked }} {{ if not .hasLiked }}
@@ -24,12 +24,12 @@
{{ .locale.Tr "gist.header.unlike" }} {{ .locale.Tr "gist.header.unlike" }}
{{ end }} {{ end }}
</button> </button>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
{{ .gist.NbLikes }} {{ .gist.NbLikes }}
</a> </a>
</form> </form>
{{ if ne .userLogged.ID .gist.User.ID }} {{ if ne .userLogged.ID .gist.User.ID }}
<form id="fork" class="ml-2 flex items-center " method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/fork"> <form id="fork" class="ml-2 flex items-center " method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/fork">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="ml-auto focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <button type="submit" class="ml-auto focus-within:z-10 text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
@@ -37,7 +37,7 @@
</svg> </svg>
{{ .locale.Tr "gist.header.fork" }} {{ .locale.Tr "gist.header.fork" }}
</button> </button>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
{{ .gist.NbForks }} {{ .gist.NbForks }}
</a> </a>
</form> </form>
@@ -50,7 +50,7 @@
</svg> </svg>
{{ .locale.Tr "gist.header.like" }} {{ .locale.Tr "gist.header.like" }}
</a> </a>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/likes" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
{{ .gist.NbLikes }} {{ .gist.NbLikes }}
</a> </a>
</div> </div>
@@ -61,21 +61,21 @@
</svg> </svg>
{{ .locale.Tr "gist.header.fork" }} {{ .locale.Tr "gist.header.fork" }}
</a> </a>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/forks" class="text-slate-700 dark:text-slate-300 relative inline-flex align-middle items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 -ml-px text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
{{ .gist.NbForks }} {{ .gist.NbForks }}
</a> </a>
</div> </div>
{{ end }} {{ end }}
{{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }} {{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }}
<div class="ml-2 flex items-center"> <div class="ml-2 flex items-center">
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/edit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/edit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg> </svg>
{{ .locale.Tr "gist.header.edit" }} {{ .locale.Tr "gist.header.edit" }}
</a> </a>
</div> </div>
<form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/delete"> <form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/delete">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -89,12 +89,12 @@
</div> </div>
</div> </div>
{{ if .gist.Forked }} {{ if .gist.Forked }}
<p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.forked-from" }} <a href="{{ $.c.ExternalUrl }}/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Uuid }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a></p> <p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.forked-from" }} <a href="{{ $.c.ExternalUrl }}/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Identifier }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a></p>
{{ end }} {{ end }}
<p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.last-active" }} <span class="moment-timestamp"> {{ .gist.UpdatedAt }} </span> <p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.last-active" }} <span class="moment-timestamp"> {{ .gist.UpdatedAt }} </span>
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }} {{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}
</p> </p>
<p class="mt-3 max-w-2xl text-slate-700 dark:text-slate-300">{{ .gist.Description }}</p> <p class="mt-1 text-sm max-w-2xl text-slate-600 dark:text-slate-400">{{ .gist.Description }}</p>
</header> </header>
<main class="mt-4"> <main class="mt-4">
@@ -102,20 +102,20 @@
<div class="sm:hidden"> <div class="sm:hidden">
<label for="gist-tabs" class="sr-only">{{ .locale.Tr "gist.header.select-tab" }}</label> <label for="gist-tabs" class="sr-only">{{ .locale.Tr "gist.header.select-tab" }}</label>
<select id="gist-tabs" name="tabs" class="block bg-gray-50 dark:bg-gray-800 w-full pl-3 pr-10 py-2 text-base border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md"> <select id="gist-tabs" name="tabs" class="block bg-gray-50 dark:bg-gray-800 w-full pl-3 pr-10 py-2 text-base border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-md">
<option {{ if eq .page "code"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}">{{ .locale.Tr "gist.header.code" }}</option> <option {{ if eq .page "code"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Identifier }}">{{ .locale.Tr "gist.header.code" }}</option>
<option {{ if eq .page "revisions"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions">{{ .locale.Tr "gist.header.revisions" }} ({{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }})</option> <option {{ if eq .page "revisions"}}selected{{end}} data-url="/{{ .gist.User.Username }}/{{ .gist.Identifier }}/revisions">{{ .locale.Tr "gist.header.revisions" }} ({{ if .nbCommits }}{{ .nbCommits }}{{else}}0{{ end }})</option>
</select> </select>
</div> </div>
<div class="hidden sm:block"> <div class="hidden sm:block">
<div class="border-b flex border-gray-200 dark:border-gray-700"> <div class="border-b flex border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex-auto space-x-4" aria-label="Tabs"> <nav class="-mb-px flex-auto space-x-4" aria-label="Tabs">
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "code"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" aria-current="page"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "code"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" aria-current="page">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
</svg> </svg>
{{ .locale.Tr "gist.header.code" }} {{ .locale.Tr "gist.header.code" }}
</a> </a>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/revisions" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "revisions"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/revisions" class="inline-flex items-center text-slate-700 dark:text-slate-300 {{ if eq .page "revisions"}}border-slate-500 dark:border-slate-300 {{else}}border-transparent hover:border-gray-700 dark:hover:border-gray-200{{end}} hover:text-slate-700 dark:hover:text-slate-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg> </svg>
@@ -128,13 +128,16 @@
<div class="flex rounded-md shadow-sm"> <div class="flex rounded-md shadow-sm">
<div class="relative"> <div class="relative">
<button type="button" id="gist-menu-toggle" class="relative text-xs inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3 focus-within:z-10 -mr-px"> <button type="button" id="gist-menu-toggle" class="relative text-xs inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3 focus-within:z-10 -mr-px">
<span id="gist-menu-title" class="whitespace-nowrap"></span> <span id="gist-menu-title" class="whitespace-nowrap">{{ .locale.Tr "gist.header.embed" }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg> </svg>
</button> </button>
<div class="absolute left-0 z-10 mt-2 w-56 origin-top-left bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1"> <div class="absolute left-0 z-10 mt-2 w-56 origin-top-left bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
<div class="py-1 cursor-pointer border-1 rounded-md border-gray-200 dark:border-gray-700 hidden" id="gist-menu-copy" role="none"> <div class="py-1 cursor-pointer border-1 rounded-md border-gray-200 dark:border-gray-700 hidden" id="gist-menu-copy" role="none">
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-share" data-link="{{ .embedScript }}"><p>{{ .locale.Tr "gist.header.embed" }}</p>
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.embed-help" }}</p>
</div>
{{ if .httpCloneUrl }} {{ if .httpCloneUrl }}
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-http" data-link="{{ .httpCloneUrl }}"><p>{{ .locale.Tr "gist.header.clone-http" .httpProtocol }}</p> <div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-http" data-link="{{ .httpCloneUrl }}"><p>{{ .locale.Tr "gist.header.clone-http" .httpProtocol }}</p>
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-http-help" }}</p> <p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-http-help" }}</p>
@@ -145,25 +148,22 @@
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-ssh-help" }}</p> <p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.clone-ssh-help" }}</p>
</div> </div>
{{ end }} {{ end }}
<div class="text-slate-700 dark:text-slate-300 block px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 gist-menu-item" role="menuitem" id="gist-menu-share" data-link="{{ .httpCopyUrl }}"><p>{{ .locale.Tr "gist.header.share" }}</p>
<p class="text-xs font-normal text-gray-600 dark:text-gray-400">{{ .locale.Tr "gist.header.share-help" }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="relative flex flex-grow items-stretch focus-within:z-10"> <div class="relative flex flex-grow items-stretch focus-within:z-10">
<input id="gist-menu-input" value="" class="block code bg-white dark:bg-gray-900 w-full rounded-none border border-gray-200 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2"> <input readonly id="gist-menu-input" value="{{.embedScript}}" class="block code bg-white dark:bg-gray-900 w-full rounded-none border border-gray-200 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2 py-1">
</div> </div>
<button id="gist-menu-button-copy" type="button" class="relative text-xs -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <button id="gist-menu-button-copy" type="button" class="relative text-xs -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" />
</svg> </svg>
</button> </button>
</div> </div>
</div> </div>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "gist.header.download-zip" }}</a> {{ .locale.Tr "gist.header.download-zip" }}</a>
</div> </div>
</div> </div>

View File

@@ -14,9 +14,13 @@
</div> </div>
</div> </div>
<dt>Log level</dt><dd>{{ .c.LogLevel }}</dd> <dt>Log level</dt><dd>{{ .c.LogLevel }}</dd>
<dt>Log output</dt><dd>{{ .c.LogOutput }}</dd>
<dt>External URL</dt><dd>{{ .c.ExternalUrl }}</dd> <dt>External URL</dt><dd>{{ .c.ExternalUrl }}</dd>
<dt>Opengist home</dt><dd>{{ .c.OpengistHome }}</dd> <dt>Opengist home</dt><dd>{{ .c.OpengistHome }}</dd>
<dt>DB filename</dt><dd>{{ .c.DBFilename }}</dd> <dt>DB filename</dt><dd>{{ .c.DBFilename }}</dd>
<dt>Index Enabled</dt><dd>{{ .c.IndexEnabled }}</dd>
<dt>Index Dirname</dt><dd>{{ .c.IndexDirname }}</dd>
<dt>Git default branch</dt><dd>{{ .c.GitDefaultBranch }}</dd>
<dt>SQLite Journal Mode</dt><dd>{{ .c.SqliteJournalMode }}</dd> <dt>SQLite Journal Mode</dt><dd>{{ .c.SqliteJournalMode }}</dd>
<div class="relative col-span-3 mt-4"> <div class="relative col-span-3 mt-4">
<div class="absolute inset-0 flex items-center" aria-hidden="true"> <div class="absolute inset-0 flex items-center" aria-hidden="true">
@@ -52,6 +56,9 @@
</div> </div>
<dt>Github Client key</dt><dd>{{ .c.GithubClientKey }}</dd> <dt>Github Client key</dt><dd>{{ .c.GithubClientKey }}</dd>
<dt>Github Secret</dt><dd>{{ .c.GithubSecret }}</dd> <dt>Github Secret</dt><dd>{{ .c.GithubSecret }}</dd>
<dt>GitLab client Key</dt><dd>{{ .c.GitlabClientKey }}</dd>
<dt>GitLab Secret</dt><dd>{{ .c.GitlabSecret }}</dd>
<dt>GitLab URL</dt><dd>{{ .c.GitlabUrl }}</dd>
<dt>Gitea client Key</dt><dd>{{ .c.GiteaClientKey }}</dd> <dt>Gitea client Key</dt><dd>{{ .c.GiteaClientKey }}</dd>
<dt>Gitea Secret</dt><dd>{{ .c.GiteaSecret }}</dd> <dt>Gitea Secret</dt><dd>{{ .c.GiteaSecret }}</dd>
<dt>Gitea URL</dt><dd>{{ .c.GiteaUrl }}</dd> <dt>Gitea URL</dt><dd>{{ .c.GiteaUrl }}</dd>

View File

@@ -21,14 +21,14 @@
{{ range $gist := .data }} {{ range $gist := .data }}
<tr> <tr>
<td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-700 dark:text-slate-300 sm:pl-0">{{ $gist.ID }}</td> <td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-slate-700 dark:text-slate-300 sm:pl-0">{{ $gist.ID }}</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a></td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Identifier }}">{{ $gist.Title }}</a></td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a></td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a></td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.Private }}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.Private }}</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbFiles }}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbFiles }}</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbLikes }}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300">{{ $gist.NbLikes }}</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $gist.CreatedAt }}</span></td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $gist.CreatedAt }}</span></td>
<td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"> <td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<form action="/admin-panel/gists/{{ $gist.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.gists.delete_confirm" }}')"> <form action="{{ $.c.ExternalUrl }}/admin-panel/gists/{{ $gist.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.gists.delete_confirm" }}')">
{{ $.csrfHtml }} {{ $.csrfHtml }}
<button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button> <button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button>
</form> </form>

View File

@@ -56,24 +56,42 @@
<span class="text-base font-bold leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.actions" }}</span> <span class="text-base font-bold leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.actions" }}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<form action="/admin-panel/sync-fs" method="POST"> <form action="{{ $.c.ExternalUrl }}/admin-panel/sync-fs" method="POST">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" {{ if .syncReposFromFS }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"> <button type="submit" {{ if .syncReposFromFS }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.sync-fs" }} {{ .locale.Tr "admin.actions.sync-fs" }}
</button> </button>
</form> </form>
<form action="/admin-panel/sync-db" method="POST"> <form action="{{ $.c.ExternalUrl }}/admin-panel/sync-db" method="POST">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" {{ if .syncReposFromDB }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromDB }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"> <button type="submit" {{ if .syncReposFromDB }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromDB }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.sync-db" }} {{ .locale.Tr "admin.actions.sync-db" }}
</button> </button>
</form> </form>
<form action="/admin-panel/gc-repos" method="POST"> <form action="{{ $.c.ExternalUrl }}/admin-panel/gc-repos" method="POST">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" {{ if .gitGcRepos }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .gitGcRepos }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"> <button type="submit" {{ if .gitGcRepos }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .gitGcRepos }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.git-gc" }} {{ .locale.Tr "admin.actions.git-gc" }}
</button> </button>
</form> </form>
<form action="{{ $.c.ExternalUrl }}/admin-panel/sync-previews" method="POST">
{{ .csrfHtml }}
<button type="submit" {{ if .syncGistPreviews }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncGistPreviews }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.sync-previews" }}
</button>
</form>
<form action="{{ $.c.ExternalUrl }}/admin-panel/reset-hooks" method="POST">
{{ .csrfHtml }}
<button type="submit" {{ if .resetHooks }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .resetHooks }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.reset-hooks" }}
</button>
</form>
<form action="{{ $.c.ExternalUrl }}/admin-panel/index-gists" method="POST">
{{ .csrfHtml }}
<button type="submit" {{ if .indexGists }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .indexGists }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "admin.actions.index-gists" }}
</button>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $user.Username }}">{{ $user.Username }}</a></td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><a href="{{ $.c.ExternalUrl }}/{{ $user.Username }}">{{ $user.Username }}</a></td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $user.CreatedAt }}</span></td> <td class="whitespace-nowrap px-2 py-2 text-sm text-slate-700 dark:text-slate-300"><span class="moment-timestamp-date">{{ $user.CreatedAt }}</span></td>
<td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"> <td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<form action="/admin-panel/users/{{ $user.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.users.delete_confirm" }}')"> <form action="{{ $.c.ExternalUrl }}/admin-panel/users/{{ $user.ID }}/delete" method="POST" onsubmit="return confirm('{{ $.locale.Tr "admin.users.delete_confirm" }}')">
{{ $.csrfHtml }} {{ $.csrfHtml }}
<button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button> <button type="submit" class="text-rose-500 hover:text-rose-600">{{ $.locale.Tr "admin.delete" }}</button>
</form> </form>

View File

@@ -31,7 +31,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div id="sort-gists-dropdown" class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1"> <div id="sort-gists-dropdown" class="hidden absolute right-0 z-10 mt-2 w-max origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
<div class="" role="none"> <div class="" role="none">
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem"> <a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
{{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-created" }} {{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-created" }}
@@ -108,74 +108,11 @@
<div> <div>
{{ if ne (len .gists) 0 }} {{ if ne (len .gists) 0 }}
{{ range $gist := .gists }} {{ range $gist := .gists }}
<div class="mb-8"> {{ $nest := dict "gist" $gist "c" $.c "locale" $.locale "DisableGravatar" $.DisableGravatar "searchQuery" $.searchQuery }}
<div class="flex "> {{ template "_gist_preview" $nest }}
<div class="div">
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">
<img class="h-10 w-10 rounded-md mr-2 border border-gray-200 dark:border-gray-700 my-1" src="{{ avatarUrl $gist.User $.DisableGravatar }}" alt="{{ $gist.User.Username }}'s Avatar">
</a>
</div>
<div class="flex-auto">
<div class="flex flex-col lg:flex-row">
<h4 class="text-md leading-tight break-all py-1 flex-auto">
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}">{{ $gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a class="font-bold" href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}">{{ $gist.Title }}</a>
</h4>
<div class="flex space-x-4 lg:flex-row flex py-1 lg:py-0 lg:ml-auto text-slate-500">
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="whitespace-nowrap">{{ $gist.NbLikes }} {{ $.locale.Tr "gist.list.likes" }}</span>
</div>
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
</svg>
<span class="whitespace-nowrap">{{ $gist.NbForks }} {{ $.locale.Tr "gist.list.forks" }}</span>
</div>
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
</svg>
<span class="whitespace-nowrap">{{ $gist.NbFiles }} {{ $.locale.Tr "gist.list.files" }}</span>
</div>
</div>
</div>
<h5 class="text-sm text-slate-500 pb-1">{{ $.locale.Tr "gist.list.last-active" }} <span class="moment-timestamp">{{ $gist.UpdatedAt }}</span>
{{ if $gist.Forked }} • {{ $.locale.Tr "gist.list.forked-from" }} <a href="{{ $.c.ExternalUrl }}/{{ $gist.Forked.User.Username }}/{{ $gist.Forked.Uuid }}">{{ $gist.Forked.User.Username }}/{{ $gist.Forked.Title }}</a> {{ end }}
{{ if $gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr $gist.Private false }} </span>{{ end }}</h5>
<h6 class="text-xs text-slate-700 dark:text-slate-300 py-1">{{ $gist.Description }}</h6>
</div>
</div>
<a href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}" class="text-slate-700 dark:text-slate-300">
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">
<div class="code overflow-auto">
{{ if isMarkdown $gist.PreviewFilename }}
<div class="markdown markdown-body p-8">{{ $gist.Preview }}</div>
{{ else }}
<table class="table-code w-full whitespace-pre" data-filename="{{ $gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
{{ $ii := "1" }}
{{ $i := toInt $ii }}
{{ range $line := lines $gist.Preview }}
<tr>
<td class="select-none line-num px-4">{{$i}}</td>
<td class="line-code">{{ $line }}</td>
</tr>
{{ $i = inc $i }}
{{ end }}
</tbody>
</table>
{{ end }}
</div>
</div>
</a>
</div>
{{ end }} {{ end }}
{{ template "pagination" . }} {{ template "_pagination" . }}
{{ else }} {{ else }}
<div class="text-center"> <div class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">

View File

@@ -16,7 +16,7 @@
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
{{ if not .disableForm }} {{ if not .disableForm }}
<form class="space-y-6" action="#" method="post"> <form class="space-y-6" method="post">
<div> <div>
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "auth.username" }} </label> <label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "auth.username" }} </label>
<div class="mt-1"> <div class="mt-1">
@@ -51,7 +51,7 @@
{{ .csrfHtml }} {{ .csrfHtml }}
</form> </form>
{{ end }} {{ end }}
{{ if or .githubOauth .giteaOauth .oidcOauth }} {{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
{{ if not .disableForm }} {{ if not .disableForm }}
<div class="relative my-4"> <div class="relative my-4">
<div class="absolute inset-0 flex items-center" aria-hidden="true"> <div class="absolute inset-0 flex items-center" aria-hidden="true">
@@ -66,6 +66,11 @@
{{ .locale.Tr "auth.github-oauth" }} {{ .locale.Tr "auth.github-oauth" }}
</a> </a>
{{ end }} {{ end }}
{{ if .gitlabOauth }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "auth.gitlab-oauth" }}
</a>
{{ end }}
{{ if .giteaOauth }} {{ if .giteaOauth }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"> <a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "auth.gitea-oauth" }} {{ .locale.Tr "auth.gitea-oauth" }}

View File

@@ -8,19 +8,24 @@
</header> </header>
<main class="mt-4"> <main class="mt-4">
<form id="create" class="space-y-4" method="post" action="/"> <form id="create" class="space-y-4" method="post" action="{{ $.c.ExternalUrl }}/">
<div class="grid grid-cols-12 gap-x-4"> <div>
<div class="col-span-8 sm:col-span-4"> <p class="cursor-pointer select-none" id="gist-metadata-btn">Metadata ▼</p>
<div class="mt-1"> <div class="grid grid-cols-12 gap-x-4 mt-1 hidden" id="gist-metadata">
<input type="text" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md"> <div class="col-span-8 sm:col-span-4">
<div class="mt-1">
<input type="text" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="250">
</div>
</div>
<div class="col-span-12 sm:col-span-8">
<div class="mt-1">
<input type="text" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="1000">
</div>
</div>
<div class="col-span-6 sm:col-span-3 mt-2">
<input type="text" placeholder="{{ .locale.Tr "gist.new.url" }}" name="url" id="url" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="32">
</div> </div>
</div> </div>
<div class="col-span-12 sm:col-span-8">
<div class="mt-1">
<input type="text" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
</div>
</div>
</div> </div>
<div id="editors" class="space-y-4"> <div id="editors" class="space-y-4">
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 editor"> <div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 editor">
@@ -50,7 +55,7 @@
</select> </select>
</div> </div>
</div> </div>
<input type="hidden" value="" name="content" class="form-filecontent"> <input type="hidden" value="" name="content" class="form-filecontent" autocomplete="off">
</div> </div>
</div> </div>
@@ -66,7 +71,7 @@
</svg> </svg>
</button> </button>
<div id="gist-menu-visibility" class="hidden absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="gist-visibility-menu-button"> <div id="gist-menu-visibility" class="hidden absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="gist-visibility-menu-button">
<div class="rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none" role="none"> <div class="rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none" role="none" style="word-break: keep-all">
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-public-button" }}" data-visibility="0" role="menuitem">{{ .locale.Tr "gist.public" }}</span> <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-public-button" }}" data-visibility="0" role="menuitem">{{ .locale.Tr "gist.public" }}</span>
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-unlisted-button" }}" data-visibility="1" role="menuitem">{{ .locale.Tr "gist.unlisted" }}</span> <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-unlisted-button" }}" data-visibility="1" role="menuitem">{{ .locale.Tr "gist.unlisted" }}</span>
<span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-private-button" }}" data-visibility="2" role="menuitem">{{ .locale.Tr "gist.private" }}</span> <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.new.create-private-button" }}" data-visibility="2" role="menuitem">{{ .locale.Tr "gist.private" }}</span>

View File

@@ -8,7 +8,7 @@
</h1> </h1>
</div> </div>
<div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto"> <div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto">
<form id="visibility" class="flex items-center whitespace-nowrap" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/visibility"> <form id="visibility" class="flex items-center whitespace-nowrap" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/visibility">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="ml-auto relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <button type="submit" class="ml-auto relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
{{ if eq .gist.Private 2 }} {{ if eq .gist.Private 2 }}
@@ -21,10 +21,10 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> <path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg> </svg>
{{ end }} {{ end }}
{{ .locale.Tr "gist.edit.change-visibility" }} {{ visibilityStr (inc .gist.Private) true }} {{ .locale.Tr "gist.edit.change-visibility" }} {{ visibilityStr .gist.Private.Next true }}
</button> </button>
</form> </form>
<form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/delete"> <form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/delete">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> <button type="submit" class="relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-rose-600 dark:text-rose-400 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -37,19 +37,24 @@
</div> </div>
</header> </header>
<main class="mt-4"> <main class="mt-4">
<form id="create" class="space-y-4" method="post" action="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/edit"> <form id="create" class="space-y-4" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/edit">
<div class="grid grid-cols-12 gap-x-4"> <div>
<div class="col-span-8 sm:col-span-4"> <p class="cursor-pointer select-none" id="gist-metadata-btn">Metadata ▼</p>
<div class="mt-1"> <div class="grid grid-cols-12 gap-x-4 mt-1 hidden" id="gist-metadata">
<input type="text" value="{{ .gist.Title }}" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md"> <div class="col-span-8 sm:col-span-4">
<div class="mt-1">
<input type="text" value="{{ .gist.Title }}" placeholder="{{ .locale.Tr "gist.new.title" }}" name="title" id="title" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="250">
</div>
</div>
<div class="col-span-12 sm:col-span-8">
<div class="mt-1">
<input type="text" value="{{ .gist.Description }}" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="1000">
</div>
</div>
<div class="col-span-6 sm:col-span-3 mt-2">
<input type="text" value="{{ .gist.URL }}" placeholder="{{ .locale.Tr "gist.new.url" }}" name="url" id="url" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="32">
</div> </div>
</div> </div>
<div class="col-span-12 sm:col-span-8">
<div class="mt-1">
<input type="text" value="{{ .gist.Description }}" placeholder="{{ .locale.Tr "gist.new.description" }}" name="description" id="description" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
</div>
</div>
</div> </div>
<div id="editors" class="space-y-4"> <div id="editors" class="space-y-4">
{{ range $file := .files }} {{ range $file := .files }}
@@ -85,14 +90,14 @@
</select> </select>
</div> </div>
</div> </div>
<input type="hidden" value="{{ $file.Content }}" name="content" class="form-filecontent"> <input type="hidden" value="{{ $file.Content }}" name="content" class="form-filecontent" autocomplete="off">
</div> </div>
{{ end }} {{ end }}
</div> </div>
<div class="flex"> <div class="flex">
<button type="button" id="add-file" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-white bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">{{ .locale.Tr "gist.new.add-file" }}</button> <button type="button" id="add-file" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-white bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">{{ .locale.Tr "gist.new.add-file" }}</button>
<a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Uuid }}" class="ml-auto inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 text-rose-600 dark:text-rose-400 hover:text-rose-700">{{ .locale.Tr "gist.edit.cancel" }}</a> <a href="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}" class="ml-auto inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 text-rose-600 dark:text-rose-400 hover:text-rose-700">{{ .locale.Tr "gist.edit.cancel" }}</a>
<button type="submit" class="ml-2 inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "gist.edit.save" }}</button> <button type="submit" class="ml-2 inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "gist.edit.save" }}</button>
</div> </div>
{{ .csrfHtml }} {{ .csrfHtml }}

View File

@@ -15,7 +15,7 @@
<p class="text-sm text-slate-500">{{ $.locale.Tr "gist.list.forked" }} <span class="moment-timestamp">{{ $gist.CreatedAt }}</span></p> <p class="text-sm text-slate-500">{{ $.locale.Tr "gist.list.forked" }} <span class="moment-timestamp">{{ $gist.CreatedAt }}</span></p>
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
<a class="ml-auto text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-md border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3" href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Uuid }}"> <a class="ml-auto text-slate-700 dark:text-slate-300 relative inline-flex items-center space-x-2 rounded-md border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3" href="{{ $.c.ExternalUrl }}/{{ $gist.User.Username }}/{{ $gist.Identifier }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
</svg> </svg>

View File

@@ -3,8 +3,8 @@
{{ if .files }} {{ if .files }}
<div class="grid gap-y-4"> <div class="grid gap-y-4">
{{ range $file := .files }} {{ range $file := .files }}
{{ $csv := csvFile $file }} {{ $csv := csvFile $file.File }}
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto"> <div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto" data-file="{{ $file.Filename }}">
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto block"> <div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto block">
<div class="ml-4 py-1.5 flex"> <div class="ml-4 py-1.5 flex">
@@ -12,10 +12,14 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /> <path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg> </svg>
<a href="{{ $.c.ExternalUrl }}#file-{{ slug $file.Filename }}" class="text-slate-700 dark:text-slate-300 hover:text-black dark:hover:text-white ml-2">{{ $file.Filename }}</a></span> <a href="#file-{{ slug $file.Filename }}" class="hover:text-primary-600 ml-2 mr-1">{{ $file.Filename }}</a>
<span class="hidden sm:block">
<span class="text-gray-400"> · {{ $file.HumanSize }} · {{ $file.Type }}</span>
</span>
</span>
<span class="isolate inline-flex rounded-md shadow-sm mr-2"> <span class="isolate inline-flex rounded-md shadow-sm mr-2">
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}" class="relative inline-flex items-center rounded-l-md bg-white text-gray-500 dark:text-slate-300 float-right px-2.5 py-1 leading-4 text-xs font-medium dark:bg-gray-600 border border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-slate-700 dark:hover:text-slate-300 select-none"> <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}" class="relative inline-flex items-center rounded-l-md bg-white text-gray-500 dark:text-slate-300 float-right px-2.5 py-1 leading-4 text-xs font-medium dark:bg-gray-600 border border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-slate-700 dark:hover:text-slate-300 select-none">
{{ $.locale.Tr "gist.raw" }} {{ $.locale.Tr "gist.raw" }}
</a> </a>
<button type="button" class="relative -ml-px inline-flex items-center bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700 copy-gist-btn"> <button type="button" class="relative -ml-px inline-flex items-center bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700 copy-gist-btn">
@@ -23,7 +27,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg> </svg>
</button> </button>
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/download/{{ $.commit }}/{{$file.Filename}}" class="relative -ml-px inline-flex items-center rounded-r-md bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700"> <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/download/{{ $.commit }}/{{$file.Filename}}" class="relative -ml-px inline-flex items-center rounded-r-md bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg> </svg>
@@ -34,7 +38,7 @@
</div> </div>
{{ if $file.Truncated }} {{ if $file.Truncated }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700"> <div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-truncated" }} <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}.</a> {{ $.locale.Tr "gist.file-truncated" }} <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}.</a>
</div> </div>
{{ end }} {{ end }}
{{ if and (not $csv) (isCsv $file.Filename) }} {{ if and (not $csv) (isCsv $file.Filename) }}
@@ -63,16 +67,16 @@
{{ end }} {{ end }}
</table> </table>
{{ else if isMarkdown $file.Filename }} {{ else if isMarkdown $file.Filename }}
<div class="markdown markdown-body p-8">{{ $file.Content }}</div> <div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
{{ else }} {{ else }}
<div class="code"> <div class="code">
{{ $fileslug := slug $file.Filename }} {{ $fileslug := slug $file.Filename }}
{{ if ne $file.Content "" }} {{ if ne $file.Content "" }}
<table class="table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;"> <table class="chroma table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody> <tbody>
{{ $ii := "1" }} {{ $ii := "1" }}
{{ $i := toInt $ii }} {{ $i := toInt $ii }}
{{ range $line := lines $file.Content }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line }}</td></tr>{{ $i = inc $i }}{{ end }} {{ range $line := $file.Lines }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line | safe }}</td></tr>{{ $i = inc $i }}{{ end }}
</tbody> </tbody>
</table> </table>
{{ end }} {{ end }}
@@ -90,5 +94,12 @@
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.no-content" }}</h3> <h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.no-content" }}</h3>
</div> </div>
{{ end }} {{ end }}
<!-- make sure tailwind knows those classes -->
<button type="button" style="top: 1em !important; right: 1em !important;" class="hidden md-code-copy-btn absolute right-0 top-0 focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>
<div class="accent-gray-400"></div>
<script type="module" src="{{ asset "gist.ts" }}"></script>
{{ template "gist_footer" .}} {{ template "gist_footer" .}}
{{ template "footer" .}} {{ template "footer" .}}

53
templates/pages/gist_embed.html vendored Normal file
View File

@@ -0,0 +1,53 @@
<div class="opengist-embed" id="{{ .gist.Identifier }}">
<div class="html {{.dark}}">
{{ range $file := .files }}
<div class="rounded-md border-1 border-gray-100 dark:border-gray-800 overflow-auto mb-4">
<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">
<a target="_blank" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}#file-{{ slug $file.Filename }}"><span class="font-bold text-gray-700 dark:text-gray-200">{{ $file.Filename }}</span> · {{ $file.HumanSize }} · {{ $file.Type }}</a>
<span class="float-right"><a target="_blank" href="{{ $.baseHttpUrl }}">Hosted via Opengist</a> · <span class="text-gray-700 dark:text-gray-200 font-bold"><a target="_blank" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}">view raw</a></span></span>
</div>
{{ if $file.Truncated }}
<div class="text-xs px-4 bg-gray-50 py-1.5 border-b-1 border-gray-100 dark:border-gray-700">
{{ $.locale.Tr "gist.file-truncated" }} <a target="_blank" class="text-primary-600" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}.</a>
</div>
{{ end }}
{{ $csv := csvFile $file.File }}
{{ if $csv }}
<table class="csv-table">
<thead>
<tr>
{{ range $csv.Header }}
<th>{{ . }}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range $csv.Rows }}
<tr>
{{ range . }}
<td>{{ . }}</td>
{{ end }}
</tr>
{{ end }}
</table>
{{ else if isMarkdown $file.Filename }}
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
{{ else }}
<div class="code dark:bg-gray-900">
{{ $fileslug := slug $file.Filename }}
{{ if ne $file.Content "" }}
<table class="chroma table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
{{ $ii := "1" }}
{{ $i := toInt $ii }}
{{ range $line := $file.Lines }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line | safe }}</td></tr>{{ $i = inc $i }}{{ end }}
</tbody>
</table>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>

View File

@@ -16,7 +16,7 @@
{{ end }} {{ end }}
</div> </div>
<div class="flex justify-center space-x-2 mt-4"> <div class="flex justify-center space-x-2 mt-4">
{{ template "pagination" . }} {{ template "_pagination" . }}
</div> </div>
{{ else }} {{ else }}
<div class="text-center"> <div class="text-center">

View File

@@ -12,7 +12,7 @@
</svg> </svg>
{{ $user := (index $.emails $commit.AuthorEmail) }} {{ $user := (index $.emails $commit.AuthorEmail) }}
<img class="h-5 w-5 rounded-full inline" src="{{if $user }}{{ avatarUrl $user $.DisableGravatar }}{{else}}{{defaultAvatar}}{{end}}" {{if $user }}alt="{{ $user.Username }}'s Avatar"{{end}} /> <img class="h-5 w-5 rounded-full inline" src="{{if $user }}{{ avatarUrl $user $.DisableGravatar }}{{else}}{{defaultAvatar}}{{end}}" {{if $user }}alt="{{ $user.Username }}'s Avatar"{{end}} />
<span class="font-bold">{{if $user}}<a href="{{ $.c.ExternalUrl }}/{{$user.Username}}" class="text-slate-300 hover:text-slate-300 hover:underline">{{ $commit.AuthorName }}</a>{{else}}{{ $commit.AuthorName }}{{end}}</span> {{ $.locale.Tr "gist.revision.revised" }} <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/rev/{{ $commit.Hash }}">{{ $.locale.Tr "gist.revision.go-to-revision" }}</a></h3> <span class="font-bold">{{if $user}}<a href="{{ $.c.ExternalUrl }}/{{$user.Username}}" class="text-slate-300 hover:text-slate-300 hover:underline">{{ $commit.AuthorName }}</a>{{else}}{{ $commit.AuthorName }}{{end}}</span> {{ $.locale.Tr "gist.revision.revised" }} <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/rev/{{ $commit.Hash }}">{{ $.locale.Tr "gist.revision.go-to-revision" }}</a></h3>
{{ if ne $commit.Changed "" }} {{ if ne $commit.Changed "" }}
<p class="text-sm float-right py-2"> <p class="text-sm float-right py-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline-flex"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline-flex">
@@ -50,7 +50,7 @@
{{ else if eq $file.Content "" }} {{ else if eq $file.Content "" }}
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.empty-file" }}</p> <p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.empty-file" }}</p>
{{ else }} {{ else }}
<table class="code table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0"> <table class="code chroma table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
<tbody> <tbody>
{{ $left := 0 }} {{ $left := 0 }}
{{ $right := 0 }} {{ $right := 0 }}
@@ -98,7 +98,7 @@
{{end}} {{end}}
</div> </div>
<div class="flex justify-center space-x-2"> <div class="flex justify-center space-x-2">
{{ template "pagination" . }} {{ template "_pagination" . }}
</div> </div>
{{ else }} {{ else }}
<div class="text-center"> <div class="text-center">

40
templates/pages/search.html vendored Normal file
View File

@@ -0,0 +1,40 @@
{{ template "header" .}}
<div class="py-10">
<header class="pb-4 ">
<div class="flex">
<div class="flex-auto">
<h1 class="text-2xl font-bold leading-tight">{{ .nbHits }} {{ .locale.Tr "gist.search.found" }}</h1>
</div>
</div>
</header>
<main>
{{ if ne (len .gists) 0 }}
<div class="md:grid md:grid-cols-12 gap-x-4">
<div class="md:col-span-3 pb-4">
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto">
{{ range $lang, $count := .langs }}
<a href="{{ $.c.ExternalUrl }}/search?q={{ addMetadataToSearchQuery $.searchQuery "language" $lang }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100">
{{ $lang }} ({{ $count }})
</a>
{{end}}
</div>
</div>
<div class="md:col-span-9">
{{ range $gist := .gists }}
{{ $nest := dict "gist" $gist "c" $.c "locale" $.locale "DisableGravatar" $.DisableGravatar }}
{{ template "_gist_preview" $nest }}
{{ end }}
</div>
</div>
{{ template "_pagination" . }}
{{ else }}
<div class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5" />
</svg>
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.search.no-results" }}</h3>
</div>
{{ end }}
</main>
</div>
{{ template "footer" .}}

View File

@@ -6,90 +6,146 @@
</div> </div>
</header> </header>
<main> <main>
<div class="space-y-4"> <div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="sm:grid {{ if or .githubOauth .giteaOauth .oidcOauth }}grid-cols-3{{else}}grid-cols-2{{end}} gap-x-4 md:gap-x-8"> <div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8 space-y-8 md:space-y-0">
<div class="w-full"> <div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10 h-full">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300"> <h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.email" }} {{ .locale.Tr "settings.change-username" }}
</h2> </h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4"> <form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/username" method="post">
{{ .locale.Tr "settings.email-help" }}
</h3>
<form class="space-y-6" action="/settings/email" method="post">
<div> <div>
<div class="mt-1"> <div class="mt-1">
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"> <input id="username-change" name="username" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div> </div>
</div> </div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.email-set" }}</button> <input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{ .locale.Tr "settings.change-username" }}
</button>
{{ .csrfHtml }} {{ .csrfHtml }}
</form> </form>
</div> </div>
</div> </div>
{{ if or .githubOauth .giteaOauth .oidcOauth }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
{{ .locale.Tr "settings.link-accounts" }}
</h2>
<div class="gap-y-2">
{{ if .githubOauth }}
{{ if .userLogged.GithubID }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitHub account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-github-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-github-account" }}
</a>
{{ end }}
{{ end }}
{{ if .giteaOauth }}
{{ if .userLogged.GiteaID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your Gitea account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitea-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitea-account" }}
</a>
{{ end }}
{{ end }}
{{ if .oidcOauth }}
{{ if .userLogged.OIDCID }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
Unlink OpenID account
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Link OpenID account
</a>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
<div class="w-full"> <div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300"> <h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.delete-account" }} {{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</h2> </h2>
<form class="space-y-6" action="/settings/account" method="post" onsubmit="return confirm('{{ .locale.Tr "settings.delete-account-confirm" }}')"> <h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
<input type="hidden" name="_method" value="DELETE"> {{if .hasPassword}}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 mt-2">{{ .locale.Tr "settings.delete-account" }}</button> {{ .locale.Tr "settings.change-password-help" }}
{{else}}
{{ .locale.Tr "settings.create-password-help" }}
{{end}}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/password" method="post">
<div>
<label for="password-change" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.password-label-title" }} </label>
<div class="mt-1">
<input id="password-change" name="password" type="password" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</button>
{{ .csrfHtml }} {{ .csrfHtml }}
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.email" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.email-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/email" method="post">
<div>
<div class="mt-1">
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.email-set" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
{{ .locale.Tr "settings.link-accounts" }}
</h2>
<div class="gap-y-2">
{{ if .githubOauth }}
{{ if .userLogged.GithubID }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitHub account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-github-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-github-account" }}
</a>
{{ end }}
{{ end }}
{{ if .gitlabOauth }}
{{ if .userLogged.GitlabID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitLab account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitlab-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitlab-account" }}
</a>
{{ end }}
{{ end }}
{{ if .giteaOauth }}
{{ if .userLogged.GiteaID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your Gitea account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitea-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitea-account" }}
</a>
{{ end }}
{{ end }}
{{ if .oidcOauth }}
{{ if .userLogged.OIDCID }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
Unlink OpenID account
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Link OpenID account
</a>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8"> <div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full"> <div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
@@ -99,7 +155,7 @@
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4"> <h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.add-ssh-key-help" }} {{ .locale.Tr "settings.add-ssh-key-help" }}
</h3> </h3>
<form class="space-y-6" action="/settings/ssh-keys" method="post"> <form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/ssh-keys" method="post">
<div> <div>
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-title" }} </label> <label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-title" }} </label>
<div class="mt-1"> <div class="mt-1">
@@ -138,7 +194,7 @@
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p> <p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
{{ end }} {{ end }}
</div> </div>
<form action="/settings/ssh-keys/{{.ID}}" method="post" class="inline-block" onsubmit="return confirm('{{ $.locale.Tr "settings.delete-ssh-key-confirm" }}')"> <form action="{{ $.c.ExternalUrl }}/settings/ssh-keys/{{.ID}}" method="post" class="inline-block" onsubmit="return confirm('{{ $.locale.Tr "settings.delete-ssh-key-confirm" }}')">
<input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }} {{ $.csrfHtml }}
@@ -152,6 +208,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.delete-account" }}
</h2>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/account" method="post" onsubmit="return confirm('{{ .locale.Tr "settings.delete-account-confirm" }}')">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 mt-2">{{ .locale.Tr "settings.delete-account" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
</div> </div>
</main> </main>
</div> </div>

76
templates/partials/_gist_preview.html vendored Normal file
View File

@@ -0,0 +1,76 @@
{{ define "_gist_preview" }}
<div class="mb-8">
<div class="flex ">
<div class="div">
<a href="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}">
<img class="h-10 w-10 rounded-md mr-2 border border-gray-200 dark:border-gray-700 my-1" src="{{ avatarUrl .gist.User .DisableGravatar }}" alt="{{ .gist.User.Username }}'s Avatar">
</a>
</div>
<div class="flex-auto">
<div class="flex flex-col lg:flex-row">
<h4 class="text-md leading-tight break-all py-1 flex-auto">
<a href="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}">{{ .gist.User.Username }}</a> <span class="text-slate-700 dark:text-slate-300">/</span> <a class="font-bold" href="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}">{{ .gist.Title }}</a>
</h4>
<div class="flex space-x-4 lg:flex-row flex py-1 lg:py-0 lg:ml-auto text-slate-500">
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="whitespace-nowrap">{{ .gist.NbLikes }} {{ .locale.Tr "gist.list.likes" }}</span>
</div>
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
</svg>
<span class="whitespace-nowrap">{{ .gist.NbForks }} {{ .locale.Tr "gist.list.forks" }}</span>
</div>
<div class="flex items-center float-right text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-flex">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
</svg>
<span class="whitespace-nowrap">{{ .gist.NbFiles }} {{ .locale.Tr "gist.list.files" }}</span>
</div>
</div>
</div>
<h5 class="text-sm text-slate-500 pb-1">{{ .locale.Tr "gist.list.last-active" }} <span class="moment-timestamp">{{ .gist.UpdatedAt }}</span>
{{ if .gist.Forked }} • {{ .locale.Tr "gist.list.forked-from" }} <a href="{{ .c.ExternalUrl }}/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Identifier }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a> {{ end }}
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}</h5>
<h6 class="text-xs text-slate-700 dark:text-slate-300 py-1">{{ .gist.Description }}</h6>
</div>
</div>
<a href="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}" class="text-slate-700 dark:text-slate-300">
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">
<div class="code overflow-auto">
{{ if .gist.PreviewFilename }}
{{ if isMarkdown .gist.PreviewFilename }}
<div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div>
{{ else }}
<table class="chroma table-code w-full whitespace-pre" data-filename="{{ .gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
{{ $ii := "1" }}
{{ $i := toInt $ii }}
{{ range $line := .gist.Lines }}
<tr>
<td class="select-none line-num px-4">{{$i}}</td>
<td class="line-code">{{ $line | safe }}</td>
</tr>
{{ $i = inc $i }}
{{ end }}
</tbody>
</table>
{{ end }}
{{ else }}
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.no-content" }}</p></div>
{{ end }}
</div>
</div>
</a>
</div>
{{ end }}

View File

@@ -1,4 +1,4 @@
{{ define "pagination" }} {{ define "_pagination" }}
<div class="flex justify-center space-x-2"> <div class="flex justify-center space-x-2">
{{ if .prevPage }} {{ if .prevPage }}
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .prevPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4"> <a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .prevPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true
},
"files": [
"public/main.ts",
"public/editor.ts",
],
}