1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
<p align="center">
<picture>
<img alt="ugit logo" width="250" src="./assets/ugit.svg" />
</picture>
<h3 align="center">µgit</h3>
</p>
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 <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](#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.<name>`.
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/<username>.keys > path/to/authorized_keys
```
Nushell
```sh
http get https://github.com/<username>.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).
|