TL;DR: Geminus manages Git and
Jujutsu repositories, and
does so in a sane way. It solves the problem of "where do I clone this repo
to?", or "where did I have this repo cloned again?". It does so in a way that's
as simple, convenient and foolproof as possible. Running
gem https://git.my-remote.com/alice/my-repo clones my-repo to
root/git.my-remote.com/alice/my-repo (where root is
~/git by default). That's it. That's the whole thing. If you think this
project is overill for just that, you're absolutely correct, and you can leave
now. Feel free to create your own shell snippet to do just that, and make sure
to check out the shell snippets in shell.zig. This whole work is dedicated
to the public domain, so feel free to take what you need. :) To the rest, here's
what this actually solves:
- Repos are always where you expect them to be.
- You don't accidentally clone multiple times.
- Repos with the same name don't conflict.
- You don't have to think where to clone a repository to.
Doesen't sound all that useful at first? Truth be told, it might not make a big difference compared to just blindly copying repos to random locations, but it takes another thing off your mind, permanently. We already do this for git forges, why not do it for local machines as well?
Because a native executable cannot change its parent shell's current directory,
gem init <shell> emits shell wrappers that call the binary and cd to the
returned checkout path.
Yes, I know it's got the same binary name as the CLI tool for managing Ruby gems. No, I do not care. To get started with Geminus, skip to the Usage section.
Geminus is built with Zig 0.16. The repository provides a Nix flake for local
development, package builds, CI checks, and shell-integration tests. The
installed binary is called gem; the project/package name is geminus.
gem <repo-ref> clone or enter the canonical checkout
gem where <repo-ref> print the intended local checkout path
gem fix fix the current checkout path/remote
gem config print effective ZON configuration
gem init nushell|bash|zsh|fish|elvish
emit shell integration
Options:
--json machine-readable JSON output
--shell tab-separated shell-wrapper output
Accepted reference forms include:
https://github.com/owner/repo.git
ssh://git@github.com/owner/repo.git
git://github.com/owner/repo.git
git@github.com:owner/repo.git
github.com/owner/repo
git clone git@github.com:owner/repo.git
https://github.com/owner/repo/tree/branch-name
https://github.com/owner/repo/pull/123
https://github.com/owner/repo/issues?q=is%3Aissue+state%3Aopen
https://gitlab.com/group/project/-/merge_requests/17
https://git.example.com/group/sub/project/-/merge_requests/17
Default command. Resolve the canonical local path, enter the existing checkout when it already exists and matches the same remote, or clone it when missing. The plain text output is the final path.
Run inside a repository. gem finds the repository root, reads the configured
remote, computes the canonical path, updates the remote URL, and moves the
checkout when it is inside the configured root and the destination is free.
Print the effective configuration as ZON after defaults and user overrides are applied.
Emit a shell wrapper. The wrapper calls gem in a machine-readable mode and
changes the parent shell's current directory to the returned checkout path.
Shell integration is sadly needed, since gem needs to change the shell's current directory to the location of the repository. This is only possible by utilizing a shell script.
Bash:
eval "$(gem init bash)"Zsh:
eval "$(gem init zsh)"Fish:
gem init fish | sourceNushell:
gem init nushell | save --force ~/.config/nushell/autoload/gem.nuElvish:
mkdir -p ~/.config/elvish
gem init elvish >> ~/.config/elvish/rc.elvConfiguration is written in
Zig object notation and to either $XDG_CONFIG_HOME/geminus/config.zon, or to ~/.config/geminus/config.zon as a fallback.
The default configuration looks like this:
.{
.root = "~/git",
.layout = "{forge}/{owner}/{repo}",
.vcs = .auto,
.preserve_case = true,
.default_remote = "origin",
.hosts = .{
.{
.host = "github.com",
.alias = "github.com",
.preferred_protocol = .ssh,
},
.{
.host = "gitlab.com",
.alias = "gitlab.com",
.preferred_protocol = .ssh,
},
.{
.host = "codeberg.org",
.alias = "codeberg.org",
.preferred_protocol = .ssh,
},
},
}An example user config could look like this:
.{
.root = "~/src",
.layout = "{forge}/{path}",
.vcs = .jj_colocated,
.preserve_case = true,
.default_remote = "origin",
.hosts = .{
.{
.host = "git.example.com",
.alias = "example.com",
.preferred_protocol = .https,
},
},
}The root of of the directory where all cloned repositories should be located. Both ~ and ~/... expand through $HOME, and the (expanded) root must be
absolute.
layout refers to the repository's relative path under root. Supported tokens are:
{forge} normalized host alias, e.g. codeberg.org
{host} raw input host, e.g. https://codeberg.org
{owner} first repository path segment e.g. alice
{repo} final repository path segment without .git, e.g. my-repo
{path} full repository path without .git, e.g. codeberg.org/alice/my-repo
{vcs} git or jj
One of .auto, .git, .jj_colocated, or .jj_standalone. .auto colocates repositories using jj when available and falls back to git.
When true, repository paths keep remote casing. When false, path components
are lowercased.
Remote name used by gem fix. Defaults to origin.
Host overrides. Each entry has a required host, optional alias, and optional
preferred_protocol. Protocol values are .ssh, .https, .git, .file, and
.preserve.
Plain text is optimized for direct shell use:
/home/user/git/github.com/owner/repo
JSON is intended for structured integrations:
gem --json where github.com/owner/repoShell output is a stable tab-separated format for shell wrappers:
gem --shell github.com/owner/repoTo build Geminus, you'll need a copy of the Zig compiler, version 0.16.X.
You can then build it using:
zig build -Doptimize=ReleaseSafeTo install the binary and man page under a prefix, run:
zig build -Doptimize=ReleaseSafe --prefix ~/.localThis installs:
~/.local/bin/gem
~/.local/share/man/man1/gem.1
This repository provides Geminus as a Nix flake output under both .#default and .#gem.
You can add the gem package to your system as follows:
Using Tack:
# pins.toml
[inputs]
[inputs.geminus]
url = "github:manic-systems/geminus"Then add it to your system configuration.
Using flakes:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
geminus = {
url = "github:manic-systems/geminus";
# Optionally:
# inputs.nixpkgs.follows = "nixpkgs"
};
};
outputs = { nixpkgs, geminus }: {
nixosConfigurations.foo = nixpkgs.lib.nixosSystem {
modules = [
(
{ pkgs, ... }:
{
environment.systemPackages = [
geminus.packages.${pkgs.stdenv.hostPlatform.system}.gem
];
}
)
];
};
};
}0 success
1 repository or command operation failed
2 usage, parse, plan, or configuration error
4 conflict or refused move
6 clone failed