diff options
| author | Leitwolf11 | 2026-11-12 23:41:27 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-12 23:41:27 +0300 |
| commit | 83e0ad4586d90bf02c3a5bfeb1ab92fd90e51703 (patch) | |
| tree | 41c12c9a54223df2d681554679408307282319fd | |
| parent | 80d46cbffc87da9bf2665935e563ab556c1a267d (diff) | |
| download | progmaweb-ops/git-mirrors.tar.gz | |
Add read-only Git mirrors on git.progmasoft.comops/git-mirrors
git.progmasoft.com shows the public repositories under development
(tazel, tazelish, tazel.net, catch3, progmasoft-cpp, progmaweb, website)
as read-only mirrors with cgit, and serves clones over smart HTTP, such
as git clone https://git.progmasoft.com/tazel. GitHub stays where
development happens; private and archived repositories are not mirrored.
ops/git holds what runs on the server:
- cgitrc, the header, footer and stylesheet: the look of progmasoft.com
(its tokens, the four-tile brand with "Git" beside it, the dark
footer), light like its default, no scripts, no rendered READMEs.
- git-mirror.sh with git-mirror.service/.timer: every ten minutes, as
gitmirror, branches and tags only from GitHub, pruned, the default
branch followed, repacked with a bitmap when something changed.
- fcgiwrap@cgit drop-ins: cgit and git-http-backend run as cgit with
systemd sandboxing (no network, read-only system, exposure 0.7), on
a socket only nginx can open.
- selinux/progmasoft_cgit.te: fcgiwrap runs in httpd_t and the two
CGIs move into git_script_t; the module allows only that move under
the NoNewPrivileges that the sandboxing sets.
- nginx/git.conf: HSTS, a content security policy without scripts,
per-address request and connection limits, 403 for receive-pack and
the dumb protocol, buffered clones, and the inline handlers of cgit's
menus removed.
ops/README.md describes the files, the labels, and why the name is not
behind the Cloudflare proxy: through it a clone of catch3 never reached
the client.
Deployed and checked live: every page answers 200, unknown ones 404,
full clones of tazel, progmaweb and catch3 work, a push answers 403,
no SELinux denials, no console errors, no horizontal scroll at 390 px.
| -rw-r--r-- | ops/README.md | 46 | ||||
| -rw-r--r-- | ops/git/cgitrc | 77 | ||||
| -rw-r--r-- | ops/git/git-mirror.sh | 54 | ||||
| -rw-r--r-- | ops/git/include/footer.html | 6 | ||||
| -rw-r--r-- | ops/git/include/head.html | 1 | ||||
| -rw-r--r-- | ops/git/include/header.html | 19 | ||||
| -rw-r--r-- | ops/git/nginx/git.conf | 113 | ||||
| -rw-r--r-- | ops/git/repositories | 9 | ||||
| -rw-r--r-- | ops/git/selinux/progmasoft_cgit.te | 18 | ||||
| -rw-r--r-- | ops/git/static/icon.svg | 7 | ||||
| -rw-r--r-- | ops/git/static/progmasoft-git.css | 716 | ||||
| -rw-r--r-- | ops/git/systemd/fcgiwrap@cgit.service.d/hardening.conf | 37 | ||||
| -rw-r--r-- | ops/git/systemd/fcgiwrap@cgit.service.d/processes.conf | 8 | ||||
| -rw-r--r-- | ops/git/systemd/fcgiwrap@cgit.socket.d/nginx.conf | 7 | ||||
| -rw-r--r-- | ops/git/systemd/git-mirror.service | 44 | ||||
| -rw-r--r-- | ops/git/systemd/git-mirror.timer | 13 |
16 files changed, 1175 insertions, 0 deletions
diff --git a/ops/README.md b/ops/README.md index 0457499..9022227 100644 --- a/ops/README.md +++ b/ops/README.md @@ -296,3 +296,49 @@ ssh root@91.151.88.130 'chown -R root:root /srv/maven && restorecon -R /srv/mave ``` Published versions are never changed. The certificate of `progmasoft.com` must name `maven.progmasoft.com`. + +## Git mirrors + +`git.progmasoft.com` shows read-only mirrors of the public repositories under development, such as +`https://git.progmasoft.com/tazel`. GitHub stays where development happens: issues, pull requests and CI, and the source +links of the sites. Private and archived repositories are not mirrored. The files are in `ops/git`: + +| File | Installed as | +| --- | --- | +| `cgitrc` | `/etc/cgitrc`: the repositories, their sections and descriptions, and the look | +| `include/head.html`, `header.html`, `footer.html` | `/etc/cgit/progmasoft/`: the header and footer of progmasoft.com | +| `static/progmasoft-git.css`, `static/icon.svg` | `/srv/git/www/static/`: the tokens and brand of `apps/web/app/globals.css` | +| `repositories` | `/etc/git-mirror/repositories`: what is mirrored, one name per line | +| `git-mirror.sh` | `/usr/local/libexec/git-mirror`, labelled `bin_t` | +| `systemd/git-mirror.service`, `.timer` | `/etc/systemd/system/`: every ten minutes, as `gitmirror` | +| `systemd/fcgiwrap@cgit.*.d/` | `/etc/systemd/system/`: cgit and git-http-backend as `cgit`, socket for `nginx` only | +| `selinux/progmasoft_cgit.te` | the module `progmasoft_cgit` | +| `nginx/git.conf` | `/etc/nginx/conf.d/git.conf` | + +Packages: `git-core`, and `cgit` and `fcgiwrap` from EPEL; `checkpolicy` builds the module. + +- **Mirrors.** `git-mirror` fetches only branches and tags from `https://github.com/Progmasoft/<name>.git` (never the + pull-request refs), prunes what GitHub removed, follows its default branch, and repacks into one pack with a bitmap + when something changed, so a clone starts sending at once. It needs no credentials. A new repository needs a line in + `repositories` and a `repo.url` block in `cgitrc`. +- **Read only.** Nginx answers 403 to `git-receive-pack` and to the dumb protocol; git-http-backend gets no user; the + mirrors belong to `gitmirror` and are read as `cgit`, trusted through `safe.directory` for that backend only. +- **SELinux.** `/srv/git/mirrors` is `git_sys_content_t` and `/var/cache/cgit` `git_rw_content_t` (package default). + `/usr/bin/fcgiwrap` is `httpd_exec_t`, so fcgiwrap runs in `httpd_t` and Nginx may connect to it, and + `git-http-backend` is `git_script_exec_t` like cgit, so both move into `git_script_t`. systemd's sandboxing sets + NoNewPrivileges; the module `progmasoft_cgit` allows exactly that one move under it. + + ```sh + semanage fcontext -a -t git_sys_content_t '/srv/git/mirrors(/.*)?' + semanage fcontext -a -t bin_t /usr/local/libexec/git-mirror + semanage fcontext -a -t httpd_exec_t /usr/bin/fcgiwrap + semanage fcontext -a -t git_script_exec_t /usr/libexec/git-core/git-http-backend + restorecon -RF /srv/git /usr/local/libexec/git-mirror /usr/sbin/fcgiwrap /usr/libexec/git-core/git-http-backend + ``` + +- **No Cloudflare proxy.** `git.progmasoft.com` is a DNS-only A record: through the proxy, a clone of a large repository + (catch3, about 25 MB) never reached the client, while the origin served it in under a minute. The address of the server + is public through `mail.progmasoft.com` anyway; Nginx limits requests and connections per address itself. +- **Look.** No README is rendered (cgit would include its HTML unfiltered) and no script runs: the content security + policy allows none, and Nginx removes the inline `onchange` handlers of cgit's menus, whose buttons do the same. +- **Certificate.** The certificate of `progmasoft.com` names `git.progmasoft.com` (webroot, `--expand`). diff --git a/ops/git/cgitrc b/ops/git/cgitrc new file mode 100644 index 0000000..5da9211 --- /dev/null +++ b/ops/git/cgitrc @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +# cgit of https://git.progmasoft.com: read-only mirrors of the public repositories of Progmasoft +# that are under development. Development happens on GitHub; git-mirror.timer fetches from there. +# Installed as /etc/cgitrc. + +root-title=Progmasoft Git +root-desc=Read-only mirrors of the repositories of Progmasoft. Issues and pull requests are on GitHub. +virtual-root=/ +remove-suffix=1 + +# The look of progmasoft.com: its stylesheet, header and footer. +css=/static/progmasoft-git.css +favicon=/static/icon.svg +logo= +head-include=/etc/cgit/progmasoft/head.html +header=/etc/cgit/progmasoft/header.html +footer=/etc/cgit/progmasoft/footer.html + +# Clones go to git-http-backend (smart HTTP, upload-pack only); cgit's own dumb HTTP stays off. +enable-http-clone=0 +clone-url=https://git.progmasoft.com/$CGIT_REPO_URL + +# No README is rendered: without a filter cgit would include its HTML as it is. +enable-index-owner=0 +enable-index-links=0 +enable-blame=0 +enable-commit-graph=0 +enable-log-filecount=1 +enable-log-linecount=0 +noplainemail=1 +max-stats= +snapshots=tar.gz +side-by-side-diffs=0 +robots=index, nofollow + +cache-root=/var/cache/cgit +cache-size=1000 + +section=Tazel +repo.url=tazel +repo.path=/srv/git/mirrors/tazel.git +repo.desc=A build system and package manager for C, C++, Objective-C, Objective-C++ and Zig, with Kotlin project files. +repo.homepage=https://github.com/Progmasoft/tazel + +repo.url=tazelish +repo.path=/srv/git/mirrors/tazelish.git +repo.desc=Installs Tazel and chooses which version of it runs. +repo.homepage=https://github.com/Progmasoft/tazelish + +repo.url=tazel.net +repo.path=/srv/git/mirrors/tazel.net.git +repo.desc=The site of Tazel, tazel.net. +repo.homepage=https://github.com/Progmasoft/tazel.net + +section=C++ +repo.url=catch3 +repo.path=/srv/git/mirrors/catch3.git +repo.desc=Progmasoft Catch3: an independently maintained C++17 test framework with a parallel runner, property checks, snapshots and structured results. +repo.homepage=https://github.com/Progmasoft/catch3 + +repo.url=progmasoft-cpp +repo.path=/srv/git/mirrors/progmasoft-cpp.git +repo.desc=The C++ style guide of Progmasoft: naming, formatting, language rules, errors, ownership, ABI, testing and tooling. +repo.homepage=https://github.com/Progmasoft/progmasoft-cpp + +section=Sites +repo.url=progmaweb +repo.path=/srv/git/mirrors/progmaweb.git +repo.desc=The site and account service of Progmasoft: a Next.js frontend and an ASP.NET Core API. +repo.homepage=https://github.com/Progmasoft/progmaweb + +repo.url=website +repo.path=/srv/git/mirrors/website.git +repo.desc=The site of tala.run: a Vue frontend and an ASP.NET Core API. +repo.homepage=https://github.com/Progmasoft/website diff --git a/ops/git/git-mirror.sh b/ops/git/git-mirror.sh new file mode 100644 index 0000000..25bed02 --- /dev/null +++ b/ops/git/git-mirror.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +# Mirrors the repositories of /etc/git-mirror/repositories from https://github.com/Progmasoft into +# /srv/git/mirrors/<name>.git: branches and tags only, never the pull-request refs of GitHub, with +# what GitHub removed removed here too. Runs as gitmirror from git-mirror.service; installed as +# /usr/local/libexec/git-mirror. +set -eu + +list=${GIT_MIRROR_LIST:-/etc/git-mirror/repositories} +root=${GIT_MIRROR_ROOT:-/srv/git/mirrors} +status=0 + +# A for loop, not a pipe: a pipe would run the loop in a subshell and lose the status. +names=$(grep -Ev '^[[:space:]]*(#|$)' "$list") +for name in $names; do + case "$name" in + *[!A-Za-z0-9._-]* | .* ) echo "git-mirror: skipping the name '$name'" >&2; status=1; continue ;; + esac + repository="$root/$name.git" + if [ ! -d "$repository" ]; then + git init --quiet --bare "$repository.new" + git -C "$repository.new" config remote.origin.url "https://github.com/Progmasoft/$name.git" + git -C "$repository.new" config remote.origin.fetch '+refs/heads/*:refs/heads/*' + git -C "$repository.new" config --add remote.origin.fetch '+refs/tags/*:refs/tags/*' + git -C "$repository.new" config remote.origin.tagOpt --no-tags + git -C "$repository.new" config http.receivepack false + mv "$repository.new" "$repository" + fi + # A clone is served from one pack with a bitmap, so it starts sending at once. + git -C "$repository" config repack.writeBitmaps true + git -C "$repository" config pack.writeBitmapHashCache true + git -C "$repository" config gc.auto 0 + mkdir -p "$repository/info/web" + before=$(git -C "$repository" for-each-ref --format="%(objectname) %(refname)" | sha256sum) + if git -C "$repository" fetch --quiet --prune origin; then + after=$(git -C "$repository" for-each-ref --format="%(objectname) %(refname)" | sha256sum) + if [ "$before" != "$after" ] || [ ! -f "$repository/info/web/repacked" ]; then + git -C "$repository" repack -a -d -b -q + touch "$repository/info/web/repacked" + fi + # HEAD follows the default branch of GitHub. + head=$(git -C "$repository" ls-remote --symref origin HEAD | sed -n 's|^ref: refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$|\1|p') + [ -n "$head" ] && git -C "$repository" symbolic-ref HEAD "refs/heads/$head" + # cgit shows the age of the last change of a repository from this file. + git -C "$repository" for-each-ref --sort=-committerdate --count=1 \ + --format='%(committerdate:iso8601)' refs/heads > "$repository/info/web/last-modified" + else + echo "git-mirror: fetching $name failed" >&2 + status=1 + fi +done +exit "$status" diff --git a/ops/git/include/footer.html b/ops/git/include/footer.html new file mode 100644 index 0000000..3e4f5f1 --- /dev/null +++ b/ops/git/include/footer.html @@ -0,0 +1,6 @@ +<footer class="site-footer"> + <div class="shell footer-bottom"> + <span>© 2026 Progmasoft</span> + <span>Mirrored from GitHub every ten minutes.</span> + </div> +</footer> diff --git a/ops/git/include/head.html b/ops/git/include/head.html new file mode 100644 index 0000000..2376219 --- /dev/null +++ b/ops/git/include/head.html @@ -0,0 +1 @@ +<meta name="theme-color" content="#0066b8"> diff --git a/ops/git/include/header.html b/ops/git/include/header.html new file mode 100644 index 0000000..8b14244 --- /dev/null +++ b/ops/git/include/header.html @@ -0,0 +1,19 @@ +<header class="site-header"> + <div class="shell header-inner"> + <a class="brand" href="https://git.progmasoft.com/"> + <span class="brand-mark" aria-hidden="true"> + <span class="brand-tile brand-tile-slate"></span> + <span class="brand-tile brand-tile-teal"></span> + <span class="brand-tile brand-tile-violet"></span> + <span class="brand-tile brand-tile-orange"></span> + </span> + <strong class="brand-name">Progmasoft</strong> + <span class="brand-product">Git</span> + </a> + <nav class="primary-nav" aria-label="Primary"> + <a href="https://progmasoft.com/">Progmasoft</a> + <a class="button button-small" href="https://github.com/Progmasoft">GitHub</a> + </nav> + </div> +</header> +<p class="shell mirror-note">Read-only mirrors. Development, issues and pull requests are on GitHub.</p> diff --git a/ops/git/nginx/git.conf b/ops/git/nginx/git.conf new file mode 100644 index 0000000..c11dce4 --- /dev/null +++ b/ops/git/nginx/git.conf @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +# https://git.progmasoft.com: cgit over read-only mirrors, and clones over smart HTTP with +# git-http-backend (upload-pack only). Installed as /etc/nginx/conf.d/git.conf. + +limit_req_zone $binary_remote_addr zone=git_pages:10m rate=10r/s; +limit_req_zone $binary_remote_addr zone=git_clones:10m rate=1r/s; +limit_conn_zone $binary_remote_addr zone=git_connections:10m; + +server { + listen 80; + listen [::]:80; + server_name git.progmasoft.com; + + location ^~ /.well-known/acme-challenge/ { + root /var/lib/letsencrypt; + default_type text/plain; + } + + location / { + return 308 https://git.progmasoft.com$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name git.progmasoft.com; + + ssl_certificate /etc/letsencrypt/live/progmasoft.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/progmasoft.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + + # Nothing is uploaded here: a clone sends only the list of what it wants. + client_max_body_size 1m; + limit_conn git_connections 8; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + # cgit writes no scripts; its diffstat bars set their width in style attributes. + add_header Content-Security-Policy "default-src 'none'; style-src-elem 'self'; style-src-attr 'unsafe-inline'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'" always; + + location ^~ /static/ { + alias /srv/git/www/static/; + expires 1d; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Content-Security-Policy "default-src 'none'; style-src 'self'" always; + } + + location = /robots.txt { + default_type text/plain; + return 200 "User-agent: *\nDisallow: /*/snapshot/\nDisallow: /*/log/\nDisallow: /*/diff/\nDisallow: /*/patch/\n"; + } + + # Pushing is never possible: no receive-pack, and the dumb protocol is off. + location ~ ^/[A-Za-z0-9._-]+/git-receive-pack$ { + return 403; + } + + location ~ ^/(?<git_repo>[A-Za-z0-9._-]+?)(?:\.git)?/(?<git_path>info/refs|git-upload-pack)$ { + set $git_service ""; + if ($git_path = "info/refs") { + set $git_service $arg_service; + } + if ($git_path = "git-upload-pack") { + set $git_service "git-upload-pack"; + } + if ($git_service != "git-upload-pack") { + return 403; + } + limit_req zone=git_clones burst=10 nodelay; + + fastcgi_pass unix:/run/fcgiwrap/fcgiwrap-cgit.sock; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/libexec/git-core/git-http-backend; + fastcgi_param GIT_PROJECT_ROOT /srv/git/mirrors; + fastcgi_param GIT_HTTP_EXPORT_ALL ""; + fastcgi_param PATH_INFO /$git_repo.git/$git_path; + fastcgi_param REMOTE_USER ""; + # The mirrors belong to gitmirror and are read as cgit; only this backend trusts them. + fastcgi_param GIT_CONFIG_COUNT 1; + fastcgi_param GIT_CONFIG_KEY_0 safe.directory; + fastcgi_param GIT_CONFIG_VALUE_0 /srv/git/mirrors/*; + fastcgi_read_timeout 300s; + # Nginx buffers the pack (to a file when it is large), so a slow reader never stalls + # git-http-backend behind fcgiwrap. + fastcgi_buffering on; + fastcgi_max_temp_file_size 2048m; + } + + location / { + limit_req zone=git_pages burst=40 nodelay; + # The menus of cgit (branch, diff options) submit themselves from inline handlers, which the + # policy above blocks; their buttons ("switch", "reload") do the same without a script. + sub_filter "" ""; + sub_filter_once off; + + fastcgi_pass unix:/run/fcgiwrap/fcgiwrap-cgit.sock; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/cgi-bin/cgit; + fastcgi_param CGIT_CONFIG /etc/cgitrc; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_read_timeout 60s; + } +} diff --git a/ops/git/repositories b/ops/git/repositories new file mode 100644 index 0000000..bdd0a89 --- /dev/null +++ b/ops/git/repositories @@ -0,0 +1,9 @@ +# The repositories git-mirror.service mirrors from https://github.com/Progmasoft, one per line. +# Each must also have a repo.url entry in cgitrc. Installed as /etc/git-mirror/repositories. +tazel +tazelish +tazel.net +catch3 +progmasoft-cpp +progmaweb +website diff --git a/ops/git/selinux/progmasoft_cgit.te b/ops/git/selinux/progmasoft_cgit.te new file mode 100644 index 0000000..d4d5d75 --- /dev/null +++ b/ops/git/selinux/progmasoft_cgit.te @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +# fcgiwrap@cgit runs in httpd_t under NoNewPrivileges, which systemd sets for its sandboxing. +# This lets cgit and git-http-backend move from there into git_script_t, the narrower domain the +# policy gives them; nothing else is allowed. Built and loaded with: +# checkmodule -M -m -o progmasoft_cgit.mod progmasoft_cgit.te +# semodule_package -o progmasoft_cgit.pp -m progmasoft_cgit.mod +# semodule -i progmasoft_cgit.pp +module progmasoft_cgit 1.0; + +require { + type httpd_t; + type git_script_t; + class process2 nnp_transition; +} + +allow httpd_t git_script_t:process2 nnp_transition; diff --git a/ops/git/static/icon.svg b/ops/git/static/icon.svg new file mode 100644 index 0000000..c7c9ead --- /dev/null +++ b/ops/git/static/icon.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 100 312 312"> + <title>Progmasoft</title> + <rect x="112" y="112" width="132" height="132" fill="#3b4658"/> + <rect x="268" y="112" width="132" height="132" fill="#1e7188"/> + <rect x="112" y="268" width="132" height="132" fill="#7655fa"/> + <rect x="268" y="268" width="132" height="132" fill="#dc7644"/> +</svg> diff --git a/ops/git/static/progmasoft-git.css b/ops/git/static/progmasoft-git.css new file mode 100644 index 0000000..11f765c --- /dev/null +++ b/ops/git/static/progmasoft-git.css @@ -0,0 +1,716 @@ +/* SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> */ +/* SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 */ + +/* The look of progmasoft.com for cgit: the same tokens, brand, header and footer as + apps/web/app/globals.css, applied to the markup cgit writes. Light, like the default of + progmasoft.com; this site keeps no preferences. */ + +:root { + --blue-700: #004e8c; + --blue-600: #0066b8; + --blue-500: #0078d4; + --blue-100: #dff0ff; + --green-600: #16825d; + --ink-950: #121417; + --ink-800: #2a2f35; + --ink-650: #4c545d; + --ink-500: #68717c; + --line: #d9dee4; + --line-strong: #b9c1ca; + --surface: #ffffff; + --surface-muted: #f5f7f9; + --surface-blue: #f1f8ff; + --header-surface: rgb(255 255 255 / 94%); + --danger: #b42318; + --radius-sm: 4px; + --radius-md: 8px; + --shell: 1180px; + --mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace; + color-scheme: light; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--surface); + color: var(--ink-950); + font-family: + "Segoe UI Variable", + "Segoe UI", + system-ui, + -apple-system, + BlinkMacSystemFont, + sans-serif; + font-size: 15px; + line-height: 1.5; + text-rendering: optimizeLegibility; + /* The header and footer reach the edges of the window from inside div#cgit. */ + overflow-x: hidden; +} + +a { + color: var(--blue-600); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +:focus-visible { + outline: 3px solid #ffbf47; + outline-offset: 3px; +} + +.shell { + width: min(calc(100% - 40px), var(--shell)); + margin-inline: auto; +} + +/* ----- Header and footer, as on progmasoft.com ----- */ + +.site-header { + min-height: 68px; + border-bottom: 1px solid var(--line); + background: var(--header-surface); +} + +.header-inner { + min-height: 68px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 11px; + color: var(--ink-950); +} + +.brand:hover { + text-decoration: none; +} + +.brand-mark { + width: 24px; + height: 24px; + display: grid; + flex: 0 0 auto; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 2px; +} + +.brand-tile { + display: block; +} + +.brand-tile-slate { + background: #3b4658; +} + +.brand-tile-teal { + background: #1e7188; +} + +.brand-tile-violet { + background: #7655fa; +} + +.brand-tile-orange { + background: #dc7644; +} + +.brand-name { + color: #8e0000; + font-size: 20px; + font-weight: 650; + letter-spacing: -0.02em; + line-height: 1; +} + +.brand-product { + border-left: 2px solid var(--ink-800); + margin-left: 5px; + padding-left: 16px; + color: var(--ink-800); + font-size: 16px; + font-weight: 600; +} + +.primary-nav { + display: flex; + align-items: center; + gap: 28px; +} + +.primary-nav a { + color: var(--ink-800); + font-weight: 600; +} + +.primary-nav a:hover { + color: var(--blue-600); + text-decoration: none; +} + +.button { + display: inline-flex; + align-items: center; + border-radius: var(--radius-sm); + background: var(--blue-600); + color: #fff; + font-weight: 700; +} + +.primary-nav a.button, +.primary-nav a.button:hover { + color: #fff; +} + +.button:hover { + background: var(--blue-700); + text-decoration: none; +} + +.button-small { + min-height: 36px; + padding: 7px 14px; + font-size: 14px; +} + +.mirror-note { + margin-block: 18px 0; + border: 1px solid var(--line); + border-radius: var(--radius-md); + padding: 10px 14px; + background: var(--surface-blue); + color: var(--ink-650); + font-size: 14px; +} + +.site-footer { + margin-top: 56px; + border-top: 1px solid #343a40; + background: #20252a; + color: #d8dde2; +} + +.footer-bottom { + min-height: 64px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + font-size: 14px; +} + +/* ----- The page cgit writes ----- */ + +div#cgit { + width: min(calc(100% - 40px), var(--shell)); + margin-inline: auto; +} + +/* cgit writes the header before div#cgit and the footer inside it; the footer still reaches the + edges of the window. */ +div#cgit > .site-footer { + margin-inline: calc(50% - 50vw); + padding-inline: calc(50vw - 50%); +} + +table#header { + width: 100%; + margin-top: 28px; + border-collapse: collapse; +} + +table#header td.logo { + display: none; +} + +table#header td.main { + color: var(--ink-950); + font-size: 30px; + font-weight: 700; + letter-spacing: -0.02em; +} + +table#header td.main a { + color: var(--ink-950); +} + +table#header td.sub { + padding-top: 4px; + color: var(--ink-650); + font-size: 16px; +} + +table#header td.form { + text-align: right; + vertical-align: middle; +} + +table#header td.form form { + display: inline-flex; + gap: 6px; + align-items: center; +} + +table#header td.form select { + max-width: 260px; +} + +select, +input[type="search"], +input[type="text"], +input[type="submit"] { + min-height: 34px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 5px 9px; + background: var(--surface); + color: var(--ink-800); + font: inherit; + font-size: 14px; +} + +input[type="submit"] { + border-color: var(--blue-600); + background: var(--blue-600); + color: #fff; + font-weight: 700; + cursor: pointer; +} + +input[type="submit"]:hover { + background: var(--blue-700); +} + +table.tabs { + width: 100%; + margin-top: 22px; + border-collapse: collapse; + border-bottom: 1px solid var(--line); +} + +table.tabs td { + padding: 0; + vertical-align: bottom; +} + +table.tabs td a { + display: inline-block; + margin-right: 22px; + border-bottom: 3px solid transparent; + padding: 10px 0 9px; + color: var(--ink-650); + font-weight: 600; +} + +table.tabs td a.active, +table.tabs td a:hover { + border-bottom-color: var(--blue-600); + color: var(--ink-950); + text-decoration: none; +} + +table.tabs td.form { + padding-bottom: 8px; + text-align: right; +} + +table.tabs td.form form { + display: inline-flex; + gap: 6px; +} + +div.path { + margin-top: 14px; + color: var(--ink-650); + font-family: var(--mono); + font-size: 13px; +} + +div.content { + margin-top: 20px; + overflow-x: auto; +} + +div.cgit-panel { + float: right; + margin: 0 0 14px 18px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + padding: 10px 12px; + background: var(--surface-muted); + font-size: 13px; +} + +div.cgit-panel b { + display: block; + margin-bottom: 6px; +} + +/* Lists: the repositories, refs, log and tree. */ +table.list { + width: 100%; + border: 1px solid var(--line); + border-radius: var(--radius-md); + border-collapse: separate; + border-spacing: 0; + overflow: hidden; +} + +table.list th { + border-bottom: 1px solid var(--line); + padding: 10px 12px; + background: var(--surface-muted); + color: var(--ink-650); + font-size: 13px; + font-weight: 700; + text-align: left; +} + +table.list td { + border-bottom: 1px solid var(--line); + padding: 9px 12px; + vertical-align: top; +} + +table.list tr:last-child td { + border-bottom: 0; +} + +table.list tr:hover td { + background: var(--surface-blue); +} + +table.list tr.nohover:hover td, +table.list tr.nohover-highlight:hover td { + background: transparent; +} + +/* The empty rows cgit puts between the parts of a summary. */ +table.list tr.nohover > td[colspan] { + height: 14px; + border-bottom: 0; + padding: 0; + font-size: 0; +} + +table.list tr.nohover > th { + border-top: 1px solid var(--line); +} + +table.list th a { + color: inherit; +} + +table.list td.reposection { + padding-top: 20px; + background: var(--surface); + color: var(--ink-950); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +table.list td.toplevel-repo a, +table.list td.sublevel-repo a { + font-weight: 650; +} + +table.list td.commitgraph, +table.list td.ls-mode, +table.list td.ls-size, +table.list td.sha1, +td.ls-mode, +td.sha1 { + color: var(--ink-500); + font-family: var(--mono); + font-size: 13px; + white-space: nowrap; +} + +table.list td.ls-size { + text-align: right; +} + +a.ls-dir { + font-weight: 650; +} + +a.ls-blob, +a.ls-dir, +a.ls-mod { + font-family: var(--mono); + font-size: 14px; +} + +span.age-mins, +span.age-hours { + color: var(--green-600); + font-weight: 600; +} + +span.age-days, +span.age-weeks, +span.age-months, +span.age-years { + color: var(--ink-500); +} + +a.branch-deco, +a.tag-deco, +a.tag-annotated-deco, +a.remote-deco, +a.deco { + margin-left: 6px; + border: 1px solid var(--line-strong); + border-radius: 999px; + padding: 1px 8px; + background: var(--surface-muted); + color: var(--ink-800); + font-size: 12px; + font-weight: 600; +} + +a.branch-deco { + border-color: #a7d3b9; + background: #eaf7ef; + color: var(--green-600); +} + +a.tag-deco, +a.tag-annotated-deco { + border-color: #bcd8f2; + background: var(--surface-blue); + color: var(--blue-700); +} + +ul.pager { + display: flex; + gap: 8px; + justify-content: center; + margin: 18px 0 0; + padding: 0; + list-style: none; |