Git Operations
Arial uses Git worktrees to provide isolated execution environments for each workstream. This document covers the Git operations layer.
Overview
Each workstream runs in its own Git worktree on a dedicated branch. This provides:
- Isolation - Workstreams can't interfere with each other
- Parallelism - Multiple agents can modify files simultaneously
- Clean merges - Each workstream produces a single commit
Worktree Model
Repository Root
├── .git/ # Main Git directory
├── .arial/
│ └── worktrees/
│ ├── auth-system/ # Worktree for auth workstream
│ │ ├── src/
│ │ └── ...
│ └── payment-flow/ # Worktree for payment workstream
│ ├── src/
│ └── ...
└── src/ # Main working directoryEach worktree:
- Has its own working directory
- Shares the
.gitdatabase with the main repo - Operates on its own branch
- Can be modified independently
Git Module
All Git operations are wrapped in lib/git.ts using child process execution.
Worktree Operations
// Create a new worktree
async function createWorktree(
sourceDir: string,
worktreeDir: string,
branch: string,
baseBranch: string
): Promise<Result<void>>
// Remove a worktree
async function removeWorktree(
sourceDir: string,
worktreeDir: string
): Promise<Result<void>>
// List all worktrees
async function listWorktrees(
cwd: string
): Promise<Result<WorktreeInfo[]>>Branch Operations
// Create a new branch from base
async function createBranch(
cwd: string,
branch: string,
baseBranch: string
): Promise<Result<void>>
// Checkout an existing branch
async function checkoutBranch(
cwd: string,
branch: string
): Promise<Result<void>>
// Delete a branch
async function deleteBranch(
cwd: string,
branch: string,
force: boolean
): Promise<Result<void>>
// Get current branch name
async function getCurrentBranch(
cwd: string
): Promise<Result<string>>
// Get default branch (main/master)
async function getDefaultBranch(
cwd: string
): Promise<Result<string>>Merge Operations
// Merge a branch into current
async function mergeBranch(
cwd: string,
branch: string
): Promise<Result<void>>
// Abort a merge in progress
async function abortMerge(
cwd: string
): Promise<Result<void>>
// Check if there are conflicts
async function hasConflicts(
cwd: string
): Promise<boolean>
// Get list of conflicted files
async function getConflictedFiles(
cwd: string
): Promise<Result<string[]>>Content Operations
// Stage all changes
async function stageAll(
cwd: string
): Promise<Result<void>>
// Create a commit
async function commit(
cwd: string,
message: string
): Promise<Result<string>> // Returns commit hash
// Push to remote
async function push(
cwd: string,
branch: string
): Promise<Result<void>>
// Pull from remote
async function pull(
cwd: string,
branch: string
): Promise<Result<void>>
// Get diff from base
async function getDiff(
cwd: string,
base: string
): Promise<Result<string>>
// Get working tree status
async function getStatus(
cwd: string
): Promise<Result<GitStatus>>Worktree Lifecycle
Creation
When a workstream starts:
// 1. Create the branch from base
await createBranch(repoRoot, `arial/${workstreamId}`, baseBranch)
// 2. Create the worktree
await createWorktree(
repoRoot,
`.arial/worktrees/${workstreamId}`,
`arial/${workstreamId}`,
baseBranch
)Execution
The agent runs within the worktree:
// Agent operates in worktree directory
const workDir = `.arial/worktrees/${workstreamId}`
// All file operations happen here
// Changes are isolated from other workstreamsCompletion
When the workstream finishes:
// 1. Stage all changes
await stageAll(worktreePath)
// 2. Create commit
await commit(worktreePath, `feat: ${workstream.title}`)
// 3. Merge into base branch (from main repo)
await checkoutBranch(repoRoot, baseBranch)
await mergeBranch(repoRoot, workstream.branch)Cleanup
After successful merge:
// 1. Remove worktree
await removeWorktree(repoRoot, worktreePath)
// 2. Delete branch
await deleteBranch(repoRoot, workstream.branch, true)Branch Naming
Workstream branches follow a consistent pattern:
arial/{workstream-id}Examples:
arial/auth-systemarial/payment-flowarial/user-profile
This prefix makes Arial branches easy to identify and clean up.
Merge Strategy
Arial uses a sequential merge strategy:
Base Branch (main)
│
├── arial/auth-system ────┐
│ │ merge
│ ◄────┘
│
├── arial/payment-flow ───┐
│ │ merge
│ ◄────┘
│
└── arial/user-profile ───┐
│ merge
◄────┘Benefits:
- Predictable merge order
- Each merge is a clean fast-forward or simple recursive merge
- Conflicts are resolved one at a time
Conflict Resolution
When merge conflicts occur:
Cleanup Command
The arial cleanup command handles orphaned resources:
// Find stale worktrees
const worktrees = await listWorktrees(repoRoot)
const stale = worktrees.filter(w =>
w.branch.startsWith('arial/') &&
!activeWorkstreams.includes(w.branch)
)
// Find stale branches
const branches = await listBranches(repoRoot)
const staleBranches = branches.filter(b =>
b.startsWith('arial/') &&
!activeWorkstreams.includes(b)
)
// Clean up
for (const wt of stale) {
await removeWorktree(repoRoot, wt.path)
}
for (const branch of staleBranches) {
await deleteBranch(repoRoot, branch, true)
}Error Handling
Git operations return Result types:
const result = await createWorktree(...)
if (!result.ok) {
console.error(`Failed to create worktree: ${result.error}`)
return
}Common errors:
- Branch already exists
- Worktree path conflicts
- Merge conflicts
- Network errors (push/pull)
Performance Notes
- Worktrees share Git objects, so creation is fast
- Multiple worktrees can run Git operations in parallel
- Merges are sequential to avoid race conditions
- Large repos benefit from shallow clones in CI