Appearance
Git Changes Panel
The right sidebar's Git tab shows the working-tree status of the active project's git repository, with a built-in unified-diff viewer for any selected file plus quick actions for commit, pull, and push.
What it shows
- Branch chip at the top with ahead/behind counters when an upstream is configured.
- Staged section - files with index-vs-HEAD changes (
git add-ed but not committed). - Changed section - files with worktree-vs-index changes.
- Untracked section - files not in git, marked with
??. - Selection checkboxes on each row so you can choose files for bulk stage / unstage operations.
- A diff viewer at the bottom of the panel that displays the unified patch for whichever file is selected, with
+/−lines colorized. - Commit / Pull / Push buttons for quick actions in the sidebar.
Commitopens a dialog and commits the currently staged changes only.Pullrunsgit pull --ff-onlyso the UI never blocks on an interactive merge flow.Pushrunsgit pushfor the current branch/upstream. - Stage selected / Unstage selected buttons for moving the checked files in or out of the index without leaving the app.
Clicking any row opens its diff. Untracked files have no git diff; the viewer shows a placeholder for them.
The panel refreshes when the workspace gains focus (so coming back from a terminal pane shows fresh state). Background polling is intentionally off to save battery on Android - use the refresh button in the panel header for an explicit re-fetch.
If the project's working directory is not a git repository, the panel says so without raising an error. Genuine errors (git not on PATH, peer unreachable for a project on another node) are surfaced as a banner.
Wire protocol
GitStatus
jsonc
// client → daemon
{ "type": "git_status", "project_id": "<uuid>" }The daemon shells out to:
git -C <project.working_dir> status --porcelain=v2 --branch -zwith LANG=C and LC_ALL=C for parse stability, and parses the result into a git_status_result.
jsonc
// daemon → client (success)
{
"type": "git_status_result",
"project_id": "<uuid>",
"is_repo": true,
"branch": "main",
"upstream": "origin/main",
"ahead": 1,
"behind": 0,
"staged": [{ "path": "src/lib.rs", "status": "M" }],
"unstaged": [{ "path": "README.md", "status": "M" }],
"untracked": ["new.txt"]
}is_repo = false is not an error - it means the directory is not a git repository (or git exits with the "not a git repository" message). git_error is reserved for project-not-found, peer-unreachable, and spawn-failure cases.
status is the single-letter porcelain v2 code: M modified, A added, D deleted, R renamed, C copied, T type changed, U unmerged. For renames and copies the old_path field carries the original path.
GitDiff
jsonc
// client → daemon
{ "type": "git_diff", "project_id": "<uuid>", "path": "src/lib.rs", "staged": false }staged = false returns the worktree-vs-index diff (git diff); staged = true returns the index-vs-HEAD diff (git diff --cached). Path traversal (.., absolute paths) is rejected before invoking git.
jsonc
// daemon → client
{
"type": "git_diff_result",
"project_id": "<uuid>",
"path": "src/lib.rs",
"staged": false,
"patch": "diff --git a/src/lib.rs b/src/lib.rs\n@@ -1 +1 @@\n-old\n+new\n"
}The patch is the raw git diff text (color-disabled with --no-color). The frontend parses it line-by-line for syntax-coloring; the daemon does not pre-render.
Errors
jsonc
{
"type": "git_error",
"project_id": "<uuid>",
"message": "git: not on PATH"
}GitCommit, GitPull, GitPush
jsonc
// client → daemon
{ "type": "git_commit", "project_id": "<uuid>", "message": "Ship the sidebar actions" }
{ "type": "git_pull", "project_id": "<uuid>" }
{ "type": "git_push", "project_id": "<uuid>" }git_commit commits staged changes only with git commit -m <message>. git_pull runs git pull --ff-only. git_push runs git push.
jsonc
// daemon → client (success)
{
"type": "git_action_result",
"project_id": "<uuid>",
"action": "commit",
"message": "[main abc1234] Ship the sidebar actions"
}jsonc
// daemon → client (failure)
{
"type": "git_action_error",
"project_id": "<uuid>",
"action": "pull",
"message": "fatal: Not possible to fast-forward, aborting."
}GitStage, GitUnstage
jsonc
// client → daemon
{ "type": "git_stage", "project_id": "<uuid>", "paths": ["src/lib.rs", "README.md"] }
{ "type": "git_unstage", "project_id": "<uuid>", "paths": ["src/lib.rs"] }git_stage runs git add -- <paths...>. git_unstage runs git restore --staged -- <paths...>.
Both operations answer with the same git_action_result / git_action_error envelope used by commit/pull/push, with action set to stage or unstage.
Multi-node behavior
When the project's node_id points at a peer other than the daemon receiving the request, the daemon forwards the message over the inter-node WebSocket (mirroring ListOsUsers / GetDaemonLog / ListProjectFiles) and relays the peer's reply back. The client sees a single response.
Run-as-user
When the project has a run_as_users entry for the local node, both git status and git diff are wrapped in the same shim used for session spawns (runuser --session-command on Unix, the hive-runas.exe helper on Windows). This means the user's ~/.gitconfig, credentials, hooks, and gitignore behave exactly as they do in their own shell.
Caching and invalidation
- Status is cached per project. A new
git_status_resultinvalidates all cached diffs for that project (the working tree may have moved). - Diffs are cached per
(project, path, staged-flag)triple. Invalidated by a new status push or explicit project reset.
Frontend integration
- Pinia store:
src/stores/git.ts(useGitStore) - Components:
src/components/sidebar-right/GitChangesPanel.vue- header, lists, section sticky-headers, status row clickable → diffsrc/components/sidebar-right/DiffView.vue- header chip + colorized<pre>patch viewer
- Tauri commands in
src-tauri/src/commands/git.rs:git_status(projectId),git_diff(projectId, path, staged),git_commit(projectId, message),git_pull(projectId),git_push(projectId),git_stage(projectId, paths),git_unstage(projectId, paths). - Events:
hive:git-status,hive:git-diff,hive:git-error,hive:git-action-result,hive:git-action-error(registered insrc/composables/useTauriEvents.ts)
Limitations
- Commit still includes the current staged set only. If you need to commit a subset, first select those files and use
Stage selected/Unstage selectedso the index matches exactly what you want to commit. - No diff for untracked files (git doesn't produce one).
- No file watcher; refresh on focus + manual button only.
Pullis fast-forward only. Non-fast-forward updates still need to be resolved in a terminal or external Git client.- The diff viewer uses a plain
<pre>with class-based coloring, not CodeMirror's merge view - keeping the panel lightweight.