TUI for multi-repo git forge monitoring across GitHub and Forgejo. See branch status, dirty working trees, ahead/behind counts, open PRs, CI runs, issues, recent branches, and commit activity across all your repos without leaving the terminal.
{o,o}
___ _ __ ___|)_)|___ __
/ _ \ '__/ _ \ \ / / _ \
| (_| | | | (_) \ V / __/
\__, |_| \___/ \_/ \___|
|___/
one repo to rule them all
- Multi-profile: define any number of profiles (personal, work org, etc.) and switch instantly with
H/Lor by clicking the profile tab bar; an All view merges every profile at once - Clone:
grove cloneenumerates all repos for each profile's code-hosting remote and clones any that are missing locally; groups can route clones to separate subdirectories - Dashboard (tab 1): all repos in one view with branch, dirty/clean state, sync status, open PR count, branch count, CI status, last author, and last commit time
- Pull Requests (tab 2): open PRs across all repos with review status and checks
- CI Runs (tab 3): recent workflow / pipeline runs across all repos with pass/fail/running status
- Branches (tab 4): all remote branches with PR and merge indicators
- Activity (tab 5): recent commits across repos with inline diff viewer
- Issues (tab 6): open forge issues across all repos with labels, assignees, milestones, and age
- Detail pane: full repo detail with local/remote branches, open PRs, open issues, CI runs, recent commits, and stats; item-level cursor with contextual actions per item type
- Diff viewer: scrollable inline
git showoutput with syntax colouring; respects your configured diff pager (delta,bat); navigate between commits with[/]and between files with{/} - External tools:
spaceopens diffnav (commits) or lazygit (repos) based on context;eopens$EDITOR/ nvim at repo root - Grouped / flat view: toggle between config-defined groups and a flat sorted list
- Cycle filters: quickly narrow by author, subject prefix, repository, or date bucket
- Sort: ascending/descending by date, author, subject/name, or repository
- Block-jump navigation: jump between repo blocks
[ ], subject blocks( ), or config groups{ } - Mouse support: scroll wheel, click to select, tab-bar clicks, double-click to open
- Profile persistence: reopens on the same profile you left
- Screensaver: bouncing logo after a configurable idle timeout
- Update notifications: footer and
grove --versionshow when a newer release is available - Catppuccin Mocha colour palette
Grove is a multi-repo orchestration layer — it sits above single-repo tools and below the browser, giving you a terminal-native overview across all your repositories.
| Tool | Scope | Strength | Gap grove fills |
|---|---|---|---|
gh CLI |
One repo / one entity at a time | Scriptable, full API access | No cross-repo dashboard; no directory-aware clone routing |
| lazygit | Single repo, local git | Deep interactive git operations | No forge API (PRs, CI, issues); no multi-repo view |
| gh-dash | PRs and issues across repos | Focused PR review workflow | No local git state, branches, activity, or CI overview |
| grove | All repos, all signals | Unified dashboard + detail drill-down | — |
Grove doesn't replace these tools — it launches them. space opens lazygit or diffnav for deep single-repo work; o opens the active forge in the browser; gh-dash handles GitHub-backed PR review. Grove is the flight-control layer that ties them together across repos, showing you where to focus before you drill down.
brew tap alcxyz/tap
brew install grovenix profile install github:alcxyz/groveOr in a flake:
inputs.grove.url = "github:alcxyz/grove";yay -S grove-tui-binRequires Go 1.22+. GitHub-backed profiles also require the gh CLI authenticated with gh auth login. Forgejo-backed profiles use API tokens configured in token_file.
git clone git@github.com:alcxyz/grove.git
cd grove
go build -o grove ./cmd/grove
mv grove ~/.local/bin/On first run grove writes an example config to $XDG_CONFIG_HOME/grove/config.yaml (usually ~/.config/grove/config.yaml). Edit it to match your setup:
profiles:
- name: Work
owner: my-org # code-hosting owner; defaults all concerns unless overridden
forge: forgejo
instance_url: https://git.example.com
token_file: ~/.config/forgejo/token
base_paths:
- ~/dev/git/my-org
prefixes:
- service-
- platform-
groups:
- name: Services
match: service- # matches repo names starting with "service-"
- name: Platform
match: platform-
- name: Personal
owner: my-user
base_paths:
- ~/dev/git/personal
- ~/nix
prefixes: [] # all git repos in base_paths are scanned
social:
owner: my-github-username
forge: github
ci:
owner: my-github-username
forge: github
groups:
- name: nix
match_path: ~/nix # matches repos under this directory
- name: pages
match: github.io # matches repo names containing "github.io"
code:
owner: my-github-username
forge: github
social:
owner: my-github-username
forge: github
ci:
owner: my-github-username
forge: github
repos:
- name: annaetattoo # local directory name
code:
owner: my-github-username
repo: annaetattoo.github.io # remote repo name, if different
forge: github
social:
owner: my-github-username
repo: annaetattoo.github.io
forge: github
ci:
owner: my-github-username
repo: annaetattoo.github.io
forge: github
refresh_secs: 300
screensaver_secs: 300Groups support two matching strategies: match matches against the repo name (prefix or substring), and match_path matches against the repo's filesystem path (prefix). Both can be used on the same group — either matching puts the repo in that group. First matching group wins; unmatched repos go to "other".
Remote resolution is concern-specific:
- top-level
owner/forge/instance_url/token_file/clone_protodefine the default code-hosting remote socialoverrides PR + issue sourcingcioverrides workflow / pipeline sourcing- a group may override
code,social, andcifor every repo it matches - a repo entry under
repos:may overridecode,social, andcifor one specific repo repoinside any remote block overrides the remote repository name when it differs from the local directory namessh_hostoverrides the SSH clone host when it differs from the forge web/API host
Resolution order is: profile defaults, then matching group override, then per-repo override.
The legacy single-profile format (base_path, org, prefixes at the top level) is still supported and auto-migrates to a single profile.
You can also pass one or more paths on the command line to override the first profile's base_paths:
grove ~/dev/git/other-org
grove ~/dir1 ~/dir2| Path | Purpose |
|---|---|
$XDG_CONFIG_HOME/grove/config.yaml |
Config (falls back to ~/.grove.yaml); profile-based format |
$XDG_CACHE_HOME/grove/ |
Cached PR / branch / activity / CI run data + UI state |
$XDG_STATE_HOME/grove/grove.log |
Runtime log |
| Key | Action |
|---|---|
j / k |
Move down / up |
h / l |
Previous / next tab |
H / L |
Previous / next profile (cycles through All) |
gg / G |
First / last item |
tab / shift+tab |
Jump 5 / 10 / 20 / 25 lines (accelerates on rapid press) |
{ } |
Jump between config groups |
[ ] |
Jump between repo blocks |
( ) |
Jump between CI status blocks (tab 1) / subject / branch / message blocks |
1–6 |
Switch to tab directly |
| Key | Action |
|---|---|
/ |
Open text filter |
esc |
Clear active filter / close pane |
d / D |
Cycle by author, sort by author |
s / S |
Cycle by subject prefix, sort by name / title |
a / A |
Cycle by repository, sort by repository |
f / F |
Cycle by date, sort by date / updated |
x / X |
Cycle / sort by PR count (tab 1) / review status (tab 2) / has-PR (tab 4) |
c / C |
Cycle / sort by branch count (tab 1) / merged (tab 4) / branch prefix (tab 3) |
v / V |
Cycle / sort by CI status (tabs 1, 3) / checks result (tab 2) |
The x / c / v keys follow the spatial layout of the columns they target (PR / Br / CI on the dashboard).
Date buckets: today, yesterday, this week, last week, this month, last month, this quarter, last quarter.
| Key | Action |
|---|---|
enter |
Open detail pane (tabs 1-4, 6) / open diff (tab 5) |
o |
Open in browser on the active forge (all tabs and views) |
space |
Per-tab tool: gh-dash (PRs), checkout + lazygit (branches), workflow in editor (CI), diffnav (activity), lazygit (dashboard) |
e |
Open $EDITOR / nvim at repo root (all tabs and views) |
p |
git pull current repo (all tabs) |
r |
Refresh current tab |
R |
Toggle auto-refresh |
ctrl+f |
git fetch all repos |
g (single, 400ms) |
Toggle grouped / flat view |
, |
Show config preview for the active profile; in detail view, show resolved repo config |
? |
Toggle help overlay |
! |
About: version, config path, cache and log locations |
q / ctrl+c |
Quit |
The detail pane opens with enter on any tab and shows full repo info with an item-level cursor:
| Key | Action |
|---|---|
j / k |
Select next / previous item |
{ } |
Jump between sections (branches, PRs, issues, CI, commits) |
[ ] |
Previous / next repo (follows source tab, skips duplicates) |
gg / G |
First / last item |
, |
Show resolved config for this repo |
space |
Contextual: diffnav (commits), gh-dash (PRs), checkout + lazygit (branches), workflow editor (CI) |
o |
Contextual: open item on the active forge (PR URL, commit, branch, CI run) |
e |
Open editor at repo root |
esc |
Close detail pane |
The diff view opens with enter on the Activity tab:
| Key | Action |
|---|---|
j / k |
Scroll up / down |
{ } |
Jump between files in the diff |
[ ] |
Previous / next commit |
gg / G |
Top / bottom |
space |
Open in diffnav |
o |
Open commit in browser |
e |
Open editor at repo root |
esc |
Close diff view |
Tab 3 shows recent workflow / pipeline runs across all repos:
| Column | Meaning |
|---|---|
| Repository | Short repo name |
| Workflow | Workflow file display name |
| Branch | Branch the run was triggered on |
| Status | ✓ success / ✗ failure / ● in_progress / ⊘ cancelled |
| Event | Trigger event (push, pull_request, schedule, etc.) |
| When | Time since last update |
o opens the run in the browser. enter opens the repo's detail pane. r refreshes. Filtering and sorting (s/S workflow, a/A repo, f/F date) all work as on other tabs.
The Dashboard tab (tab 1) also shows a compact CI status icon (✓ / ✗ / ● / —) in the CI column, reflecting the latest run for each repo.
| Column | Meaning |
|---|---|
PR |
Open PR count, colour scales blue to yellow to red |
Br |
Branch count, colour scales blue to yellow to red |
Is |
Open issue count, colour scales blue to yellow to red |
CI |
Latest CI run: ✓ success / ✗ failure / ● running / — no data |
● (tab 4) |
Branch has an open PR |
∈ (tab 4) |
Branch is merged into the default branch |
Checks (tab 2) |
PR status check rollup: ✓ pass / ✗ fail / ● pending / — none |
Grove hands off to external tools via the space and e keys:
| Context | space opens |
e opens |
|---|---|---|
| Dashboard | lazygit | $EDITOR / nvim |
| PRs tab | gh-dash for GitHub-backed repos | $EDITOR / nvim |
| Branches tab | checkout branch + lazygit (restores branch on exit) | $EDITOR / nvim |
| CI tab | workflow .yml in editor |
$EDITOR / nvim |
| Activity tab | diffnav | $EDITOR / nvim |
| Detail pane (commit) | diffnav | $EDITOR / nvim |
| Detail pane (PR) | gh-dash for GitHub-backed repos | $EDITOR / nvim |
| Detail pane (branch) | checkout + lazygit | $EDITOR / nvim |
| Detail pane (CI run) | workflow .yml in editor |
$EDITOR / nvim |
| Detail pane (local branch) | lazygit | $EDITOR / nvim |
| Diff view | diffnav | $EDITOR / nvim |
If a tool is not found on PATH or there is no valid target (no forge URL, no repo selected), a status message is shown instead of failing silently.
| Action | Effect |
|---|---|
| Scroll wheel | Scroll 3 lines per tick (works in detail and diff views too) |
| Click tab bar | Switch tab |
| Click profile bar | Switch profile |
| Click row | Select item |
| Double-click row | Open (same as enter) |
Grove caches forge API responses to disk so the UI opens instantly and remains usable while refreshes happen in the background.
What is cached
| File | Content | Cap |
|---|---|---|
prs.json |
Open pull requests | 500 items |
branches.json |
Remote branches | 2 000 items |
activity.json |
Recent commits | 100 items |
runs.json |
CI workflow runs | 500 items |
issues.json |
Open issues | 500 items |
state.json |
UI state (active profile) | — |
Each data file is a JSON object { "cached_at": <RFC3339>, "config_key": <string>, "data": [...] }.
Startup — all five cache files are read before the TUI launches. The UI renders immediately with the cached data; fresh data loads in the background and replaces it without any visual flicker. The last active profile is restored from state.json.
TTL — controlled by refresh_secs in config (default 300 s). On startup and on every tab switch, grove checks whether the data for that tab is older than the TTL. If so, a background fetch is triggered automatically. Auto-refresh (toggled with R) repeats this on a timer.
Cache invalidation — each file stores a config_key derived from the resolved remote config and prefixes of all configured profiles. If the config changes (new profile, different owner, changed prefixes, or different concern-specific remotes), the key changes and all cached data is treated as a miss, forcing a full refresh on next launch.
Forcing a refresh — press r to refresh the active tab immediately, or ctrl+f to git fetch all repos.
GitHub API calls go through the gh CLI. Forgejo calls go through direct HTTP requests with token auth. GitHub-backed API calls are limited to 5 concurrent gh invocations at a time (a buffered semaphore channel in internal/gh) to stay inside rate limits.
grove clone uses a separate semaphore capped at 8 concurrent clones, since git clone is network-bound rather than API-bound.
grove clone enumerates all repos for each profile's code-hosting remote and clones any that are not already present locally:
grove clone # all profiles
grove clone Personal # one profile by name
grove clone Work Personal # multiple profilesRepos are cloned into the profile's base_paths[0] by default. If a group has its own base_path, repos matching that group's prefix are cloned there instead:
groups:
- name: Services
match: service-
base_path: ~/dev/git/my-org/services # cloned here
- name: Platform
match: platform-
# no base_path → falls back to profile base_paths[0]Existing repos (detected by the presence of a .git directory) are skipped. Up to 8 clones run in parallel.
Releases are automated. To publish a new version:
- Bump the
VERSIONfile - Open and merge the GitHub PR from
devtomain - Wait for GitHub
mainCI to finish - Fast-forward the Forgejo mirror to the exact GitHub
maincommit
CI runs tests, creates a git tag from VERSION, and triggers goreleaser which builds binaries and publishes to GitHub Releases, Homebrew, and AUR.
Grove is a split-host repo: Forgejo is the primary code host, while GitHub remains the integration authority for PRs, CI, releases, and distribution. Do not merge the same dev -> main change on both hosts. See ADR-011.
MIT. See LICENSE.
Support
- BTC:
bc1pzdt3rjhnme90ev577n0cnxvlwvclf4ys84t2kfeu9rd3rqpaaafsgmxrfa - ETH / ERC-20:
0x2122c7817381B74762318b506c19600fF8B8372c