Files
openchamber/packages/web/server/lib/git/DOCUMENTATION.md
T
Bohdan Triapitsyn f645d57c93 Stage, unstage, and discard individual diff hunks
Add per-hunk staging, unstaging, and discarding to the Changes diff
view, so a single change region inside a file can be acted on in
isolation instead of forcing whole-file stage/revert. The change is
wired end-to-end across the web server, the shared UI runtime API
contract, and the VS Code extension, with Electron inheriting the web
path unchanged (it boots the server in-process).

Server
------
- New `applyHunk(directory, filePath, { patch, action })` in
  packages/web/server/lib/git/service.js. It resolves the repository
  context and validates the file path with the same helpers used by
  stageFiles/unstageFiles (resolveGitFileContext +
  validateRepositoryFilePaths), then writes the single-hunk patch to a
  temporary file in the OS temp dir (never inside the repo, so it
  cannot show up as an untracked file) and runs `git apply` with flags
  chosen per action:
    stage   -> git apply --cached          (working tree -> index)
    unstage -> git apply --cached --reverse (index -> working tree)
    discard -> git apply --reverse          (revert in working tree)
  A `git apply --check` runs first with the same flags, so a stale
  hunk that no longer applies fails with a clear "Hunk no longer
  applies - refresh and try again" message instead of leaving a
  partial mutation. The patch's target path is parsed and must match
  the requested file (with /dev/null tolerated for new/deleted files),
  preventing a patch from silently targeting a different path. The
  whole operation runs inside withGitIndexMutationQueue to avoid
  racing with concurrent stage/unstage. The temp file is removed in a
  finally block.
- New `POST /api/git/apply-hunk` route in routes.js, registered
  alongside stage/unstage. Validates directory, path, non-empty patch,
  and action before delegating.
- DOCUMENTATION.md updated with the new service entry.

Patch extraction
----------------
- packages/ui/src/lib/diff/patchFileDiff.ts gains
  splitPatchIntoHunks(patch) and extractHunkPatch(patch, hunkIndex).
  They keep the original file header (diff --git / index / --- / +++)
  and emit exactly one @@ hunk per standalone patch, which is what
  `git apply` expects. Each emitted patch is guaranteed to end with a
  trailing newline (without it git apply reports "corrupt patch").

Runtime API contract
--------------------
- GitAPI (packages/ui/src/lib/api/types.ts) gains optional
  stageGitHunk / unstageGitHunk / revertGitHunk, matching the
  stageGitFiles? / unstageGitFiles? precedent so runtimes that do not
  support it degrade gracefully.
- gitApi.ts delegates to the registered runtime git API, falling back
  to gitApiHttp, exactly like the existing whole-file helpers.
- gitApiHttp.ts posts to /api/git/apply-hunk.
- Web runtime composes the three methods in packages/web/src/api/git.ts.

VS Code parity
--------------
- packages/vscode/src/gitService.ts adds applyGitHunk(), implemented
  natively with the existing execGit helper + a temp patch file +
  `git apply` (--cached / --cached --reverse / --reverse), mirroring
  the server's --check-first safety and temp-file cleanup.
- bridge-git-runtime.ts handles the new api:git/apply-hunk bridge
  message; webview/api/git.ts sends it. VS Code users get identical
  stage/unstage/discard-hunk behavior.

UI
--
- New DiffHunkActions component renders a compact per-hunk strip
  above each expanded file diff in the Changes view. Each hunk chip
  shows its +additions / -deletions counts and offers:
    working scope -> Stage + Discard
    staged scope  -> Unstage
  Clicking extracts that hunk's standalone patch via
  extractHunkPatch(patch, hunkIndex) and calls the runtime git API.
  Because the chip index comes directly from fileDiff.hunks[] and the
  patch is sliced in the same order, the hunk the user sees is always
  the hunk that gets applied. While any action is in flight all buttons
  disable to prevent conflicting concurrent mutations; the per-hunk
  spinner reflects in-flight state.
- DiffView wires DiffHunkActions into InlineDiffViewer (text diffs
  only; binary/image and full-file-content modes are excluded since
  they have no patch). MultiFileDiffEntry passes directory/staged
  through and handles onHunkApplied by bumping the diff reload nonce
  (so the file's diff re-fetches and the affected hunk disappears)
  and refreshing git status (so file counts and the staged/changed
  scope update). Hunk actions are therefore available wherever the
  default patch-context diff is shown.

i18n
----
- 10 new keys (diffView.hunk.*) added to all 9 locales (en, es, fr,
  ko, pl, pt-BR, uk, zh-CN, zh-TW), including stage/unstage/discard
  labels, tooltips with the hunk index, a stale-hunk error message,
  and an unsupported-runtime fallback.

Tests
-----
- packages/ui/src/lib/diff/patchFileDiff.test.ts covers
  splitHunks/extractHunkPatch: multi-hunk split, header preservation,
  single-hunk and empty patches, out-of-range indices.
- service.test.js adds an applyHunk suite that builds real temp repos
  with two separate hunks and verifies: staging one hunk leaves the
  other unstaged, discarding reverts only the targeted hunk in the
  working tree, unstaging removes only one hunk from the index, and a
  retargeted patch (different file path) is rejected. Also covers
  invalid-action / missing-hunk-header validation.
- packages/web/src/api/git.test.ts mock completed with the new methods
  (and previously-missing exports that prevented the test from
  loading) and asserts the three hunk methods are exposed.
- routes.test.js continues to pass under bun.

CHANGELOG updated under [Unreleased].
2026-06-14 10:58:23 +03:00

11 KiB

Git Module Documentation

Purpose

This module provides Git repository operations for the web server runtime, including repository management, branch/worktree operations, status/diff queries, commit handling, and merge/rebase workflows.

Entrypoints and structure

  • packages/web/server/lib/git/: Git module directory containing all Git-related functionality.
    • index.js: Public API entry point imported by packages/web/server/index.js.
    • routes.js: Express route registration for /api/git/* endpoints.
    • service.js: Core Git operations (repository, branch, worktree, commit, merge/rebase, status/diff, log).
    • credentials.js: Git credentials management.
    • identity-storage.js: Git identity (user.name, user.email) storage.

Public API

The following functions are exported and used by the web server:

Repository Operations

  • isGitRepository(directory): Check if a directory is a Git repository.
  • getGlobalIdentity(): Get global Git user.name, user.email, and core.sshCommand.
  • getCurrentIdentity(directory): Get local Git identity (fallback to global if not set locally).
  • hasLocalIdentity(directory): Check if local Git identity is configured.
  • setLocalIdentity(directory, profile): Set local Git identity (userName, userEmail, authType, sshKey/host).
  • getRemoteUrl(directory, remoteName): Get URL for a specific remote.

Status and Diff Operations

  • getStatus(directory): Get comprehensive Git status including current branch, tracking, ahead/behind, file changes, diff stats, merge/rebase state.
  • getDiff(directory, { path, staged, contextLines }): Get diff output for files or entire working tree.
  • getRangeDiff(directory, { base, head, path, contextLines }): Get diff between two refs.
  • getRangeFiles(directory, { base, head }): Get list of changed files between two refs.
  • getFileDiff(directory, { path, staged }): Get original and modified file contents for a single file (handles images as data URLs).
  • collectDiffs(directory, files): Collect diff output for multiple files.
  • revertFile(directory, filePath, options): Revert a file. Default scope all discards staged and working-tree changes; scope working discards only unstaged/working-tree changes.
  • stageFile(directory, filePath): Add one file path to the index.
  • unstageFile(directory, filePath): Remove one file path from the index while preserving working-tree content.
  • applyHunk(directory, filePath, options): Apply a single-hunk patch via git apply. options.action is stage (git apply --cached), unstage (git apply --cached --reverse), or discard (git apply --reverse in the working tree). The patch is written to a temp file; a --check runs first so a stale hunk fails with a clear "refresh and try again" error instead of a partial mutation. The patch target path must match the requested file.

Branch Operations

  • getBranches(directory): Get list of local and remote branches (filtered to active remote branches).
  • createBranch(directory, branchName, options): Create and checkout a new branch.
  • checkoutBranch(directory, branchName): Checkout an existing branch.
  • deleteBranch(directory, branch, options): Delete a branch (supports force flag).
  • renameBranch(directory, oldName, newName): Rename a branch and preserve upstream tracking.
  • getRemotes(directory): Get list of configured remotes.

Worktree Operations

  • getWorktrees(directory): List all git worktrees for a repository.
  • validateWorktreeCreate(directory, input): Validate worktree creation parameters (mode, branchName, startRef, upstream config).
  • createWorktree(directory, input): Create a new worktree (supports 'new' and 'existing' modes, upstream setup).
  • removeWorktree(directory, input): Remove a worktree (optionally delete local branch).
  • isLinkedWorktree(directory): Check if directory is a linked worktree (not primary).

Commit and Remote Operations

  • commit(directory, message, options): Create a commit from the current index. options.stageFiles may be provided with options.files by older callers to stage only selected unstaged rows before committing, but the shared Git panel now stages/unstages explicitly before commit.
  • pull(directory, options): Pull changes from remote.
  • push(directory, options): Push changes to remote (auto-sets upstream if needed).
  • fetch(directory, options): Fetch changes from remote.
  • removeRemote(directory, options): Remove a configured remote (except origin).
  • deleteRemoteBranch(directory, options): Delete a remote branch.

Log Operations

  • getLog(directory, options): Get commit history with stats (supports maxCount, from, to, file filters).
  • getCommitFiles(directory, commitHash): Get file changes for a specific commit.
  • getCommitFileDiff(directory, hash, filePath, isBinary): Get before/after content for a specific file in a commit. Returns { original, modified, isBinary }. Runs git show <hash>^:<path> and git show <hash>:<path> in parallel; returns empty strings on failure (added/deleted/root-commit edge cases).

Merge and Rebase Operations

  • rebase(directory, options): Start a rebase onto a target branch.
  • abortRebase(directory): Abort an in-progress rebase.
  • continueRebase(directory): Continue a rebase after conflict resolution.
  • merge(directory, options): Merge a branch into current branch.
  • abortMerge(directory): Abort an in-progress merge.
  • continueMerge(directory): Continue a merge after conflict resolution.
  • getConflictDetails(directory): Get detailed conflict information including operation type, unmerged files, and diff.

Stash Operations

  • listStashes(directory): List stash entries with ref, message, relative time, and hash.
  • countStashFiles(directory, refs): Batch-count changed files for stash refs with bounded concurrency.
  • stashPush(directory, options): Stash changes, always including untracked files, with optional message.
  • stashApply(directory, options): Apply a stash by ref without removing it.
  • stashPop(directory, options): Apply a stash by ref and drop it only after a successful apply.
  • stashDrop(directory, options): Drop a stash by ref.

Internal Helpers

The following functions are internal helpers used by exported functions:

  • buildSshCommand(sshKeyPath): Build SSH command string for git config.
  • buildGitEnv(): Build Git environment with SSH_AUTH_SOCK resolution.
  • createGit(directory): Create simple-git instance with environment.
  • normalizeDirectoryPath(value): Normalize directory paths (supports ~ expansion).
  • cleanBranchName(branch): Remove refs/heads/ or refs/ prefixes.
  • parseWorktreePorcelain(raw): Parse git worktree list --porcelain output.
  • resolveWorktreeProjectContext(directory): Resolve project context (projectID, primaryWorktree, worktreeRoot).
  • resolveCandidateDirectory(...): Generate unique worktree directory candidates.
  • resolveBranchForExistingMode(...): Resolve branch for existing-mode worktree creation.
  • applyUpstreamConfiguration(...): Set upstream tracking for new branches.
  • And various other internal helpers for Git command execution and parsing.

Response Contracts

Status Response

  • current: Current branch name.
  • tracking: Upstream branch (e.g., 'origin/main').
  • ahead: Number of commits ahead of upstream.
  • behind: Number of commits behind upstream.
  • upstreamComparison: Optional comparison against upstream/<current-branch>, with { remote, branch, ahead, behind }.
  • files: Array of file objects with path, index, working_dir status codes.
  • isClean: Boolean indicating if working tree is clean.
  • diffStats: Object mapping file paths to { insertions, deletions }.
  • mergeInProgress: Object with { head, message } if merge in progress.
  • rebaseInProgress: Object with { headName, onto } if rebase in progress.

Staged and unstaged change handling

  • status.files exposes both index and working_dir codes. Shared UI uses these as separate scopes: staged rows are derived from non-empty index statuses, while unstaged rows are derived from working_dir statuses and untracked files.
  • A file with both staged and unstaged changes can appear in both UI sections. Staged rows request diffs with staged: true; unstaged rows request normal working-tree diffs.
  • The shared Git panel exposes explicit staging actions. Unstaged rows use stageFile, staged rows use unstageFile, and commits operate on the current staged index.
  • stageFiles remains supported for callers that need to stage a selected unstaged subset as part of commit. In that mode the server temporarily unstages unrelated index entries, stages stageFiles, commits from the index, then restores temporarily unstaged entries.

Worktree Create/Remove Response

  • head: HEAD commit SHA.
  • name: Worktree name.
  • branch: Local branch name.
  • path: Absolute path to worktree directory.
  • directoryCreated: Present when create returned after the target directory exists while background Git/bootstrap work continues.
  • bootstrapStatus: Background setup status, with pending, ready, or failed.
  • Fast-create background failures remove OpenCode sandbox metadata for directories that never became Git worktrees, and remove the pre-created directory only if it is still empty. User-created files are never recursively deleted by this cleanup.

Log Response

  • all: Array of commit objects with hash, date, message, author info, stats.
  • latest: Latest commit object or null.
  • total: Total number of commits.

Notes for Contributors

Adding a New Git Operation

  1. Add the function to packages/web/server/lib/git/service.js.
  2. Export the function if it's part of the public API.
  3. Use createGit(directory) to get a simple-git instance with the correct environment.
  4. Use runGitCommand(cwd, args) for direct git command execution with better error handling.
  5. Use runGitCommandOrThrow(cwd, args, fallbackMessage) for commands that must succeed.
  6. Return consistent error messages; use parseGitErrorText(error) to extract meaningful git errors.
  7. Update this file with the new function in the appropriate API section.

SSH Key Handling

  • SSH keys are escaped and validated via escapeSshKeyPath to prevent command injection.
  • On Windows, paths are converted to MSYS format (C:/path/c/path).
  • SSH_AUTH_SOCK is automatically resolved via resolveSshAuthSock (checks GPG agent, gpgconf).

Worktree Naming

  • Worktree names are slugified via slugWorktreeName.
  • Random names use adjectives/nouns from OPENCODE_ADJECTIVES and OPENCODE_NOUNS lists.
  • Branches created for new worktrees use openchamber/<worktree-name> pattern.

Cross-Platform Considerations

  • Use normalizeDirectoryPath for all directory inputs to handle ~ and path separators.
  • Use canonicalPath for path comparisons to handle case-insensitive filesystems (Windows).
  • Windows Git commands use MSYS/MinGW paths; avoid direct Windows paths in git commands.

Error Handling

  • All exported functions should throw errors with descriptive messages.
  • Use console.error for logging Git operation failures.
  • Return structured objects for operations that need partial success reporting (e.g., merge/rebase conflicts).

Testing

  • Run bun run type-check, bun run lint, and bun run build before finalizing changes.
  • Consider edge cases: non-Git directories, missing remotes, conflict states, concurrent worktree operations.