├── .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