├── .dockerignore ├── .env ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── Dockerfile.dev ├── INSTALL.md ├── LICENSE ├── README.md ├── cog.toml ├── crates ├── gill-app │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── assets │ │ ├── css │ │ │ ├── fonts │ │ │ │ ├── tabler-icons.eot │ │ │ │ ├── tabler-icons.svg │ │ │ │ ├── tabler-icons.ttf │ │ │ │ ├── tabler-icons.woff │ │ │ │ └── tabler-icons.woff2 │ │ │ ├── style.css │ │ │ ├── tabler-icons.css │ │ │ └── tailwind.min.css │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── gill_web_markdown.js │ │ │ ├── gill_web_markdown_bg.wasm │ │ │ ├── markdown.js │ │ │ └── uri-helpers.js │ ├── src │ │ ├── api │ │ │ ├── mod.rs │ │ │ ├── repository.rs │ │ │ └── user.rs │ │ ├── apub │ │ │ ├── common.rs │ │ │ ├── mod.rs │ │ │ ├── repository │ │ │ │ ├── fork.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── star.rs │ │ │ │ └── watch.rs │ │ │ ├── ticket │ │ │ │ ├── accept.rs │ │ │ │ ├── comment │ │ │ │ │ ├── create.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── mod.rs │ │ │ │ └── offer.rs │ │ │ └── user │ │ │ │ ├── follow.rs │ │ │ │ └── mod.rs │ │ ├── domain │ │ │ ├── apub.rs │ │ │ ├── commit │ │ │ │ └── mod.rs │ │ │ ├── id.rs │ │ │ ├── issue │ │ │ │ ├── comment │ │ │ │ │ ├── create.rs │ │ │ │ │ ├── digest.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── create.rs │ │ │ │ ├── digest.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── pull_request │ │ │ │ ├── comment.rs │ │ │ │ └── mod.rs │ │ │ ├── repository │ │ │ │ ├── branch.rs │ │ │ │ ├── create.rs │ │ │ │ ├── digest.rs │ │ │ │ ├── mod.rs │ │ │ │ └── stats.rs │ │ │ └── user │ │ │ │ ├── create.rs │ │ │ │ ├── mod.rs │ │ │ │ └── ssh_key.rs │ │ ├── error.rs │ │ ├── instance.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── oauth │ │ │ ├── mod.rs │ │ │ └── service.rs │ │ ├── state.rs │ │ ├── view │ │ │ ├── component.rs │ │ │ ├── dto.rs │ │ │ ├── filters.rs │ │ │ ├── follow.rs │ │ │ ├── index.rs │ │ │ ├── mod.rs │ │ │ ├── repository │ │ │ │ ├── activity.rs │ │ │ │ ├── blob.rs │ │ │ │ ├── commits.rs │ │ │ │ ├── create.rs │ │ │ │ ├── diff.rs │ │ │ │ ├── issues │ │ │ │ │ ├── close.rs │ │ │ │ │ ├── comment.rs │ │ │ │ │ ├── create.rs │ │ │ │ │ ├── list_view.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── view.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pull_request │ │ │ │ │ ├── comment.rs │ │ │ │ │ ├── commits.rs │ │ │ │ │ ├── compare.rs │ │ │ │ │ ├── create.rs │ │ │ │ │ ├── diff.rs │ │ │ │ │ ├── list_view.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── view.rs │ │ │ │ ├── tree.rs │ │ │ │ └── user_content.rs │ │ │ └── user │ │ │ │ ├── mod.rs │ │ │ │ ├── profile.rs │ │ │ │ ├── settings.rs │ │ │ │ └── ssh_key.rs │ │ └── webfinger │ │ │ └── mod.rs │ ├── tailwind.config.js │ └── templates │ │ ├── base.html │ │ ├── base_repository.html │ │ ├── components │ │ └── markdown-preview-form.html │ │ ├── index.html │ │ ├── navbar.html │ │ ├── repository │ │ ├── branch.html │ │ ├── commit-diff.html │ │ ├── components │ │ │ ├── clone-button.html │ │ │ ├── fork-button.html │ │ │ ├── star-button.html │ │ │ └── watch-button.html │ │ ├── create.html │ │ ├── diff.html │ │ ├── federated │ │ │ ├── header.html │ │ │ └── view.html │ │ ├── header.html │ │ ├── history.html │ │ ├── issues │ │ │ ├── issue.html │ │ │ └── list.html │ │ ├── pulls │ │ │ ├── commit-diff.html │ │ │ ├── commits.html │ │ │ ├── compare.html │ │ │ ├── diff.html │ │ │ ├── list.html │ │ │ ├── nav.html │ │ │ ├── pull.html │ │ │ └── summary.html │ │ └── tree │ │ │ ├── blob.html │ │ │ ├── empty.html │ │ │ └── tree.html │ │ └── user │ │ ├── profile.html │ │ └── settings.html ├── gill-authorize-derive │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── gill-db │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── migrations │ │ └── 20221115110623_base_schema.sql │ └── src │ │ ├── lib.rs │ │ ├── pagination.rs │ │ ├── repository │ │ ├── branch.rs │ │ ├── create.rs │ │ ├── digest.rs │ │ ├── fork.rs │ │ ├── issue │ │ │ ├── comment.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── pull_request │ │ │ ├── comment.rs │ │ │ └── mod.rs │ │ ├── star.rs │ │ └── watch.rs │ │ ├── subscribe.rs │ │ └── user │ │ ├── follow.rs │ │ ├── mod.rs │ │ └── ssh_keys.rs ├── gill-git-server │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── pack-serve.rs │ │ └── post-receive.rs ├── gill-git │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── clone.rs │ │ ├── commits.rs │ │ ├── diffs │ │ ├── commit.rs │ │ ├── mod.rs │ │ └── tree.rs │ │ ├── init.rs │ │ ├── lib.rs │ │ ├── merge.rs │ │ ├── ssh.rs │ │ └── traversal.rs ├── gill-markdown │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── gill-settings │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── gill-syntax │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── src │ │ ├── diff.rs │ │ ├── highlight.rs │ │ └── lib.rs │ ├── syntax.bin │ └── theme.bin ├── gill-web-markdown │ ├── .appveyor.yml │ ├── .gitignore │ ├── .travis.yml │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE_APACHE │ ├── LICENSE_MIT │ ├── README.md │ ├── src │ │ └── lib.rs │ └── tests │ │ └── web.rs └── syntect-plugin │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ └── main.rs ├── docker-compose.dev.yml ├── docker-compose.yml ├── docker ├── dev │ ├── config-instance-1.toml │ ├── config-instance-2.toml │ ├── home │ │ └── entrypoint-debug.sh │ ├── home2 │ │ ├── .ssh │ │ │ └── authorized_keys │ │ ├── entrypoint-debug.sh │ │ └── entrypoint.sh │ └── init.sql ├── entrypoint.sh ├── init.sql └── sshd_config ├── docs └── assets │ ├── sc_1.png │ ├── sc_2.png │ ├── sc_3.png │ └── sc_4.png ├── justfile └── sqlx-data.json /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !crates/** 3 | !Cargo.toml 4 | !Cargo.lock 5 | !sqlx-data.json 6 | !/etc/ssh/sshd_config 7 | !/opt/gill/migrations/** 8 | !docker/entrypoint.sh 9 | !docker/sshd_config 10 | !target/armv7-unknown-linux-musleabihf/release/gill-app 11 | !target/armv7-unknown-linux-musleabihf/release/gill-git-server 12 | !target/armv7-unknown-linux-musleabihf/release/post-receive 13 | !target/aarch64-unknown-linux-musl/release/gill-app 14 | !target/aarch64-unknown-linux-musl/release/gill-git-server 15 | !target/aarch64-unknown-linux-musl/release/post-receive 16 | !target/x86_64-unknown-linux-musl/release/gill-app 17 | !target/x86_64-unknown-linux-musl/release/gill-git-server 18 | !target/x86_64-unknown-linux-musl/release/post-receive -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Only needed by sqlx to pick the database url 2 | DATABASE_URL="postgres://postgres:postgres@localhost/gill" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | crates/gill-app/assets/** linguist-vendored 2 | docker/** linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: oknozor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/target 3 | db-data/ 4 | docker/sshd.env 5 | docker/dev/home/* 6 | docker/dev/home2/* 7 | 8 | !docker/dev/home/entrypoint-debug.sh 9 | !docker/dev/home2/entrypoint-debug.sh 10 | !docker/home/entrypoint.sh 11 | !docker/dev/home2/entrypoint.sh 12 | !docker/dev/home/config.toml 13 | !docker/dev/home2/config.toml 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "syntect"] 2 | path = syntect 3 | url = https://github.com/sublimehq/Packages.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | 4 | [workspace.dependencies] 5 | tokio = { version = "1.21.2", features = ["full"] } 6 | sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "any", "postgres", "offline", "chrono", "uuid"] } 7 | axum = { version = "0.6.0", default-features = false, features = ["json", "headers", "macros", 'http2'] } 8 | activitypub_federation = { git = "https://github.com/LemmyNet/activitypub-federation-rust", features = ["axum"] } 9 | axum-macros = "0.3.0" 10 | tower-http = { version = "0.3.4", features = ["trace", "fs"] } 11 | reqwest = { version = "0.11.13", features = ["json", "native-tls-vendored"] } 12 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 13 | tracing = "0.1" 14 | serde = { version = "1.0.147", features = ["derive"] } 15 | serde_json = "1.0.87" 16 | anyhow = "1.0.68" 17 | speculoos = "0.11.0" 18 | thiserror = "1.0.38" 19 | once_cell = "1.17.0" 20 | chrono = { version = "0.4.23", features = ["serde"] } 21 | syntect = "5.0.0" 22 | url = "2.3.1" 23 | 24 | [profile.release] 25 | opt-level = "s" 26 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = ["SQLX_OFFLINE=true"] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Note that the following build needs binaries to be precompiled for the target 2 | # architectures. Use the `build-all` just recipies to build for all targets. 3 | FROM alpine as arm-builder 4 | COPY ./target/armv7-unknown-linux-musleabihf/release/gill-app /gill-app 5 | COPY ./target/armv7-unknown-linux-musleabihf/release/gill-git-server /gill-git-server 6 | COPY ./target/armv7-unknown-linux-musleabihf/release/post-receive /post-receive 7 | 8 | FROM alpine as arm64-builder 9 | COPY ./target/aarch64-unknown-linux-musl/release/gill-app /gill-app 10 | COPY ./target/aarch64-unknown-linux-musl/release/gill-git-server /gill-git-server 11 | COPY ./target/aarch64-unknown-linux-musl/release/post-receive /post-receive 12 | 13 | FROM alpine as amd64-builder 14 | COPY ./target/x86_64-unknown-linux-musl/release/gill-app /gill-app 15 | COPY ./target/x86_64-unknown-linux-musl/release/gill-git-server /gill-git-server 16 | COPY ./target/x86_64-unknown-linux-musl/release/post-receive /post-receive 17 | 18 | FROM ${TARGETARCH}-builder AS builder 19 | 20 | FROM alpine 21 | MAINTAINER Paul Delafosse "paul.delafosse@protonmail.com" 22 | RUN apk --no-cache add openssh git 23 | 24 | # Setup sshd 25 | COPY docker/sshd_config /etc/ssh/sshd_config 26 | 27 | RUN adduser -D -s /bin/sh git \ 28 | && echo git:gill | chpasswd # This is needed so the user is not locked 29 | WORKDIR /home/git 30 | USER git 31 | 32 | #Prepare workdir 33 | RUN mkdir .ssh \ 34 | && touch .ssh/authorized_keys \ 35 | && chmod 700 .ssh \ 36 | && chmod -R 600 .ssh/* 37 | 38 | # Install binaries 39 | COPY --from=builder /gill-app /usr/bin/gill-app 40 | COPY --from=builder /gill-git-server /usr/bin/gill-git-server 41 | COPY --from=builder /post-receive /usr/share/git-core/templates/hooks/post-receive 42 | 43 | # Install assets 44 | COPY crates/gill-db/migrations /opt/gill/migrations 45 | COPY crates/gill-app/assets/ /opt/gill/assets 46 | 47 | EXPOSE 22 48 | EXPOSE 3000 49 | 50 | USER root 51 | 52 | RUN mkdir /root/.ssh \ 53 | && chmod 700 /root/.ssh 54 | 55 | COPY ./docker/entrypoint.sh /entrypoint.sh 56 | 57 | CMD ["gill-app"] 58 | ENTRYPOINT ["/entrypoint.sh"] 59 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM docker.io/alpine:3.4 2 | MAINTAINER Paul Delafosse "paul.delafosse@protonmail.com" 3 | 4 | RUN apk add --no-cache \ 5 | openssh \ 6 | bash \ 7 | git \ 8 | curl 9 | 10 | # Setup sshd 11 | RUN ssh-keygen -A 12 | 13 | RUN adduser -D -s /bin/bash git \ 14 | && echo git:12345 | chpasswd \ 15 | && mkdir /home/git/.ssh \ 16 | && touch /home/git/.ssh/authorized_keys \ 17 | && chown -R git:git /home/git/.ssh \ 18 | && chmod 700 /home/git/.ssh \ 19 | && chmod -R 600 /home/git/.ssh/* 20 | 21 | COPY docker/sshd_config /etc/ssh/sshd_config 22 | 23 | EXPOSE 22 24 | EXPOSE 3000 25 | 26 | # Prepare workdir 27 | WORKDIR /home/git 28 | RUN mkdir bin 29 | 30 | COPY target/x86_64-unknown-linux-musl/release/gill-app ./bin/gill-app 31 | COPY target/x86_64-unknown-linux-musl/release/gill-git-server ./bin/gill-git-server 32 | COPY target/x86_64-unknown-linux-musl/release/post-receive /usr/share/git-core/templates/hooks/post-receive 33 | 34 | COPY crates/gill-app/assets/ /opt/gill/assets 35 | COPY docker/dev/home/* ./ 36 | 37 | RUN chown -R git:git ./ 38 | 39 | ENTRYPOINT ["./entrypoint.sh"] -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Config 2 | 3 | First create a `config.toml` file and edit it according to your setup. 4 | ```toml 5 | # The public domain where gill will be served. 6 | domain = "example.gill.org" 7 | # If set to true gill is expected to be exposed over a non secure HTTP connection. 8 | debug = false 9 | # Internal port exposing the web application. 10 | port = 3000 11 | # Sshd exposed port, depending on your environemnt you might not want to use port 22. 12 | ssh_port = 2222 13 | 14 | # Gill's database endpoint configuration 15 | [database] 16 | host = "gill.example.db" 17 | port = 5432 18 | database = "gill" 19 | user = "postgres" 20 | password = "password" 21 | 22 | # Gill's openid connect provider 23 | [oauth_provider] 24 | client_id = "gill" 25 | client_secret = "n5obgGTk855H1Mx3b2YG2JCO8Bc6WGq1" 26 | provider = "https://keycloak.cloud.hoohoot.org" 27 | user_info_url = "/auth/realms/hoohoot/protocol/openid-connect/userinfo" 28 | auth_url = "/auth/realms/hoohoot/protocol/openid-connect/auth" 29 | token_url = "/auth/realms/hoohoot/protocol/openid-connect/token" 30 | ``` 31 | 32 | ## Creating the ssh keys 33 | 34 | In order to run the sshd server along with gill you need to generate sshd host keys. 35 | 36 | ```shell 37 | mkdir -p /tmp/etc/ssh 38 | ssh-keygen -A -f /tmp 39 | echo "GILL_SSH_ECDSA_PUB: '`cat /tmp/etc/ssh/ssh_host_ecdsa_key.pub`'" >> sshd.env 40 | echo "GILL_SSH_ECDSA: '`cat /tmp/etc/ssh/ssh_host_ecdsa_key`'" >> sshd.env 41 | echo "GILL_SSH_ED25519_PUB: '`cat /tmp/etc/ssh/ssh_host_ed25519_key.pub`'" >> sshd.env 42 | echo "GILL_SSH_ED25519: '`cat /tmp/etc/ssh/ssh_host_ed25519_key`'" >> sshd.env 43 | echo "GILL_SSH_RSA_PUB: '`cat /tmp/etc/ssh/ssh_host_rsa_key.pub`'" >> sshd.env 44 | echo "GILL_SSH_RSA: '`cat /tmp/etc/ssh/ssh_host_rsa_key`'" >> sshd.env 45 | ``` 46 | 47 | ## Docker compose 48 | 49 | ```yaml 50 | version: '3.9' 51 | services: 52 | gill: 53 | image: "gillpub/gill:latest" 54 | restart: unless-stopped 55 | container_name: gill 56 | env_file: 57 | - sshd.env 58 | ports: 59 | - "3000:3000" 60 | - "2222:22" 61 | volumes: 62 | - ./gill_data:/home/git 63 | - ./config.toml:/opt/gill/config.toml 64 | networks: 65 | - gill 66 | 67 | volumes: 68 | gill_data: 69 | ``` 70 | 71 | **Run:** 72 | `docker compose up -d` 73 | 74 | ## Reverse proxy 75 | 76 | Here is an example of exposing gill with nginx: 77 | 78 | ```nginx 79 | server { 80 | server_name home-raspberry.gill.pub; 81 | 82 | listen 443 ssl; # managed by Certbot 83 | ssl_certificate /etc/letsencrypt/live/home-raspberry.gill.pub/fullchain.pem; # managed by Certbot 84 | ssl_certificate_key /etc/letsencrypt/live/home-raspberry.gill.pub/privkey.pem; # managed by Certbot 85 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 86 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 87 | 88 | location / { 89 | proxy_set_header X-Real-IP $remote_addr; 90 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 91 | proxy_set_header Host $http_host; 92 | proxy_pass http://192.168.0.17:3000/; 93 | } 94 | } 95 | 96 | 97 | server { 98 | if ($host = home-raspberry.gill.pub) { 99 | return 301 https://$host$request_uri; 100 | } # managed by Certbot 101 | 102 | 103 | listen 80; 104 | server_name home-raspberry.gill.pub; 105 | return 404; # managed by Certbot 106 | } 107 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paul Delafosse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cog.toml: -------------------------------------------------------------------------------- 1 | branch_whitelist = ["main"] 2 | ignore_merge_commits = true 3 | tag_prefix = "v" 4 | 5 | pre_bump_hooks = [ 6 | "SQLX_OFFLINE=true cargo test", 7 | "SQLX_OFFLINE=true cargo clippy", 8 | "cargo fmt --all", 9 | "SQLX_OFFLINE=true cargo build --release", 10 | ] 11 | 12 | post_bump_hooks = [ 13 | "git push", 14 | "git push origin --tags", 15 | ] 16 | 17 | pre_package_bump_hooks = [ 18 | "cargo set-version {{version}}" 19 | ] 20 | 21 | [packages] 22 | gill-app = { path = "crates/gill-app" } 23 | gill-authorize-derive = { path = "crates/gill-authorize-derive", public_api = false } 24 | gill-db = { path = "crates/gill-db", public_api = false } 25 | gill-git = { path = "crates/gill-git", public_api = false } 26 | gill-git-server = { path = "crates/gill-git-server" } 27 | gill-markdown = { path = "crates/gill-markdown", public_api = false } 28 | gill-settings = { path = "crates/gill-settings" } 29 | gill-syntax = { path = "crates/gill-syntax" } 30 | gill-web-markdown = { path = "crates/gill-web-markdown" } 31 | syntect-plugin = { path = "crates/syntect-plugin", public_api = false } 32 | 33 | [changelog] 34 | template = "monorepo_remote" 35 | package_template = "package_remote" 36 | remote = "github.com" 37 | repository = "gill" 38 | owner = "oknozor" 39 | authors = [ 40 | { signature = "Paul Delafosse", username = "oknozor" }, 41 | ] 42 | 43 | 44 | -------------------------------------------------------------------------------- /crates/gill-app/.gitignore: -------------------------------------------------------------------------------- 1 | sqlx-data.json 2 | -------------------------------------------------------------------------------- /crates/gill-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-app" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # We need to make the background job crate compatible with tokio runtime 8 | # to remove this 9 | actix-rt = "2.7.0" 10 | 11 | tokio.workspace = true 12 | sqlx.workspace = true 13 | tracing-subscriber.workspace = true 14 | tracing.workspace = true 15 | anyhow.workspace = true 16 | serde.workspace = true 17 | serde_json.workspace = true 18 | reqwest.workspace = true 19 | tower-http.workspace = true 20 | axum.workspace = true 21 | axum-macros.workspace = true 22 | activitypub_federation.workspace = true 23 | once_cell.workspace = true 24 | chrono.workspace = true 25 | 26 | gill-db = { path = "../gill-db" } 27 | gill-git = { path = "../gill-git" } 28 | gill-settings = { path = "../gill-settings" } 29 | gill-syntax = { path = "../gill-syntax" } 30 | gill-markdown = { path = "../gill-markdown" } 31 | gill-authorize-derive = { path = "../gill-authorize-derive" } 32 | 33 | uuid = "1.2.2" 34 | enum_delegate = "0.2.0" 35 | activitystreams-kinds = "0.3.0" 36 | webfinger = "0.5.1" 37 | tower = "0.4.13" 38 | url.workspace = true 39 | base64 = "0.21.0" 40 | headers = "0.3" 41 | oauth2 = "4.1" 42 | async-session = "3.0.0" 43 | http = "0.2" 44 | askama = "0.11" 45 | 46 | [dev-dependencies] 47 | tower = "0.4.13" 48 | speculoos.workspace = true 49 | archunit_rs = { git = "https://github.com/oknozor/archunit_rs.git", branch = "feat/layer-rules" } -------------------------------------------------------------------------------- /crates/gill-app/assets/css/fonts/tabler-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-app/assets/css/fonts/tabler-icons.eot -------------------------------------------------------------------------------- /crates/gill-app/assets/css/fonts/tabler-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-app/assets/css/fonts/tabler-icons.ttf -------------------------------------------------------------------------------- /crates/gill-app/assets/css/fonts/tabler-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-app/assets/css/fonts/tabler-icons.woff -------------------------------------------------------------------------------- /crates/gill-app/assets/css/fonts/tabler-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-app/assets/css/fonts/tabler-icons.woff2 -------------------------------------------------------------------------------- /crates/gill-app/assets/css/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", 7 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 8 | } 9 | 10 | @layer components { 11 | /* 12 | The class name are one character long on purpose to avoid bloating the generated diff 13 | We are using apply here only because otherwise, 14 | tailwind cli does not pick the class in the generated diff. 15 | This should not be used if not absolutely needed. 16 | see: https://tailwindcss.com/docs/reusing-styles#avoiding-premature-abstraction 17 | */ 18 | /* file diff container */ 19 | .d { 20 | @apply flex flex-col whitespace-pre-wrap rounded-md border-2 border-slate-200; 21 | } 22 | 23 | /* file diff header (icon + filename) */ 24 | .h { 25 | @apply flex flex-row items-center p-2 justify-items-center font-bold border-b-2 border-slate-200; 26 | } 27 | 28 | /* code line number */ 29 | .l { 30 | @apply pr-3 text-right bg-zinc-200; 31 | } 32 | 33 | /* code line */ 34 | .c { 35 | @apply pl-5 w-full; 36 | } 37 | } 38 | 39 | @layer prism { 40 | pre[class*=language-] { 41 | @apply bg-slate-600 rounded 42 | } 43 | 44 | code[class*=language-], pre[class*=language-] { 45 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 46 | line-height: 1.5; 47 | -moz-tab-size: 4; 48 | tab-size: 4; 49 | -webkit-hyphens: none; 50 | -moz-hyphens: none; 51 | -ms-hyphens: none; 52 | hyphens: none; 53 | } 54 | 55 | pre[class*=language-] { 56 | padding: 1em; 57 | margin: .5em 0; 58 | overflow: auto; 59 | } 60 | 61 | :not(pre) > code[class*=language-], pre[class*=language-] { 62 | background: #272822; 63 | } 64 | 65 | :not(pre) > code[class*=language-] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.cdata, .token.comment, .token.doctype, .token.prolog { 72 | @apply text-slate-400 73 | } 74 | 75 | /* 76 | .token.punctuation { 77 | } 78 | 79 | .token.namespace { 80 | } 81 | */ 82 | 83 | .token.constant, .token.deleted, .token.property, .token.symbol, .token.tag { 84 | @apply text-red-400 85 | } 86 | 87 | .token.boolean, .token.number { 88 | @apply text-violet-400 89 | } 90 | 91 | .token.attr-name, .token.builtin, .token.char, .token.inserted, .token.selector, .token.string { 92 | @apply text-blue-800 93 | } 94 | 95 | .language-css .token.string, .style, .token.entity, .token.operator, .token.url, .token.variable { 96 | @apply text-green-600 97 | } 98 | 99 | .token.class-name { 100 | @apply text-slate-800 101 | } 102 | 103 | .token.atrule, .token.attr-value, .token.function { 104 | @apply text-indigo-400 105 | } 106 | 107 | .token.keyword { 108 | @apply text-red-600 109 | } 110 | 111 | .token.important, .token.regex { 112 | @apply text-amber-400 113 | } 114 | 115 | .token.bold, .token.important { 116 | @apply font-bold 117 | } 118 | 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | 123 | .token.entity { 124 | cursor: help 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/gill-app/assets/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import("./markdown.js") 2 | .catch(e => console.error("Error importing `markdown.js`:", e)) 3 | 4 | -------------------------------------------------------------------------------- /crates/gill-app/assets/js/gill_web_markdown_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-app/assets/js/gill_web_markdown_bg.wasm -------------------------------------------------------------------------------- /crates/gill-app/assets/js/markdown.js: -------------------------------------------------------------------------------- 1 | import {default as init, render_markdown} from './gill_web_markdown.js' 2 | 3 | async function run() { 4 | await init(); 5 | } 6 | 7 | run() 8 | .then(() => { 9 | window.render_markdown = render_markdown; 10 | window.dispatchEvent(new CustomEvent("WasmLoaded", {})); 11 | }) 12 | .catch(err => console.error("failed to init wasm module: " + err)); -------------------------------------------------------------------------------- /crates/gill-app/assets/js/uri-helpers.js: -------------------------------------------------------------------------------- 1 | const navigation = (branch) => { 2 | let currentBranch = encodeURIComponent(branch); 3 | let {proto, host, user, repository, blobOrTree, currentPath} = pathInfo(); 4 | let parts = currentPath.split("/"); 5 | let linkElements = [] 6 | linkElements.push(`${repository}`); 7 | let link = ""; 8 | for (let i = 0; i < parts.length; i++) { 9 | let pathPart = parts[i]; 10 | link = link + "/" + pathPart; 11 | let part = decodeURIComponent(pathPart); 12 | if (blobOrTree === "blob" && pathPart === parts[parts.length - 1]) { 13 | linkElements.push(`${part}`) 14 | } else { 15 | linkElements.push(`${part}`); 16 | } 17 | } 18 | 19 | let navigationLinks = document.getElementById("navigation"); 20 | navigationLinks.innerHTML = linkElements.join(" / ") 21 | } 22 | 23 | 24 | const setBranchDropDownLink = (branch) => { 25 | let {proto, host, user, repository, blobOrTree, currentPath} = pathInfo(); 26 | let branchUri = encodeURIComponent(branch); 27 | let href; 28 | if (!blobOrTree) { 29 | href = `${proto}//${host}/${user}/${repository}/tree/${branchUri}` 30 | } else { 31 | if (currentPath) { 32 | href = `${proto}//${host}/${user}/${repository}/${blobOrTree}/${branchUri}/${currentPath}` 33 | } else { 34 | href = `${proto}//${host}/${user}/${repository}/${blobOrTree}/${branchUri}` 35 | } 36 | 37 | } 38 | 39 | let link = document.getElementById(`branch-${branch}`); 40 | link.setAttribute("href", href); 41 | } 42 | 43 | const generateTreeLink = (path, treeOrBLob, currentBranch) => { 44 | let branch = encodeURIComponent(currentBranch); 45 | let {proto, host, user, repository, currentPath} = pathInfo(); 46 | let link = document.getElementById(path); 47 | let href; 48 | 49 | if (currentPath) { 50 | href = `${proto}//${host}/${user}/${repository}/${treeOrBLob}/${branch}/${currentPath}/${path}` 51 | } else { 52 | href = `${proto}//${host}/${user}/${repository}/${treeOrBLob}/${branch}/${path}` 53 | } 54 | 55 | link.setAttribute("href", href); 56 | } 57 | 58 | const pathInfo = () => { 59 | let proto = window.location.protocol; 60 | let host = window.location.host; 61 | let parts = window.location.pathname.split("/"); 62 | parts.shift(); 63 | 64 | let user = parts.shift(); 65 | let repository = parts.shift(); 66 | let blobOrTree = parts.shift(); 67 | let _currentBranch = parts.shift(); 68 | let currentPath = parts.join("/"); 69 | 70 | return { 71 | proto, 72 | host, 73 | user, 74 | repository, 75 | blobOrTree, 76 | currentPath, 77 | } 78 | } -------------------------------------------------------------------------------- /crates/gill-app/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::oauth; 2 | use crate::state::AppState; 3 | use axum::{ 4 | middleware, 5 | routing::{get, post}, 6 | Router, 7 | }; 8 | 9 | pub mod repository; 10 | pub mod user; 11 | 12 | pub fn router(state: AppState) -> Router { 13 | let public = Router::new() 14 | .route("/health", get(|| async { "Pong" })) 15 | .route("/health/", get(|| async { "Pong" })) 16 | .route("/users", post(user::create)) 17 | .route("/users/", post(user::create)); 18 | 19 | let authenticated = Router::new() 20 | .route("/users/ssh_key/add", post(user::register_ssh_key)) 21 | .route("/users/ssh_key/add/", post(user::register_ssh_key)) 22 | .route("/repositories/create", post(repository::init)) 23 | .route("/repositories/create/", post(repository::init)) 24 | .route_layer(middleware::from_fn(oauth::service::auth)); 25 | 26 | public.merge(authenticated).with_state(state) 27 | } 28 | -------------------------------------------------------------------------------- /crates/gill-app/src/api/repository.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppResult; 2 | 3 | use activitypub_federation::core::signatures::generate_actor_keypair; 4 | 5 | use crate::domain::id::ActivityPubId; 6 | use crate::domain::repository::create::CreateRepository; 7 | use crate::domain::user::User; 8 | use axum::http::StatusCode; 9 | use axum::response::{IntoResponse, Response}; 10 | use axum::Extension; 11 | use axum::Json; 12 | use gill_settings::SETTINGS; 13 | use serde::Deserialize; 14 | use sqlx::PgPool; 15 | 16 | use url::Url; 17 | 18 | #[derive(Deserialize)] 19 | pub struct CreateRepositoryCommand { 20 | pub name: String, 21 | pub summary: Option, 22 | } 23 | 24 | impl CreateRepositoryCommand { 25 | fn map_to_domain(self, user: &User) -> AppResult { 26 | let apub_id = self.generate_activity_pub_id(user); 27 | let clone_uri = self.generate_clone_uri(user); 28 | let key_pair = generate_actor_keypair()?; 29 | let activity_pub_id = ActivityPubId::try_from(apub_id.clone())?; 30 | 31 | // Note that for now 'ticket_tracked_by' and 'send_patches_to' are 32 | // the local repository owner by default. We might want to change this later 33 | Ok(CreateRepository { 34 | activity_pub_id: activity_pub_id.clone(), 35 | name: self.name, 36 | summary: self.summary, 37 | private: false, 38 | inbox_url: Url::parse(&format!("{apub_id}/inbox"))?, 39 | outbox_url: Url::parse(&format!("{apub_id}/outbox"))?, 40 | followers_url: Url::parse(&format!("{apub_id}/followers"))?, 41 | attributed_to: user.activity_pub_id.clone(), 42 | clone_uri: Url::parse(&clone_uri)?, 43 | public_key: key_pair.public_key, 44 | private_key: Some(key_pair.private_key), 45 | ticket_tracked_by: activity_pub_id.clone(), 46 | send_patches_to: activity_pub_id, 47 | domain: SETTINGS.domain.to_string(), 48 | is_local: true, 49 | }) 50 | } 51 | 52 | fn generate_clone_uri(&self, user: &User) -> String { 53 | let ssh_port = SETTINGS.ssh_port; 54 | let domain = SETTINGS.domain_url().expect("valid domain"); 55 | let host = domain.host_str().expect("valid host"); 56 | let repository_name = &self.name; 57 | let username = &user.username; 58 | 59 | if ssh_port == 22 { 60 | format!("git@{host}:{username}/{repository_name}.git") 61 | } else { 62 | format!("ssh://git@{host}:{ssh_port}/~/{username}/{repository_name}.git") 63 | } 64 | } 65 | 66 | fn generate_activity_pub_id(&self, user: &User) -> String { 67 | let protocol = &SETTINGS.protocol(); 68 | let user_name = user.username.clone(); 69 | let domain = &SETTINGS.domain; 70 | format!( 71 | "{protocol}://{domain}/users/{user_name}/repositories/{}", 72 | self.name 73 | ) 74 | } 75 | } 76 | 77 | pub async fn init( 78 | Extension(db): Extension, 79 | Extension(user): Extension, 80 | Json(repository): Json, 81 | ) -> AppResult { 82 | let create_repository_command = repository.map_to_domain(&user)?; 83 | let repository = create_repository_command.save(&db).await?; 84 | gill_git::init::init_bare(&user.username, &repository.name)?; 85 | Ok(StatusCode::NO_CONTENT.into_response()) 86 | } 87 | -------------------------------------------------------------------------------- /crates/gill-app/src/api/user.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::id::ActivityPubId; 2 | use crate::domain::user::create::CreateUser; 3 | use crate::domain::user::ssh_key::CreateSSHKey; 4 | use crate::domain::user::ssh_key::RawSshkey; 5 | use crate::domain::user::User; 6 | use crate::error::AppResult; 7 | use activitypub_federation::core::signatures::generate_actor_keypair; 8 | use axum::http::StatusCode; 9 | use axum::response::{IntoResponse, Response}; 10 | use axum::{Extension, Json}; 11 | use axum_macros::debug_handler; 12 | use gill_settings::SETTINGS; 13 | use serde::{Deserialize, Serialize}; 14 | use sqlx::PgPool; 15 | use url::Url; 16 | 17 | #[derive(Deserialize)] 18 | pub struct CreateUserCommand { 19 | pub username: String, 20 | pub email: String, 21 | } 22 | 23 | pub async fn create( 24 | Extension(db): Extension, 25 | Json(user): Json, 26 | ) -> AppResult { 27 | let keys = generate_actor_keypair()?; 28 | let protocol = SETTINGS.protocol(); 29 | let domain = &SETTINGS.domain; 30 | let username = user.username; 31 | let apub_id = format!("{protocol}://{domain}/users/{username}"); 32 | let user = CreateUser { 33 | username: username.clone(), 34 | email: Some(user.email), 35 | private_key: Some(keys.private_key), 36 | public_key: keys.public_key, 37 | followers_url: Url::parse(&format!("{apub_id}/followers"))?, 38 | outbox_url: Url::parse(&format!("{apub_id}/outbox"))?, 39 | inbox_url: Url::parse(&format!("{apub_id}/inbox"))?, 40 | activity_pub_id: ActivityPubId::try_from(apub_id)?, 41 | domain: SETTINGS.domain.clone(), 42 | is_local: true, 43 | }; 44 | 45 | user.save(&db).await?; 46 | Ok((StatusCode::NO_CONTENT, ()).into_response()) 47 | } 48 | 49 | #[derive(Serialize, Deserialize)] 50 | pub struct CreateSSHKeyDto { 51 | pub name: String, 52 | pub key: String, 53 | } 54 | 55 | impl From for CreateSSHKey { 56 | fn from(val: CreateSSHKeyDto) -> Self { 57 | CreateSSHKey { 58 | name: val.name, 59 | key: val.key, 60 | } 61 | } 62 | } 63 | 64 | #[debug_handler] 65 | pub async fn register_ssh_key( 66 | Extension(user): Extension, 67 | Extension(pool): Extension, 68 | Json(ssh_key): Json, 69 | ) -> AppResult { 70 | let key_name = ssh_key.name; 71 | let raw_key = RawSshkey::from(ssh_key.key); 72 | let (key_type, key) = raw_key.key_parts(); 73 | user.add_ssh_key(&key_name, key, key_type, &pool).await?; 74 | gill_git::ssh::append_key(raw_key.full_key(), user.id).expect("Failed to append ssh key"); 75 | Ok((StatusCode::NO_CONTENT, ()).into_response()) 76 | } 77 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/repository/fork.rs: -------------------------------------------------------------------------------- 1 | use crate::apub::common::{GillActivity, GillApubObject}; 2 | use crate::domain::repository::Repository; 3 | use crate::domain::user::User; 4 | use crate::error::AppError; 5 | use crate::instance::InstanceHandle; 6 | use activitypub_federation::core::object_id::ObjectId; 7 | use activitypub_federation::data::Data; 8 | use activitypub_federation::traits::ActivityHandler; 9 | use activitystreams_kinds::activity::CreateType; 10 | use axum::async_trait; 11 | use serde::{Deserialize, Serialize}; 12 | use url::Url; 13 | 14 | #[derive(Deserialize, Serialize, Clone, Debug)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Fork { 17 | id: Url, 18 | pub repository: ObjectId, 19 | pub fork: ObjectId, 20 | pub forked_by: ObjectId, 21 | r#type: CreateType, 22 | } 23 | 24 | impl GillActivity for Fork { 25 | fn forward_addresses(&self) -> Vec<&Url> { 26 | vec![] 27 | } 28 | } 29 | 30 | impl Fork { 31 | pub fn new( 32 | forked_by: ObjectId, 33 | repository: ObjectId, 34 | fork: ObjectId, 35 | id: Url, 36 | ) -> Fork { 37 | Fork { 38 | id, 39 | repository, 40 | fork, 41 | forked_by, 42 | r#type: Default::default(), 43 | } 44 | } 45 | } 46 | 47 | #[async_trait] 48 | impl ActivityHandler for Fork { 49 | type DataType = InstanceHandle; 50 | type Error = AppError; 51 | 52 | fn id(&self) -> &Url { 53 | &self.id 54 | } 55 | 56 | fn actor(&self) -> &Url { 57 | self.forked_by.inner() 58 | } 59 | 60 | async fn receive( 61 | self, 62 | data: &Data, 63 | _request_counter: &mut i32, 64 | ) -> Result<(), Self::Error> { 65 | let user = ObjectId::::new(self.forked_by) 66 | .dereference_local(data) 67 | .await?; 68 | 69 | let repository = ObjectId::::new(self.repository) 70 | .dereference(data, data.local_instance(), &mut 0) 71 | .await?; 72 | 73 | let fork = ObjectId::::new(self.fork) 74 | .dereference(data, data.local_instance(), &mut 0) 75 | .await?; 76 | 77 | repository 78 | .add_fork(fork.local_id(), user.local_id(), data.database()) 79 | .await?; 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/repository/star.rs: -------------------------------------------------------------------------------- 1 | use crate::apub::common::GillActivity; 2 | use crate::domain::repository::Repository; 3 | use crate::domain::user::User; 4 | use crate::error::AppError; 5 | use crate::instance::InstanceHandle; 6 | use activitypub_federation::core::object_id::ObjectId; 7 | use activitypub_federation::data::Data; 8 | use activitypub_federation::traits::ActivityHandler; 9 | use activitystreams_kinds::activity::LikeType; 10 | use axum::async_trait; 11 | use serde::{Deserialize, Serialize}; 12 | use url::Url; 13 | 14 | #[derive(Deserialize, Serialize, Clone, Debug)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Star { 17 | id: Url, 18 | pub user: ObjectId, 19 | pub repository: ObjectId, 20 | r#type: LikeType, 21 | } 22 | 23 | impl GillActivity for Star { 24 | fn forward_addresses(&self) -> Vec<&Url> { 25 | vec![] 26 | } 27 | } 28 | 29 | impl Star { 30 | pub fn new(user: ObjectId, repository: ObjectId, id: Url) -> Star { 31 | Star { 32 | id, 33 | user, 34 | repository, 35 | r#type: Default::default(), 36 | } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl ActivityHandler for Star { 42 | type DataType = InstanceHandle; 43 | type Error = AppError; 44 | 45 | fn id(&self) -> &Url { 46 | &self.id 47 | } 48 | 49 | fn actor(&self) -> &Url { 50 | self.user.inner() 51 | } 52 | 53 | async fn receive( 54 | self, 55 | data: &Data, 56 | _request_counter: &mut i32, 57 | ) -> Result<(), Self::Error> { 58 | let user = ObjectId::::new(self.user) 59 | .dereference_local(data) 60 | .await?; 61 | 62 | let repository = ObjectId::::new(self.repository) 63 | .dereference(data, data.local_instance(), &mut 0) 64 | .await?; 65 | 66 | repository.add_star(&user, data).await?; 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/repository/watch.rs: -------------------------------------------------------------------------------- 1 | use crate::apub::common::GillActivity; 2 | use crate::domain::repository::Repository; 3 | use crate::domain::user::User; 4 | use crate::error::AppError; 5 | use crate::instance::InstanceHandle; 6 | use activitypub_federation::core::object_id::ObjectId; 7 | use activitypub_federation::data::Data; 8 | use activitypub_federation::traits::ActivityHandler; 9 | use activitystreams_kinds::activity::FollowType; 10 | use axum::async_trait; 11 | use serde::{Deserialize, Serialize}; 12 | use url::Url; 13 | 14 | #[derive(Deserialize, Serialize, Clone, Debug)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Watch { 17 | id: Url, 18 | pub user: ObjectId, 19 | pub repository: ObjectId, 20 | r#type: FollowType, 21 | } 22 | 23 | impl GillActivity for Watch { 24 | fn forward_addresses(&self) -> Vec<&Url> { 25 | vec![] 26 | } 27 | } 28 | 29 | impl Watch { 30 | pub fn new(user: ObjectId, repository: ObjectId, id: Url) -> Watch { 31 | Watch { 32 | id, 33 | user, 34 | repository, 35 | r#type: Default::default(), 36 | } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl ActivityHandler for Watch { 42 | type DataType = InstanceHandle; 43 | type Error = AppError; 44 | 45 | fn id(&self) -> &Url { 46 | &self.id 47 | } 48 | 49 | fn actor(&self) -> &Url { 50 | self.user.inner() 51 | } 52 | 53 | async fn receive( 54 | self, 55 | data: &Data, 56 | _request_counter: &mut i32, 57 | ) -> Result<(), Self::Error> { 58 | let user = ObjectId::::new(self.user) 59 | .dereference_local(data) 60 | .await?; 61 | 62 | let repository = ObjectId::::new(self.repository) 63 | .dereference(data, data.local_instance(), &mut 0) 64 | .await?; 65 | 66 | repository.add_watcher(&user, data).await?; 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/ticket/accept.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::instance::InstanceHandle; 3 | 4 | use activitypub_federation::deser::helpers::deserialize_one_or_many; 5 | 6 | use crate::domain::issue::Issue; 7 | 8 | use crate::apub::common::{is_local, GillActivity}; 9 | use crate::domain::repository::Repository; 10 | use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler}; 11 | use activitystreams_kinds::activity::AcceptType; 12 | use axum::async_trait; 13 | use serde::{Deserialize, Serialize}; 14 | use url::Url; 15 | 16 | #[derive(Clone, Debug, Deserialize, Serialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct AcceptTicket { 19 | /// Activity id 20 | pub(crate) id: Url, 21 | #[serde(rename = "type")] 22 | pub(crate) kind: AcceptType, 23 | /// The repository managing this ticket 24 | pub(crate) actor: ObjectId, 25 | /// Collection of this repository follower's inboxes and the 26 | /// offer author inbox 27 | #[serde(deserialize_with = "deserialize_one_or_many")] 28 | pub(crate) to: Vec, 29 | // Todo: make this accept the whole offer object as well 30 | /// the offer activity or its id 31 | pub(crate) object: Url, 32 | /// The accepted ticket 33 | pub(crate) result: ObjectId, 34 | } 35 | 36 | impl GillActivity for AcceptTicket { 37 | fn forward_addresses(&self) -> Vec<&Url> { 38 | self.to.iter().filter(|url| is_local(url)).collect() 39 | } 40 | } 41 | 42 | #[async_trait] 43 | impl ActivityHandler for AcceptTicket { 44 | type DataType = InstanceHandle; 45 | type Error = AppError; 46 | 47 | fn id(&self) -> &Url { 48 | &self.id 49 | } 50 | 51 | fn actor(&self) -> &Url { 52 | self.actor.inner() 53 | } 54 | 55 | async fn receive( 56 | self, 57 | data: &Data, 58 | request_counter: &mut i32, 59 | ) -> Result<(), Self::Error> { 60 | ObjectId::::new(self.actor) 61 | .dereference(data, &data.local_instance, request_counter) 62 | .await?; 63 | 64 | let issue = self 65 | .result 66 | .dereference(data, &data.local_instance, request_counter) 67 | .await?; 68 | 69 | issue.save(data.database()).await?; 70 | 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/ticket/comment/create.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::instance::InstanceHandle; 3 | 4 | use crate::apub::ticket::comment::ApubIssueComment; 5 | 6 | use activitypub_federation::deser::helpers::deserialize_one_or_many; 7 | 8 | use crate::apub::common::{is_local, GillActivity, GillApubObject}; 9 | 10 | use crate::domain::issue::Issue; 11 | use crate::domain::user::User; 12 | 13 | use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler}; 14 | use activitystreams_kinds::activity::CreateType; 15 | use axum::async_trait; 16 | use serde::{Deserialize, Serialize}; 17 | use url::Url; 18 | 19 | #[derive(Clone, Debug, Deserialize, Serialize)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct CreateTicketComment { 22 | pub(crate) actor: ObjectId, 23 | #[serde(deserialize_with = "deserialize_one_or_many")] 24 | pub(crate) to: Vec, 25 | pub(crate) object: ApubIssueComment, 26 | #[serde(rename = "type")] 27 | pub(crate) kind: CreateType, 28 | pub(crate) id: Url, 29 | } 30 | 31 | impl GillActivity for CreateTicketComment { 32 | fn forward_addresses(&self) -> Vec<&Url> { 33 | self.to.iter().filter(|url| is_local(url)).collect() 34 | } 35 | } 36 | 37 | #[async_trait] 38 | impl ActivityHandler for CreateTicketComment { 39 | type DataType = InstanceHandle; 40 | type Error = AppError; 41 | 42 | fn id(&self) -> &Url { 43 | self.object.id.inner() 44 | } 45 | 46 | fn actor(&self) -> &Url { 47 | self.actor.inner() 48 | } 49 | 50 | async fn receive( 51 | self, 52 | instance: &Data, 53 | request_counter: &mut i32, 54 | ) -> Result<(), Self::Error> { 55 | if self.object.id.dereference_local(instance).await.is_ok() { 56 | return Ok(()); 57 | }; 58 | 59 | let user = ObjectId::::new(self.actor) 60 | .dereference_local(instance) 61 | .await?; 62 | 63 | let comment = self 64 | .object 65 | .id 66 | .dereference(instance, &instance.local_instance, request_counter) 67 | .await?; 68 | 69 | let comment = comment.save(instance.database()).await?; 70 | let issue: ObjectId = comment.context.into(); 71 | 72 | let issue = issue 73 | .dereference(instance, &instance.local_instance, request_counter) 74 | .await?; 75 | 76 | issue 77 | .add_subscriber(user.local_id(), instance.database()) 78 | .await?; 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/gill-app/src/apub/user/follow.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::instance::InstanceHandle; 3 | 4 | use crate::apub::common::{GillActivity, GillApubObject}; 5 | use crate::domain::user::User; 6 | use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler}; 7 | use activitystreams_kinds::activity::FollowType; 8 | use axum::async_trait; 9 | use serde::{Deserialize, Serialize}; 10 | use url::Url; 11 | 12 | #[derive(Deserialize, Serialize, Clone, Debug)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct Follow { 15 | id: Url, 16 | pub follower: ObjectId, 17 | pub followed: ObjectId, 18 | r#type: FollowType, 19 | } 20 | 21 | impl GillActivity for Follow { 22 | fn forward_addresses(&self) -> Vec<&Url> { 23 | vec![] 24 | } 25 | } 26 | 27 | impl Follow { 28 | pub fn new(follower: ObjectId, followed: ObjectId, id: Url) -> Follow { 29 | Follow { 30 | id, 31 | follower, 32 | followed, 33 | r#type: Default::default(), 34 | } 35 | } 36 | } 37 | 38 | #[async_trait] 39 | impl ActivityHandler for Follow { 40 | type DataType = InstanceHandle; 41 | type Error = AppError; 42 | 43 | fn id(&self) -> &Url { 44 | &self.id 45 | } 46 | 47 | fn actor(&self) -> &Url { 48 | self.follower.inner() 49 | } 50 | 51 | async fn receive( 52 | self, 53 | data: &Data, 54 | _request_counter: &mut i32, 55 | ) -> Result<(), Self::Error> { 56 | let followed = ObjectId::::new(self.followed) 57 | .dereference_local(data) 58 | .await?; 59 | 60 | let follower = ObjectId::::new(self.follower) 61 | .dereference(data, data.local_instance(), &mut 0) 62 | .await?; 63 | 64 | followed 65 | .add_follower(follower.local_id(), data.database()) 66 | .await?; 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/apub.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::PgPool; 3 | 4 | pub async fn inbox_for_url(url: &str, db: &PgPool) -> Result> { 5 | gill_db::inbox_for_url(url, db).await.map_err(Into::into) 6 | } 7 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/commit/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::pull_request::PullRequest; 2 | use crate::domain::repository::Repository; 3 | use crate::domain::user::User; 4 | use crate::error::AppResult; 5 | use gill_git::commits::OwnedCommit; 6 | use gill_git::diffs::Diff; 7 | use gill_git::GitRepository; 8 | use sqlx::PgPool; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct Commit { 12 | pub id: String, 13 | pub summary: String, 14 | pub body: Option, 15 | pub author: Author, 16 | pub created_at: u32, 17 | pub authored_at: u32, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub enum Author { 22 | Known(String), 23 | Raw(String), 24 | } 25 | 26 | impl Commit { 27 | async fn from_git_commit(commit: OwnedCommit, db: &PgPool) -> AppResult { 28 | let author = User::by_email(&commit.email, db).await; 29 | let author = match author { 30 | Ok(author) => Author::Known(author.username), 31 | Err(_) => Author::Raw(commit.author), 32 | }; 33 | 34 | Ok(Self { 35 | id: commit.id, 36 | summary: commit.summary, 37 | body: None, 38 | author, 39 | created_at: commit.created_at, 40 | authored_at: commit.authored_at, 41 | }) 42 | } 43 | } 44 | 45 | impl Repository { 46 | pub async fn history( 47 | owner: &str, 48 | name: &str, 49 | branch: &str, 50 | db: &PgPool, 51 | ) -> AppResult> { 52 | let repo = GitRepository::open(owner, name)?; 53 | let git_commits = repo.history(branch)?; 54 | let mut commits = vec![]; 55 | 56 | // TODO: Sql query to resolve all username onces 57 | for commit in git_commits { 58 | let commit = Commit::from_git_commit(commit, db).await?; 59 | commits.push(commit) 60 | } 61 | 62 | Ok(commits) 63 | } 64 | 65 | pub async fn get_commits_for_pull_request( 66 | owner: &str, 67 | name: &str, 68 | pull_request: &PullRequest, 69 | db: &PgPool, 70 | ) -> AppResult> { 71 | let repo = GitRepository::open(owner, name)?; 72 | let git_commits = 73 | repo.list_commits_between_ref(&pull_request.base, &pull_request.compare)?; 74 | let mut commits = vec![]; 75 | 76 | // TODO: Sql query to resolve all username onces 77 | for commit in git_commits { 78 | let commit = Commit::from_git_commit(commit, db).await?; 79 | commits.push(commit) 80 | } 81 | 82 | Ok(commits) 83 | } 84 | 85 | pub async fn commit_with_diff( 86 | owner: &str, 87 | name: &str, 88 | sha: &str, 89 | db: &PgPool, 90 | ) -> AppResult<(Commit, Vec)> { 91 | let repo = GitRepository::open(owner, name)?; 92 | let git_commits = repo.find_commit(sha)?; 93 | let commit = Commit::from_git_commit(git_commits, db).await?; 94 | let diff = repo.commit_diff(sha)?; 95 | Ok((commit, diff)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/id.rs: -------------------------------------------------------------------------------- 1 | use activitypub_federation::core::object_id::ObjectId; 2 | use activitypub_federation::traits::ApubObject; 3 | use serde::{Deserialize, Serialize}; 4 | use std::marker::PhantomData; 5 | use url::Url; 6 | 7 | #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)] 8 | pub struct ActivityPubId { 9 | phantom_data: PhantomData, 10 | inner: Url, 11 | } 12 | 13 | impl From> for ActivityPubId 14 | where 15 | T: ApubObject + Send + 'static, 16 | for<'de2> ::ApubType: serde::Deserialize<'de2>, 17 | { 18 | fn from(id: ObjectId) -> Self { 19 | ActivityPubId { 20 | phantom_data: PhantomData, 21 | inner: id.into_inner(), 22 | } 23 | } 24 | } 25 | 26 | impl From for ActivityPubId 27 | where 28 | T: ApubObject + Send + 'static, 29 | for<'de2> ::ApubType: serde::Deserialize<'de2>, 30 | { 31 | fn from(url: Url) -> Self { 32 | ActivityPubId { 33 | phantom_data: PhantomData, 34 | inner: url, 35 | } 36 | } 37 | } 38 | 39 | impl From> for ObjectId 40 | where 41 | T: ApubObject + Send + 'static, 42 | for<'de2> ::ApubType: serde::Deserialize<'de2>, 43 | { 44 | fn from(val: ActivityPubId) -> Self { 45 | ObjectId::new(val.inner) 46 | } 47 | } 48 | 49 | impl From> for Url 50 | where 51 | T: ApubObject + Send + 'static, 52 | for<'de2> ::ApubType: serde::Deserialize<'de2>, 53 | { 54 | fn from(val: ActivityPubId) -> Self { 55 | val.inner 56 | } 57 | } 58 | 59 | impl TryFrom for ActivityPubId 60 | where 61 | T: ApubObject + Send + 'static, 62 | for<'de2> ::ApubType: Deserialize<'de2>, 63 | { 64 | type Error = url::ParseError; 65 | 66 | fn try_from(value: String) -> Result { 67 | Ok(ActivityPubId { 68 | phantom_data: PhantomData, 69 | inner: Url::parse(&value)?, 70 | }) 71 | } 72 | } 73 | 74 | impl ToString for ActivityPubId 75 | where 76 | T: ApubObject + Send + 'static, 77 | { 78 | fn to_string(&self) -> String { 79 | self.inner.to_string() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/issue/comment/digest.rs: -------------------------------------------------------------------------------- 1 | use gill_db::repository::issue::comment::IssueCommentDigest as IssueCommentDigestEntity; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct IssueCommentDigest { 6 | pub id: Uuid, 7 | pub repository_id: i32, 8 | pub created_by: String, 9 | pub content: String, 10 | } 11 | 12 | impl From for IssueCommentDigest { 13 | fn from(comment: IssueCommentDigestEntity) -> Self { 14 | Self { 15 | id: comment.id, 16 | repository_id: comment.repository_id, 17 | created_by: comment.created_by, 18 | content: comment.content, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/issue/digest.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::issue::comment::digest::IssueCommentDigest; 2 | use crate::domain::issue::IssueState; 3 | use crate::error::AppResult; 4 | use gill_db::repository::issue::IssueDigest as IssueDigestEntity; 5 | use sqlx::PgPool; 6 | use std::cmp::Ordering; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub struct IssueDigest { 10 | pub repository_id: i32, 11 | pub number: i32, 12 | pub opened_by: String, 13 | pub title: String, 14 | pub content: String, 15 | pub state: IssueState, 16 | } 17 | 18 | impl From for IssueDigest { 19 | fn from(issue: IssueDigestEntity) -> Self { 20 | Self { 21 | repository_id: issue.repository_id, 22 | number: issue.number, 23 | opened_by: issue.opened_by, 24 | title: issue.title, 25 | content: issue.content, 26 | state: issue.state.into(), 27 | } 28 | } 29 | } 30 | 31 | impl From for IssueDigestEntity { 32 | fn from(val: IssueDigest) -> Self { 33 | IssueDigestEntity { 34 | repository_id: val.repository_id, 35 | number: val.number, 36 | opened_by: val.opened_by, 37 | title: val.title, 38 | content: val.content, 39 | state: val.state.into(), 40 | } 41 | } 42 | } 43 | 44 | impl PartialOrd for IssueDigest { 45 | fn partial_cmp(&self, other: &IssueDigest) -> Option { 46 | match (&self.state, &other.state) { 47 | (IssueState::Open, IssueState::Closed) => Some(Ordering::Less), 48 | (_, _) => Some(self.number.cmp(&other.number)), 49 | } 50 | } 51 | } 52 | 53 | impl Ord for IssueDigest { 54 | fn cmp(&self, other: &Self) -> Ordering { 55 | self.partial_cmp(other).unwrap() 56 | } 57 | } 58 | 59 | impl IssueDigest { 60 | pub async fn get_comments(&self, db: &PgPool) -> AppResult> { 61 | let issue: IssueDigestEntity = self.clone().into(); 62 | let comments = issue.get_comments(db).await?; 63 | Ok(comments.into_iter().map(IssueCommentDigest::from).collect()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apub; 2 | pub mod commit; 3 | pub mod id; 4 | pub mod issue; 5 | pub mod pull_request; 6 | pub mod repository; 7 | pub mod user; 8 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/pull_request/comment.rs: -------------------------------------------------------------------------------- 1 | use gill_db::repository::pull_request::comment::PullRequestComment as CommentEntity; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct PullRequestComment { 5 | pub id: i32, 6 | pub repository_id: i32, 7 | pub created_by: String, 8 | pub content: String, 9 | } 10 | 11 | impl From for PullRequestComment { 12 | fn from(comment: CommentEntity) -> Self { 13 | Self { 14 | id: comment.id, 15 | repository_id: comment.repository_id, 16 | created_by: comment.created_by, 17 | content: comment.content, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/repository/branch.rs: -------------------------------------------------------------------------------- 1 | use gill_db::repository::branch::Branch as BranchEntity; 2 | 3 | #[derive(Clone, PartialEq, Eq, Debug)] 4 | pub struct Branch { 5 | pub name: String, 6 | pub repository_id: i32, 7 | pub is_default: bool, 8 | } 9 | 10 | impl From for Branch { 11 | fn from(branch: BranchEntity) -> Self { 12 | Self { 13 | name: branch.name, 14 | repository_id: branch.repository_id, 15 | is_default: branch.is_default, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/repository/create.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::id::ActivityPubId; 2 | use crate::domain::repository::Repository; 3 | use crate::domain::user::User; 4 | use crate::error::AppResult; 5 | use gill_db::repository::create::CreateRepository as CreateRepositoryEntity; 6 | use gill_db::Insert; 7 | use sqlx::PgPool; 8 | use url::Url; 9 | 10 | pub struct CreateRepository { 11 | pub activity_pub_id: ActivityPubId, 12 | pub name: String, 13 | pub summary: Option, 14 | pub private: bool, 15 | pub inbox_url: Url, 16 | pub outbox_url: Url, 17 | pub followers_url: Url, 18 | pub attributed_to: ActivityPubId, 19 | pub clone_uri: Url, 20 | pub public_key: String, 21 | pub private_key: Option, 22 | pub ticket_tracked_by: ActivityPubId, 23 | pub send_patches_to: ActivityPubId, 24 | pub domain: String, 25 | pub is_local: bool, 26 | } 27 | 28 | impl From for CreateRepositoryEntity { 29 | fn from(repository: CreateRepository) -> Self { 30 | CreateRepositoryEntity { 31 | activity_pub_id: repository.activity_pub_id.to_string(), 32 | name: repository.name, 33 | summary: repository.summary, 34 | private: repository.private, 35 | inbox_url: repository.inbox_url.to_string(), 36 | outbox_url: repository.outbox_url.to_string(), 37 | followers_url: repository.followers_url.to_string(), 38 | attributed_to: repository.attributed_to.to_string(), 39 | clone_uri: repository.clone_uri.to_string(), 40 | public_key: repository.public_key, 41 | private_key: repository.private_key, 42 | ticket_tracked_by: repository.ticket_tracked_by.to_string(), 43 | send_patches_to: repository.send_patches_to.to_string(), 44 | domain: repository.domain, 45 | is_local: repository.is_local, 46 | } 47 | } 48 | } 49 | 50 | impl CreateRepository { 51 | pub async fn save(self, db: &PgPool) -> AppResult { 52 | let entity: CreateRepositoryEntity = self.into(); 53 | let repository = entity.insert(db).await?; 54 | Repository::try_from(repository).map_err(Into::into) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/repository/digest.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppResult; 2 | 3 | use gill_db::repository::digest::RepositoryDigest as RepositoryDigestEntity; 4 | use sqlx::PgPool; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct RepositoryDigest { 8 | pub id: i32, 9 | pub name: String, 10 | pub owner: String, 11 | pub domain: String, 12 | pub summary: Option, 13 | pub star_count: Option, 14 | pub fork_count: Option, 15 | pub watch_count: Option, 16 | pub clone_url: String, 17 | } 18 | 19 | impl From for RepositoryDigest { 20 | fn from(digest: RepositoryDigestEntity) -> Self { 21 | Self { 22 | id: digest.id, 23 | name: digest.name, 24 | owner: digest.owner, 25 | domain: digest.domain, 26 | summary: digest.summary, 27 | star_count: digest.star_count, 28 | fork_count: digest.fork_count, 29 | watch_count: digest.watch_count, 30 | clone_url: digest.clone_url, 31 | } 32 | } 33 | } 34 | 35 | impl RepositoryDigest { 36 | pub async fn all_local(limit: i64, offset: i64, db: &PgPool) -> AppResult> { 37 | let repositories = RepositoryDigestEntity::all_local(limit, offset, db).await?; 38 | Ok(repositories 39 | .into_iter() 40 | .map(RepositoryDigest::from) 41 | .collect()) 42 | } 43 | 44 | pub async fn all_federated(limit: i64, offset: i64, db: &PgPool) -> AppResult> { 45 | let repositories = RepositoryDigestEntity::all_federated(limit, offset, db).await?; 46 | Ok(repositories 47 | .into_iter() 48 | .map(RepositoryDigest::from) 49 | .collect()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/repository/stats.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::digest::RepositoryDigest; 2 | use gill_db::repository::digest::RepositoryLight; 3 | use sqlx::PgPool; 4 | 5 | #[derive(Debug)] 6 | pub struct RepositoryStats { 7 | pub fork_count: u32, 8 | pub star_count: u32, 9 | pub watch_count: u32, 10 | pub clone_url: String, 11 | } 12 | 13 | impl From for RepositoryStats { 14 | fn from(stats: RepositoryLight) -> Self { 15 | Self { 16 | watch_count: stats.watch_count.unwrap_or(0) as u32, 17 | fork_count: stats.fork_count.unwrap_or(0) as u32, 18 | star_count: stats.star_count.unwrap_or(0) as u32, 19 | clone_url: stats.clone_url, 20 | } 21 | } 22 | } 23 | 24 | impl From<&RepositoryDigest> for RepositoryStats { 25 | fn from(repo: &RepositoryDigest) -> Self { 26 | Self { 27 | watch_count: repo.watch_count.unwrap_or(0) as u32, 28 | fork_count: repo.fork_count.unwrap_or(0) as u32, 29 | star_count: repo.star_count.unwrap_or(0) as u32, 30 | clone_url: repo.clone_url.clone(), 31 | } 32 | } 33 | } 34 | 35 | impl RepositoryStats { 36 | pub async fn get( 37 | owner: &str, 38 | repository: &str, 39 | db: &PgPool, 40 | ) -> anyhow::Result { 41 | let repo = RepositoryLight::stats_by_namespace(owner, repository, db).await?; 42 | Ok(RepositoryStats::from(repo)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/user/create.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::id::ActivityPubId; 2 | use crate::domain::user::User; 3 | use crate::error::AppResult; 4 | use gill_db::user::CreateUser as CreateUserEntity; 5 | use gill_db::Insert; 6 | use sqlx::PgPool; 7 | use url::Url; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct CreateUser { 11 | pub username: String, 12 | pub email: Option, 13 | pub private_key: Option, 14 | pub public_key: String, 15 | pub activity_pub_id: ActivityPubId, 16 | pub outbox_url: Url, 17 | pub inbox_url: Url, 18 | pub domain: String, 19 | pub followers_url: Url, 20 | pub is_local: bool, 21 | } 22 | 23 | impl From for CreateUserEntity { 24 | fn from(val: CreateUser) -> Self { 25 | CreateUserEntity { 26 | username: val.username, 27 | email: val.email, 28 | private_key: val.private_key, 29 | public_key: val.public_key, 30 | activity_pub_id: val.activity_pub_id.to_string(), 31 | outbox_url: val.outbox_url.to_string(), 32 | inbox_url: val.inbox_url.to_string(), 33 | domain: val.domain, 34 | followers_url: val.followers_url.to_string(), 35 | is_local: val.is_local, 36 | } 37 | } 38 | } 39 | 40 | impl CreateUser { 41 | pub async fn save(self, db: &PgPool) -> AppResult { 42 | let entity: CreateUserEntity = self.into(); 43 | let user = entity.insert(db).await?; 44 | User::try_from(user).map_err(Into::into) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/gill-app/src/domain/user/ssh_key.rs: -------------------------------------------------------------------------------- 1 | use gill_db::user::CreateSSHKey as CreateSSHKeyEntity; 2 | 3 | pub struct CreateSSHKey { 4 | pub name: String, 5 | pub key: String, 6 | } 7 | 8 | impl From for CreateSSHKeyEntity { 9 | fn from(val: CreateSSHKey) -> Self { 10 | CreateSSHKeyEntity { 11 | name: val.name, 12 | key: val.key, 13 | } 14 | } 15 | } 16 | 17 | pub struct RawSshkey { 18 | inner: String, 19 | } 20 | 21 | impl From for RawSshkey { 22 | fn from(inner: String) -> Self { 23 | RawSshkey { inner } 24 | } 25 | } 26 | 27 | impl RawSshkey { 28 | pub fn key_parts(&self) -> (&str, &str) { 29 | let key = self.inner.trim(); 30 | let mut parts = key.split(' '); 31 | let key_type = parts.next().expect("ssh key type"); 32 | let key = parts.next().expect("ssh key"); 33 | (key_type, key) 34 | } 35 | 36 | pub fn full_key(&self) -> &str { 37 | &self.inner 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/gill-app/src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::response::{IntoResponse, Response}; 2 | use http::StatusCode; 3 | use tracing::error; 4 | 5 | pub type AppResult = Result; 6 | 7 | #[derive(Debug)] 8 | pub enum AppError { 9 | Internal(anyhow::Error), 10 | Unauthorized, 11 | NotFound, 12 | } 13 | 14 | impl From for AppError 15 | where 16 | T: Into, 17 | { 18 | fn from(t: T) -> Self { 19 | let err = t.into(); 20 | error!("{err}"); 21 | AppError::Internal(err) 22 | } 23 | } 24 | 25 | impl IntoResponse for AppError { 26 | fn into_response(self) -> Response { 27 | match self { 28 | AppError::Internal(error) => { 29 | (StatusCode::INTERNAL_SERVER_ERROR, format!("{error}")).into_response() 30 | } 31 | AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED").into_response(), 32 | AppError::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND").into_response(), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/gill-app/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::user::User; 2 | use crate::oauth::Oauth2User; 3 | use sqlx::PgPool; 4 | 5 | pub mod api; 6 | pub mod apub; 7 | pub mod domain; 8 | pub mod error; 9 | pub mod instance; 10 | pub mod oauth; 11 | pub mod state; 12 | pub mod view; 13 | pub mod webfinger; 14 | 15 | async fn get_connected_user_username(db: &PgPool, user: Option) -> Option { 16 | get_connected_user(db, user).await.map(|user| user.username) 17 | } 18 | 19 | async fn get_connected_user(db: &PgPool, user: Option) -> Option { 20 | let email = user.map(|user| user.email); 21 | match email { 22 | Some(email) => User::by_email(&email, db).await.ok(), 23 | None => None, 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod test { 29 | use archunit_rs::rule::{ArchRuleBuilder, CheckRule}; 30 | use archunit_rs::{ExludeModules, Modules}; 31 | 32 | #[test] 33 | fn only_domain_should_access_database() { 34 | Modules::that(ExludeModules::cfg_test()) 35 | .reside_in_a_module("gill_app::api") 36 | .or() 37 | .reside_in_a_module("gill_app::apub") 38 | .or() 39 | .reside_in_a_module("gill_app::oauth") 40 | .or() 41 | .reside_in_a_module("gill_app::view") 42 | .or() 43 | .reside_in_a_module("gill_app::webfinger") 44 | .should() 45 | .only_have_dependency_module() 46 | .that() 47 | .does_not_have_simple_name("gill_db*") 48 | .check(); 49 | } 50 | 51 | #[test] 52 | fn domain_should_not_use_axum() { 53 | Modules::that(ExludeModules::cfg_test()) 54 | .reside_in_a_module("gill_app::domain") 55 | .should() 56 | .only_have_dependency_module() 57 | .that() 58 | .does_not_have_simple_name("axum") 59 | .check(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/gill-app/src/main.rs: -------------------------------------------------------------------------------- 1 | use gill_app::instance::Instance; 2 | use gill_settings::SETTINGS; 3 | use sqlx::postgres::PgPoolOptions; 4 | use std::time::Duration; 5 | use tracing_subscriber::layer::SubscriberExt; 6 | use tracing_subscriber::util::SubscriberInitExt; 7 | 8 | #[actix_rt::main] 9 | async fn main() -> anyhow::Result<()> { 10 | tracing_subscriber::registry() 11 | .with(tracing_subscriber::EnvFilter::new( 12 | std::env::var("RUST_LOG").unwrap_or_else(|_| { 13 | "activitypub_federation=debug,gill_ipc=debug,gill_git=debug,gill_app=debug,tower_http=debug".into() 14 | }), 15 | )) 16 | .with(tracing_subscriber::fmt::layer()) 17 | .init(); 18 | 19 | let connection_url = &SETTINGS.database_url(); 20 | 21 | tracing::debug!("Connecting to {connection_url}"); 22 | let db = PgPoolOptions::new() 23 | .max_connections(10) 24 | .idle_timeout(Duration::from_secs(3)) 25 | .connect(connection_url) 26 | .await 27 | .expect("can connect to database"); 28 | 29 | sqlx::migrate!("../gill-db/migrations").run(&db).await?; 30 | 31 | tracing::debug!("Loading config: {:?}", *SETTINGS); 32 | let instance = Instance::new(SETTINGS.domain.to_string(), db).unwrap(); 33 | Instance::listen(&instance).await?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /crates/gill-app/src/oauth/service.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::user::User; 2 | use crate::oauth::Oauth2User; 3 | use axum::{ 4 | http, 5 | http::{Request, StatusCode}, 6 | middleware::Next, 7 | response::Response, 8 | }; 9 | use gill_settings::SETTINGS; 10 | use once_cell::sync::Lazy; 11 | use serde_json::Value; 12 | use sqlx::PgPool; 13 | 14 | static CLIENT: Lazy = Lazy::new(reqwest::Client::new); 15 | 16 | pub async fn auth(mut req: Request, next: Next) -> Result { 17 | tracing::debug!("Authenticating user for rest API, (Mandatory)"); 18 | let auth_header = req 19 | .headers() 20 | .get(http::header::AUTHORIZATION) 21 | .and_then(|header| header.to_str().ok()); 22 | 23 | let auth_header = match auth_header { 24 | Some(auth_header) => auth_header, 25 | None => { 26 | return Err(StatusCode::UNAUTHORIZED); 27 | } 28 | }; 29 | 30 | tracing::debug!("Got bearer {auth_header}"); 31 | 32 | match user_info(auth_header).await { 33 | Ok(current_user) => { 34 | let Some(pool) = req.extensions().get::() else { 35 | return Err(StatusCode::INTERNAL_SERVER_ERROR); 36 | }; 37 | 38 | match User::by_email(¤t_user.email, pool).await { 39 | Err(err) => { 40 | tracing::error!( 41 | "Error fetching current user '{}': {err:?}", 42 | current_user.email 43 | ); 44 | Err(StatusCode::INTERNAL_SERVER_ERROR) 45 | } 46 | Ok(user) => { 47 | tracing::debug!("Insert user into request context"); 48 | req.extensions_mut().insert(user); 49 | Ok(next.run(req).await) 50 | } 51 | } 52 | } 53 | Err(err) => { 54 | tracing::error!("User info failed {err}"); 55 | Err(StatusCode::FORBIDDEN) 56 | } 57 | } 58 | } 59 | 60 | async fn user_info(bearer: &str) -> anyhow::Result { 61 | let value: Value = CLIENT 62 | .get(&SETTINGS.oauth_provider.user_info_url()) 63 | .header("Authorization", bearer) 64 | .send() 65 | .await? 66 | .json() 67 | .await?; 68 | 69 | tracing::debug!("UserInfo response: {value:?}"); 70 | 71 | serde_json::from_value(value).map_err(Into::into) 72 | } 73 | -------------------------------------------------------------------------------- /crates/gill-app/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::instance::InstanceHandle; 2 | use async_session::MemoryStore; 3 | use axum::extract::FromRef; 4 | use oauth2::basic::BasicClient; 5 | 6 | #[derive(Clone)] 7 | pub struct AppState { 8 | pub store: MemoryStore, 9 | pub oauth_client: BasicClient, 10 | pub instance: InstanceHandle, 11 | } 12 | 13 | impl FromRef for MemoryStore { 14 | fn from_ref(state: &AppState) -> Self { 15 | state.store.clone() 16 | } 17 | } 18 | 19 | impl FromRef for BasicClient { 20 | fn from_ref(state: &AppState) -> Self { 21 | state.oauth_client.clone() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/component.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | 3 | #[derive(Template, Debug)] 4 | #[template(path = "components/markdown-preview-form.html", ext = "html")] 5 | pub struct MarkdownPreviewForm { 6 | pub with_title: bool, 7 | pub action_href: String, 8 | pub submit_value: String, 9 | pub owner: String, 10 | pub repository: String, 11 | } 12 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/dto.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::digest::RepositoryDigest; 2 | use crate::domain::repository::stats::RepositoryStats; 3 | 4 | pub struct RepositoryDto { 5 | pub owner: String, 6 | pub name: String, 7 | pub description: Option, 8 | pub stats: RepositoryStats, 9 | } 10 | 11 | pub struct FederatedRepositoryDto { 12 | pub owner: String, 13 | pub name: String, 14 | pub domain: String, 15 | pub description: Option, 16 | pub stats: RepositoryStats, 17 | } 18 | 19 | impl From for FederatedRepositoryDto { 20 | fn from(repo: RepositoryDigest) -> Self { 21 | let stats = RepositoryStats::from(&repo); 22 | FederatedRepositoryDto { 23 | owner: repo.owner, 24 | name: repo.name, 25 | domain: repo.domain, 26 | description: repo.summary, 27 | stats, 28 | } 29 | } 30 | } 31 | 32 | impl From for RepositoryDto { 33 | fn from(repo: RepositoryDigest) -> Self { 34 | let stats = RepositoryStats::from(&repo); 35 | RepositoryDto { 36 | owner: repo.owner, 37 | name: repo.name, 38 | description: repo.summary, 39 | stats, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/filters.rs: -------------------------------------------------------------------------------- 1 | // This filter does not have extra arguments 2 | pub fn sha_digest(s: T) -> askama::Result { 3 | let s = s.to_string(); 4 | Ok(s[0..7].to_string()) 5 | } 6 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/index.rs: -------------------------------------------------------------------------------- 1 | use crate::oauth::Oauth2User; 2 | use crate::view::HtmlTemplate; 3 | use askama::Template; 4 | use axum::response::IntoResponse; 5 | use axum::Extension; 6 | 7 | use crate::domain::repository::digest::RepositoryDigest; 8 | use crate::error::AppResult; 9 | use crate::get_connected_user_username; 10 | use crate::view::dto::{FederatedRepositoryDto, RepositoryDto}; 11 | use sqlx::PgPool; 12 | 13 | pub struct ActivityDto {} 14 | 15 | #[derive(Template)] 16 | #[template(path = "index.html")] 17 | struct LandingPageTemplate { 18 | user: Option, 19 | local_repositories: Vec, 20 | federated_repositories: Vec, 21 | } 22 | 23 | pub async fn index( 24 | Extension(db): Extension, 25 | user: Option, 26 | ) -> AppResult { 27 | let username = get_connected_user_username(&db, user).await; 28 | let local_repositories = RepositoryDigest::all_local(10, 0, &db).await?; 29 | let local_repositories = local_repositories 30 | .into_iter() 31 | .map(RepositoryDto::from) 32 | .collect(); 33 | 34 | let federated_repositories = RepositoryDigest::all_federated(10, 0, &db).await?; 35 | let federated_repositories = federated_repositories 36 | .into_iter() 37 | .map(FederatedRepositoryDto::from) 38 | .collect(); 39 | 40 | let template = LandingPageTemplate { 41 | user: username, 42 | local_repositories, 43 | federated_repositories, 44 | }; 45 | 46 | Ok(HtmlTemplate(template)) 47 | } 48 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::oauth; 2 | 3 | use crate::state::AppState; 4 | use crate::view::follow::follow_form; 5 | use askama::{DynTemplate, Template}; 6 | use axum::http::StatusCode; 7 | use axum::response::Response; 8 | use axum::response::{Html, IntoResponse}; 9 | use axum::routing::get; 10 | use axum::Router; 11 | 12 | pub mod component; 13 | pub mod dto; 14 | mod filters; 15 | pub mod follow; 16 | pub mod index; 17 | pub mod repository; 18 | pub mod user; 19 | 20 | pub struct HtmlTemplate(T); 21 | 22 | pub struct DynHtmlTemplate(T); 23 | 24 | impl HtmlTemplate { 25 | pub fn inner(self) -> T { 26 | self.0 27 | } 28 | } 29 | 30 | pub fn router(app_state: AppState) -> Router { 31 | Router::new() 32 | .merge(repository::routes()) 33 | .merge(user::routes()) 34 | .route("/", get(index::index)) 35 | .route("/auth/gill/", get(oauth::openid_auth)) 36 | .route("/auth/gill", get(oauth::openid_auth)) 37 | .route("/auth/authorized/", get(oauth::login_authorized)) 38 | .route("/auth/authorized", get(oauth::login_authorized)) 39 | .route("/logout/", get(oauth::logout)) 40 | .route("/follow_user", get(follow_form)) 41 | .route("/follow_user/", get(follow_form)) 42 | .with_state(app_state) 43 | } 44 | 45 | impl IntoResponse for HtmlTemplate 46 | where 47 | T: Template, 48 | { 49 | fn into_response(self) -> Response { 50 | match self.0.render() { 51 | Ok(html) => Html(html).into_response(), 52 | Err(err) => ( 53 | StatusCode::INTERNAL_SERVER_ERROR, 54 | format!("Failed to render template. Error: {err}"), 55 | ) 56 | .into_response(), 57 | } 58 | } 59 | } 60 | 61 | impl IntoResponse for DynHtmlTemplate> { 62 | fn into_response(self) -> Response { 63 | match self.0.dyn_render() { 64 | Ok(html) => Html(html).into_response(), 65 | Err(err) => ( 66 | StatusCode::INTERNAL_SERVER_ERROR, 67 | format!("Failed to render template. Error: {err}"), 68 | ) 69 | .into_response(), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/activity.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::Repository; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use crate::state::AppState; 6 | use axum::extract::{Path, State}; 7 | use axum::response::{IntoResponse, Response}; 8 | use axum::Extension; 9 | use gill_authorize_derive::authorized; 10 | use http::StatusCode; 11 | use sqlx::PgPool; 12 | 13 | #[authorized] 14 | pub async fn star( 15 | State(state): State, 16 | user: Option, 17 | Extension(db): Extension, 18 | Path((owner, repository)): Path<(String, String)>, 19 | ) -> AppResult { 20 | Repository::by_namespace(&owner, &repository, &db) 21 | .await? 22 | .add_star(&user, &state.instance) 23 | .await?; 24 | 25 | Ok(StatusCode::NO_CONTENT.into_response()) 26 | } 27 | 28 | #[authorized] 29 | pub async fn watch( 30 | State(state): State, 31 | user: Option, 32 | Extension(db): Extension, 33 | Path((owner, repository)): Path<(String, String)>, 34 | ) -> AppResult { 35 | Repository::by_namespace(&owner, &repository, &db) 36 | .await? 37 | .add_watcher(&user, &state.instance) 38 | .await?; 39 | 40 | Ok(StatusCode::NO_CONTENT.into_response()) 41 | } 42 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/commits.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppResult; 2 | use crate::oauth::Oauth2User; 3 | use crate::view::repository::{get_repository_branches, BranchDto, Tab}; 4 | use crate::view::HtmlTemplate; 5 | 6 | use askama::Template; 7 | use axum::extract::Path; 8 | use axum::Extension; 9 | 10 | use crate::domain::commit::Author; 11 | use crate::domain::commit::Commit; 12 | use crate::domain::repository::stats::RepositoryStats; 13 | use crate::domain::repository::Repository; 14 | use crate::get_connected_user_username; 15 | use crate::view::filters; 16 | 17 | use gill_syntax::diff::diff2html; 18 | use sqlx::PgPool; 19 | 20 | #[derive(Template, Debug)] 21 | #[template(path = "repository/history.html")] 22 | pub struct CommitLogTemplate { 23 | repository: String, 24 | owner: String, 25 | stats: RepositoryStats, 26 | commits: Vec, 27 | branches: Vec, 28 | current_branch: Option, 29 | user: Option, 30 | tab: Tab, 31 | } 32 | 33 | pub async fn git_log( 34 | user: Option, 35 | Path((owner, repository, current_branch)): Path<(String, String, String)>, 36 | Extension(db): Extension, 37 | ) -> AppResult> { 38 | let connected_username = get_connected_user_username(&db, user).await; 39 | let commits = Repository::history(&owner, &repository, ¤t_branch, &db).await?; 40 | let branches = get_repository_branches(&owner, &repository, ¤t_branch, &db).await?; 41 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 42 | 43 | Ok(HtmlTemplate(CommitLogTemplate { 44 | repository, 45 | owner, 46 | stats, 47 | commits, 48 | branches, 49 | current_branch: Some(current_branch), 50 | user: connected_username, 51 | tab: Tab::History, 52 | })) 53 | } 54 | 55 | #[derive(Template, Debug)] 56 | #[template(path = "repository/commit-diff.html")] 57 | pub struct CommitDiffTemplate { 58 | // TODO 59 | _repository: String, 60 | // TODO 61 | _owner: String, 62 | // TODO 63 | _stats: RepositoryStats, 64 | commit: Commit, 65 | diff: String, 66 | // TODO 67 | _current_branch: Option, 68 | user: Option, 69 | // TODO 70 | _tab: Tab, 71 | } 72 | 73 | pub async fn commit_diff( 74 | user: Option, 75 | Path((owner, repository, sha)): Path<(String, String, String)>, 76 | Extension(db): Extension, 77 | ) -> AppResult> { 78 | let connected_username = get_connected_user_username(&db, user).await; 79 | let (commit, diff) = Repository::commit_with_diff(&owner, &repository, &sha, &db).await?; 80 | let diff = diff2html(&diff)?; 81 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 82 | 83 | Ok(HtmlTemplate(CommitDiffTemplate { 84 | _repository: repository, 85 | _owner: owner, 86 | _stats: stats, 87 | commit, 88 | diff, 89 | _current_branch: None, 90 | user: connected_username, 91 | _tab: Tab::History, 92 | })) 93 | } 94 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/diff.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppResult; 2 | use crate::get_connected_user_username; 3 | use crate::oauth::Oauth2User; 4 | use crate::view::HtmlTemplate; 5 | use askama::Template; 6 | use axum::extract::{Path, Query}; 7 | use axum::Extension; 8 | use gill_git::GitRepository; 9 | use gill_syntax::diff::diff2html; 10 | use serde::Deserialize; 11 | use sqlx::PgPool; 12 | 13 | #[derive(Deserialize)] 14 | pub struct DiffQuery { 15 | from: String, 16 | to: String, 17 | } 18 | 19 | #[derive(Template)] 20 | #[template(path = "repository/diff.html")] 21 | pub struct GitDiffTemplate { 22 | diff: String, 23 | user: Option, 24 | } 25 | 26 | pub async fn view( 27 | user: Option, 28 | Path((owner, repository)): Path<(String, String)>, 29 | Query(diff): Query, 30 | Extension(db): Extension, 31 | ) -> AppResult> { 32 | let connected_username = get_connected_user_username(&db, user).await; 33 | let repo = GitRepository::open(&owner, &repository)?; 34 | let diff = repo.diff(&diff.from, &diff.to)?; 35 | let diff = diff2html(&diff)?; 36 | 37 | Ok(HtmlTemplate(GitDiffTemplate { 38 | diff, 39 | user: connected_username, 40 | })) 41 | } 42 | 43 | pub async fn get_diff( 44 | Path((owner, repository)): Path<(String, String)>, 45 | Query(diff): Query, 46 | ) -> AppResult { 47 | let repo = GitRepository::open(&owner, &repository)?; 48 | let diff = repo.diff(&diff.from, &diff.to)?; 49 | let diff = diff2html(&diff)?; 50 | 51 | Ok(diff) 52 | } 53 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/close.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::Repository; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use axum::extract::Path; 6 | use axum::response::Redirect; 7 | use axum::Extension; 8 | use gill_authorize_derive::authorized; 9 | use sqlx::PgPool; 10 | 11 | #[authorized] 12 | pub async fn close( 13 | user: Option, 14 | Extension(db): Extension, 15 | Path((owner, repository, issue_number)): Path<(String, String, i32)>, 16 | ) -> AppResult { 17 | Repository::by_namespace(&owner, &repository, &db) 18 | .await? 19 | .close_issue(issue_number, user.activity_pub_id, &db) 20 | .await?; 21 | 22 | Ok(Redirect::to(&format!( 23 | "/{owner}/{repository}/issues/{issue_number}" 24 | ))) 25 | } 26 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/comment.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::issue::comment::create::CreateIssueCommentCommand; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use crate::state::AppState; 6 | use axum::extract::{Path, State}; 7 | use axum::response::Redirect; 8 | use axum::{Extension, Form}; 9 | 10 | use gill_authorize_derive::authorized; 11 | use serde::Deserialize; 12 | use sqlx::PgPool; 13 | 14 | #[derive(Deserialize, Debug)] 15 | pub struct IssueCommentForm { 16 | pub content: String, 17 | } 18 | 19 | #[authorized] 20 | pub async fn comment( 21 | user: Option, 22 | Path((owner, repository, issue_number)): Path<(String, String, i32)>, 23 | State(state): State, 24 | Extension(db): Extension, 25 | Form(input): Form, 26 | ) -> AppResult { 27 | let create_comment = CreateIssueCommentCommand { 28 | owner: &owner, 29 | repository: &repository, 30 | author_id: user.id, 31 | issue_number, 32 | content: &input.content, 33 | }; 34 | 35 | create_comment.execute(&state.instance).await?; 36 | 37 | Ok(Redirect::to(&format!( 38 | "/{owner}/{repository}/issues/{issue_number}" 39 | ))) 40 | } 41 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/create.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::issue::create::CreateIssueCommand; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use crate::state::AppState; 6 | use axum::extract::{Path, State}; 7 | use axum::response::Redirect; 8 | use axum::{Extension, Form}; 9 | 10 | use gill_authorize_derive::authorized; 11 | use serde::Deserialize; 12 | use sqlx::PgPool; 13 | 14 | #[derive(Deserialize, Debug)] 15 | pub struct CreateIssueForm { 16 | pub title: String, 17 | pub content: String, 18 | } 19 | 20 | #[authorized] 21 | pub async fn create( 22 | user: Option, 23 | Path((owner, repository)): Path<(String, String)>, 24 | State(state): State, 25 | Extension(db): Extension, 26 | Form(form): Form, 27 | ) -> AppResult { 28 | CreateIssueCommand::from(form) 29 | .execute(&repository, &owner, user, &state.instance) 30 | .await?; 31 | 32 | Ok(Redirect::to(&format!("/{owner}/{repository}/issues"))) 33 | } 34 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/list_view.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::stats::RepositoryStats; 2 | use crate::error::AppResult; 3 | use crate::get_connected_user_username; 4 | use crate::oauth::Oauth2User; 5 | 6 | use crate::view::HtmlTemplate; 7 | 8 | use askama::Template; 9 | use axum::extract::Path; 10 | use axum::Extension; 11 | 12 | use crate::domain::issue::digest::IssueDigest; 13 | use crate::domain::issue::IssueState; 14 | use crate::domain::repository::Repository; 15 | use crate::view::component::MarkdownPreviewForm; 16 | use crate::view::repository::Tab; 17 | use sqlx::PgPool; 18 | 19 | #[derive(Template, Debug)] 20 | #[template(path = "repository/issues/list.html")] 21 | pub struct IssuesTemplate { 22 | user: Option, 23 | owner: String, 24 | repository: String, 25 | issues: Option>, 26 | stats: RepositoryStats, 27 | current_branch: Option, 28 | markdown_preview_form: MarkdownPreviewForm, 29 | tab: Tab, 30 | } 31 | 32 | pub async fn list_view( 33 | user: Option, 34 | Extension(db): Extension, 35 | Path((owner, repository)): Path<(String, String)>, 36 | ) -> AppResult> { 37 | let connected_username = get_connected_user_username(&db, user).await; 38 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 39 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 40 | let issues = repo.list_issues(&db).await?; 41 | let pull_requests = (!issues.is_empty()).then_some(issues); 42 | let current_branch = repo.get_default_branch(&db).await.map(|branch| branch.name); 43 | 44 | let action_href = format!("/{owner}/{repository}/issues/create"); 45 | 46 | Ok(HtmlTemplate(IssuesTemplate { 47 | user: connected_username, 48 | owner: owner.clone(), 49 | repository: repository.clone(), 50 | issues: pull_requests, 51 | stats, 52 | current_branch, 53 | tab: Tab::Issues, 54 | markdown_preview_form: MarkdownPreviewForm { 55 | with_title: true, 56 | action_href, 57 | submit_value: "Create issue".to_string(), 58 | owner, 59 | repository, 60 | }, 61 | })) 62 | } 63 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::state::AppState; 2 | use crate::view::repository::issues::close::close; 3 | use crate::view::repository::issues::comment::comment; 4 | use crate::view::repository::issues::create::create; 5 | use crate::view::repository::issues::list_view::list_view; 6 | use crate::view::repository::issues::view::view; 7 | use axum::routing::get; 8 | use axum::Router; 9 | 10 | pub mod close; 11 | pub mod comment; 12 | pub mod create; 13 | pub mod list_view; 14 | pub mod view; 15 | 16 | pub fn router() -> Router { 17 | Router::new() 18 | .route("/:owner/:repository/issues", get(list_view)) 19 | .route("/:owner/:repository/issues/:number", get(view)) 20 | .route("/:owner/:repository/issues/:number/comment", get(comment)) 21 | .route("/:owner/:repository/issues/:number/close", get(close)) 22 | .route("/:owner/:repository/issues/create", get(create)) 23 | } 24 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/issues/view.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::stats::RepositoryStats; 2 | use crate::error::AppResult; 3 | use crate::oauth::Oauth2User; 4 | use crate::view::component::MarkdownPreviewForm; 5 | 6 | use crate::get_connected_user_username; 7 | 8 | use crate::view::HtmlTemplate; 9 | 10 | use askama::Template; 11 | use axum::extract::Path; 12 | 13 | use axum::Extension; 14 | 15 | use crate::domain::issue::comment::digest::IssueCommentDigest; 16 | use crate::domain::issue::digest::IssueDigest; 17 | use crate::domain::issue::IssueState; 18 | use crate::domain::repository::Repository; 19 | use crate::view::repository::Tab; 20 | use sqlx::PgPool; 21 | 22 | #[derive(Template, Debug)] 23 | #[template(path = "repository/issues/issue.html")] 24 | pub struct IssueTemplate { 25 | user: Option, 26 | owner: String, 27 | repository: String, 28 | issue: IssueDigest, 29 | stats: RepositoryStats, 30 | current_branch: Option, 31 | comments: Vec, 32 | markdown_preview_form: MarkdownPreviewForm, 33 | tab: Tab, 34 | } 35 | 36 | pub async fn view( 37 | user: Option, 38 | Extension(db): Extension, 39 | Path((owner, repository, issue_number)): Path<(String, String, i32)>, 40 | ) -> AppResult> { 41 | let connected_username = get_connected_user_username(&db, user).await; 42 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 43 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 44 | let issue = repo.get_issue_digest(issue_number, &db).await?; 45 | let comments = issue.get_comments(&db).await?; 46 | let current_branch = repo.get_default_branch(&db).await.map(|branch| branch.name); 47 | 48 | let action_href = format!("/{owner}/{repository}/issues/{issue_number}/comment"); 49 | Ok(HtmlTemplate(IssueTemplate { 50 | user: connected_username, 51 | owner: owner.clone(), 52 | repository: repository.clone(), 53 | issue, 54 | stats, 55 | current_branch, 56 | comments, 57 | markdown_preview_form: MarkdownPreviewForm { 58 | with_title: false, 59 | action_href, 60 | submit_value: "Comment".to_string(), 61 | owner, 62 | repository, 63 | }, 64 | tab: Tab::Issues, 65 | })) 66 | } 67 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::user::User; 2 | use crate::error::AppResult; 3 | use crate::state::AppState; 4 | 5 | use axum::routing::{get, post}; 6 | use axum::Router; 7 | use sqlx::PgPool; 8 | use std::fmt; 9 | use std::fmt::Formatter; 10 | 11 | pub mod activity; 12 | pub mod blob; 13 | pub mod commits; 14 | pub mod create; 15 | pub mod diff; 16 | pub mod issues; 17 | pub mod pull_request; 18 | pub mod tree; 19 | pub mod user_content; 20 | 21 | #[derive(Debug)] 22 | pub enum Tab { 23 | Code, 24 | Issues, 25 | PullRequests, 26 | History, 27 | } 28 | 29 | pub fn routes() -> Router { 30 | let router = Router::new() 31 | .route("/new", get(create::view)) 32 | .route("/create-repository", get(create::submit)) 33 | .route("/:owner/:repository", get(tree::root)) 34 | .route("/:owner/:repository/tree/:branch", get(tree::tree_root)) 35 | .route("/:owner/:repository/tree/:branch/*tree", get(tree::tree)) 36 | .route("/:owner/:repository/blob/:branch/*blob", get(blob::blob)) 37 | .route( 38 | "/:owner/:repository/commits/:branch/", 39 | get(commits::git_log), 40 | ) 41 | .route("/:owner/:repository/commits/:branch", get(commits::git_log)) 42 | .route("/:owner/:repository/commit/:sha", get(commits::commit_diff)) 43 | .route("/:owner/:repository/diff", get(diff::view)) 44 | .route("/:owner/:repository/get_diff", get(diff::get_diff)) 45 | .route("/:owner/:repository/star", post(activity::star)) 46 | .route("/:owner/:repository/watch", post(activity::watch)) 47 | .route("/:owner/:repository/*path", get(user_content::image)); 48 | 49 | router.merge(pull_request::router()).merge(issues::router()) 50 | } 51 | 52 | #[derive(Debug)] 53 | pub struct BranchDto { 54 | name: String, 55 | is_default: bool, 56 | is_current: bool, 57 | } 58 | 59 | async fn get_repository_branches( 60 | owner: &str, 61 | repository: &str, 62 | current_branch: &str, 63 | db: &PgPool, 64 | ) -> AppResult> { 65 | let user = User::by_name(owner, db).await.unwrap(); 66 | let repository = user.get_local_repository_by_name(repository, db).await?; 67 | let branches = repository.list_branches(20, 0, db).await?; 68 | let branches = branches 69 | .into_iter() 70 | .map(|branch| { 71 | let is_current = branch.name == current_branch; 72 | 73 | BranchDto { 74 | name: branch.name, 75 | is_default: branch.is_default, 76 | is_current, 77 | } 78 | }) 79 | .collect(); 80 | 81 | Ok(branches) 82 | } 83 | 84 | pub fn tree_and_blob_from_query(path: &str) -> (Option<&str>, &str) { 85 | match path.rsplit_once('/') { 86 | None => (None, path), 87 | Some((tree, blob_name)) => { 88 | if !tree.is_empty() { 89 | (Some(tree), blob_name) 90 | } else { 91 | (None, blob_name) 92 | } 93 | } 94 | } 95 | } 96 | 97 | impl fmt::Display for BranchDto { 98 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 99 | writeln!(f, "{self:?}") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/comment.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::Repository; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use axum::extract::Path; 6 | use axum::response::Redirect; 7 | use axum::{Extension, Form}; 8 | use gill_authorize_derive::authorized; 9 | use serde::Deserialize; 10 | use sqlx::PgPool; 11 | 12 | #[derive(Deserialize, Debug)] 13 | pub struct CommentPullRequestForm { 14 | pub content: String, 15 | } 16 | 17 | #[authorized] 18 | pub async fn comment( 19 | user: Option, 20 | Extension(db): Extension, 21 | Path((owner, repository, pull_request_number)): Path<(String, String, i32)>, 22 | Form(input): Form, 23 | ) -> AppResult { 24 | Repository::by_namespace(&owner, &repository, &db) 25 | .await? 26 | .get_pull_request(pull_request_number, &db) 27 | .await? 28 | .comment(&input.content, user.id, &db) 29 | .await?; 30 | 31 | Ok(Redirect::to(&format!( 32 | "/{owner}/{repository}/pulls/{pull_request_number}" 33 | ))) 34 | } 35 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/compare.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppResult; 2 | use crate::get_connected_user_username; 3 | use crate::oauth::Oauth2User; 4 | use crate::view::repository::{get_repository_branches, BranchDto, Tab}; 5 | use crate::view::HtmlTemplate; 6 | use anyhow::anyhow; 7 | use askama::Template; 8 | use axum::extract::Path; 9 | use axum::Extension; 10 | 11 | use crate::domain::repository::stats::RepositoryStats; 12 | use crate::domain::repository::Repository; 13 | use sqlx::PgPool; 14 | 15 | #[derive(Template, Debug)] 16 | #[template(path = "repository/pulls/compare.html")] 17 | pub struct CompareTemplate { 18 | user: Option, 19 | owner: String, 20 | repository: String, 21 | stats: RepositoryStats, 22 | branches: Vec, 23 | current_branch: Option, 24 | tab: Tab, 25 | } 26 | 27 | pub async fn compare( 28 | user: Option, 29 | Extension(db): Extension, 30 | Path((owner, repository)): Path<(String, String)>, 31 | ) -> AppResult> { 32 | let connected_username = get_connected_user_username(&db, user).await; 33 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 34 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 35 | let current_branch = repo 36 | .get_default_branch(&db) 37 | .await 38 | .ok_or_else(|| anyhow!("No default branch"))?; 39 | 40 | let current_branch = current_branch.name; 41 | let branches = get_repository_branches(&owner, &repository, ¤t_branch, &db).await?; 42 | 43 | Ok(HtmlTemplate(CompareTemplate { 44 | user: connected_username, 45 | owner, 46 | repository, 47 | stats, 48 | branches, 49 | current_branch: Some(current_branch), 50 | tab: Tab::PullRequests, 51 | })) 52 | } 53 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/create.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::Repository; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | use axum::extract::Path; 6 | use axum::response::Redirect; 7 | use axum::{Extension, Form}; 8 | use gill_authorize_derive::authorized; 9 | use serde::Deserialize; 10 | use sqlx::PgPool; 11 | 12 | #[derive(Deserialize, Debug)] 13 | pub struct CreatePullRequestForm { 14 | pub title: String, 15 | pub description: String, 16 | pub base: String, 17 | pub compare: String, 18 | } 19 | 20 | #[authorized] 21 | pub async fn create( 22 | user: Option, 23 | Extension(db): Extension, 24 | Path((owner, repository)): Path<(String, String)>, 25 | Form(input): Form, 26 | ) -> AppResult { 27 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 28 | repo.create_pull_request( 29 | user.id, 30 | &input.title, 31 | Some(&input.description.escape_default().to_string()), 32 | &input.base, 33 | &input.compare, 34 | &db, 35 | ) 36 | .await?; 37 | Ok(Redirect::to(&format!("/{owner}/{repository}/pulls"))) 38 | } 39 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/diff.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::pull_request::PullRequest; 2 | use crate::domain::pull_request::PullRequestState; 3 | use crate::domain::repository::stats::RepositoryStats; 4 | use crate::domain::repository::Repository; 5 | use crate::error::AppError; 6 | use crate::get_connected_user_username; 7 | use crate::oauth::Oauth2User; 8 | use crate::view::repository::Tab; 9 | use crate::view::HtmlTemplate; 10 | use askama::Template; 11 | use axum::extract::Path; 12 | use axum::Extension; 13 | use gill_syntax::diff::diff2html; 14 | use sqlx::PgPool; 15 | 16 | #[derive(Template, Debug)] 17 | #[template(path = "repository/pulls/diff.html")] 18 | pub struct PullRequestDiffTemplate { 19 | user: Option, 20 | owner: String, 21 | repository: String, 22 | pull_request: PullRequest, 23 | stats: RepositoryStats, 24 | current_branch: Option, 25 | diff: String, 26 | tab: Tab, 27 | } 28 | 29 | pub async fn diff( 30 | user: Option, 31 | Extension(db): Extension, 32 | Path((owner, repository, pull_request_number)): Path<(String, String, i32)>, 33 | ) -> Result, AppError> { 34 | let connected_username = get_connected_user_username(&db, user).await; 35 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 36 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 37 | let pull_request = repo.get_pull_request(pull_request_number, &db).await?; 38 | let current_branch = repo.get_default_branch(&db).await.map(|branch| branch.name); 39 | let diff = pull_request.get_diff(&owner, &repository)?; 40 | let diff = diff2html(&diff)?; 41 | Ok(HtmlTemplate(PullRequestDiffTemplate { 42 | user: connected_username, 43 | owner: owner.clone(), 44 | repository: repository.clone(), 45 | pull_request, 46 | stats, 47 | current_branch, 48 | diff, 49 | tab: Tab::PullRequests, 50 | })) 51 | } 52 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/list_view.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::stats::RepositoryStats; 2 | use crate::error::AppResult; 3 | use crate::get_connected_user_username; 4 | use crate::oauth::Oauth2User; 5 | 6 | use crate::view::HtmlTemplate; 7 | 8 | use crate::domain::pull_request::{PullRequest, PullRequestState}; 9 | use crate::domain::repository::Repository; 10 | use crate::view::repository::Tab; 11 | use askama::Template; 12 | use axum::extract::Path; 13 | use axum::Extension; 14 | use sqlx::PgPool; 15 | 16 | #[derive(Template, Debug)] 17 | #[template(path = "repository/pulls/list.html")] 18 | pub struct PullRequestsTemplate { 19 | user: Option, 20 | owner: String, 21 | repository: String, 22 | pull_requests: Option>, 23 | stats: RepositoryStats, 24 | current_branch: Option, 25 | tab: Tab, 26 | } 27 | 28 | pub async fn list_view( 29 | user: Option, 30 | Extension(db): Extension, 31 | Path((owner, repository)): Path<(String, String)>, 32 | ) -> AppResult> { 33 | let connected_username = get_connected_user_username(&db, user).await; 34 | let stats = RepositoryStats::get(&owner, &repository, &db).await?; 35 | let repo = Repository::by_namespace(&owner, &repository, &db).await?; 36 | let pull_requests = repo.list_pull_requests(&db).await?; 37 | let pull_requests = (!pull_requests.is_empty()).then_some(pull_requests); 38 | let current_branch = repo.get_default_branch(&db).await.map(|branch| branch.name); 39 | 40 | Ok(HtmlTemplate(PullRequestsTemplate { 41 | user: connected_username, 42 | owner, 43 | repository, 44 | pull_requests, 45 | stats, 46 | current_branch, 47 | tab: Tab::PullRequests, 48 | })) 49 | } 50 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/pull_request/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::state::AppState; 2 | use crate::view::repository::pull_request::comment::comment; 3 | use crate::view::repository::pull_request::commits::{commit_diff, commits}; 4 | use crate::view::repository::pull_request::compare::compare; 5 | use crate::view::repository::pull_request::create::create; 6 | use crate::view::repository::pull_request::diff::diff; 7 | use crate::view::repository::pull_request::list_view::list_view; 8 | use crate::view::repository::pull_request::view::{close, merge, view}; 9 | use axum::routing::get; 10 | use axum::Router; 11 | 12 | pub mod comment; 13 | pub mod commits; 14 | pub mod compare; 15 | pub mod create; 16 | pub mod diff; 17 | pub mod list_view; 18 | pub mod view; 19 | 20 | pub fn router() -> Router { 21 | Router::new() 22 | .route("/:owner/:repository/pulls", get(list_view)) 23 | .route("/:owner/:repository/pulls/:number", get(view)) 24 | .route("/:owner/:repository/pulls/:number/diff", get(diff)) 25 | .route("/:owner/:repository/pulls/:number/commits", get(commits)) 26 | .route( 27 | "/:owner/:repository/pulls/:number/commits/:sha", 28 | get(commit_diff), 29 | ) 30 | .route("/:owner/:repository/pulls/:number/comment", get(comment)) 31 | .route("/:owner/:repository/pulls/:number/merge", get(merge)) 32 | .route( 33 | "/:owner/:repository/pulls/:number/rebase", 34 | get(view::rebase), 35 | ) 36 | .route("/:owner/:repository/pulls/:number/close", get(close)) 37 | .route("/:owner/:repository/pulls/create", get(create)) 38 | .route("/:owner/:repository/compare", get(compare)) 39 | } 40 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/repository/user_content.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::repository::Repository; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::view::repository::tree_and_blob_from_query; 4 | use axum::extract::Path; 5 | use axum::Extension; 6 | use gill_git::traversal::BlobMime; 7 | use gill_git::GitRepository; 8 | use sqlx::PgPool; 9 | 10 | pub async fn image( 11 | Path((owner, repository)): Path<(String, String)>, 12 | Path(path): Path>, 13 | Extension(db): Extension, 14 | ) -> AppResult> { 15 | let path = path.last().unwrap(); 16 | let (tree, blob_name) = tree_and_blob_from_query(path); 17 | let repo = GitRepository::open(&owner, &repository)?; 18 | let repo_entity = Repository::by_namespace(&owner, &repository, &db).await?; 19 | let branch = repo_entity 20 | .get_default_branch(&db) 21 | .await 22 | .ok_or(AppError::NotFound)?; 23 | let tree = repo.get_tree_for_path(Some(&branch.name), tree)?; 24 | let blob = tree 25 | .blobs 26 | .iter() 27 | .find(|blob| blob.filename() == blob_name) 28 | .unwrap(); 29 | let blob = match repo.blob_mime(blob) { 30 | BlobMime::Image => repo.blob_bytes(blob).ok(), 31 | _ => None, 32 | }; 33 | 34 | blob.ok_or(AppError::NotFound) 35 | } 36 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/user/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::state::AppState; 2 | use axum::routing::get; 3 | use axum::Router; 4 | 5 | pub mod profile; 6 | pub mod settings; 7 | pub mod ssh_key; 8 | 9 | pub fn routes() -> Router { 10 | Router::new() 11 | .route("/:owner", get(profile::user_view)) 12 | .route("/:owner/", get(profile::user_view)) 13 | .route("/settings/profile", get(settings::settings)) 14 | .route("/settings/profile/add-ssh-key", get(ssh_key::add)) 15 | } 16 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/user/profile.rs: -------------------------------------------------------------------------------- 1 | use crate::oauth::Oauth2User; 2 | use crate::view::HtmlTemplate; 3 | use askama::Template; 4 | use axum::extract::{Path, Query}; 5 | 6 | use axum::Extension; 7 | 8 | use crate::view::dto::RepositoryDto; 9 | 10 | use crate::domain::user::User; 11 | use crate::get_connected_user_username; 12 | use serde::Deserialize; 13 | use sqlx::PgPool; 14 | 15 | #[derive(Deserialize)] 16 | pub struct UserProfileQuery { 17 | #[serde(default)] 18 | tab: Tab, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | #[serde(rename_all = "lowercase")] 23 | pub enum Tab { 24 | Profile, 25 | Repositories, 26 | Stars, 27 | } 28 | 29 | impl Default for Tab { 30 | fn default() -> Self { 31 | Self::Repositories 32 | } 33 | } 34 | 35 | #[derive(Template)] 36 | #[template(path = "user/profile.html")] 37 | pub struct UserPageTemplate { 38 | profile_username: String, 39 | user: Option, 40 | repositories: Vec, 41 | stars: Vec, 42 | // TODO 43 | _tab: Tab, 44 | } 45 | 46 | pub async fn user_view( 47 | connected_user: Option, 48 | Path(user): Path, 49 | Query(page): Query, 50 | Extension(db): Extension, 51 | ) -> Result, crate::error::AppError> { 52 | let profile_username = user; 53 | let user = User::by_name(&profile_username, &db).await?; 54 | 55 | let repositories = user 56 | .list_repositories(20, 0, &db) 57 | .await? 58 | .into_iter() 59 | .map(RepositoryDto::from) 60 | .collect(); 61 | 62 | let stars = user 63 | .starred_repositories(20, 0, &db) 64 | .await? 65 | .into_iter() 66 | .map(RepositoryDto::from) 67 | .collect(); 68 | 69 | let username = get_connected_user_username(&db, connected_user).await; 70 | 71 | let template = UserPageTemplate { 72 | profile_username, 73 | user: username, 74 | repositories, 75 | stars, 76 | _tab: page.tab, 77 | }; 78 | 79 | Ok(HtmlTemplate(template)) 80 | } 81 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/user/settings.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::user::User; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user_username; 4 | use crate::oauth::Oauth2User; 5 | use crate::view::HtmlTemplate; 6 | use askama::Template; 7 | use axum::extract::Query; 8 | use axum::response::IntoResponse; 9 | use axum::Extension; 10 | use serde::Deserialize; 11 | use sqlx::PgPool; 12 | 13 | #[derive(Deserialize)] 14 | pub struct UserSettingsQuery { 15 | #[serde(default)] 16 | tab: Tab, 17 | } 18 | 19 | #[derive(Deserialize)] 20 | #[serde(rename_all = "kebab-case")] 21 | pub enum Tab { 22 | SshKey, 23 | Profile, 24 | } 25 | 26 | impl Default for Tab { 27 | fn default() -> Self { 28 | Self::Profile 29 | } 30 | } 31 | 32 | #[derive(Template)] 33 | #[template(path = "user/settings.html")] 34 | pub struct UserSettingsTemplate { 35 | user: Option, 36 | // TODO 37 | _tab: Tab, 38 | } 39 | 40 | pub async fn settings( 41 | connected_user: Option, 42 | Query(page): Query, 43 | Extension(db): Extension, 44 | ) -> AppResult { 45 | let Some(user) = get_connected_user_username(&db, connected_user).await else { 46 | return Err(AppError::Unauthorized); 47 | }; 48 | 49 | let user = User::by_name(&user, &db).await?; 50 | 51 | Ok(HtmlTemplate(UserSettingsTemplate { 52 | user: Some(user.username), 53 | _tab: page.tab, 54 | })) 55 | } 56 | -------------------------------------------------------------------------------- /crates/gill-app/src/view/user/ssh_key.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::user::ssh_key::RawSshkey; 2 | use crate::error::{AppError, AppResult}; 3 | use crate::get_connected_user; 4 | use crate::oauth::Oauth2User; 5 | 6 | use axum::response::Redirect; 7 | use axum::{Extension, Form}; 8 | 9 | use gill_authorize_derive::authorized; 10 | use serde::Deserialize; 11 | use sqlx::PgPool; 12 | 13 | #[derive(Deserialize, Debug)] 14 | pub struct AddSshKeyForm { 15 | pub title: String, 16 | pub key: String, 17 | } 18 | 19 | #[authorized] 20 | pub async fn add( 21 | user: Option, 22 | Extension(db): Extension, 23 | Form(input): Form, 24 | ) -> AppResult { 25 | let raw_key = RawSshkey::from(input.key); 26 | let (key_type, key) = raw_key.key_parts(); 27 | user.add_ssh_key(&input.title, key, key_type, &db).await?; 28 | gill_git::ssh::append_key(raw_key.full_key(), user.id).expect("Failed to append ssh key"); 29 | Ok(Redirect::to("/settings/profile?tab=ssh-key")) 30 | } 31 | -------------------------------------------------------------------------------- /crates/gill-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | '**/*.{html,js,css}', 5 | ], 6 | plugins: [ 7 | require('@tailwindcss/typography'), 8 | require('@tailwindcss/forms'), 9 | require('@tailwindcss/line-clamp'), 10 | require('@tailwindcss/aspect-ratio'), 11 | ], 12 | theme: { 13 | fontFamily: { 14 | 'body': ['"Open Sans"'], 15 | }, 16 | extend: { 17 | typography(theme) { 18 | return { 19 | slate: { 20 | css: { 21 | '--tw-prose-bullets': theme('colors.slate[900]'), 22 | '--tw-prose-pre-bg': theme('colors.slate[100]'), 23 | '--tw-prose-pre-code': theme('colors.slate[900]'), 24 | 'p': { 25 | 'line-height': 1.5 26 | }, 27 | 'a': { 28 | color: theme('colors.blue[600]'), 29 | 'text-decoration': 'none', 30 | }, 31 | 'a:hover': { 32 | 'text-decoration': 'underline', 33 | }, 34 | 'code::before': { 35 | content: 'none', 36 | }, 37 | 'code::after': { 38 | content: 'none' 39 | }, 40 | 'img': { 41 | 'display': 'inline-block', 42 | 'margin-top': '0em', 43 | 'margin-bottom': '0.25em', 44 | }, 45 | 'h1, h2': { 46 | 'padding-bottom': '20px', 47 | 'border-bottom-width': '1px', 48 | 'border-color': theme('colors.slate.300'), 49 | }, 50 | 'h1, h2, h3, h4': { 51 | 'margin-top': '0', 52 | }, 53 | 'li': { 54 | 'margin-top': '0.25em', 55 | 'margin-bottom': '0.25em', 56 | }, 57 | 'li *': { 58 | 'margin-top': 0, 59 | 'margin-bottom': 0, 60 | }, 61 | ':not(pre) > code': { 62 | color: theme('colors.slate.700'), 63 | backgroundColor: theme('colors.stone.100'), 64 | borderRadius: theme('borderRadius.DEFAULT'), 65 | paddingLeft: theme('spacing[1]'), 66 | paddingRight: theme('spacing[1]'), 67 | paddingTop: '3px', 68 | paddingBottom: '3px', 69 | }, 70 | }, 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/gill-app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gill 6 | 7 | 8 | 9 | 10 | {% block head %}{% endblock %} 11 | 12 | 13 | 14 | {% include "navbar.html" %} 15 |
16 |
17 | {% block content_left %}{% endblock %} 18 |
19 |
20 | {% block content %}{% endblock %} 21 |
22 |
23 | {% block content_right %}{% endblock %} 24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /crates/gill-app/templates/base_repository.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gill 6 | 7 | 8 | 9 | 10 | {% block head %}{% endblock %} 11 | 12 | 13 | 14 | {% include "navbar.html" %} 15 |
16 |
17 | {% block content_left %}{% endblock %} 18 |
19 |
20 | {% match current_branch %} 21 | {% when Some with (current_branch) %} 22 | {% let current_branch = current_branch %} 23 | {% include "repository/header.html" %} 24 | {% when None %} 25 | {% include "repository/federated/header.html" %} 26 | {% endmatch %} 27 | {% block content %}{% endblock %} 28 |
29 |
30 | {% block content_right %}{% endblock %} 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/branch.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 |
25 |
26 | 33 | 35 |
36 | 37 |
38 | 64 |
65 |
-------------------------------------------------------------------------------- /crates/gill-app/templates/repository/commit-diff.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |
11 | {{commit.summary}} 12 | {{commit.id|sha_digest}} 13 |
14 | {% match commit.body %} 15 | {% when Some with (body) %} 16 | {{body}} 17 | {% when None %} 18 | {% endmatch %} 19 | {% match commit.author %} 20 | {% when Author::Known with (author) %} 21 | 22 | @{{author}} 23 | 24 | {% when Author::Raw with (author) %} 25 | {{author}} 26 | {% endmatch %} 27 |
28 | {{diff|safe}} 29 |
30 | {% endblock %} 31 | 32 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/components/clone-button.html: -------------------------------------------------------------------------------- 1 | 34 | 35 |
36 | 42 | 51 |
52 | 53 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/components/fork-button.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/components/star-button.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/components/watch-button.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | 13 | 19 |
20 |
21 | 25 | 31 |
32 | 37 |
38 |
39 | {% endblock %} 40 | 41 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/diff.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 | {{diff|safe}} 10 |
11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/federated/header.html: -------------------------------------------------------------------------------- 1 | 31 |
32 |
33 |
34 | 35 | {{owner}} 38 | / 39 | {{repository}} 42 |
43 |
44 | {% include "repository/components/watch-button.html" %} 45 | {% include "repository/components/fork-button.html" %} 46 | {% include "repository/components/star-button.html" %} 47 | {% include "repository/components/clone-button.html" %} 48 |
49 |
50 |
51 | 58 | 65 | 72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/federated/view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "header.html" %} 5 | {% endblock %} 6 | 7 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/history.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | {% block content %} 6 | 7 | {% let current_branch = current_branch.as_ref().unwrap() %} 8 | {% include "branch.html" %} 9 | 10 | 11 |
12 | {% for commit in commits %} 13 |
14 | 16 | {{commit.id|sha_digest}} 17 | 18 | 20 | {{commit.summary}} 21 | 22 | {% match commit.author %} 23 | {% when Author::Known with (author) %} 24 | 26 | @{{author}} 27 | 28 | {% when Author::Raw with (author) %} 29 | {{author}} 30 | {% endmatch %} 31 |
32 | {% endfor %} 33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/issues/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block content %} 9 | 19 |
20 | {% match user %} 21 | {%- when Some with (user) -%} 22 |
23 | 28 |
29 | {%- when None -%} 30 | {%- endmatch -%} 31 | 32 | 33 | {%- match issues -%} 34 | {%- when Some with (issues) -%} 35 |
36 | {%- for issue in issues -%} 37 |
39 |
40 | {% match issue.state %} 41 | {% when IssueState::Open %} 42 | 43 | {% when IssueState::Closed %} 44 | 45 | {% endmatch %} 46 | 47 |
48 |
49 | #{{issue.number}} 50 | Opened by {{issue.opened_by}} 51 |
52 |
53 | {%- endfor -%} 54 |
55 | {%- when None -%} 56 | No issues 57 | {%- endmatch -%} 58 |
59 | 60 | 63 | {% endblock %} 64 | 65 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/commit-diff.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 | {% include "repository/pulls/summary.html" %} 10 | {% include "repository/pulls/nav.html" %} 11 |
12 |
13 | {{commit.summary}} 14 | {{commit.id|sha_digest}} 15 |
16 | {% match commit.body %} 17 | {% when Some with (body) %} 18 | {{body}} 19 | {% when None %} 20 | {% endmatch %} 21 | {% match commit.author %} 22 | {% when Author::Known with (author) %} 23 | 24 | @{{author}} 25 | 26 | {% when Author::Raw with (author) %} 27 | {{author}} 28 | {% endmatch %} 29 |
30 | {{diff|safe}} 31 |
32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/commits.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 | {% include "repository/pulls/summary.html" %} 10 | {% include "repository/pulls/nav.html" %} 11 | 12 |
13 | {% for commit in commits %} 14 |
15 | 17 | {{commit.id|sha_digest}} 18 | 19 | 21 | {{commit.summary}} 22 | 23 | {% match commit.author %} 24 | {% when Author::Known with (author) %} 25 | 27 | @{{author}} 28 | 29 | {% when Author::Raw with (author) %} 30 | {{author}} 31 | {% endmatch %} 32 |
33 | {% endfor %} 34 |
35 |
36 | 37 | 38 | {% endblock %} 39 | 40 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/diff.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 | {% include "repository/pulls/summary.html" %} 10 | {% include "repository/pulls/nav.html" %} 11 | {{diff|safe}} 12 |
13 | 14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 | 9 | 18 |
19 | {% match user %} 20 | {%- when Some with (user) -%} 21 |
22 | 27 |
28 | {%- when None -%} 29 | {%- endmatch -%} 30 | 31 | 32 | {%- match pull_requests -%} 33 | {%- when Some with (pulls) -%} 34 |
35 | {%- for pr in pulls -%} 36 |
38 |
39 | {% match pr.state %} 40 | {% when PullRequestState::Open %} 41 | 42 | {% when PullRequestState::Closed %} 43 | 44 | {% when PullRequestState::Merged %} 45 | 46 | {% endmatch %} 47 | {{pr.title}} 48 |
49 |
50 | #{{pr.number}} 51 | Opened by {{pr.opened_by}} 52 |
53 |
54 | {%- endfor -%} 55 |
56 | {%- when None -%} 57 | No opened pull requests 58 | {%- endmatch -%} 59 |
60 | 61 | {% endblock %} 62 | 63 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/nav.html: -------------------------------------------------------------------------------- 1 | 14 |
15 | 19 | 23 | 27 | 28 | 50 |
51 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/pulls/summary.html: -------------------------------------------------------------------------------- 1 |

{{pull_request.title}} 2 | 3 | #{{pull_request.number}} 4 | 5 |

6 |
7 | {%- match pull_request.state -%} 8 | {%- when PullRequestState::Open -%} 9 |
10 | 11 | Open 12 |
13 | {%- when PullRequestState::Closed -%} 14 |
15 | 16 | Closed 17 |
18 | {%- when Merged -%} 19 |
20 | 21 | Merged 22 |
23 | {%- endmatch -%} 24 |   25 |

26 | {{pull_request.opened_by}} wants to merge 27 | {{pull_request.compare}} into 28 | {{pull_request.base}} 29 | 35 |

36 |
-------------------------------------------------------------------------------- /crates/gill-app/templates/repository/tree/blob.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | {% let current_branch = current_branch.as_ref().unwrap() %} 8 | {% include "../branch.html" %} 9 |
10 | {%- match blob -%} 11 | {%- when Highlighted with {content, language} -%} 12 |
13 | {{- content|safe -}} 14 |
15 | {%- when PlainText with (blob) -%} 16 |
17 | {{- blob|safe -}} 18 |
19 | {%- when Image with (blob) -%} 20 |
21 | blob image 22 |
23 | {%- when Binary with {content, filename} -%} 24 |
25 | Download 26 | 29 |
30 | {%- endmatch -%} 31 |
32 | {% endblock %} 33 | 34 | 35 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/tree/empty.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 | Repository appears to be empty 9 |
10 | {% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /crates/gill-app/templates/repository/tree/tree.html: -------------------------------------------------------------------------------- 1 | {% extends "base_repository.html" %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | {% let current_branch = current_branch.as_ref().unwrap() %} 8 | {% include "../branch.html" %} 9 |
10 |
11 | {% for dir in tree.trees %} 12 |
13 | 14 | {{dir.filename}} 15 | {{dir.commit_summary}} 17 | 18 |
19 | {% endfor %} 20 | {% for blob in tree.blobs %} 21 |
22 | 23 | {{blob.filename}} 24 | {{blob.commit_summary}} 26 | 27 |
28 | {% endfor %} 29 |
30 | {%- match readme -%} 31 | {%- when Some with (readme) -%} 32 | 33 | 34 |
36 | {{- readme|safe -}} 37 |
38 | {%- when None -%} 39 | {%- endmatch -%} 40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /crates/gill-authorize-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-authorize-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | syn = {version = "1", features = ["full"] } 14 | quote = "1" -------------------------------------------------------------------------------- /crates/gill-authorize-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse, parse_quote, ItemFn}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn authorized(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let mut input_fn = parse::(item).expect("Expected a function"); 8 | let mut statements = input_fn.block.stmts.clone(); 9 | 10 | statements.insert( 11 | 0, 12 | parse_quote! { 13 | let Some(user) = get_connected_user(&db, user).await else { 14 | return Err(AppError::Unauthorized); 15 | }; 16 | }, 17 | ); 18 | 19 | input_fn.block.stmts = statements; 20 | TokenStream::from(quote! {#input_fn}) 21 | } 22 | -------------------------------------------------------------------------------- /crates/gill-db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-db" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | sqlx = { workspace = true } 8 | chrono.workspace = true 9 | async-trait = "0.1.61" 10 | 11 | [dev-dependencies] 12 | speculoos.workspace = true 13 | anyhow.workspace = true -------------------------------------------------------------------------------- /crates/gill-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use sqlx::PgPool; 4 | 5 | pub mod pagination; 6 | pub mod repository; 7 | pub mod subscribe; 8 | pub mod user; 9 | 10 | pub use sqlx::postgres::PgPoolOptions; 11 | 12 | #[async_trait] 13 | pub trait Insert { 14 | type Output; 15 | async fn insert(self, db: &PgPool) -> sqlx::Result; 16 | } 17 | 18 | pub async fn inbox_for_url(url: &str, db: &PgPool) -> sqlx::Result> { 19 | let inboxes = sqlx::query_scalar!( 20 | // language=PostgreSQL 21 | r#" 22 | SELECT DISTINCT member.inbox_url 23 | FROM users u 24 | LEFT JOIN repository r ON r.attributed_to = u.activity_pub_id 25 | LEFT JOIN user_follow uf on u.id = uf.user_id AND r.id IS NULL 26 | LEFT JOIN repository_watch rw on r.id = rw.repository_id AND r.id IS NOT NULL 27 | JOIN users as member on member.id = uf.follower_id OR member.id = rw.watched_by 28 | WHERE r.followers_url = $1 29 | OR u.followers_url = $1 30 | "#, 31 | url, 32 | ) 33 | .fetch_all(db) 34 | .await?; 35 | 36 | Ok(inboxes) 37 | } 38 | -------------------------------------------------------------------------------- /crates/gill-db/src/pagination.rs: -------------------------------------------------------------------------------- 1 | pub struct Pagination { 2 | pub limit: i64, 3 | pub offset: i64, 4 | } 5 | 6 | impl Default for Pagination { 7 | fn default() -> Self { 8 | Self { 9 | limit: 30, 10 | offset: 0, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/branch.rs: -------------------------------------------------------------------------------- 1 | use sqlx::PgPool; 2 | 3 | #[derive(sqlx::FromRow, Debug)] 4 | pub struct Branch { 5 | pub name: String, 6 | pub repository_id: i32, 7 | pub is_default: bool, 8 | } 9 | 10 | impl Branch { 11 | pub(crate) async fn create( 12 | name: &str, 13 | repository_id: i32, 14 | is_default: bool, 15 | db: &PgPool, 16 | ) -> sqlx::Result { 17 | let branch = sqlx::query_as!( 18 | Branch, 19 | // language=PostgreSQL 20 | r#" 21 | insert into "branch"(name, repository_id, is_default) 22 | values ($1, $2, $3) 23 | returning name, repository_id, is_default 24 | "#, 25 | name, 26 | repository_id, 27 | is_default 28 | ) 29 | .fetch_one(db) 30 | .await?; 31 | 32 | Ok(branch) 33 | } 34 | 35 | pub(crate) async fn get(name: &str, repository_id: i32, db: &PgPool) -> Option { 36 | sqlx::query_as!( 37 | Branch, 38 | // language=PostgreSQL 39 | r#" 40 | SELECT name, repository_id, is_default FROM branch 41 | WHERE name = $1 AND repository_id = $2 42 | "#, 43 | name, 44 | repository_id 45 | ) 46 | .fetch_one(db) 47 | .await 48 | .ok() 49 | } 50 | 51 | pub(crate) async fn make_default(self, is_default: bool, db: &PgPool) -> sqlx::Result<()> { 52 | sqlx::query_as!( 53 | Branch, 54 | // language=PostgreSQL 55 | r#" 56 | UPDATE branch SET is_default = $1 57 | WHERE name = $2 AND repository_id = $3 58 | "#, 59 | is_default, 60 | self.name, 61 | self.repository_id, 62 | ) 63 | .execute(db) 64 | .await?; 65 | 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/create.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::Repository; 2 | use crate::Insert; 3 | use async_trait::async_trait; 4 | use sqlx::PgPool; 5 | 6 | pub struct CreateRepository { 7 | pub activity_pub_id: String, 8 | pub name: String, 9 | pub summary: Option, 10 | pub private: bool, 11 | pub inbox_url: String, 12 | pub outbox_url: String, 13 | pub followers_url: String, 14 | pub attributed_to: String, 15 | pub clone_uri: String, 16 | pub public_key: String, 17 | pub private_key: Option, 18 | pub ticket_tracked_by: String, 19 | pub send_patches_to: String, 20 | pub domain: String, 21 | pub is_local: bool, 22 | } 23 | 24 | #[async_trait] 25 | impl Insert for CreateRepository { 26 | type Output = Repository; 27 | 28 | async fn insert(self, db: &PgPool) -> sqlx::Result { 29 | let repository = sqlx::query_as!( 30 | Repository, 31 | // language=PostgreSQL 32 | r#" 33 | insert into repository( 34 | activity_pub_id, 35 | name, 36 | summary, 37 | private, 38 | inbox_url, 39 | outbox_url, 40 | followers_url, 41 | attributed_to, 42 | clone_uri, 43 | public_key, 44 | private_key, 45 | ticket_tracked_by, 46 | send_patches_to, 47 | domain, 48 | is_local 49 | ) 50 | values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) 51 | returning *; 52 | "#, 53 | self.activity_pub_id, 54 | self.name, 55 | self.summary, 56 | self.private, 57 | self.inbox_url, 58 | self.outbox_url, 59 | self.followers_url, 60 | self.attributed_to, 61 | self.clone_uri, 62 | self.public_key, 63 | self.private_key, 64 | self.ticket_tracked_by, 65 | self.send_patches_to, 66 | self.domain, 67 | self.is_local, 68 | ) 69 | .fetch_one(db) 70 | .await?; 71 | 72 | Ok(repository) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/fork.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::Repository; 2 | use crate::user::User; 3 | use sqlx::PgPool; 4 | 5 | impl Repository { 6 | pub async fn add_fork(&self, fork_id: i32, forked_by: i32, db: &PgPool) -> sqlx::Result<()> { 7 | sqlx::query!( 8 | // language=PostgreSQL 9 | r#" 10 | INSERT INTO gill.public.repository_fork (repository_id, fork_id, forked_by) 11 | VALUES ($1, $2, $3) 12 | "#, 13 | self.id, 14 | fork_id, 15 | forked_by 16 | ) 17 | .execute(db) 18 | .await?; 19 | 20 | Ok(()) 21 | } 22 | 23 | pub async fn get_forked_by( 24 | &self, 25 | limit: i64, 26 | offset: i64, 27 | db: &PgPool, 28 | ) -> sqlx::Result> { 29 | let watchers = sqlx::query_as!( 30 | User, 31 | // language=PostgreSQL 32 | r#" 33 | SELECT u.id, username, domain, email, public_key, private_key, inbox_url, outbox_url, 34 | followers_url, is_local, activity_pub_id 35 | FROM repository_fork f 36 | JOIN users u ON f.forked_by = u.id 37 | LIMIT $1 38 | OFFSET $2 39 | "#, 40 | limit, 41 | offset 42 | ) 43 | .fetch_all(db) 44 | .await?; 45 | 46 | Ok(watchers) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/pull_request/comment.rs: -------------------------------------------------------------------------------- 1 | #[derive(sqlx::FromRow, Debug)] 2 | pub struct PullRequestComment { 3 | pub id: i32, 4 | pub repository_id: i32, 5 | pub created_by: String, 6 | pub content: String, 7 | } 8 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/star.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::Repository; 2 | use crate::user::User; 3 | use sqlx::PgPool; 4 | 5 | impl Repository { 6 | pub async fn add_star(&self, starred_by: i32, db: &PgPool) -> sqlx::Result<()> { 7 | sqlx::query!( 8 | // language=PostgreSQL 9 | r#" 10 | INSERT INTO repository_star (repository_id, starred_by) 11 | VALUES ($1, $2) 12 | "#, 13 | self.id, 14 | starred_by 15 | ) 16 | .execute(db) 17 | .await?; 18 | 19 | Ok(()) 20 | } 21 | 22 | pub async fn get_starred_by( 23 | &self, 24 | limit: i64, 25 | offset: i64, 26 | db: &PgPool, 27 | ) -> sqlx::Result> { 28 | let stars = sqlx::query_as!( 29 | User, 30 | // language=PostgreSQL 31 | r#" 32 | SELECT u.id, username, domain, email, public_key, private_key, inbox_url, outbox_url, 33 | followers_url, is_local, activity_pub_id 34 | FROM repository_star s 35 | JOIN users u ON s.starred_by = u.id 36 | LIMIT $1 37 | OFFSET $2 38 | "#, 39 | limit, 40 | offset 41 | ) 42 | .fetch_all(db) 43 | .await?; 44 | 45 | Ok(stars) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/gill-db/src/repository/watch.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::Repository; 2 | use crate::user::User; 3 | use sqlx::PgPool; 4 | 5 | impl Repository { 6 | pub async fn add_watcher(&self, watcher_id: i32, db: &PgPool) -> sqlx::Result<()> { 7 | sqlx::query!( 8 | // language=PostgreSQL 9 | r#" 10 | INSERT INTO repository_watch (repository_id, watched_by) 11 | VALUES ($1, $2) 12 | "#, 13 | self.id, 14 | watcher_id 15 | ) 16 | .execute(db) 17 | .await?; 18 | 19 | Ok(()) 20 | } 21 | 22 | pub async fn get_watchers( 23 | &self, 24 | limit: i64, 25 | offset: i64, 26 | db: &PgPool, 27 | ) -> sqlx::Result> { 28 | let watchers = sqlx::query_as!( 29 | User, 30 | // language=PostgreSQL 31 | r#" 32 | SELECT u.id, username, domain, email, public_key, private_key, inbox_url, outbox_url, 33 | followers_url, is_local, activity_pub_id 34 | FROM repository_watch w 35 | JOIN users u ON w.watched_by = u.id 36 | LIMIT $1 37 | OFFSET $2 38 | "#, 39 | limit, 40 | offset 41 | ) 42 | .fetch_all(db) 43 | .await?; 44 | 45 | Ok(watchers) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/gill-db/src/subscribe.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::issue::Issue; 2 | 3 | use sqlx::PgPool; 4 | 5 | impl Issue { 6 | pub async fn add_subscriber(&self, subscriber_id: i32, db: &PgPool) -> sqlx::Result<()> { 7 | sqlx::query!( 8 | // language=PostgreSQL 9 | r#" 10 | insert into "issue_subscriber"(repository_id, number, subscriber) 11 | values ($1, $2, $3) 12 | "#, 13 | self.repository_id, 14 | self.number, 15 | subscriber_id 16 | ) 17 | .execute(db) 18 | .await?; 19 | 20 | Ok(()) 21 | } 22 | 23 | pub async fn has_subscriber(&self, subscriber_id: i32, db: &PgPool) -> sqlx::Result { 24 | let has_subscriber = sqlx::query!( 25 | // language=PostgreSQL 26 | r#" 27 | SELECT 28 | CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END as has_subscriber 29 | FROM issue_subscriber 30 | WHERE repository_id = $1 AND number = $2 AND subscriber = $3; 31 | "#, 32 | self.repository_id, 33 | self.number, 34 | subscriber_id 35 | ) 36 | .fetch_one(db) 37 | .await?; 38 | 39 | Ok(has_subscriber.has_subscriber.unwrap_or_default()) 40 | } 41 | 42 | pub async fn get_subscribers_inbox( 43 | &self, 44 | limit: i64, 45 | offset: i64, 46 | db: &PgPool, 47 | ) -> sqlx::Result> { 48 | let inboxes = sqlx::query!( 49 | // language=PostgreSQL 50 | r#" 51 | SELECT inbox_url 52 | FROM issue_subscriber s 53 | JOIN users u ON s.subscriber = u.id 54 | LIMIT $1 55 | OFFSET $2 56 | "#, 57 | limit, 58 | offset 59 | ) 60 | .fetch_all(db) 61 | .await?; 62 | 63 | Ok(inboxes 64 | .into_iter() 65 | .map(|subscriber| subscriber.inbox_url) 66 | .collect()) 67 | } 68 | 69 | pub async fn get_subscribers_activity_pub_ids( 70 | &self, 71 | limit: i64, 72 | offset: i64, 73 | db: &PgPool, 74 | ) -> sqlx::Result> { 75 | let subscriber = sqlx::query!( 76 | // language=PostgreSQL 77 | r#" 78 | SELECT u.activity_pub_id 79 | FROM issue_subscriber s 80 | JOIN users u ON s.subscriber = u.id 81 | LIMIT $1 82 | OFFSET $2 83 | "#, 84 | limit, 85 | offset 86 | ) 87 | .fetch_all(db) 88 | .await?; 89 | 90 | Ok(subscriber 91 | .into_iter() 92 | .map(|subscriber| subscriber.activity_pub_id) 93 | .collect()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/gill-db/src/user/follow.rs: -------------------------------------------------------------------------------- 1 | use crate::user::User; 2 | use sqlx::PgPool; 3 | 4 | impl User { 5 | pub async fn add_follower(&self, follower_id: i32, db: &PgPool) -> sqlx::Result<()> { 6 | sqlx::query!( 7 | // language=PostgreSQL 8 | r#" 9 | insert into "user_follow"( 10 | user_id, 11 | follower_id) 12 | values ($1, $2) 13 | "#, 14 | self.id, 15 | follower_id 16 | ) 17 | .execute(db) 18 | .await?; 19 | 20 | Ok(()) 21 | } 22 | 23 | pub async fn get_followers( 24 | &self, 25 | limit: i64, 26 | offset: i64, 27 | db: &PgPool, 28 | ) -> sqlx::Result> { 29 | let followers = sqlx::query_as!( 30 | User, 31 | // language=PostgreSQL 32 | r#" 33 | SELECT u.id, username, domain, email, public_key, private_key, inbox_url, outbox_url, 34 | followers_url, is_local, activity_pub_id 35 | FROM user_follow f 36 | JOIN users u ON f.user_id = u.id 37 | LIMIT $1 38 | OFFSET $2 39 | "#, 40 | limit, 41 | offset 42 | ) 43 | .fetch_all(db) 44 | .await?; 45 | 46 | Ok(followers) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/gill-db/src/user/ssh_keys.rs: -------------------------------------------------------------------------------- 1 | use crate::user::User; 2 | use sqlx::FromRow; 3 | use sqlx::PgPool; 4 | 5 | #[derive(Debug, FromRow)] 6 | pub struct SshKey { 7 | pub id: i32, 8 | pub key: String, 9 | pub name: String, 10 | pub key_type: String, 11 | pub owner_id: i32, 12 | } 13 | 14 | impl SshKey { 15 | pub async fn get(ssh_key: &str, key_type: &str, pool: &PgPool) -> sqlx::Result> { 16 | let key = sqlx::query_as!( 17 | SshKey, 18 | // language=PostgreSQL 19 | r#" 20 | select * from ssh_key 21 | where key_type = $1 AND key = $2; 22 | "#, 23 | key_type, 24 | ssh_key, 25 | ) 26 | .fetch_optional(pool) 27 | .await?; 28 | 29 | Ok(key) 30 | } 31 | } 32 | 33 | impl User { 34 | pub async fn add_ssh_key( 35 | &self, 36 | key_name: &str, 37 | ssh_key: &str, 38 | key_type: &str, 39 | pool: &PgPool, 40 | ) -> sqlx::Result<()> { 41 | sqlx::query!( 42 | // language=PostgreSQL 43 | r#" 44 | insert into "ssh_key"(owner_id, key, name, key_type) 45 | values ($1, $2, $3, $4) 46 | "#, 47 | &self.id, 48 | ssh_key, 49 | key_name, 50 | key_type, 51 | ) 52 | .execute(pool) 53 | .await?; 54 | 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/gill-git-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [gill-git-server-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..gill-git-server-v0.1.0) - 2023-01-18 6 | #### Features 7 | - implement activity pub ticket comment - ([e7cad5a](https://github.com/oknozor/gill/commit/e7cad5a48d5f9ba66c20b5e0ebdefe0ca6bf88bd)) - [@oknozor](https://github.com/oknozor) 8 | - syntect diff again - ([dff84e5](https://github.com/oknozor/gill/commit/dff84e5c269702d09934505ff4e90a092a068143)) - [@oknozor](https://github.com/oknozor) 9 | - add ssh key from UI - ([d7fa4c9](https://github.com/oknozor/gill/commit/d7fa4c9a751ca6cc46cd6ec5bd7292fb67c76c23)) - [@oknozor](https://github.com/oknozor) 10 | - fine grained ssh permission - ([3dc4b26](https://github.com/oknozor/gill/commit/3dc4b26f7f777a921245d99e9d3d0a32aa3807ce)) - [@oknozor](https://github.com/oknozor) 11 | - add settings view skeleton - ([9580561](https://github.com/oknozor/gill/commit/9580561876e837d0f077e7388b1341fa91700aab)) - [@oknozor](https://github.com/oknozor) 12 | #### Miscellaneous Chores 13 | - fmt & clippy - ([bc7b579](https://github.com/oknozor/gill/commit/bc7b57935fc2ec725f58610a0f56285725102043)) - [@oknozor](https://github.com/oknozor) 14 | - slowly adding templates - ([5f96d95](https://github.com/oknozor/gill/commit/5f96d9529a9aef13cc3b6034dd21683c25d797d6)) - [@oknozor](https://github.com/oknozor) 15 | #### Refactoring 16 | - extract syntax highlight to a dedicated crate - ([0875ad9](https://github.com/oknozor/gill/commit/0875ad900f819309209827df89b6639444fa9006)) - [@oknozor](https://github.com/oknozor) 17 | - single app for all http endpoint - ([e50141f](https://github.com/oknozor/gill/commit/e50141f5549ff6c050051a37fa3a00a5d055dd75)) - [@oknozor](https://github.com/oknozor) 18 | 19 | - - - 20 | 21 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/gill-git-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-git-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.66" 10 | shellwords = "1.1.0" 11 | tokio.workspace = true 12 | gill-db = { path = "../gill-db" } 13 | gill-settings = { path = "../gill-settings" } 14 | 15 | [[bin]] 16 | name = "post-receive" 17 | path = "src/post-receive.rs" 18 | 19 | [[bin]] 20 | name = "gill-git-server" 21 | path = "src/pack-serve.rs" -------------------------------------------------------------------------------- /crates/gill-git-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/gill-git-server/src/pack-serve.rs: -------------------------------------------------------------------------------- 1 | use gill_db::repository::Repository; 2 | use gill_db::user::User; 3 | use gill_db::PgPoolOptions; 4 | use gill_settings::SETTINGS; 5 | use std::env; 6 | use std::fs::OpenOptions; 7 | use std::io::Write; 8 | use std::process::{exit, Command, Stdio}; 9 | use std::time::Duration; 10 | 11 | #[tokio::main] 12 | async fn main() -> anyhow::Result<()> { 13 | let mut log_file = OpenOptions::new() 14 | .create(true) 15 | .write(true) 16 | .append(true) 17 | .open("/home/git/log.txt")?; 18 | 19 | let db = PgPoolOptions::new() 20 | .max_connections(1) 21 | .idle_timeout(Duration::from_secs(3)) 22 | .connect(&SETTINGS.database_url()) 23 | .await 24 | .expect("can connect to database"); 25 | 26 | let args: Vec = env::args().collect(); 27 | writeln!(log_file, "args: {args:?}")?; 28 | let [_, user_id] = args.as_slice() else { 29 | panic!("Expected user_id in pack-serve arguments {args:?}"); 30 | }; 31 | let user_id = user_id.parse()?; 32 | writeln!(log_file, "ssh connection from [user id: {user_id}]")?; 33 | let cmd = env::var("SSH_ORIGINAL_COMMAND")?; 34 | let words = shellwords::split(&cmd)?; 35 | writeln!(log_file, "command: {words:?}")?; 36 | let verb = &words[0]; 37 | let repo_path = &words[1]; 38 | let (owner, repo_name) = repo_path.rsplit_once('/').expect("/ in repo path"); 39 | // Ensure we are stripping out any absolute path components 40 | let owner = owner.split('/').last().expect("owner"); 41 | let repo_name = repo_name.strip_suffix(".git").expect(".git prefix"); 42 | 43 | if Repository::by_namespace(owner, repo_name, &db) 44 | .await 45 | .is_err() 46 | { 47 | eprintln!("Repository {owner}/{repo_name} not found"); 48 | exit(1); 49 | } else { 50 | eprintln!("Repository found {owner}/{repo_name}"); 51 | } 52 | 53 | let user = User::by_id(user_id, &db).await?; 54 | if user 55 | .get_local_repository_by_name(repo_name, &db) 56 | .await 57 | .is_err() 58 | { 59 | eprintln!("You don't have access to {owner}/{repo_name}"); 60 | exit(2); 61 | } else { 62 | eprintln!("Push access granted for user {}", user.username); 63 | } 64 | 65 | match verb.as_str() { 66 | "git-upload-pack" | "git-receive-pack" => { 67 | Command::new(verb) 68 | .current_dir("/home/git") 69 | .env("HOME", "/home/git") 70 | .stdout(Stdio::inherit()) 71 | .stdin(Stdio::inherit()) 72 | .stderr(Stdio::inherit()) 73 | .arg(repo_path) 74 | .output()?; 75 | 76 | writeln!(log_file, "cmd {cmd}")? 77 | } 78 | 79 | _ => writeln!(log_file, "Unknown command: {cmd}")?, 80 | }; 81 | 82 | log_file.flush()?; 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /crates/gill-git-server/src/post-receive.rs: -------------------------------------------------------------------------------- 1 | use gill_db::repository::Repository; 2 | use gill_db::PgPoolOptions; 3 | use gill_settings::SETTINGS; 4 | use std::env; 5 | use std::fs::OpenOptions; 6 | use std::io::Write; 7 | use std::io::{stdin, BufRead}; 8 | use std::path::PathBuf; 9 | use std::time::Duration; 10 | 11 | #[tokio::main] 12 | async fn main() -> anyhow::Result<()> { 13 | let db = PgPoolOptions::new() 14 | .max_connections(1) 15 | .idle_timeout(Duration::from_secs(3)) 16 | .connect(&SETTINGS.database_url()) 17 | .await 18 | .expect("can connect to database"); 19 | 20 | let mut log_file = OpenOptions::new() 21 | .create(true) 22 | .write(true) 23 | .append(true) 24 | .open("/home/git/post-receive-logs.txt")?; 25 | 26 | let git_dir = env::var("GIT_DIR")?; 27 | 28 | // Post receive hooks args are received via stdin 29 | let args: String = stdin().lock().lines().filter_map(Result::ok).collect(); 30 | 31 | let args: Vec<&str> = args.split(' ').collect(); 32 | 33 | let [_, _sha, git_ref] = args.as_slice() else { 34 | panic!("Unhandled post-receive hook arguments {args:?}"); 35 | }; 36 | 37 | let git_dir = PathBuf::from(git_dir).canonicalize()?; 38 | 39 | let repository_name = git_dir 40 | .file_name() 41 | .expect("GIT_DIR is not set") 42 | .to_string_lossy(); 43 | 44 | let repository_owner = git_dir 45 | .parent() 46 | .expect("Failed to get repository owner from GIT_DIR") 47 | .file_name() 48 | .expect("Failed to get owner name from GIT_DIR (Utf8 error") 49 | .to_string_lossy(); 50 | 51 | let repository_name = repository_name 52 | .strip_suffix(".git") 53 | .expect("Invalid repo path, expected '.git' suffix"); 54 | 55 | match git_ref.strip_prefix("refs/heads/") { 56 | Some(branch) => { 57 | let repo = Repository::by_namespace(&repository_owner, repository_name, &db).await?; 58 | let branches = repo.list_branches(i64::MAX, 0, &db).await?; 59 | let branches: Vec<&str> = branches.iter().map(|branch| branch.name.as_str()).collect(); 60 | 61 | if branches.is_empty() { 62 | repo.set_default_branch(branch, &db).await?; 63 | writeln!( 64 | log_file, 65 | "default branch {branch} set for {repository_owner}/{repository_name}" 66 | )?; 67 | } else if !branches.contains(&branch) { 68 | repo.create_branch(branch, &db).await?; 69 | } else { 70 | writeln!(log_file, "existing branch")?; 71 | } 72 | } 73 | None => writeln!(log_file, "branch not found")?, 74 | } 75 | 76 | log_file.flush()?; 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /crates/gill-git/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-git" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | git-repository = { version = "0.31.0", features = ["blocking-network-client"] } 8 | anyhow = "1.0.66" 9 | tracing = "0.1" 10 | imara-diff = "0.1.5" 11 | mime_guess = "2.0.4" 12 | cmd_lib = "1.3.0" 13 | 14 | [dev-dependencies] 15 | sealed_test = "1.0.0" 16 | speculoos = "0.11.0" -------------------------------------------------------------------------------- /crates/gill-git/src/diffs/commit.rs: -------------------------------------------------------------------------------- 1 | use crate::diffs::Diff; 2 | use crate::GitRepository; 3 | use git_repository::{Id, ObjectId}; 4 | 5 | impl GitRepository { 6 | pub fn commit_diff(&self, sha: &str) -> anyhow::Result> { 7 | let object_id = ObjectId::from_hex(sha.as_bytes())?; 8 | let commit = self.inner.find_object(object_id)?.try_into_commit()?; 9 | let parents: Vec = commit.parent_ids().collect(); 10 | let parent = parents.first(); 11 | let parent_tree = match parent { 12 | None => self.inner.empty_tree(), 13 | Some(parent_id) => { 14 | let parent = parent_id.object()?; 15 | parent.peel_to_tree()? 16 | } 17 | }; 18 | 19 | self.diff_tree_to_tree(parent_tree, commit.tree()?) 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use crate::GitRepository; 26 | use anyhow::{anyhow, Result}; 27 | use cmd_lib::{run_cmd, run_fun}; 28 | use sealed_test::prelude::*; 29 | use speculoos::prelude::*; 30 | use std::fs; 31 | 32 | // Helper function to create a commit and get its sha1 33 | fn git_commit(message: &str) -> anyhow::Result { 34 | run_fun!( 35 | git commit --allow-empty -q -m $message; 36 | git log --format=%H -n 1; 37 | ) 38 | .map_err(|e| anyhow!(e)) 39 | } 40 | 41 | #[sealed_test] 42 | fn should_get_diff_when_commit_has_parent() -> Result<()> { 43 | // Arrange 44 | run_cmd!(git init;)?; 45 | fs::write("file", "changes")?; 46 | run_cmd!(git add .;)?; 47 | let _ = git_commit("first commit")?; 48 | fs::write("file2", "changes")?; 49 | run_cmd!(git add .;)?; 50 | let commit_two = git_commit("second commit")?; 51 | 52 | let repo = GitRepository { 53 | inner: git_repository::open(".")?, 54 | }; 55 | 56 | // Act 57 | let diffs = repo.commit_diff(&commit_two); 58 | 59 | // Assert 60 | assert_that!(diffs).is_ok().has_length(1); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[sealed_test] 66 | fn should_get_diff_when_without_commit_parent() -> Result<()> { 67 | // Arrange 68 | run_cmd!(git init;)?; 69 | fs::write("file", "changes")?; 70 | run_cmd!(git add .;)?; 71 | let commit = git_commit("first commit")?; 72 | 73 | let repo = GitRepository { 74 | inner: git_repository::open(".")?, 75 | }; 76 | 77 | // Act 78 | let diffs = repo.commit_diff(&commit); 79 | 80 | // Assert 81 | println!("{diffs:?}"); 82 | assert_that!(diffs).is_ok().has_length(1); 83 | 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/gill-git/src/diffs/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::diffs::Diff; 2 | use crate::{ref_to_tree, GitRepository}; 3 | 4 | impl GitRepository { 5 | pub fn diff(&self, branch: &str, other: &str) -> anyhow::Result> { 6 | let repository = &self.inner; 7 | let tree = ref_to_tree(Some(&format!("heads/{branch}")), repository)?; 8 | let other = ref_to_tree(Some(&format!("heads/{other}")), repository)?; 9 | self.diff_tree_to_tree(tree, other) 10 | } 11 | } 12 | 13 | #[cfg(test)] 14 | mod test { 15 | use crate::GitRepository; 16 | use anyhow::Result; 17 | use cmd_lib::run_cmd; 18 | use sealed_test::prelude::*; 19 | use speculoos::prelude::*; 20 | use std::fs; 21 | 22 | #[sealed_test] 23 | fn should_get_diff() -> Result<()> { 24 | // Arrange 25 | run_cmd!(git init;)?; 26 | fs::write("file", "changes")?; 27 | run_cmd!( 28 | git add .; 29 | git commit -m "first commit"; 30 | git checkout -b other; 31 | )?; 32 | fs::write("file2", "changes")?; 33 | run_cmd!( 34 | git add .; 35 | git commit -m "second commit"; 36 | )?; 37 | 38 | let repo = GitRepository { 39 | inner: git_repository::open(".")?, 40 | }; 41 | 42 | // Act 43 | let diffs = repo.diff("master", "other"); 44 | 45 | // Assert 46 | assert_that!(diffs).is_ok().has_length(1); 47 | 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/gill-git/src/init.rs: -------------------------------------------------------------------------------- 1 | use crate::{GitRepository, REPO_DIR}; 2 | 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | pub fn init_bare(namespace: &str, name: &str) -> anyhow::Result { 7 | let path = PathBuf::from(REPO_DIR).join(namespace); 8 | 9 | if !path.exists() { 10 | fs::create_dir(&path).expect("Failed to create dir"); 11 | } 12 | 13 | GitRepository::init_bare(path, name) 14 | } 15 | 16 | impl GitRepository { 17 | pub fn list_branches(&self) -> anyhow::Result> { 18 | let refs = self.inner.references()?; 19 | let mut branches = vec![]; 20 | for branch in refs.local_branches()? { 21 | let branch = branch.map(|branch| branch.name().shorten().to_string()); 22 | match branch { 23 | Ok(branch) => branches.push(branch), 24 | Err(e) => tracing::error!("Failed to list branch: {e}"), 25 | } 26 | } 27 | 28 | Ok(branches) 29 | } 30 | } 31 | 32 | mod imp { 33 | use crate::GitRepository; 34 | 35 | use std::path::PathBuf; 36 | 37 | impl GitRepository { 38 | pub fn init_bare(base: PathBuf, name: &str) -> anyhow::Result { 39 | let path = base.join(format!("{name}.git")); 40 | tracing::debug!("Initializing repository {:?}", path); 41 | let repository = git_repository::init_bare(path)?; 42 | let hook_path = repository.path().join("hooks"); 43 | std::os::unix::fs::symlink( 44 | "/usr/share/git-core/templates/hooks/post-receive", 45 | hook_path.join("post-receive"), 46 | )?; 47 | 48 | Ok(GitRepository { inner: repository }) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod test { 54 | use crate::GitRepository; 55 | use cmd_lib::run_cmd; 56 | use sealed_test::prelude::*; 57 | use speculoos::prelude::*; 58 | 59 | use std::path::PathBuf; 60 | 61 | #[sealed_test] 62 | fn should_init_bare() -> anyhow::Result<()> { 63 | let repository = GitRepository::init_bare(PathBuf::from("."), "repo")?; 64 | assert_that!(repository.inner.path().to_string_lossy()).ends_with("repo.git"); 65 | Ok(()) 66 | } 67 | 68 | #[sealed_test] 69 | fn should_list_branches() -> anyhow::Result<()> { 70 | run_cmd!( 71 | git init; 72 | git commit --allow-empty -m "First commit"; 73 | git checkout -b A; 74 | git checkout -b B; 75 | 76 | )?; 77 | 78 | let repo = GitRepository { 79 | inner: git_repository::open(".")?, 80 | }; 81 | let branches = repo.list_branches(); 82 | 83 | assert_that!(branches).is_ok().contains_all_of(&[ 84 | &"master".to_string(), 85 | &"A".to_string(), 86 | &"B".to_string(), 87 | ]); 88 | 89 | Ok(()) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/gill-git/src/lib.rs: -------------------------------------------------------------------------------- 1 | use git_repository::{Commit, Id, Repository, Tree}; 2 | use std::path::PathBuf; 3 | 4 | pub mod clone; 5 | pub mod commits; 6 | pub mod diffs; 7 | pub mod init; 8 | pub mod merge; 9 | pub mod ssh; 10 | pub mod traversal; 11 | 12 | const REPO_DIR: &str = "/home/git"; 13 | 14 | #[derive(Debug)] 15 | pub struct GitRepository { 16 | inner: Repository, 17 | } 18 | 19 | impl GitRepository { 20 | pub fn open(owner: &str, name: &str) -> anyhow::Result { 21 | let path = PathBuf::from(REPO_DIR) 22 | .join(owner) 23 | .join(format!("{name}.git")); 24 | Ok(Self { 25 | inner: git_repository::open(path)?, 26 | }) 27 | } 28 | 29 | pub(crate) fn path(&self) -> PathBuf { 30 | self.inner.path().to_path_buf() 31 | } 32 | 33 | pub(crate) fn non_bare_path(&self) -> PathBuf { 34 | let mut path = self.inner.path().to_path_buf(); 35 | if !self.inner.is_bare() { 36 | return self.path(); 37 | } 38 | 39 | let filename = path 40 | .file_name() 41 | .expect("filename") 42 | .to_string_lossy() 43 | .to_string(); 44 | 45 | path.pop(); 46 | 47 | let path = path.join(format!("non-bare-copy-{filename}")); 48 | path 49 | } 50 | } 51 | 52 | pub(crate) fn ref_to_tree<'repo>( 53 | reference: Option<&str>, 54 | repo: &'repo Repository, 55 | ) -> anyhow::Result> { 56 | Ok(match reference { 57 | Some(reference) => repo 58 | .find_reference(reference)? 59 | .peel_to_id_in_place()? 60 | .object()? 61 | .try_into_commit()? 62 | .tree()?, 63 | None => repo.head()?.peel_to_commit_in_place()?.tree()?, 64 | }) 65 | } 66 | 67 | pub(crate) fn id_to_commit<'a>(id: &'a Id) -> anyhow::Result> { 68 | let object = id.try_object()?; 69 | let object = object.expect("empty"); 70 | let commit = object.try_into_commit()?; 71 | Ok(commit) 72 | } 73 | 74 | #[cfg(test)] 75 | mod test { 76 | use crate::GitRepository; 77 | use cmd_lib::run_cmd; 78 | use sealed_test::prelude::*; 79 | use speculoos::prelude::*; 80 | use std::path::PathBuf; 81 | 82 | #[sealed_test] 83 | fn should_get_repository_path() -> anyhow::Result<()> { 84 | run_cmd!(git init repo;)?; 85 | 86 | let repository = GitRepository { 87 | inner: git_repository::open("repo")?, 88 | }; 89 | 90 | assert_that!(repository.path()).is_equal_to(&PathBuf::from("repo/.git")); 91 | Ok(()) 92 | } 93 | 94 | #[sealed_test] 95 | fn should_get_bare_repository_path() -> anyhow::Result<()> { 96 | run_cmd!(git init --bare repo;)?; 97 | 98 | let repository = GitRepository { 99 | inner: git_repository::open("repo")?, 100 | }; 101 | 102 | assert_that!(repository.path()).is_equal_to(&PathBuf::from("repo")); 103 | Ok(()) 104 | } 105 | 106 | #[sealed_test] 107 | fn should_append_ssh_key() -> anyhow::Result<()> { 108 | run_cmd!(git init --bare repo;)?; 109 | 110 | let repository = GitRepository { 111 | inner: git_repository::open("repo")?, 112 | }; 113 | 114 | assert_that!(repository.path()).is_equal_to(&PathBuf::from("repo")); 115 | Ok(()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/gill-git/src/ssh.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub fn append_key(ssh_key: &str, user_id: i32) -> io::Result<()> { 4 | imp::append_ssh_key(ssh_key, user_id, "/home/git/.ssh/authorized_keys") 5 | } 6 | 7 | mod imp { 8 | use std::io::Write; 9 | use std::path::Path; 10 | use std::{fs, io}; 11 | 12 | pub fn append_ssh_key>(ssh_key: &str, user_id: i32, path: S) -> io::Result<()> { 13 | let mut file = fs::OpenOptions::new().write(true).append(true).open(path)?; 14 | 15 | writeln!(file, r#"command="gill-git-server {user_id}" {ssh_key}"#) 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod test { 21 | use sealed_test::prelude::*; 22 | use speculoos::prelude::*; 23 | use std::fs; 24 | 25 | #[sealed_test] 26 | fn should_append_key_to_file() -> anyhow::Result<()> { 27 | let ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8LAX4tJ/0CrE7CIsbi6J454nP67G0aCYK+cVHrdB3l okno@archlinux"; 28 | 29 | let rsa = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4oKbuajSfuNoLEhSqXoE+TLyFr0eopBDF3X= johnyboy@hometown"; 30 | 31 | fs::File::create("authorized_keys")?; 32 | super::imp::append_ssh_key(ed25519, 1, "authorized_keys")?; 33 | super::imp::append_ssh_key(rsa, 2, "authorized_keys")?; 34 | let result = fs::read_to_string("authorized_keys")?; 35 | 36 | assert_that!(result).is_equal_to( 37 | r#"command="gill-git-server 1" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8LAX4tJ/0CrE7CIsbi6J454nP67G0aCYK+cVHrdB3l okno@archlinux 38 | command="gill-git-server 2" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4oKbuajSfuNoLEhSqXoE+TLyFr0eopBDF3X= johnyboy@hometown 39 | "#.to_string()); 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/gill-markdown/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [gill-markdown-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..gill-markdown-v0.1.0) - 2023-01-18 6 | #### Bug Fixes 7 | - fix non bare repo paths - ([49a44b6](https://github.com/oknozor/gill/commit/49a44b6314d5afcea30389bf4fdcc0d21513aead)) - [@oknozor](https://github.com/oknozor) 8 | #### Features 9 | - **(git)** add diff for a single commit - ([1434e08](https://github.com/oknozor/gill/commit/1434e08e5716b1274e8030407777370a2d6394b8)) - [@oknozor](https://github.com/oknozor) 10 | - improve markdown rendering style - ([505546a](https://github.com/oknozor/gill/commit/505546a9ae60c92316b4c2407d029af00a3167f1)) - [@oknozor](https://github.com/oknozor) 11 | - add markdown preview and wasm module - ([910c9d3](https://github.com/oknozor/gill/commit/910c9d32cf9ae5d476611d38135a8e8be32deb36)) - [@oknozor](https://github.com/oknozor) 12 | - canonicalize image links in markdown - ([3bbfb8a](https://github.com/oknozor/gill/commit/3bbfb8aeb08d031a5a5563aa1dfb41877a792f37)) - [@oknozor](https://github.com/oknozor) 13 | 14 | - - - 15 | 16 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/gill-markdown/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-markdown" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | pulldown-cmark = "0.9.2" 10 | quick-xml = "0.27.1" 11 | 12 | [dev-dependencies] 13 | speculoos.workspace = true -------------------------------------------------------------------------------- /crates/gill-settings/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [gill-settings-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..gill-settings-v0.1.0) - 2023-01-18 6 | #### Build system 7 | - add production docker image - ([8bcdd72](https://github.com/oknozor/gill/commit/8bcdd72fb65809ce5199639a1b03d180675e567b)) - [@oknozor](https://github.com/oknozor) 8 | #### Features 9 | - implement activity pub ticket - ([3a005e5](https://github.com/oknozor/gill/commit/3a005e5fa703073585f8146e1592146d4a2bec9d)) - [@oknozor](https://github.com/oknozor) 10 | - add ssh key from UI - ([d7fa4c9](https://github.com/oknozor/gill/commit/d7fa4c9a751ca6cc46cd6ec5bd7292fb67c76c23)) - [@oknozor](https://github.com/oknozor) 11 | - implement follow activity - ([c8ad38e](https://github.com/oknozor/gill/commit/c8ad38e2eb5e607e33f36109d34c5778a9e47fed)) - [@oknozor](https://github.com/oknozor) 12 | - first apub interaction - ([25c3dcf](https://github.com/oknozor/gill/commit/25c3dcfc265ace5599106632640565437092ed6c)) - [@oknozor](https://github.com/oknozor) 13 | #### Tests 14 | - fix tests - ([ddde273](https://github.com/oknozor/gill/commit/ddde2731da692745f27631e36c3c4267a6a66b1b)) - [@oknozor](https://github.com/oknozor) 15 | 16 | - - - 17 | 18 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/gill-settings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-settings" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde.workspace = true 10 | tracing.workspace = true 11 | url.workspace = true 12 | config = "0.13.2" 13 | once_cell.workspace = true 14 | 15 | -------------------------------------------------------------------------------- /crates/gill-syntax/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [gill-syntax-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..gill-syntax-v0.1.0) - 2023-01-18 6 | #### Features 7 | - **(app)** pull request comments - ([8a5ba1f](https://github.com/oknozor/gill/commit/8a5ba1fdbbcda93fd1690d70ccfcbddbb67db5b2)) - [@oknozor](https://github.com/oknozor) 8 | - **(git)** add diff for a single commit - ([1434e08](https://github.com/oknozor/gill/commit/1434e08e5716b1274e8030407777370a2d6394b8)) - [@oknozor](https://github.com/oknozor) 9 | - rebase and merge from UI - ([a69b42d](https://github.com/oknozor/gill/commit/a69b42da3fd8fb350041914f065218f3505bedec)) - [@oknozor](https://github.com/oknozor) 10 | - base pull requests - ([058d054](https://github.com/oknozor/gill/commit/058d0546320f3b184ca5096e1cde6a8bd80fe9cc)) - [@oknozor](https://github.com/oknozor) 11 | #### Refactoring 12 | - extract syntax highlight to a dedicated crate - ([0875ad9](https://github.com/oknozor/gill/commit/0875ad900f819309209827df89b6639444fa9006)) - [@oknozor](https://github.com/oknozor) 13 | 14 | - - - 15 | 16 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/gill-syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-syntax" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | gill-git = { path = "../gill-git" } 10 | syntect.workspace = true 11 | anyhow.workspace = true 12 | once_cell.workspace = true 13 | -------------------------------------------------------------------------------- /crates/gill-syntax/src/highlight.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | 3 | use syntect::highlighting::{Color, Theme}; 4 | 5 | use crate::{highlighter_for_extension, SYNTAX_SET, THEME}; 6 | use syntect::html::{append_highlighted_html_for_styled_line, IncludeBackground}; 7 | 8 | use syntect::util::LinesWithEndings; 9 | 10 | pub fn highlight_blob(content: &str, extension: &str) -> anyhow::Result { 11 | let mut highlighter = highlighter_for_extension(extension) 12 | .ok_or_else(|| anyhow!("syntax set not found for extension: {extension}"))?; 13 | 14 | let (mut output, bg) = start_highlighted_html(&THEME); 15 | output.push_str(""); 16 | for (idx, line) in LinesWithEndings::from(content).enumerate() { 17 | let line_number = idx + 1; 18 | output.push_str(&format!(r#""#)); 19 | output.push_str(&format!( 20 | r#"{line_number}"# 21 | )); 22 | output.push_str(""); 23 | let regions = highlighter.highlight_line(line, &SYNTAX_SET)?; 24 | append_highlighted_html_for_styled_line( 25 | ®ions[..], 26 | IncludeBackground::IfDifferent(bg), 27 | &mut output, 28 | )?; 29 | output.push_str(""); 30 | output.push_str(""); 31 | } 32 | output.push_str(""); 33 | output.push_str(""); 34 | Ok(output) 35 | } 36 | 37 | pub fn start_highlighted_html(t: &Theme) -> (String, Color) { 38 | let c = t.settings.background.unwrap_or(Color::WHITE); 39 | ( 40 | format!( 41 | "\n", 42 | c.r, c.g, c.b 43 | ), 44 | c, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /crates/gill-syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use syntect::easy::HighlightLines; 3 | use syntect::highlighting::Theme; 4 | use syntect::parsing::SyntaxSet; 5 | 6 | pub mod diff; 7 | pub mod highlight; 8 | 9 | const SYNTAX_SET_DATA: &[u8] = include_bytes!("../syntax.bin"); 10 | const THEME_DATA: &[u8] = include_bytes!("../theme.bin"); 11 | 12 | pub static SYNTAX_SET: Lazy = Lazy::new(|| syntect::dumps::from_binary(SYNTAX_SET_DATA)); 13 | pub static THEME: Lazy = Lazy::new(|| syntect::dumps::from_binary(THEME_DATA)); 14 | 15 | pub fn highlighter_for_extension(extension: &str) -> Option { 16 | SYNTAX_SET 17 | .find_syntax_by_extension(extension) 18 | .map(|syntax| HighlightLines::new(syntax, &THEME)) 19 | } 20 | -------------------------------------------------------------------------------- /crates/gill-syntax/syntax.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-syntax/syntax.bin -------------------------------------------------------------------------------- /crates/gill-syntax/theme.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/crates/gill-syntax/theme.bin -------------------------------------------------------------------------------- /crates/gill-web-markdown/.appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 | - rustc -V 6 | - cargo -V 7 | 8 | build: false 9 | 10 | test_script: 11 | - cargo test --locked 12 | -------------------------------------------------------------------------------- /crates/gill-web-markdown/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /crates/gill-web-markdown/.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | 4 | cache: cargo 5 | 6 | matrix: 7 | include: 8 | 9 | # Builds with wasm-pack. 10 | - rust: beta 11 | env: RUST_BACKTRACE=1 12 | addons: 13 | firefox: latest 14 | chrome: stable 15 | before_script: 16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 18 | - cargo install-update -a 19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 20 | script: 21 | - cargo generate --git . --name testing 22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 23 | # in any of our parent dirs is problematic. 24 | - mv Cargo.toml Cargo.toml.tmpl 25 | - cd testing 26 | - wasm-pack build 27 | - wasm-pack test --chrome --firefox --headless 28 | 29 | # Builds on nightly. 30 | - rust: nightly 31 | env: RUST_BACKTRACE=1 32 | before_script: 33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 35 | - cargo install-update -a 36 | - rustup target add wasm32-unknown-unknown 37 | script: 38 | - cargo generate --git . --name testing 39 | - mv Cargo.toml Cargo.toml.tmpl 40 | - cd testing 41 | - cargo check 42 | - cargo check --target wasm32-unknown-unknown 43 | - cargo check --no-default-features 44 | - cargo check --target wasm32-unknown-unknown --no-default-features 45 | - cargo check --no-default-features --features console_error_panic_hook 46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 49 | 50 | # Builds on beta. 51 | - rust: beta 52 | env: RUST_BACKTRACE=1 53 | before_script: 54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 56 | - cargo install-update -a 57 | - rustup target add wasm32-unknown-unknown 58 | script: 59 | - cargo generate --git . --name testing 60 | - mv Cargo.toml Cargo.toml.tmpl 61 | - cd testing 62 | - cargo check 63 | - cargo check --target wasm32-unknown-unknown 64 | - cargo check --no-default-features 65 | - cargo check --target wasm32-unknown-unknown --no-default-features 66 | - cargo check --no-default-features --features console_error_panic_hook 67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 68 | # Note: no enabling the `wee_alloc` feature here because it requires 69 | # nightly for now. 70 | -------------------------------------------------------------------------------- /crates/gill-web-markdown/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [gill-web-markdown-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..gill-web-markdown-v0.1.0) - 2023-01-18 6 | #### Features 7 | - **(app)** pull request comments - ([8a5ba1f](https://github.com/oknozor/gill/commit/8a5ba1fdbbcda93fd1690d70ccfcbddbb67db5b2)) - [@oknozor](https://github.com/oknozor) 8 | - add markdown preview and wasm module - ([910c9d3](https://github.com/oknozor/gill/commit/910c9d32cf9ae5d476611d38135a8e8be32deb36)) - [@oknozor](https://github.com/oknozor) 9 | #### Refactoring 10 | - remove unused deps - ([9e67cbf](https://github.com/oknozor/gill/commit/9e67cbfe394411f9573b9a29eadb9b930670b49b)) - [@oknozor](https://github.com/oknozor) 11 | 12 | - - - 13 | 14 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/gill-web-markdown/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gill-web-markdown" 3 | version = "0.1.0" 4 | authors = ["Paul Delafosse "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2.63" 12 | gill-markdown = { path = "../gill-markdown" } 13 | wee_alloc = { version = "0.4.5", optional = true } 14 | 15 | [features] 16 | default = ["wee_alloc"] 17 | 18 | [dev-dependencies] 19 | wasm-bindgen-test = "0.3.13" -------------------------------------------------------------------------------- /crates/gill-web-markdown/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Paul Delafosse 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /crates/gill-web-markdown/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

wasm-pack-template

4 | 5 | A template for kick starting a Rust and WebAssembly project using wasm-pack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Tutorial 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 |
19 | 20 | ## About 21 | 22 | [**📚 Read this template tutorial! 📚**][template-docs] 23 | 24 | This template is designed for compiling Rust libraries into WebAssembly and 25 | publishing the resulting package to NPM. 26 | 27 | Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other 28 | templates and usages of `wasm-pack`. 29 | 30 | [tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html 31 | [template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html 32 | 33 | ## 🚴 Usage 34 | 35 | ### 🐑 Use `cargo generate` to Clone this Template 36 | 37 | [Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) 38 | 39 | ``` 40 | cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project 41 | cd my-project 42 | ``` 43 | 44 | ### 🛠️ Build with `wasm-pack build` 45 | 46 | ``` 47 | wasm-pack build 48 | ``` 49 | 50 | ### 🔬 Test in Headless Browsers with `wasm-pack test` 51 | 52 | ``` 53 | wasm-pack test --headless --firefox 54 | ``` 55 | 56 | ### 🎁 Publish to NPM with `wasm-pack publish` 57 | 58 | ``` 59 | wasm-pack publish 60 | ``` 61 | 62 | ## 🔋 Batteries Included 63 | 64 | * [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating 65 | between WebAssembly and JavaScript. 66 | * [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) 67 | for logging panic messages to the developer console. 68 | * [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized 69 | for small code size. 70 | * `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you 71 | 72 | ## License 73 | 74 | Licensed under either of 75 | 76 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 77 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 78 | 79 | at your option. 80 | 81 | ### Contribution 82 | 83 | Unless you explicitly state otherwise, any contribution intentionally 84 | submitted for inclusion in the work by you, as defined in the Apache-2.0 85 | license, shall be dual licensed as above, without any additional terms or 86 | conditions. -------------------------------------------------------------------------------- /crates/gill-web-markdown/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 4 | // allocator. 5 | #[cfg(feature = "wee_alloc")] 6 | #[global_allocator] 7 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 8 | 9 | #[wasm_bindgen] 10 | pub fn render_markdown(markdown_input: String, owner: String, repository: String) -> String { 11 | gill_markdown::render(&markdown_input, &owner, &repository) 12 | } 13 | -------------------------------------------------------------------------------- /crates/gill-web-markdown/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /crates/syntect-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## [syntect-plugin-v0.1.0](https://github.com/oknozor/gill/compare/f81dee255ce5d86aad8119a44b8232153b30daca..syntect-plugin-v0.1.0) - 2023-01-18 6 | #### Features 7 | - add templates for user profile and settings - ([2789ba7](https://github.com/oknozor/gill/commit/2789ba7d1b729af7e50e352f1cc20becaea41c78)) - [@oknozor](https://github.com/oknozor) 8 | - reorganize submodules - ([1d8b5e4](https://github.com/oknozor/gill/commit/1d8b5e408b41f5a31277989fda2d7aa6cb17b8db)) - [@oknozor](https://github.com/oknozor) 9 | - in memory assets - ([c135100](https://github.com/oknozor/gill/commit/c135100e2f3d53ed48fcbafe621c789b58c83dcc)) - [@oknozor](https://github.com/oknozor) 10 | 11 | - - - 12 | 13 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). -------------------------------------------------------------------------------- /crates/syntect-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "syntect-plugin" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | syntect.workspace = true 10 | anyhow.workspace = true -------------------------------------------------------------------------------- /crates/syntect-plugin/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use syntect::highlighting::ThemeSet; 3 | use syntect::parsing::SyntaxSetBuilder; 4 | 5 | fn main() -> Result<()> { 6 | dump_theme("theme.bin")?; 7 | dump_syntax("syntax.bin")?; 8 | Ok(()) 9 | } 10 | 11 | pub fn dump_theme(out: &str) -> Result<()> { 12 | let mut themes = ThemeSet::new(); 13 | 14 | // FIXME, we don't have a theme anymore 15 | themes 16 | .add_from_folder("syntect/default_theme.tmTheme") 17 | .expect("Failed to load syntect theme"); 18 | 19 | let theme = themes 20 | .themes 21 | .get("default_theme") 22 | .expect("Default theme missing"); 23 | 24 | syntect::dumps::dump_to_file(&theme, out)?; 25 | Ok(()) 26 | } 27 | 28 | pub fn dump_syntax(out: &str) -> Result<()> { 29 | let mut syntax_definitions = SyntaxSetBuilder::new(); 30 | syntax_definitions 31 | .add_from_folder("syntect", false) 32 | .expect("Failed to load syntax definitions"); 33 | 34 | let set = syntax_definitions.build(); 35 | syntect::dumps::dump_to_file(&set, out)?; 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | gill: 4 | build: 5 | dockerfile: Dockerfile.dev 6 | context: ./ 7 | depends_on: 8 | - postgres 9 | restart: always 10 | container_name: gill-app 11 | ports: 12 | - "3000:3000" 13 | - "2222:22" 14 | volumes: 15 | - ./docker/dev/home:/home/git 16 | - ./crates/gill-app/assets:/opt/gill/assets 17 | - ./docker/dev/config-instance-1.toml:/opt/gill/config.toml 18 | entrypoint: /home/git/entrypoint-debug.sh 19 | 20 | gill-2: 21 | build: 22 | dockerfile: Dockerfile.dev 23 | context: ./ 24 | depends_on: 25 | - postgres 26 | restart: always 27 | container_name: gill-app-2 28 | ports: 29 | - "3001:3000" 30 | - "12222:22" 31 | volumes: 32 | - ./docker/dev/home2:/home/git 33 | - ./crates/gill-app/assets:/opt/gill/assets 34 | - ./docker/dev/config-instance-2.toml:/opt/gill/config.toml 35 | entrypoint: /home/git/entrypoint-debug.sh 36 | 37 | postgres: 38 | image: docker.io/postgres:13.2 39 | restart: unless-stopped 40 | environment: 41 | POSTGRES_USER: "postgres" 42 | POSTGRES_PASSWORD: "postgres" 43 | POSTGRES_DB: "gill" 44 | ports: 45 | - "5432:5432" 46 | volumes: 47 | - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | gill: 5 | build: 6 | dockerfile: Dockerfile 7 | context: ./ 8 | image: "gillpub/gill:latest-amd" 9 | depends_on: 10 | - postgres 11 | restart: unless-stopped 12 | container_name: gill 13 | environment: 14 | GILL_DB_NAME: gill 15 | GILL_DB_HOST: postgres 16 | GILL_DB_PORT: 5432 17 | GILL_DB_USER: postgres 18 | GILL_DB_PASSWORD: postgres 19 | GILL_GILL_ASSETS: /opt/gill/assets 20 | GILL_OAUTH_CLIENT_ID: gill 21 | GILL_OAUTH_CLIENT_SECRET: 8Nup063EeIOYzSsEyVZkbo67sUpIX0Bc 22 | GILL_OAUTH_PROVIDER: https://keycloak.cloud.hoohoot.org 23 | GILL_OAUTH_USER_INFO_URL: /auth/realms/hoohoot/protocol/openid-connect/userinfo 24 | GILL_OAUTH_TOKEN_URL: /auth/realms/hoohoot/protocol/openid-connect/token 25 | GILL_OAUTH_AUTH_URL: /auth/realms/hoohoot/protocol/openid-connect/auth 26 | GILL_DOMAIN: bore.pub:45136 27 | GILL_PORT: 3000 28 | GILL_DEBUG: false 29 | env_file: 30 | - docker/sshd.env 31 | ports: 32 | - "3000:3000" 33 | - "2222:22" 34 | volumes: 35 | - gill_data:/home/git 36 | networks: 37 | - gill 38 | 39 | postgres: 40 | image: docker.io/postgres:13.2 41 | restart: unless-stopped 42 | environment: 43 | POSTGRES_USER: "postgres" 44 | POSTGRES_PASSWORD: "postgres" 45 | POSTGRES_DB: "gill" 46 | ports: 47 | - "5432:5432" 48 | volumes: 49 | - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql 50 | networks: 51 | - gill 52 | 53 | volumes: 54 | gill_data: 55 | networks: 56 | gill: -------------------------------------------------------------------------------- /docker/dev/config-instance-1.toml: -------------------------------------------------------------------------------- 1 | domain = "bore.pub:45136" 2 | debug = true 3 | port = 3000 4 | ssh_port = 2222 5 | 6 | [database] 7 | host = "postgres" 8 | port = 5432 9 | database = "gill" 10 | user = "postgres" 11 | password = "postgres" 12 | 13 | [oauth_provider] 14 | client_id = "gill" 15 | client_secret = "n5obgGTk855H1Mx3b2YG2JCO8Bc6WGq1" 16 | provider = "https://keycloak.cloud.hoohoot.org" 17 | user_info_url = "/auth/realms/hoohoot/protocol/openid-connect/userinfo" 18 | auth_url = "/auth/realms/hoohoot/protocol/openid-connect/auth" 19 | token_url = "/auth/realms/hoohoot/protocol/openid-connect/token" -------------------------------------------------------------------------------- /docker/dev/config-instance-2.toml: -------------------------------------------------------------------------------- 1 | domain = "bore.pub:37349" 2 | debug = true 3 | port = 3000 4 | ssh_port = 2222 5 | 6 | [database] 7 | host = "postgres" 8 | port = 5432 9 | database = "gill_2" 10 | user = "postgres" 11 | password = "postgres" 12 | 13 | [oauth_provider] 14 | client_id = "gill" 15 | client_secret = "n5obgGTk855H1Mx3b2YG2JCO8Bc6WGq1" 16 | provider = "https://keycloak.cloud.hoohoot.org" 17 | user_info_url = "/auth/realms/hoohoot/protocol/openid-connect/userinfo" 18 | auth_url = "/auth/realms/hoohoot/protocol/openid-connect/auth" 19 | token_url = "/auth/realms/hoohoot/protocol/openid-connect/token" -------------------------------------------------------------------------------- /docker/dev/home/entrypoint-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /usr/sbin/sshd 6 | cd /home/git 7 | 8 | tail -f /dev/null -------------------------------------------------------------------------------- /docker/dev/home2/.ssh/authorized_keys: -------------------------------------------------------------------------------- 1 | 2 | command="./bin/gill-git-server 1" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCyhPNB0Grfs5JgDQp+6Drhl7kpZR/QxH9x9n4z6aOOCpxEkz1ikeivlN3ArBIMbSam+2w9LcZzg82YQA3+OlNr4zMl+JZVQcRgVRaPWZl9Pz84wy/CoUnmIOycrA+X7tY6BYJoVF1qgUBF1kr3z+maUggIhNIH9btdxH8H3+qqjNqUoLXJhmBgt3wCVQ/1g0k+EuGldHonj7t4yR0OtAkLHHVXnmfffbAl80o3+lkrZiibTtf8EgKNkWxZsJiA+oieWSLQYhGSlu77pOZP6kjWctBqRJt1n9Mixi3UxeSUDsAPfjiBwgFQqqjDWzUq6kPrnpIJ4wUxUa0F/5b20PKH9zFWjBrzd+lYEPB+fnYA80dCiNvKzqhT8q10VahqZ4YbI+gxX4z92Mj7IVi240NCq6aKQ5EpsKJpUcozJbtv6PXITHKbPZMlCziyXZiF9LGn+wXqyqLsyB3Ls2DrOpk/sgy1a/1F9+C2uULSvvhsP+1J0az3EHHFjTADYixsVrM= okno@havrecommand="./bin/gill-git-server 1" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCyhPNB0Grfs5JgDQp+6Drhl7kpZR/QxH9x9n4z6aOOCpxEkz1ikeivlN3ArBIMbSam+2w9LcZzg82YQA3+OlNr4zMl+JZVQcRgVRaPWZl9Pz84wy/CoUnmIOycrA+X7tY6BYJoVF1qgUBF1kr3z+maUggIhNIH9btdxH8H3+qqjNqUoLXJhmBgt3wCVQ/1g0k+EuGldHonj7t4yR0OtAkLHHVXnmfffbAl80o3+lkrZiibTtf8EgKNkWxZsJiA+oieWSLQYhGSlu77pOZP6kjWctBqRJt1n9Mixi3UxeSUDsAPfjiBwgFQqqjDWzUq6kPrnpIJ4wUxUa0F/5b20PKH9zFWjBrzd+lYEPB+fnYA80dCiNvKzqhT8q10VahqZ4YbI+gxX4z92Mj7IVi240NCq6aKQ5EpsKJpUcozJbtv6PXITHKbPZMlCziyXZiF9LGn+wXqyqLsyB3Ls2DrOpk/sgy1a/1F9+C2uULSvvhsP+1J0az3EHHFjTADYixsVrM= okno@havre -------------------------------------------------------------------------------- /docker/dev/home2/entrypoint-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /usr/sbin/sshd 6 | cd /home/git 7 | 8 | tail -f /dev/null -------------------------------------------------------------------------------- /docker/dev/home2/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /usr/sbin/sshd 6 | cd /home/git 7 | su git -c "./bin/gill-app" -------------------------------------------------------------------------------- /docker/dev/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE gill; 2 | GRANT ALL PRIVILEGES ON DATABASE gill TO postgres; 3 | 4 | CREATE DATABASE gill_2; 5 | GRANT ALL PRIVILEGES ON DATABASE gill TO postgres; 6 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "$GILL_SSH_ECDSA_PUB" > /etc/ssh/ssh_host_ecdsa_key.pub 6 | echo "$GILL_SSH_ECDSA" > /etc/ssh/ssh_host_ecdsa_key 7 | echo "$GILL_SSH_ED25519_PUB" > /etc/ssh/ssh_host_ed25519_key.pub 8 | echo "$GILL_SSH_ED25519" > /etc/ssh/ssh_host_ed25519_key 9 | echo "$GILL_SSH_RSA_PUB" > /etc/ssh/ssh_host_rsa_key.pub 10 | echo "$GILL_SSH_RSA" > /etc/ssh/ssh_host_rsa_key 11 | 12 | chmod 600 /etc/ssh/ssh_host_rsa_key 13 | 14 | /usr/sbin/sshd 15 | 16 | cd /home/git 17 | su git -c "$@" -------------------------------------------------------------------------------- /docker/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE gill; 2 | GRANT ALL PRIVILEGES ON DATABASE gill TO postgres; -------------------------------------------------------------------------------- /docker/sshd_config: -------------------------------------------------------------------------------- 1 | AuthorizedKeysFile .ssh/authorized_keys 2 | PasswordAuthentication no 3 | Subsystem sftp /usr/lib/ssh/sftp-server 4 | AcceptEnv GIT_PROTOCOL 5 | HostKey /etc/ssh/ssh_host_rsa_key -------------------------------------------------------------------------------- /docs/assets/sc_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/docs/assets/sc_1.png -------------------------------------------------------------------------------- /docs/assets/sc_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/docs/assets/sc_2.png -------------------------------------------------------------------------------- /docs/assets/sc_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/docs/assets/sc_3.png -------------------------------------------------------------------------------- /docs/assets/sc_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oknozor/gill/e664cfe630e3bb3132cd3c3f204aa9224c18577c/docs/assets/sc_4.png --------------------------------------------------------------------------------