--- /dev/null
+# git.ardis.lu
+
+Configurations and scripts for my self-hosted git server.
+
+Tested and running on Debian but these directions should work on any similar `systemd` distribution.
+
+Code repository mirrors: [GitHub](https://github.com/ardislu/git.ardis.lu), [Codeberg](https://codeberg.org/ardislu/git.ardis.lu), [git.ardis.lu](https://git.ardis.lu/git.ardis.lu)
+
+## Setup
+
+1. Install required software:
+
+```
+sudo apt install git gitweb highlight ufw fcgiwrap
+```
+
+See [Caddy: Install](https://caddyserver.com/docs/install) for directions on installing `caddy`.
+
+Explanation:
+- `git`, `gitweb` are naturally required.
+- `highlight` for code syntax highlighting on the web view.
+- `ufw` for setting up firewall rules (can substitute with whatever you like).
+- `fcgiwrap` to call the `gitweb` and `git-http-backend` FastCGI scripts.
+- `caddy` for the web server to serve the web view and repos over HTTPS.
+
+2. Clone this repo somewhere on the server.
+
+3. Update `./srv/git/.ssh/authorized_keys` to include all admin SSH keys that should have **write** access to repos.
+
+4. Run `setup.sh` to create or update the `git` user, git scripts, the git repo directory `/srv/git`, and `gitweb` configurations:
+
+```
+sudo ./setup.sh
+```
+
+5. Open port 443 to allow access over HTTPS:
+
+```
+sudo ufw allow "WWW Secure"
+```
+
+## View and clone repos
+
+Browse existing repos using the web view: [https://git.ardis.lu](https://git.ardis.lu)
+
+Support for read/write:
+
+Transport protocol | Read | Write | Authorization required?
+--- | --- | --- | ---
+`ssh://` | Yes | Yes | Yes
+`https://` | Yes | No | No
+`http://` | No | No | N/A
+`git://` | No | No | N/A
+
+`http://` and `git://` are disabled because (1) it simplifies server administration and (2) the lack of integrity checks or encryption makes them subject to tampering. This configuration matches [GitHub's](https://github.blog/security/application-security/improving-git-protocol-security-github/).
+
+URLs to clone individual repos can be found within the repo page on the web view.
+
+## Helper scripts
+
+A minimal set of administrative commands.
+
+### `init`
+
+Create a new repo and optionally set its description for the web view.
+
+```
+ssh git@git.ardis.lu init <repository_name> [repository_description]
+```
+
+### `rm`
+
+Delete a repo.
+
+```
+ssh git@git.ardis.lu rm <repository_name>
+```
+
+## Uninstall
+
+To reverse the steps in `setup.sh`.
+
+3. (From another computer) Use the `rm` helper script to delete all repos:
+
+```
+ssh git@git.ardis.lu rm <repository_name>
+```
+
+4. Delete the `git` user and its home directory `/srv/git`:
+
+```
+sudo deluser --remove-home git
+```
+
+5. (Optional) Delete `git-shell` from `/etc/shells`:
+
+```
+sudo vim /etc/shells
+```
+
+### Disable `git-daemon`
+
+These steps are for reference only, they are not needed because `git-daemon` is not enabled on the server.
+
+1. Delete the `ufw` rule to open port 9418 for `git-daemon`:
+
+```
+sudo ufw delete limit 9418/tcp
+```
+
+2. Stop and delete `git-daemon.service`:
+
+```
+sudo systemctl stop git-daemon
+sudo systemctl disable git-daemon
+sudo rm /etc/systemd/system/git-daemon.service
+sudo systemctl daemon-reload
+sudo systemctl reset-failed
+```
--- /dev/null
+git.ardis.lu {
+ root /usr/share/gitweb
+ encode
+ file_server
+
+ # For root, serve gitweb for human-friendly browsing
+ reverse_proxy / unix//run/fcgiwrap.socket {
+ transport fastcgi {
+ env SCRIPT_FILENAME /usr/share/gitweb/gitweb.cgi
+ }
+ }
+
+ # For /[name] and /[name].git WITHOUT additional paths, redirect to /?p=[name].git for human-friendly browsing
+ @git_bare path_regexp ^(\/)([A-Za-z0-9_.-]+?)(\.git)?$
+ redir @git_bare /?p={re.2}.git
+
+ # For /[name].git WITH additional paths, serve git-http-backend to support git operations
+ # {method}, {path}, {query} are magic Caddy variables which correspond to the URL
+ # components from the request.
+ @git_backend path_regexp ^\/[A-Za-z0-9_.-]+\.git\/.*$
+ reverse_proxy @git_backend unix//run/fcgiwrap.socket {
+ transport fastcgi {
+ env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend
+ env GIT_PROJECT_ROOT /srv/git
+ env GIT_HTTP_EXPORT_ALL 1 # Can be any value, but it must be set to allow serving over HTTP
+ env REQUEST_METHOD {method}
+ env PATH_INFO {path}
+ env QUERY_STRING {query}
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+$projectroot = '/srv/git';
+$git_temp = '/tmp';
+
+# https://git-scm.com/docs/gitweb.conf#_changing_gitwebs_look
+$site_name = 'git.ardis.lu';
+$projects_list_description_width = 120; # Increase project description character limit from 25
+$default_projects_order = 'age'; # Sort by last commit, default is sort by project name
+
+# https://git-scm.com/docs/gitweb.conf#_features_in_feature
+$feature{'highlight'}{'default'} = [1]; # Syntax highlighting. Assumes `highlight` is available on $PATH.
+$feature{'blame'}{'default'} = [1]; # Enable full git blame
+$feature{'timed'}{'default'} = [1]; # Enable footer stats
+$feature{'snapshot'}{'default'} = ['zip', 'tgz']; # Enable .zip (gzip) and .tgz (tar) archive download
+
+# https://git-scm.com/docs/gitweb.conf#_some_optional_features_and_policies
+$omit_owner = 1; # Not needed since all repos are implied to be owned by me
\ No newline at end of file
--- /dev/null
+# This file is for reference purposes only, it is not actually used on the server.
+# Use this systemd unit to enable cloning via `git://` protocol.
+
+[Unit]
+Description=Git Daemon
+Documentation=man:git-daemon(1)
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+User=git
+Group=git
+ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/srv/git
+Restart=on-failure
+
+# Security hardening
+PrivateTmp=true
+ProtectSystem=full
+ProtectHome=true
+NoNewPrivileges=true
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+
+git_shell_path="$(command -v git-shell || true)"
+
+if [ -z "$git_shell_path" ]; then
+ echo "Error: git-shell not found in PATH. Install git or check its path." >&2
+ exit 1
+fi
+
+# Add git-shell to /etc/shells if not already there
+if ! grep -qxF "$git_shell_path" /etc/shells; then
+ echo "$git_shell_path" >> /etc/shells
+fi
+
+# Setup files in /srv/git
+mkdir -p /srv/git/.ssh
+cp -rf ./srv/git /srv
+
+# Create git user if it does not already exist
+id -u git &>/dev/null || useradd --home-dir /srv/git --shell "$git_shell_path" --no-create-home --skel /dev/null git
+
+# Set permissions for /srv/git
+chown -R git:git /srv/git
+chmod -R 755 /srv/git
+chmod 700 /srv/git/.ssh
+chmod 600 /srv/git/.ssh/authorized_keys
+
+# Globally disable safe directory checks because only I will be writing to this server. This step is required
+# or else there will be "fatal: detected dubious ownership" errors when the web server attempts to
+# serve the repos as a user other than "git".
+# See https://stackoverflow.com/a/71904131/21084807
+git config --system safe.directory '*'
+
+# Otherwise HEAD will point to "master" when using init, even if the pushed repo has renamed main branch
+git config --system init.defaultBranch main
+
+# Setup gitweb
+systemctl enable --now fcgiwrap
+cp -f ./etc/caddy/Caddyfile /etc/caddy
+systemctl restart caddy
+
+# Below steps for configuring git-daemon are for reference only.
+# It is commented out because the git:// protocol's lack of encryption or integrity checks make it
+# subject to tampering.
+
+# Add or update git-daemon.service, which enables read-only access via git:// protocol (port 9418)
+# cp -f ./etc/systemd/system/git-daemon.service /etc/systemd/system
+# systemctl daemon-reload
+# systemctl enable --now git-daemon.service
+
+# Configure ufw to open port 9418 with rate limiting
+# ufw limit 9418/tcp comment 'rate-limited git-daemon'
--- /dev/null
+no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAgbXIdAZ9p+GMj2TngGaF4X//OfzSm56eCcNuMAllcK
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env bash
+
+# Require a name to be provided
+if [ $# -lt 1 ]; then
+ echo "Usage: init <repository_name> [repository_description]" >&2
+ exit 1
+fi
+
+user='git'
+hostname='git.ardis.lu'
+
+name="$1"
+
+# Interpret all remaining arguments as the description. Importantly, passing a command like:
+# ssh git@git.ardis.lu init example_name "example description with spaces"
+# will NOT preserve the quotes when passed to the bash command (i.e., each word is interpreted
+# as a separate argument). As a workaround, just interpret all remaining arguments as the description.
+# See https://stackoverflow.com/a/53472697/21084807
+shift 1
+description="$@"
+
+# Check repo name is valid using the same convention as GitHub
+# See https://stackoverflow.com/a/59082561/21084807
+pattern='^[A-Za-z0-9_\.\-]+$'
+if ! [[ "$name" =~ $pattern ]]; then
+ echo "Error: invalid repository name '$name'" >&2
+ exit 1
+fi
+
+path="$HOME/$name.git"
+
+# Prevent overwriting an existing repo
+if [ -e "$path" ]; then
+ echo "Error: repository '$name' already exists" >&2
+ exit 1
+fi
+
+# Instantiate the repo
+mkdir -p "$path"
+cd "$path"
+git init --bare --quiet
+
+# Schedule and enable maintenance jobs
+# Commented out to simplify administration (not dealing with cron jobs to remove repos)
+# git maintenance start
+
+# Create post-update hook for dumb HTTP transport and general maintenance
+# See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_dumb_http)
+# See https://git-scm.com/docs/git-maintenance
+# See https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
+cat > "$path/hooks/post-update" << EOL
+#!/bin/sh
+git update-server-info
+git maintenance run
+EOL
+chmod a+x "$path/hooks/post-update"
+
+# Add git-daemon-export-ok file to support git protocol
+# Commented out because git-daemon is not enabled on the server
+# See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_git_protocol
+# touch "$path/git-daemon-export-ok"
+
+# Set clone URLs to show on the git web view
+cat > "$path/cloneurl" << EOL
+https://$hostname/$name.git
+ssh://$user@$hostname/~/$name.git
+$user@$hostname:$name.git
+EOL
+
+# Set description if it was provided
+if [ -n "$description" ]; then
+ echo "$description" >"$path/description"
+fi
+
+echo "Repository '$name' created"
--- /dev/null
+#!/bin/sh
+GREEN='\033[32m'
+RESET='\033[0m'
+printf "🎉 ${GREEN}Success!${RESET} Your SSH key works.\n"
--- /dev/null
+#!/usr/bin/env bash
+
+# Require a name to be provided
+if [ $# -lt 1 ]; then
+ echo "Usage: rm <repository_name>" >&2
+ exit 1
+fi
+
+name="$1"
+path="$HOME/$name.git"
+
+if ! [ -e "$path" ]; then
+ echo "Error: repository '$name' does not exist" >&2
+ exit 1
+fi
+
+# Remove scheduled maintenance from init script
+# Commented out because git maintenance start is not automatically started
+# cd "$path"
+# git maintenance stop
+# git maintenance unregister --force
+# cd "$HOME"
+
+rm -r "$path"
+
+echo "Repository '$name' deleted"