Managing git repositories using declarative TOML configuration and JSON output
- JSON by default - Pipe
arbol statusdirectly intojqfor scripting - Declarative config - Define all your repositories in a single TOML file
- Multiple accounts - Use different profiles for different machines (home, work, etc.)
- Smart status - See branch, dirty files, and sync state with remote at a glance
- Shell completion - Tab-complete repository paths in Fish shell
Requires Go 1.23+ and go-task.
git clone https://github.com/oschrenk/arbol.git
cd arbol
task installThis installs the binary to $GOPATH/bin/arbol and Fish completions to ~/.config/fish/completions/.
go build -o arbol ./cmd/arbolThe flake ships a home-manager module that installs arbol and generates
~/.config/arbol/config.toml, so the repo tree is declared in Nix rather than
hand-edited.
{
inputs.arbol.url = "github:oschrenk/arbol";
# in your home-manager configuration:
imports = [ inputs.arbol.homeModules.arbol ];
programs.arbol = {
enable = true;
accounts.default = {
default = true;
root = "~/Projects";
repos = {
tools = [
"[email protected]:oschrenk/arbol.git"
"[email protected]:oschrenk/bump.git"
];
"work.backend" = [
"[email protected]:company/api.git"
{ url = "[email protected]:company/worker.git"; name = "worker-svc"; }
];
};
};
};
}A repo is a URL string, or an attrset when the directory name should differ
from the one derived from the URL. Repos are keyed by the dotted path below
root — the same path arbol status and arbol sync take as a filter — and
the module inserts the "/" marker described under
Nested Paths on its own when a path holds both repos and
subpaths.
- Create a starter config:
arbol init- Edit
~/.config/arbol/config.toml:
[accounts.default]
default = true
root = "~/Projects"
repos.work.backend = [
{ url = "[email protected]:company/api.git" },
{ url = "[email protected]:company/worker.git" },
]
repos.personal = [
{ url = "[email protected]:me/dotfiles.git" },
]- Clone your repositories:
arbol sync- Check status:
arbol statusOutput is JSON by default, ready for jq:
$ arbol status | jq '.[0]'
{
"id": "work.backend.api",
"path": "/home/user/Projects/work/backend/api",
"branch": { "name": "main", "detached": false },
"changes": { "dirty": false, "files": 0, "last_commit": "2025-01-15T10:30:00Z" },
"remote": { "ahead": 0, "behind": 0, "diverged": false, "tracking": true }
}Use --plain for a human-readable table.
Clone missing repositories. Skips repos that already exist.
arbol sync # Sync all repos
arbol sync work.backend # Sync repos under work.backend
arbol sync --fetch # Also fetch updates for existing reposFlags:
--fetch- Rungit fetch --all --tagson existing repos
Show status of repositories. Outputs JSON by default for easy scripting and piping to tools like jq.
arbol status # JSON output (default)
arbol status personal # Filter repos under personal
arbol status --plain # Table output
arbol status --state dirty # Only repos with uncommitted changes
arbol status --state attention # Anything not clean and in sync
arbol status --plain --state dirty,ahead # Dirty or unpushed
arbol status | jq '.[] | select(.changes.dirty)' # Filter dirty reposStates:
--state takes one or more states and shows repos matching any of them.
It applies to both JSON and --plain output.
| State | Meaning |
|---|---|
attention |
Any of the states below, i.e. not clean and in sync |
dirty |
Uncommitted changes in the working tree |
ahead |
Unpushed commits |
behind |
Commits behind the upstream branch |
diverged |
Both ahead and behind |
detached |
Detached HEAD |
missing |
Not cloned yet |
no-upstream |
Branch has no upstream to compare against |
Repos whose status cannot be read are always shown, since they cannot be classified.
JSON schema:
Each entry in the output array has the following structure. The branch, changes, and remote fields are omitted for repos that are not cloned or have errors.
{
"id": "work.backend.api",
"path": "/home/user/Projects/work/backend/api",
"branch": {
"name": "main",
"detached": false
},
"changes": {
"dirty": true,
"files": 3,
"last_commit": "2025-01-15T10:30:00Z"
},
"remote": {
"ahead": 2,
"behind": 0,
"diverged": true,
"tracking": true
}
}Flags:
--state STATE,...- Only show repos in any of these states (see above)--plain- Show table output instead of JSON--no-color- Disable colored output (only with--plain)--no-headers- Hide column headers (only with--plain)--path-width N- Width of PATH column, default: 30 (only with--plain)--branch-width N- Width of BRANCH column, default: 15 (only with--plain)
Create a starter configuration file at ~/.config/arbol/config.toml.
Print version, commit hash, and build date.
Generate shell completion scripts. Supports: bash, zsh, fish, powershell.
arbol completion fish > ~/.config/fish/completions/arbol.fish--account,-a- Use a specific account instead of the default
Config location: $XDG_CONFIG_HOME/arbol/config.toml (defaults to ~/.config/arbol/config.toml)
On Nix, generate this file from programs.arbol instead of writing it by hand
— see home-manager.
[accounts.<name>]
default = true # Optional: mark as default account
root = "~/Projects" # Required: base directory for repos
repos.<path> = [
{ url = "[email protected]:user/repo.git" },
{ url = "[email protected]:user/other.git", name = "custom-dir" },
]Config paths map directly to filesystem directories:
| Config | Filesystem |
|---|---|
repos.work.backend with api |
~/Projects/work/backend/api/ |
repos.personal with dotfiles |
~/Projects/personal/dotfiles/ |
When a directory contains both repos and subdirectories, use "/":
# Repos directly in ~/Projects/personal/
repos.personal."/" = [
{ url = "[email protected]:me/dotfiles.git" },
]
# Repos in ~/Projects/personal/golang/
repos.personal.golang = [
{ url = "[email protected]:me/arbol.git" },
]Define different repo sets for different machines:
[accounts.home]
default = true
root = "~/Projects"
repos.personal = [
{ url = "[email protected]:me/dotfiles.git" },
]
repos.work.backend = [
{ url = "[email protected]:company/api.git" },
]
[accounts.work-laptop]
root = "~/Code"
# Only work repos on work laptop
repos.work.backend = [
{ url = "[email protected]:company/api.git" },
]Use with: arbol sync --account work-laptop
See EXAMPLES.md for more jq recipes.
MIT