µgit
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](https://github.com/jolheiser/ugit) or [tangled](https://tangled.org/@jolheiser.com/ugit).
For issue/bug-tracking, I recommend [git-bug](https://github.com/git-bug/git-bug).
Currently all HTML is allowed in markdown, µgit is intended to be run by/for a trusted user.
## Features
### Push-to-create
Pushing to a repo path that doesn't exist yet creates it automatically, there's no separate "create repo" step.
Combined with [push options](#push-options), this means a repo can be created and configured in the same command as its first push:
```sh
git push -o "private=false" ssh://git@example.com:8448/new-repo.git main
```
### Push options
A handful of `git push -o =` (`--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](#configuration)) |
| `tags` | comma-separated list, prefix an entry with `-` to remove it | Adds/removes tags on the repo, for filtering |
For example:
```sh
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.
### Search
The web UI's repo search is just a wrapper around `git grep` in-process.
If you need more advanced code search, consider running something like [zoekt](https://github.com/sourcegraph/zoekt).
## Deployment
### NixOS
µgit ships a `flake.nix` with a NixOS module that can run one or more independent instances via `services.ugit.`.
See `nix/module.nix` for the full list of options.
```nix
{
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";
showPrivate = false;
http.port = 8449;
};
}
];
};
};
}
```
### Everything else
Build it from source with a recent Go toolchain: `go build ./cmd/ugitd`, or `go install go.jolheiser.com/ugit/cmd/ugitd@latest`
µgit needs write access to its repo directory, SSH host key path, and (if generated) authorized_keys path.
## Configuration
µgit is configured via CLI flags, environment variables, and/or a JSONnet config file, in that order of precedence.
- CLI flags use the names below, e.g. `--ssh.port=2222`
- Environment variables are the flag name upper-cased with `.` and `-` replaced by `_`, prefixed with `UGIT_`,
e.g. `ssh.port` becomes `UGIT_SSH_PORT`
The config file is JSONnet, with dotted flag names expanded into nested keys, e.g.
```jsonnet
{
ssh: {
port: 2222
}
}
```
| Flag | Default | Description |
|--------------------------|-----------------------------|--------------------------------------------------------------|
| `--config` | `ugit.jsonnet` | 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.
### Running public and private instances
The setup I run is two `ugit` instances pointed at the same `--repo-dir`:
- a **public** instance with `--show-private=false` (the default), reverse-proxied to the internet
- a **private** instance with `--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.
## Getting your public SSH keys from another forge
Using GitHub as an example (although Gitea/GitLab should have the same URL scheme)
Ba/sh
```sh
curl https://github.com/.keys > path/to/authorized_keys
```
Nushell
```sh
http get https://github.com/.keys | save --force path/to/authorized_keys
```
## License
[MIT](LICENSE)
Lots of inspiration and some starting code used from [gitea](https://github.com/go-gitea/gitea) [(MIT)](https://github.com/go-gitea/gitea/blob/eba9c0ce48c7d43910eb77db74c6648157663ceb/LICENSE), [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE), and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license).