Minimal git server
µgit allows cloning via HTTPS/SSH, but can only be pushed to via SSH.
There are no plans to directly support issues or PR workflows.
If you wish to collaborate, please send me patches via github or tangled.
For issue/bug-tracking, I recommend git-bug.
Currently all HTML is allowed in markdown, µgit is intended to be run by/for a trusted user.
Pushing to a repo path that doesn't exist yet creates it automatically, there's no separate "create repo" step. Combined with push options, this means a repo can be created and configured in the same command as its first push:
git push -o "private=false" ssh://git@example.com:8448/new-repo.git main
A handful of git push -o <key>=<value> (--push-option) options are read on every push to update the repo's metadata, in addition to actually pushing the ref(s):
| Option | Value | Effect |
|---|---|---|
desc/description |
any string | Sets the repo description |
private |
true/false |
Sets the repo's private tag (see Configuration) |
tags |
comma-separated list, prefix an entry with - to remove it |
Adds/removes tags on the repo, for filtering |
For example:
git push -o desc="My project" -o "private=false" -o "tags=go,-wip" origin main
Push options can be repeated (-o key=value -o key2=value2) and are applied in order; unrecognized options are ignored.
The web UI's repo search is a lightweight, git grep-style regex scan over the files at the tip of the default branch, implemented in-process rather than shelling out to git.
If you need more advanced code search, consider running something like zoekt alongside µgit rather than relying on the built-in search.
µgit ships a flake.nix with a NixOS module that can run one or more independent instances via services.ugit.<name>.
Each instance gets its own user/group, home directory, and systemd service.
See nix/module.nix for the full list of options.
{
inputs.ugit.url = "git+https://git.jolheiser.com/ugit";
outputs = { self, nixpkgs, ugit, ... }: {
nixosConfigurations.example = nixpkgs.lib.nixosSystem {
modules = [
ugit.nixosModules.default
{
services.ugit.public = {
enable = true;
authorizedKeysFile = "/etc/ugit/authorized_keys";
config = {
show-private = false;
http.port = 8449;
};
};
}
];
};
};
}
The config option is written out as the instance's YAML config file (see Configuration below), so any flag documented there can be set through it.
µgit is a single statically-linked Go binary with no runtime dependencies beyond git itself on $PATH.
Build it from source with a recent Go toolchain: go build ./cmd/ugitd, or go install go.jolheiser.com/ugit/cmd/ugitd@latest
Run the resulting ugitd binary directly, or under your process supervisor of choice (systemd, runit, etc).
µgit needs write access to its repo directory, SSH host key path, and (if generated) authorized_keys path.
µgit is configured via CLI flags, environment variables, and/or a YAML config file, in that order of precedence.
--ssh.port=2222. and - replaced by _, prefixed with UGIT_,
e.g. ssh.port becomes UGIT_SSH_PORTugit.yaml, override with --config) is YAML, with dotted flag names expanded into
nested keys, e.g.
ssh:
port: 2222
| Flag | Default | Description |
|---|---|---|
--config |
ugit.yaml |
Path to config file |
--repo-dir |
.ugit |
Path to directory containing repositories |
--show-private |
false |
Show private repos in the web interface |
--log.level |
error |
Logging level: debug, info, warn, error |
--log.json |
false |
Print logs in JSON(L) format |
--ssh.enable |
true |
Enable the SSH server |
--ssh.authorized-keys |
.ssh/authorized_keys |
Path to authorized_keys |
--ssh.clone-url |
ssh://localhost:8448 |
SSH clone URL base, shown to clients |
--ssh.port |
8448 |
SSH port |
--ssh.host-key |
.ssh/ugit_ed25519 |
SSH host key path (created if it doesn't exist) |
--http.enable |
true |
Enable the HTTP server |
--http.clone-url |
http://localhost:8449 |
HTTP clone URL base, shown to clients |
--http.port |
8449 |
HTTP port |
--meta.title |
ugit |
App title, shown in the web interface |
--meta.description |
Minimal git server |
App description, shown in the web interface |
--profile.username |
Username shown on the index page | |
--profile.email |
Email shown on the index page | |
--profile.links |
name,url pair for the index page; repeat the flag for more |
Each repository also has its own private flag, which is just a tag on the repo and is set to true by default when a repo is created.
It has no effect on its own; --show-private is what decides whether repos tagged private are shown in the web interface, and it also defaults to false.
Out of the box, newly created repos are private and --show-private is off, meaning repos are hidden from the web UI by default until you opt in one way or the other.
Neither setting affects SSH access: any user in authorized_keys can always push/pull a repo, private or not.
The setup I run is two ugit instances pointed at the same --repo-dir:
--show-private=false (the default), reverse-proxied to the internet--show-private=true, reachable only over Tailscale (or another private network)Both instances see the same repositories, but only the private instance's web UI will list/serve repos marked private.
Pushing/cloning over SSH is unaffected by --show-private, it only gates the web interface.
Using GitHub as an example (although Gitea/GitLab should have the same URL scheme)
Ba/sh
curl https://github.com/<username>.keys > path/to/authorized_keys
Nushell
http get https://github.com/<username>.keys | save --force path/to/authorized_keys
Lots of inspiration and some starting code used from gitea (MIT), wish (MIT), and legit (MIT).