updated readme
51f56a7 · gmorell · 2026-05-14 04:28
Files changed
added
.dockerignore
+46
−0
@@ -0,0 +1,46 @@
| 1 | +# This file excludes paths from the Docker build context. | |
| 2 | +# | |
| 3 | +# By default, Docker's build context includes all files (and folders) in the | |
| 4 | +# current directory. Even if a file isn't copied into the container it is still sent to | |
| 5 | +# the Docker daemon. | |
| 6 | +# | |
| 7 | +# There are multiple reasons to exclude files from the build context: | |
| 8 | +# | |
| 9 | +# 1. Prevent nested folders from being copied into the container (ex: exclude | |
| 10 | +# /assets/node_modules when copying /assets) | |
| 11 | +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) | |
| 12 | +# 3. Avoid sending files containing sensitive information | |
| 13 | +# | |
| 14 | +# More information on using .dockerignore is available here: | |
| 15 | +# https://docs.docker.com/engine/reference/builder/#dockerignore-file | |
| 16 | + | |
| 17 | +.dockerignore | |
| 18 | + | |
| 19 | +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: | |
| 20 | +# | |
| 21 | +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat | |
| 22 | +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc | |
| 23 | +.git | |
| 24 | +!.git/HEAD | |
| 25 | +!.git/refs | |
| 26 | + | |
| 27 | +# Common development/test artifacts | |
| 28 | +/cover/ | |
| 29 | +/doc/ | |
| 30 | +/test/ | |
| 31 | +/tmp/ | |
| 32 | +.elixir_ls | |
| 33 | + | |
| 34 | +# Mix artifacts | |
| 35 | +/_build/ | |
| 36 | +/deps/ | |
| 37 | +*.ez | |
| 38 | + | |
| 39 | +# Generated on crash by the VM | |
| 40 | +erl_crash.dump | |
| 41 | + | |
| 42 | +# Static artifacts - These should be fetched and built inside the Docker image | |
| 43 | +# https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html#module-docker | |
| 44 | +/assets/node_modules/ | |
| 45 | +/priv/static/assets/ | |
| 46 | +/priv/static/cache_manifest.json |
added
.gitlab-ci.yml
+11
−0
@@ -0,0 +1,11 @@
| 1 | +build-master: | |
| 2 | + retry: 1 | |
| 3 | + stage: build | |
| 4 | + image: | |
| 5 | + name: gcr.io/kaniko-project/executor:v1.23.2-debug | |
| 6 | + entrypoint: [""] | |
| 7 | + only: | |
| 8 | + - master | |
| 9 | + script: | |
| 10 | + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json | |
| 11 | + - /kaniko/executor --force --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE --registry-mirror us0.regi.kube.gmp.io |
added
Dockerfile
+132
−0
@@ -0,0 +1,132 @@
| 1 | +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian | |
| 2 | +# instead of Alpine to avoid DNS resolution issues in production. | |
| 3 | +# | |
| 4 | +# https://hub.docker.com/r/hexpm/elixir/tags?name=ubuntu | |
| 5 | +# https://hub.docker.com/_/ubuntu/tags | |
| 6 | +# | |
| 7 | +# This file is based on these images: | |
| 8 | +# | |
| 9 | +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image | |
| 10 | +# - https://hub.docker.com/_/debian/tags?name=trixie-20260505-slim - for the release image | |
| 11 | +# - https://pkgs.org/ - resource for finding needed packages | |
| 12 | +# - Ex: docker.io/hexpm/elixir:1.19.5-erlang-28.4.1-debian-trixie-20260505-slim | |
| 13 | +# | |
| 14 | +ARG ELIXIR_VERSION=1.19.5 | |
| 15 | +ARG OTP_VERSION=28.4.1 | |
| 16 | +ARG DEBIAN_VERSION=trixie-20260505-slim | |
| 17 | + | |
| 18 | +ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" | |
| 19 | +ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" | |
| 20 | + | |
| 21 | +FROM ${BUILDER_IMAGE} AS builder | |
| 22 | + | |
| 23 | +# install build dependencies. | |
| 24 | +# * build-essential, git — Elixir / mix build chain | |
| 25 | +# * pkg-config, libssl-dev — gix transitively links against system openssl | |
| 26 | +# * curl, ca-certificates — needed to bootstrap rustup | |
| 27 | +RUN apt-get update \ | |
| 28 | + && apt-get install -y --no-install-recommends \ | |
| 29 | + build-essential git pkg-config libssl-dev curl ca-certificates \ | |
| 30 | + && rm -rf /var/lib/apt/lists/* | |
| 31 | + | |
| 32 | +# Rust via rustup, pinned. apt's rust on trixie is 1.85, but recent | |
| 33 | +# transitive gix deps (icu_*, home) require 1.86+; rustup keeps the | |
| 34 | +# compiler decoupled from the OS rev. Pinned to a known-good rev so | |
| 35 | +# builds are reproducible. | |
| 36 | +ENV RUSTUP_HOME=/usr/local/rustup \ | |
| 37 | + CARGO_HOME=/usr/local/cargo \ | |
| 38 | + PATH=/usr/local/cargo/bin:${PATH} \ | |
| 39 | + RUST_VERSION=1.88.0 | |
| 40 | +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | |
| 41 | + | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} \ | |
| 42 | + && rustc --version && cargo --version | |
| 43 | + | |
| 44 | +# prepare build dir | |
| 45 | +WORKDIR /app | |
| 46 | + | |
| 47 | +# install hex + rebar | |
| 48 | +RUN mix local.hex --force \ | |
| 49 | + && mix local.rebar --force | |
| 50 | + | |
| 51 | +# set build ENV | |
| 52 | +ENV MIX_ENV="prod" | |
| 53 | + | |
| 54 | +# install mix dependencies | |
| 55 | +COPY mix.exs mix.lock ./ | |
| 56 | +RUN mix deps.get --only $MIX_ENV | |
| 57 | +RUN mkdir config | |
| 58 | + | |
| 59 | +# copy compile-time config files before we compile dependencies | |
| 60 | +# to ensure any relevant config change will trigger the dependencies | |
| 61 | +# to be re-compiled. | |
| 62 | +COPY config/config.exs config/${MIX_ENV}.exs config/ | |
| 63 | +RUN mix deps.compile | |
| 64 | + | |
| 65 | +RUN mix assets.setup | |
| 66 | + | |
| 67 | +COPY priv priv | |
| 68 | + | |
| 69 | +COPY lib lib | |
| 70 | + | |
| 71 | +# Rust source for the gitgud_gix NIF — Rustler invokes `cargo` from | |
| 72 | +# this directory during `mix compile`, so it must be present. | |
| 73 | +COPY native native | |
| 74 | + | |
| 75 | +# Compile the release | |
| 76 | +RUN mix compile | |
| 77 | + | |
| 78 | +COPY assets assets | |
| 79 | + | |
| 80 | +# compile assets | |
| 81 | +RUN mix assets.deploy | |
| 82 | + | |
| 83 | +# Changes to config/runtime.exs don't require recompiling the code | |
| 84 | +COPY config/runtime.exs config/ | |
| 85 | + | |
| 86 | +COPY rel rel | |
| 87 | +RUN mix release | |
| 88 | + | |
| 89 | +# start a new build stage so that the final image will only contain | |
| 90 | +# the compiled release and other runtime necessities | |
| 91 | +FROM ${RUNNER_IMAGE} AS final | |
| 92 | + | |
| 93 | +# Runtime packages. | |
| 94 | +# * libstdc++6, openssl, libncurses6, locales, ca-certificates — Erlang runtime | |
| 95 | +# * git — the Cli backend shells out to git for ops the NIF hasn't covered | |
| 96 | +# yet (commit_tree, update_ref, plumbing for profile-README seeding, | |
| 97 | +# post-receive hook helpers). Also used by the embedded SSH daemon to | |
| 98 | +# spawn upload-pack / receive-pack per session. | |
| 99 | +# * tini — proper PID 1 / zombie reaping, recommended for any container | |
| 100 | +# image running long-lived child processes (we fork git per session). | |
| 101 | +RUN apt-get update \ | |
| 102 | + && apt-get install -y --no-install-recommends \ | |
| 103 | + libstdc++6 openssl libncurses6 locales ca-certificates \ | |
| 104 | + git tini \ | |
| 105 | + && rm -rf /var/lib/apt/lists/* | |
| 106 | + | |
| 107 | +# Set the locale | |
| 108 | +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \ | |
| 109 | + && locale-gen | |
| 110 | + | |
| 111 | +ENV LANG=en_US.UTF-8 | |
| 112 | +ENV LANGUAGE=en_US:en | |
| 113 | +ENV LC_ALL=en_US.UTF-8 | |
| 114 | + | |
| 115 | +WORKDIR "/app" | |
| 116 | +RUN chown nobody /app | |
| 117 | + | |
| 118 | +# set runner ENV | |
| 119 | +ENV MIX_ENV="prod" | |
| 120 | + | |
| 121 | +# Only copy the final release from the build stage | |
| 122 | +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/git_gud ./ | |
| 123 | + | |
| 124 | +USER nobody | |
| 125 | + | |
| 126 | +# tini as PID 1 — reaps the git child processes we fork from the embedded | |
| 127 | +# SSH daemon + plumbing helpers. Without this, zombies accumulate across | |
| 128 | +# pushes / pulls in long-lived containers. The package installs the | |
| 129 | +# binary at /usr/bin/tini on debian; symlinks for safety. | |
| 130 | +ENTRYPOINT ["/usr/bin/tini", "--"] | |
| 131 | + | |
| 132 | +CMD ["/app/bin/server"] |
modified
PROGRESS.md
+2
−2
@@ -77,8 +77,8 @@ Operators can gate who can sign up. Pattern ported from diogramos, extended from
| 77 | 77 | |
| 78 | 78 | ### Config flag |
| 79 | 79 | |
| 80 | −- **`config :git_gud, :registration_mode, :open | :invite_only | :closed`** (default `:open`). | |
| 81 | −- `Accounts.registration_mode/0` accepts both atom and string values (so env-var-driven `:runtime.exs` config works the same) and falls back to `:open` for unknown values. | |
| 80 | +- **`config :git_gud, :registration_mode, :open | :invite_only | :closed`** (default `:invite_only` — safer for fresh deploys; the test env overrides to `:open` so the existing open-registration assertions keep passing). | |
| 81 | +- `Accounts.registration_mode/0` accepts both atom and string values (so env-var-driven `:runtime.exs` config works the same) and falls back to `:invite_only` for unknown values. | |
| 82 | 82 | - `Accounts.register_user/1` short-circuits with `{:error, :registration_closed}` when the mode is `:closed` — defense in depth so a direct API call doesn't bypass the LV gate. |
| 83 | 83 | |
| 84 | 84 | ### Schema + context |
modified
README.md
+19
−6
@@ -19,10 +19,12 @@ external tools (Forgejo runner, Garage, zot) rather than reinvent them.
| 19 | 19 | - Server-side post-receive hook fans out to commit-backfill workers |
| 20 | 20 | - **Git LFS** (Batch API) over pluggable object storage |
| 21 | 21 | - **Repo browsing** |
| 22 | − - Tree, blob, commit, log views with deep linking | |
| 23 | − - **Line-level diff renderer** with Lumis-highlighted hunks on PR show, commit show, and compare; parsed diffs cached per `(base, head)` | |
| 22 | + - Tree, blob, commit, log views with deep linking and a **shared repo header** on every subpage (title + Issues/Labels/Forks/Compare/Packages/Actions/Settings strip + Fork button) | |
| 23 | + - **Clone-URL copy widget** on the repo show page — three-state expanding pill (HTTP/SSH toggle → copy button → inline URL text), one-click clipboard with execCommand fallback | |
| 24 | + - **Line-level diff renderer** with Lumis-highlighted hunks on PR show, commit show, and compare; parsed diffs cached per `(base, head)`. **Lazy per-file expansion** — large diffs keep hunks out of the DOM until you open the file | |
| 24 | 25 | - **Syntax highlighting** via [Lumis](https://lumis.sh) (tree-sitter, Rust NIF) — cached per-blob |
| 25 | 26 | - **Full GFM markdown** with cross-references (`#42`, `#x2a` hex, `owner/repo#42`), GitHub-style alerts, footnotes, task lists, fenced-code syntax highlighting — cached per-render |
| 27 | + - **Language stats**: 0.3em GitHub-style bar on the repo page driven by a per-commit Oban job; click for `/stats/languages` (full table) and `/stats/committers` (commits + churn per author) | |
| 26 | 28 | - Per-user editor theme (curated subset of Lumis themes — Dracula, Solarized, Monokai, Tokyo Night, Catppuccin, etc) — separate from the daisyUI page theme |
| 27 | 29 | - **Collaboration** |
| 28 | 30 | - Issues + comments with Write/Preview tabs, label picker, query-string autofill |
@@ -31,6 +33,16 @@ external tools (Forgejo runner, Garage, zot) rather than reinvent them.
| 31 | 33 | - Forks + fork-sync (fast-forward CAS) + compare-any-two-refs view |
| 32 | 34 | - Branch protection, deploy keys, webhooks, wikis |
| 33 | 35 | - **Labels** with HTML color picker + 16-swatch palette + live preview; **org-level labels** inherited by every repo unless the repo opts out (and re-opts in via a hidden-labels expander) |
| 36 | +- **Profiles** | |
| 37 | + - **User + org profile pages** at `/u/:handle` and `/orgs/:handle`. Display name, bio, url, location, company, public email, timezone, socials. Two-column layout with bio on the left, pinned repos + README on the right. | |
| 38 | + - **Theme-aware identicon avatars** — deterministic 5×5 mirrored grid, fills reference daisyUI CSS variables so swapping themes recolors every avatar live. No upload pipeline yet; the identicon is the avatar. | |
| 39 | + - **Pinned repos** — drag-to-reorder grid, up to 7 per user / per org, includes repos from orgs you're a member of. Removed-from-org? Your personal pins of that org's repos clean themselves up. | |
| 40 | + - **Auto-created same-name profile README** — registering `alice` provisions `alice/alice` with a seeded "Hi, I'm @alice 👋 …" `README.md` on the default branch. Edit it like any other repo; the rendered output shows on the profile. | |
| 41 | +- **Account + org governance** | |
| 42 | + - **Registration modes**: `:open` / `:invite_only` (default) / `:closed`, configurable per-instance | |
| 43 | + - **Invites** — single-use tokens with create / list / revoke per user; UI at `/users/settings/invites` | |
| 44 | + - **Org visibility**: `public` / `internal` / `private`. Repos can't be more open than their org; tightening an org cascades to over-public repos in one transaction. Public orgs are accessible to anonymous viewers; private orgs require membership. | |
| 45 | + - **Org members management** at `/orgs/:handle/settings/members` — add by handle or email, change roles, remove, with a last-admin guard so an org can never end up admin-less | |
| 34 | 46 | - **Auth & MFA** |
| 35 | 47 | - Magic-link login + email/password |
| 36 | 48 | - **TOTP** (any RFC 6238 authenticator) with QR enrollment + 10 single-use recovery codes |
@@ -42,7 +54,7 @@ external tools (Forgejo runner, Garage, zot) rather than reinvent them.
| 42 | 54 | - Inbound: Follow, Offer (federated PR), Update, Reject, Create(Note) comments, Like/Dislike as PR reviews |
| 43 | 55 | - Outbound `Offer` from the PR new page; outbound Like/Dislike on local reviews of federated PRs |
| 44 | 56 | - Per-instance moderation: allowlist mode (default), per-instance + per-actor blocks, interaction policies (`public` / `followers` / `approved`) |
| 45 | − - **Actionable reports queue** in admin: inline previews, delete content, suspend remote actors, forward to source instance via AP `Flag` | |
| 57 | + - **Actionable reports queue** in admin: inline previews, **replace-with-mod-message** (reversible — admins + the original author can `<details>`-toggle the original), suspend remote actors, forward to source instance via AP `Flag`. Re-reporting moderated content is gated at the API layer. | |
| 46 | 58 | - **`mix gitgud.federation.suggest_blocks`** — peer block lists become a *review queue*, not enforcement (cascades are deliberately declined) |
| 47 | 59 | - **CI** |
| 48 | 60 | - Speaks the **Forgejo Actions runner protocol** (Twirp) — point any modern `forgejo-runner` at it; protocol path autodetected (`ping.v1`, `runner.v1`, with/without `_apis/`) |
@@ -130,6 +142,7 @@ These all live in `config/*.exs`. Key ones:
| 130 | 142 | | `GitGud.Storage.Disk` `root` | Where Disk writes blobs | `priv/storage` | |
| 131 | 143 | | `GitGud.Ssh.Server` `enabled` | Embedded SSH daemon | `false` (dev: `true`) | |
| 132 | 144 | | `GitGud.Ssh.Server` `port` | SSH daemon port | `2222` | |
| 145 | +| `:registration_mode` | `:open` / `:invite_only` / `:closed` | `:invite_only` | | |
| 133 | 146 | | `GitGud.Federation` `federation_mode` | `:allowlist` or `:open` | `:allowlist` | |
| 134 | 147 | | `GitGud.Cache` | Nebulex cache sizing | 50k entries / 200MB | |
| 135 | 148 | | `GitGud.Accounts.TwoFactor` `required` / `grace_period_days` | Instance-wide MFA enforcement + grace window | `false` / `14` | |
@@ -218,16 +231,16 @@ external one works today and is well-tested by the Forgejo community.
| 218 | 231 | |
| 219 | 232 | See [ROADMAP.md](./ROADMAP.md). Highlights: |
| 220 | 233 | |
| 221 | −- gix NIF wiring for log / commit / tree / merge_tree (Cli fallback works) | |
| 234 | +- gix NIF wiring for log / commit / tree / diff_stats / merge_base / merge_tree (Cli fallback works; slice 56 fills in the hot paths) | |
| 222 | 235 | - In-browser PR conflict resolution |
| 223 | −- Lazy per-file diff expansion for very large diffs | |
| 236 | +- Avatar file uploads (the theme-aware identicon is the avatar today) | |
| 224 | 237 | - Native runner (low priority while the external one works) |
| 225 | 238 | - Code search (Postgres FTS handles issues/PRs; code search is its own beast) |
| 226 | 239 | - RFC 9421 (HTTP Messages) signatures when peers upgrade from draft-cavage-12 |
| 227 | 240 | - Reputation aging for new federated hosts |
| 228 | 241 | |
| 229 | 242 | See [PROGRESS.md](./PROGRESS.md) for the slice-by-slice history of how |
| 230 | −the forge was built, and what each shipped — currently through slice 42. | |
| 243 | +the forge was built, and what each shipped — currently through slice 55. | |
| 231 | 244 | |
| 232 | 245 | ## Project conventions |
| 233 | 246 |
modified
config/config.exs
+3
−2
Click to load diff…
modified
config/test.exs
+5
−0
Click to load diff…
modified
lib/git_gud/accounts.ex
+5
−5
Click to load diff…
added
lib/git_gud/release.ex
+30
−0
Click to load diff…
modified
lib/git_gud_web/controllers/page_html/home.html.heex
+29
−2
Click to load diff…
modified
mix.exs
+3
−1
Click to load diff…
modified
mix.lock
+0
−1
Click to load diff…
added
rel/overlays/bin/migrate
+5
−0
Click to load diff…
added
rel/overlays/bin/migrate.bat
+1
−0
Click to load diff…
added
rel/overlays/bin/server
+5
−0
Click to load diff…
added
rel/overlays/bin/server.bat
+2
−0
Click to load diff…
modified
test/git_gud/invites_test.exs
+5
−5
Click to load diff…
Parents: 6709221