# gitsync + git browser Self-hosted, read-only git browser for `git.christianimmanuel.de`. Repos are grouped in categories, browsable like GitHub/GitLab, and cloneable via HTTPS. Nothing is pushed to the server with git; `gitsync` on your laptop publishes the projects you list. **100% Vibecode but tested.** ## Parts | File | What | |---|---| | `gitsync` | laptop script: stages listed projects, rsyncs them to the server | | `gitsync.conf` | example config with categories | | `web/index.php`, `web/style.css` | the browser (PHP, no database) | | `apache/*.conf` | vhosts: `-common.conf` holds everything (browser, clones, private auth), the `:80` and `-le-ssl` vhosts just include it | | `Makefile` | install / uninstall / config / deploy / clean | ## Server (Debian) ```sh sudo apt install apache2 git libapache2-mod-php rsync sudo a2enmod rewrite cgi env sudo mkdir -p /var/www/html/git.christianimmanuel.de/repos sudo chown -R debian:www-data /var/www/html/git.christianimmanuel.de ``` From the laptop: `make deploy`, then on the server: ```sh sudo mv /tmp/git.christianimmanuel.de*.conf /etc/apache2/sites-available/ sudo a2ensite git.christianimmanuel.de && sudo systemctl reload apache2 sudo certbot --apache -d git.christianimmanuel.de # first time only; afterwards keep our -le-ssl.conf sudo a2ensite git.christianimmanuel.de-le-ssl && sudo systemctl reload apache2 ``` Both vhosts only `Include` `git.christianimmanuel.de-common.conf`; edit that one file for changes. The `repos/` directory is only reachable through `index.php` and `git-http-backend`. Only `git-upload-pack` is exposed, so clones work and pushes do not. ## Laptop ```sh su -c 'make install' # /usr/bin/gitsync make config # ~/.config/gitsync/gitsync.conf (only if missing) make config-update # overwrite it with the shipped config (.bak is kept) gitsync # publish ``` Config format: ``` HOST=debian@162.19.227.194 REMOTE_DIR=/var/www/html/git.christianimmanuel.de [SDL & Graphics] ~/Git/sdl_3d | 3D rendering experiments | sdl, 3d, c ``` - Git projects become bare repos (`category/name.git`) with all branches and tags. Clone URL: `https://git.christianimmanuel.de/category/name.git` - Plain directories are copied as files, shown without commits. `.gitignore` files are honoured (per directory, like git). Symlinks are followed, the target's content is published (dangling links are skipped with a warning). Build junk (`CMakeFiles`, `*.o`, `*.swp`, `__pycache__`, ...), compiled binaries and files over `MAX_SIZE` (default 50m) are skipped; `EXCLUDE=a,b` in the config adds more patterns. `STAGE=` moves the staging dir (default `~/.cache/gitsync`). - Category order on the page = order in the config. - "Recently updated" on the index sorts by the `modified` date from each project's meta file: last commit for git projects, newest file mtime for plain directories (rsync keeps mtimes). - Removing a line removes the repo from the server on the next run. - Tags are optional (third column), searchable on the site. - Origin remote (GitHub/Codeberg) is shown as upstream link if the project has one. ### Automatic daily sync ```sh make ssh-key # creates ~/.ssh/gitsync_ed25519 and prints the server-side commands (user git-local) make cron # crontab entry: 12:00 daily, only if the server answers a ping make uncron # remove it su -c 'make cron-daily' # alternative without a user crontab: /etc/cron.daily/gitsync (edit USER= in cron/gitsync first) ``` Set `HOST=git-local@…` and `SSH_KEY=~/.ssh/gitsync_ed25519` in the config (default). The upload runs with `BatchMode`, so it never hangs on a password prompt. Options: `-n` dry run, `-q` quiet, `-l` stage only, `-c` list config, `-f` ignore secret scan, `-v` verbose, `-V` version. ### Secret scan Before staging, every project is checked for private keys, AWS/GitHub/GitLab/Slack/OpenAI tokens, `api_key = "…"` / `password: '…'` style lines with a quoted literal value containing a digit (placeholders like `"your_api_key"` are ignored), and files like `.env`, `id_rsa`, `*.pem`. Git projects: tracked files and the whole history are scanned (untracked/ignored files are never published anyway). Plain directories: exactly the staged files are scanned, so ignored files don't trigger it. Files named `*example*`, `*sample*`, `*template*` or `*public*` are exempt from the filename check. A hit skips that project and prints where it was found. If it is a false positive, publish with `gitsync -f`. If it is real: rotate the key, then remove it from history: ```sh pip install git-filter-repo git filter-repo --invert-paths --path path/to/client.key --path path/to/server.key git push --force origin --all # if the project has a remote ``` `gitsync` checks the whole history because a bare clone ships every commit, not just HEAD. Staging dir is `~/.cache/gitsync` (`make clean` removes it). ## Private projects Prefix a config line with `!` to publish it under `/private` instead of the public tree: ``` [Private] !~/Git/notes | My private notes | notes ``` Everything below `/private` (pages, raw files, tarballs, `git clone`) is protected by HTTP Basic auth in Apache. Private projects are not listed on the public pages and are **not** secret-scanned. Create users on the server: ```sh sudo apt install apache2-utils sudo htpasswd -cB /var/www/html/git.christianimmanuel.de/.htpasswd NAME # -c only for the first user ``` Fetch with `wget --user=NAME --ask-password URL` or `git clone https://NAME@git.christianimmanuel.de/private/cat/name.git`. Apache enforces this in `-common.conf`; `index.php` additionally checks the login itself against `.htpasswd` (bcrypt), so pages, raw files and tarballs stay closed even with an outdated vhost. ### lfs-backup config Publish exactly the files an `lfs-backup` config lists, always private: ``` [Private] !lfs-backup < /etc/pkgusr/backup.conf | My LFS system configuration | lfs, config ``` - Layout on the site: `SECTION/path`, e.g. `user-shell/.bashrc`, `system-boot/etc/fstab`. - Globs, directories, symlinks and `!` exclusions work like in `lfs-backup`. - Secret files (private keys, `psk=`/`password=` lines) are left out, unless `SECRETS_PASS=` is set (below). **Secrets, encrypted.** With `SECRETS_PASS=~/.config/gitsync/secrets.pass` in the config, secret files go up encrypted (openssl, AES-256, PBKDF2) as `secrets-user.tar.enc` (your files) and `secrets-root.tar.enc` (root/system files). The server never sees them in plain; the web view shows them as binary files. ```sh (umask 077; openssl rand -base64 32 > ~/.config/gitsync/secrets.pass) # one line; keep a copy elsewhere! ``` Restore on another system: ```sh wget --user=NAME --ask-password https://git.christianimmanuel.de/private/private/lfs-backup/raw/secrets-root.tar.enc openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in secrets-root.tar.enc | tar -xv # asks for the passphrase ``` Files only root can read (`[root:*]`, `/etc/rc.d/rc.iptables`, …) are collected as root: - `su -c 'make cron-daily'`: the daily job collects them first (`gitsync --collect-root USER`, no password), into `/var/cache/gitsync/USER.tar` (yours, mode 600). - In a terminal, gitsync asks for the root password (`su`) when that is missing or older than a day. `gitsync -r` forces it. Enter skips. - Root only reads a `backup.conf` owned by root, and never hands out secret files. ## Scripts collection `SCRIPTS=~/Bash-Public` in the config publishes a directory of standalone scripts at `/scripts`. Every subdirectory is a group, every file a script. Lines starting with `### ` at the top of a file are shown as description (`######` lines are ignored). Each script gets a `wget … && chmod 740` line, a source view and a raw URL (`/scripts/raw//`). Same excludes, `.gitignore` and secret scan as plain directories. ## Browser features Categories, cards with description, tags, language bar and last change. Search by name/description/tag. File tree, file view, Markdown README (rendered server side), raw files, commit log with diffs, commit activity graph (per repo and site wide), `git clone` and `wget` tarball commands, dark mode. Everything is plain HTML, so it works in lynx; JavaScript only adds syntax highlighting and copy buttons. ## Make targets `install`, `uninstall`, `config`, `config-update`, `ssh-key`, `cron`, `uncron`, `deploy-web`, `deploy-apache`, `deploy`, `clean`