Git-Server git · main
gitsync + this read-only git browser
PHP 77.1% CSS 11.4% Markdown 8.6% Makefile 2.9%git clone https://git.christianimmanuel.de/web-tools/Git-Server.gitwget https://git.christianimmanuel.de/web-tools/Git-Server/archive/Git-Server.tar.gzgitsync + 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)
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.deFrom the laptop: make deploy, then on the server:
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 apache2Both 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
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 # publishConfig 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.
.gitignorefiles 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 overMAX_SIZE(default 50m) are skipped;EXCLUDE=a,bin 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
modifieddate 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
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:
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 remotegitsync 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 | notesEverything 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:
sudo apt install apache2-utils
sudo htpasswd -cB /var/www/html/git.christianimmanuel.de/.htpasswd NAME # -c only for the first userFetch 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 inlfs-backup. - Secret files (private keys,
psk=/password=lines) are left out, unlessSECRETS_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.
(umask 077; openssl rand -base64 32 > ~/.config/gitsync/secrets.pass) # one line; keep a copy elsewhere!Restore on another system:
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 passphraseFiles 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 -rforces it. Enter skips. - Root only reads a
backup.confowned 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/<group>/<file>). 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