ugit logo

µ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).