TUI for multi-repo git forge monitoring across GitHub, Forgejo, and Azure DevOps. See branch status, dirty working trees, ahead/behind counts, open PRs, CI runs, issues, milestones, 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
- Milestones (tab 7): open milestones across all repos with progress, open/closed issue counts, due date, and update 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/grove/mainOr in a flake:
inputs.grove.url = "github:alcxyz/grove/main";For an unreleased development build, point at the branch explicitly:
nix profile install github:alcxyz/grove/devDevelopment branch builds report a non-release version such as 0.9.2-dev.
yay -S grove-tui-binRequires Go 1.22+. Forgejo-backed profiles support auth_mode: token (default) with token_file, and auth_mode: tea for environments where an authenticated tea CLI is preferred. GitHub-backed profiles support auth_mode: token (default) with token_file, GH_TOKEN, or GITHUB_TOKEN, and auth_mode: gh for environments where an authenticated gh CLI is available but PATs are blocked. Azure DevOps-backed profiles use the Azure CLI DevOps extension (az repos ...) and require project in config.
For GitHub private repos or higher rate limits, use a fine-grained PAT scoped to the specific GitHub-facing repositories. Grove only needs read-only repository permissions for the configured concerns: Metadata, Pull requests, Issues and milestones, Actions, Commit statuses, and Contents when GitHub is the code remote for branch/commit metadata.
Use auth_mode: gh for GitHub organizations that do not allow PATs. In that mode Grove shells out to gh api for PRs, issues, milestones, branches, CI, and repo enumeration, and gh repo clone for GitHub clones.
Use auth_mode: tea for Forgejo/Gitea instances when you want Grove to reuse tea CLI authentication. In that mode Grove shells out to tea api for PRs, issues, milestones, branches, CI, and repo enumeration, and tea clone for Forgejo clones. Configure the login first with tea logins add. Grove calls tea api -i to read HTTP status codes and pagination totals, so it needs a tea release whose api command supports -i (checked with tea 0.15).
For Azure DevOps, authenticate outside grove with az devops login or your normal configured Azure CLI credentials. Grove does not read Azure DevOps token files directly.
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
repo_paths:
- ~/elsewhere/one-off-repo
exclude_paths:
- ~/dev/git/personal/github-only
exclude_repos:
- archived-repo
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
- name: Azure DevOps
owner: my-organization
forge: azuredevops
instance_url: https://dev.azure.com/my-organization
project: MyProject
base_paths:
- ~/dev/git/azure
prefixes: []
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".
base_paths scan immediate child directories for git repositories. repo_paths adds exact repository directories, which is useful for one-off repos that belong to a profile but live inside another profile's directory tree. exclude_paths and exclude_repos omit nested directories or specific local repo names from a profile.
Remote resolution is concern-specific:
- top-level
owner/forge/instance_url/token_file/clone_protodefine the default code-hosting remote auth_modecontrols provider authentication:token(default),ghfor GitHub CLI auth, orteafor Forgejo/Gitea CLI authprojectis required forazuredevopsremotes; Azure DevOps uses Azure CLI authentication (az devops login/ configuredaz) instead of grove-managed token filessocialoverrides 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 forge 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–7 |
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) / state (tab 7) |
c / C |
Cycle / sort by branch count (tab 1) / merged (tab 4) / branch prefix (tab 3) / due date (tab 7) |
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-7) / 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) |
P |
git push 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.
Tab 7 shows open repository milestones across GitHub and Forgejo profiles:
| Column | Meaning |
|---|---|
| Repository | Short repo name |
| Milestone | Milestone title |
| State | Forge milestone state |
| Progress | Closed issues as a percentage of total issues |
| Open | Open issue count |
| Closed | Closed issue count |
| Due | Due date, highlighted when overdue |
| Updated | Time since last update |
o opens the milestone in the browser. enter opens the repo's detail pane when the milestone maps to a local repo. Filtering and sorting work by repository, title, state, due date, and updated date. Issue rows still include milestone names for context, but milestone progress and scheduling live in this tab.
| 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 |
| PR checks (tab 2) | PR status rollup from Actions workflow runs and commit statuses: ✓ 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 |
After the Branches-tab lazygit session, grove restores your original checkout unless you switched branches inside lazygit. $EDITOR may include arguments, such as code --wait, quoted as in a shell (nothing is expanded, and on Windows backslashes stay path separators); a path to an executable is used whole even if it contains spaces. Browser links open with open on macOS and xdg-open elsewhere.
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 |
milestones.json |
Open milestones | 500 items |
state.json |
UI state (active profile) | — |
Each data file is a JSON object { "cached_at": <RFC3339>, "config_key": <string>, "data": [...] }.
Startup — all six data 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, on by default) reloads local repo state every refresh_secs, along with the active tab's forge data; on the Dashboard it reloads PRs, CI runs, and issues. Only one load per tab runs at a time.
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 and Forgejo calls use direct HTTP requests with token auth. GitHub-backed API calls are limited to 5 concurrent requests at a time to stay inside rate limits. When GitHub reports an exhausted rate limit, grove shows it as a rate-limit error (with the reset time when known) rather than an authentication failure.
API requests time out after 30 seconds, and gh, tea, and az reads after two minutes, so a stalled connection cannot hang a refresh.
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.
dev is the default development branch and main contains released work.
Feature pull requests target dev. To publish a new version:
- Bump
VERSIONondevto a new plain release version such as0.10.0. - Open a same-repository pull request from
devtomain. - Wait for the promotion policy, code checks, release snapshot, and Nix build to pass.
- Merge the promotion with a merge commit, not a squash. CI creates the
version tag and publishes the GitHub release, Homebrew formula, and AUR
package from
main. - Set the next development version on
dev, such as0.10.1-dev.
The promotion policy rejects feature branches, fork branches named dev,
missing version bumps, and reused release tags. GitHub is Grove's source of
truth for branches, pull requests, CI, and releases. Secondary mirrors copy
that history and do not create separate integration commits. See
ADR-011.
MIT. See LICENSE.
Support
- BTC:
bc1pzdt3rjhnme90ev577n0cnxvlwvclf4ys84t2kfeu9rd3rqpaaafsgmxrfa - ETH / ERC-20:
0x2122c7817381B74762318b506c19600fF8B8372c