Skip to content

Branch Management

AI coding agents need branch awareness for several critical operations: computing diffs against a base branch for review prompts, detecting the remote default branch for PR creation, tracking upstream distance to warn about divergence, and listing local branches for user selection. None of the reference implementations actually switch or create branches programmatically — they read branch state and use it to inform context construction and git operations. The hard part is not reading git branch; it is building a reliable fallback chain that works across bare repos, detached HEAD states, repos with no remotes, and repos where the remote default branch is not main.

Aider takes the simplest approach: it delegates entirely to GitPython.

Before examining branch operations, it is important to understand Aider’s error handling foundation. The ANY_GIT_ERROR tuple (repo.py:9-36) catches 15+ exception types:

ANY_GIT_ERROR = [
git.exc.ODBError, # Object database errors
git.exc.GitError, # Generic git errors
git.exc.InvalidGitRepositoryError,
git.exc.GitCommandNotFound,
OSError, # File I/O failures
IndexError, # Collection access
BufferError, # Memory buffer issues
TypeError, # Type mismatches (detached HEAD)
ValueError, # Invalid values
AttributeError, # Missing attributes
AssertionError, # Assertion failures
TimeoutError, # Command timeouts
]
ANY_GIT_ERROR = tuple(ANY_GIT_ERROR)

The inclusion of TypeError is specifically for detached HEAD states where self.repo.active_branch raises TypeError. This broad catch is used throughout repo.py and commands.py to handle the unpredictable ways git operations can fail.

In aider/repo.py, Aider accesses the current branch via self.repo.active_branch (GitPython’s Head object). This is used in two places:

  1. Diff generation (repo.py:380-403): The code first attempts to read the active branch and check whether it has any commits:
try:
active_branch = self.repo.active_branch
try:
commits = self.repo.iter_commits(active_branch)
current_branch_has_commits = any(commits)
except ANY_GIT_ERROR:
pass
except (TypeError,) + ANY_GIT_ERROR:
pass # Detached HEAD or branch error

If the branch has commits, diffs run against HEAD (repo.py:399): args = ["HEAD", "--"] + list(fnames). If it is a new branch with no commits (initial commit scenario), Aider falls back to two-stage diffing:

# Staged changes
index_args = ["--cached"] + ["--"] + list(fnames)
diffs += self.repo.git.diff(*index_args, ...).decode(...)
# Working directory changes
wd_args = ["--"] + list(fnames)
diffs += self.repo.git.diff(*wd_args, ...).decode(...)
  1. Push/pull detection (commands.py:608-612): The /commit workflow reads the current branch name and checks for a remote upstream:
local_head = self.coder.repo.repo.git.rev_parse("HEAD")
current_branch = self.coder.repo.repo.active_branch.name
try:
remote_head = self.coder.repo.repo.git.rev_parse(f"origin/{current_branch}")
has_origin = True
except ANY_GIT_ERROR:
has_origin = False

If has_origin is true and local_head == remote_head, the commit has already been pushed and undo is no longer possible.

get_head_commit_sha() (repo.py:609-615) retrieves the HEAD commit hash:

def get_head_commit_sha(self, short=False):
commit = self.get_head_commit() # repo.head.commit
if not commit:
return
if short:
return commit.hexsha[:7]
return commit.hexsha

Commit message generation (repo.py:326-363) uses the LLM to produce messages from diffs. It builds a content string with the diff, tries each configured model in sequence (respecting token limits), and returns the first successful response.

The /git command (commands.py:961-985) passes arbitrary git commands to a subprocess:

def cmd_git(self, args):
args = "git " + args
env = dict(subprocess.os.environ)
env["GIT_EDITOR"] = "true" # Suppress editor prompts
result = subprocess.run(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=env, shell=True,
encoding=self.io.encoding, errors="replace",
)

GIT_EDITOR=true prevents interactive editors from opening during operations like git rebase. The shell=True parameter allows the user to use shell features (pipes, wildcards) in their git commands.

  • No default branch detection. Aider never resolves whether the remote default is main, master, or something else.
  • No upstream distance tracking. It does not count ahead/behind commits.
  • No merge-base computation. Diffs are always against HEAD, not against a common ancestor with a base branch.
  • No branch listing for user selection. The /git command passes arbitrary git commands through to a subprocess.
  • No handling of detached HEAD state beyond catching ANY_GIT_ERROR.

Codex has the most comprehensive branch management across two files: codex-rs/core/src/git_info.rs (1246 lines) for async queries used by the TUI and agent loop, and codex-rs/utils/git/src/branch.rs (257 lines) for synchronous merge-base computation used by the review system.

All git commands in git_info.rs go through run_git_command_with_timeout() (git_info.rs:264-277):

const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5);
async fn run_git_command_with_timeout(
args: &[&str],
cwd: &Path,
) -> Option<std::process::Output> {
let mut command = Command::new("git");
command
.env("GIT_OPTIONAL_LOCKS", "0")
.args(args)
.current_dir(cwd)
.kill_on_drop(true);
let result = timeout(GIT_COMMAND_TIMEOUT, command.output()).await;
match result {
Ok(Ok(output)) => Some(output),
_ => None,
}
}

The 5-second timeout (GIT_COMMAND_TIMEOUT at line 47) prevents the TUI from hanging on large repositories or unresponsive network operations. kill_on_drop(true) ensures the child process is terminated if the timeout fires. GIT_OPTIONAL_LOCKS=0 prevents unnecessary index lock acquisition during read-only operations.

collect_git_info() (git_info.rs:59-111) runs three git commands in parallel via tokio::join!:

pub async fn collect_git_info(cwd: &Path) -> Option<GitInfo> {
let is_git_repo = run_git_command_with_timeout(&["rev-parse", "--git-dir"], cwd)
.await?.status.success();
if !is_git_repo {
return None;
}
let (commit_result, branch_result, url_result) = tokio::join!(
run_git_command_with_timeout(&["rev-parse", "HEAD"], cwd),
run_git_command_with_timeout(&["rev-parse", "--abbrev-ref", "HEAD"], cwd),
run_git_command_with_timeout(&["remote", "get-url", "origin"], cwd)
);
let mut git_info = GitInfo {
commit_hash: None,
branch: None,
repository_url: None,
};
// Process commit hash
if let Some(output) = commit_result
&& output.status.success()
&& let Ok(hash) = String::from_utf8(output.stdout)
{
git_info.commit_hash = Some(hash.trim().to_string());
}
// Process branch — filter out "HEAD" (detached state)
if let Some(output) = branch_result
&& output.status.success()
&& let Ok(branch) = String::from_utf8(output.stdout)
{
let branch = branch.trim();
if branch != "HEAD" {
git_info.branch = Some(branch.to_string());
}
}
// Process repository URL
if let Some(output) = url_result
&& output.status.success()
&& let Ok(url) = String::from_utf8(output.stdout)
{
git_info.repository_url = Some(url.trim().to_string());
}
Some(git_info)
}

The parallel execution means the total time is bounded by the slowest of the three commands (max 5 seconds each), not their sum. The branch field is None when rev-parse --abbrev-ref HEAD returns the literal string "HEAD" (detached HEAD state). This is used by the TUI status line.

get_git_remote_urls() (git_info.rs:114-135) parses git remote -v output:

fn parse_git_remote_urls(stdout: &str) -> Option<BTreeMap<String, String>> {
let mut remotes = BTreeMap::new();
for line in stdout.lines() {
let Some(fetch_line) = line.strip_suffix(" (fetch)") else {
continue; // Only process fetch lines, not push
};
let Some((name, url_part)) = fetch_line
.split_once('\t')
.or_else(|| fetch_line.split_once(' '))
else {
continue;
};
remotes.insert(name.to_string(), url_part.trim_start().to_string());
}
if remotes.is_empty() { None } else { Some(remotes) }
}

The parser only processes (fetch) lines (not (push)) and handles both tab and space delimiters. Results are stored in a BTreeMap for consistent ordering.

Default Branch Detection: The Three-Level Fallback

Section titled “Default Branch Detection: The Three-Level Fallback”

get_default_branch() (git_info.rs:302-345) implements a priority chain:

Level 1 — Symbolic ref (git_info.rs:307-323): For each remote (origin prioritized first via get_git_remotes() at line 289 which sorts origin to position 0), Codex runs:

git symbolic-ref --quiet refs/remotes/{remote}/HEAD

If the output is refs/remotes/origin/main, it extracts the branch name after the last / via rsplit_once('/'). The --quiet flag suppresses errors for remotes that have not had their HEAD fetched.

Level 2 — Remote show (git_info.rs:326-340): If symbolic-ref fails for all remotes, Codex falls back to:

git remote show {remote}

It parses the output line-by-line looking for strip_prefix("HEAD branch:") and extracts the branch name. This requires network access (it queries the remote) and is slower than symbolic-ref. The 5-second timeout is critical here — git remote show on an unreachable remote can hang indefinitely without it.

Level 3 — Local heuristic (get_default_branch_local() at git_info.rs:358-377): As a last resort, Codex checks whether main or master exists locally:

async fn get_default_branch_local(cwd: &Path) -> Option<String> {
for candidate in ["main", "master"] {
if let Some(verify) = run_git_command_with_timeout(
&["rev-parse", "--verify", "--quiet", &format!("refs/heads/{candidate}")],
cwd,
).await && verify.status.success()
{
return Some(candidate.to_string());
}
}
None
}

The first candidate that succeeds is returned. If neither exists, the function returns None.

Branch Ancestry: Building a Priority Vector

Section titled “Branch Ancestry: Building a Priority Vector”

branch_ancestry() (git_info.rs:381-446) builds an ordered list of branches that are relevant to the current HEAD, used to find the closest remote branch for diff generation:

  1. Get current branch via git rev-parse --abbrev-ref HEAD (git_info.rs:383-393). Filters out the literal string "HEAD" (detached state).

  2. Get default branch via get_default_branch() (git_info.rs:396).

  3. Build the ancestry vector with de-duplication via HashSet<String> (git_info.rs:437):

    • Add current branch (if not detached).
    • Add default branch (if different from current — checked via !seen.contains(&db)).
    • For each remote (origin first), run git for-each-ref --format=%(refname:short) --contains=HEAD refs/remotes/{remote} to find remote branches that contain the current HEAD commit. Strip the {remote}/ prefix and add unique branches.
  4. Return the vector even if empty — callers handle the empty case.

The --contains=HEAD flag in step 3 is important: it only returns branches that have the current HEAD in their history. This means if HEAD is on a feature branch that has been merged into main, both feature and main will appear. Branches that diverged before HEAD will not appear.

git_diff_to_remote() (git_info.rs:249-261) orchestrates the full pipeline to find the best merge base for review:

  1. Verify git root exists.
  2. Get remotes.
  3. Get branch ancestry (the priority vector).
  4. Call find_closest_sha() (git_info.rs:531-553) which iterates through the ancestry and calls branch_remote_and_distance() for each branch, tracking the one with the minimum commit distance.
  5. Call diff_against_sha() with the winning SHA to produce the actual diff.

find_closest_sha() implements a minimum-distance selection:

async fn find_closest_sha(
cwd: &Path,
branches: &[String],
remotes: &[String]
) -> Option<GitSha> {
let mut closest_sha: Option<(GitSha, usize)> = None;
for branch in branches {
let Some((maybe_remote_sha, distance)) =
branch_remote_and_distance(cwd, branch, remotes).await
else { continue };
let Some(remote_sha) = maybe_remote_sha else { continue };
match &closest_sha {
None => closest_sha = Some((remote_sha, distance)),
Some((_, best_distance)) if distance < *best_distance => {
closest_sha = Some((remote_sha, distance));
}
_ => {}
}
}
closest_sha.map(|(sha, _)| sha)
}

branch_remote_and_distance() (git_info.rs:452-528) does two things for each branch:

  1. Find remote SHA: For each remote, run git rev-parse --verify --quiet refs/remotes/{remote}/{branch}. First match wins. If the remote ref does not exist, continue to the next remote.

  2. Count distance: Run git rev-list --count {branch}..HEAD (preferred). If the local branch ref does not exist, fall back to git rev-list --count refs/remotes/{remote}/{branch}..HEAD. The distance is the number of commits HEAD is ahead of the reference. Lower distance means the branch is more relevant (closer to HEAD).

diff_against_sha() (git_info.rs:555-608) produces the actual diff for review:

async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
let output = run_git_command_with_timeout(
&["diff", "--no-textconv", "--no-ext-diff", &sha.0], cwd
).await?;
// Exit codes: 0 = no diff, 1 = diff present, other = error
let exit_ok = output.status.code().is_some_and(|c| c == 0 || c == 1);
if !exit_ok { return None; }
let mut diff = String::from_utf8(output.stdout).ok()?;
// Append diffs for untracked files
if let Some(untracked_output) = run_git_command_with_timeout(
&["ls-files", "--others", "--exclude-standard"], cwd
).await && untracked_output.status.success() {
let untracked: Vec<String> = /* parse output */;
// Parallel diffs for each untracked file
let futures = untracked.into_iter().map(|file| async move {
run_git_command_with_timeout(
&["diff", "--no-textconv", "--no-ext-diff", "--binary",
"--no-index", "--", null_device, &file],
cwd
).await
});
let results = join_all(futures).await;
for extra in results.into_iter().flatten() {
if let Ok(s) = String::from_utf8(extra.stdout) {
diff.push_str(&s);
}
}
}
Some(diff)
}

Key details: --no-textconv and --no-ext-diff suppress custom diff drivers for deterministic output. Exit code 1 is valid (means diff is non-empty). Untracked files are diffed against /dev/null (or NUL on Windows) using --no-index for files outside the index. The parallel join_all runs all untracked file diffs concurrently.

branch.rs in the utils/git crate provides synchronous merge-base computation for the review prompt system.

merge_base_with_head() (branch.rs:15-48) implements a six-step flow:

  1. Ensure git repository (ensure_git_repository()).
  2. Resolve repository root.
  3. Resolve HEAD — return None if no HEAD (empty repo).
  4. Resolve branch ref via resolve_branch_ref() (branch.rs:50-66) which runs git rev-parse --verify {branch}. Maps GitCommand errors to None (branch does not exist), propagating other errors.
  5. Upstream preference: resolve_upstream_if_remote_ahead() (branch.rs:68-117) checks whether the remote tracking branch is ahead of the local branch:
fn resolve_upstream_if_remote_ahead(
repo_root: &Path,
branch: &str,
) -> Result<Option<String>, GitToolingError> {
// Get upstream name
let upstream = run_git_for_stdout(repo_root, vec![
"rev-parse", "--abbrev-ref", "--symbolic-full-name",
&format!("{branch}@{{upstream}}")
], None)?;
// Count ahead/behind with left-right
let counts = run_git_for_stdout(repo_root, vec![
"rev-list", "--left-right", "--count",
&format!("{branch}...{upstream}")
], None)?;
let mut parts = counts.split_whitespace();
let _left: i64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
let right: i64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
// Return upstream only if remote is ahead
if right > 0 { Ok(Some(upstream)) } else { Ok(None) }
}

The {branch}@{upstream} syntax resolves to the configured tracking branch (e.g., origin/main for a local main branch). The --left-right --count with three dots (...) gives bidirectional counts: left = local ahead, right = remote ahead. If the remote is ahead (right > 0), the upstream ref is preferred as the merge-base base, ensuring reviews compare against the latest remote state.

  1. Compute merge-base: git merge-base HEAD {preferred_ref}.

This merge-base feeds into review_prompts.rs:39-68 where review_prompt() formats the diff into a prompt string. If merge-base fails, the fallback prompt instructs the LLM to compute the merge-base itself using inline git commands.

  • local_git_branches() (git_info.rs:640-664): Lists branches via git branch --format=%(refname:short), sorts alphabetically, then moves the default branch to position 0 for the TUI branch picker. The move is done by finding the position with iter().position(), removing with Vec::remove(), and inserting at index 0.

  • current_branch_name() (git_info.rs:667-676): Simple wrapper around git branch --show-current. Returns None for empty output (detached HEAD).

  • get_has_changes() (git_info.rs:153-160): Quick dirty check via git status --porcelain, returns Some(true) if output is non-empty, None on timeout or error.

OpenCode takes an event-driven approach with two key files: project/vcs.ts for reactive branch tracking and util/git.ts for low-level git command execution.

currentBranch() (vcs.ts:31-39) runs:

async function currentBranch() {
return $`git rev-parse --abbrev-ref HEAD`
.quiet()
.nothrow()
.cwd(Instance.worktree)
.text()
.then((x) => x.trim())
.catch(() => undefined)
}

.quiet() suppresses stdout/stderr output. .nothrow() prevents non-zero exit codes from throwing. .catch(() => undefined) returns undefined on any error (detached HEAD, not a repo, etc.).

vcs.ts:41-67 sets up a reactive state via Instance.state():

const state = Instance.state(
async () => {
if (Instance.project.vcs !== "git") {
return { branch: async () => undefined, unsubscribe: undefined }
}
let current = await currentBranch()
log.info("initialized", { branch: current })
const unsubscribe = Bus.subscribe(
FileWatcher.Event.Updated,
async (evt) => {
if (!evt.properties.file.endsWith("HEAD")) return
const next = await currentBranch()
if (next !== current) {
log.info("branch changed", { from: current, to: next })
current = next
Bus.publish(Event.BranchUpdated, { branch: next })
}
}
)
return { branch: async () => current, unsubscribe }
},
async (state) => { state.unsubscribe?.() },
)

The implementation is selective about what it watches:

  1. The file watcher (file/watcher.ts) uses @parcel/watcher with platform-specific backends (inotify on Linux, fs-events on macOS, Windows API on Windows).
  2. For the .git directory specifically, it subscribes to the git dir (resolved via git rev-parse --git-dir), but explicitly ignores everything except HEAD by building an ignore list from readdir(vcsDir) filtered to exclude "HEAD" (watcher.ts:99-100).
  3. When a HEAD file change is detected, currentBranch() is re-evaluated. If the branch name differs from the last known value, it publishes Event.BranchUpdated via the bus system.

The Event.BranchUpdated event (vcs.ts:14-19) carries a Zod-validated schema: z.object({ branch: z.string().optional() }). The optional() allows undefined to propagate through for detached HEAD states.

Lazy initialization: The state is only created on first call to init() or branch(). The cleanup function unsubscribes from the file watcher when the state is destroyed.

git() in util/git.ts:19-64 provides two execution paths:

export async function git(
args: string[],
opts: { cwd: string; env?: Record<string, string> }
): Promise<GitResult> {
if (Flag.OPENCODE_CLIENT === "acp") {
const proc = Bun.spawn(["git", ...args], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
cwd: opts.cwd,
})
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).arrayBuffer(),
new Response(proc.stderr).arrayBuffer(),
])
return { exitCode, text: () => Buffer.from(stdout).toString(), stdout, stderr }
}
const result = await $`git ${args}`.quiet().nothrow().cwd(opts.cwd)
return { exitCode: result.exitCode, text: () => result.text(), stdout: result.stdout, stderr: result.stderr }
}
  1. ACP client mode: Uses Bun.spawn() with stdin: "ignore" (prevents a Windows deadlock where inherited stdin blocks the child process) and Promise.all for parallel stdout/stderr reads (prevents pipe buffer deadlock). The concurrent read pattern is critical: if git fills the stdout buffer before stderr is read, the process blocks.

  2. Normal mode: Uses Bun’s $ shell with .quiet().nothrow() for simpler, faster execution.

Both paths return a GitResult with exitCode, text(), stdout, and stderr.

  • No default branch detection. There is no equivalent to Codex’s three-level fallback.
  • No merge-base computation. OpenCode does not compute diffs against a base branch.
  • No upstream distance tracking.
  • No branch listing for selection (the TUI does not have a branch picker).

All three implementations must handle detached HEAD. Aider catches ANY_GIT_ERROR exceptions (specifically TypeError) from GitPython. Codex filters out the literal string "HEAD" from git rev-parse --abbrev-ref HEAD output (line 97 in git_info.rs). OpenCode returns undefined and propagates the absence through the bus system. Any branch-aware feature must treat detached HEAD as a first-class state, not an error.

The fastest way to get the remote default branch is git symbolic-ref refs/remotes/origin/HEAD, but this only works after a git fetch or git clone has set up the remote HEAD reference. Codex handles this by falling back to git remote show (which queries the network) and then to local heuristics. Agents that run in CI or on fresh clones without --single-branch should not rely on symbolic-ref alone.

git rev-list --count A..B counts commits reachable from B but not from A. This is not the same as git rev-list --left-right --count A...B (three dots) which gives both ahead and behind counts. Codex uses both forms depending on context: --count for simple distance in find_closest_sha(), --left-right --count for the upstream check in branch.rs. Using the wrong form gives misleading numbers when branches have diverged.

The difference: A..B (two dots) is an asymmetric range — only commits in B not in A. A...B (three dots) is a symmetric difference — commits in either A or B but not both. The --left-right flag partitions the symmetric difference into left (A’s unique commits) and right (B’s unique commits).

Codex’s 5-second timeout on all git commands is critical. On repositories with millions of objects, operations like git remote show can take 30+ seconds because they query the network. Without a timeout, the TUI freezes. OpenCode uses .nothrow() for error suppression but has no explicit timeout — a slow network can hang the event loop.

OpenCode’s approach of watching only the HEAD file inside .git/ is correct. Watching the entire .git/ directory would fire on every git status, git stash, index updates, and reflog writes — thousands of events that are irrelevant to branch detection. The ignore-everything-except-HEAD pattern (watcher.ts:100) is essential for performance.

Codex always puts origin first in its remote list (git_info.rs:289-292). This matters because git remote show on a secondary remote can return a different default branch than origin. If a repository has both origin pointing to GitHub and upstream pointing to a fork, the default branch resolution differs. OpenOxide should make remote priority explicit and configurable.

The --contains=HEAD flag in branch_ancestry() requires git to walk the commit graph from every remote branch back to check if it contains HEAD. On repositories with thousands of remote branches and deep history, this can exceed the 5-second timeout. Codex’s timeout protection handles this gracefully (returns None), but a smarter approach might limit the query to a configurable set of remotes.

Aider handles empty repositories (no commits) by catching the error from iter_commits() and falling back to --cached diffing. Codex returns None from resolve_head() in branch.rs for empty repos. OpenCode’s rev-parse --abbrev-ref HEAD returns a non-zero exit code. Any branch operation must handle the “no commits yet” state as a valid, non-error scenario.

Branch management belongs in an openoxide-git crate that wraps all git CLI operations behind an async interface with timeout protection.

pub struct GitInfo {
pub commit_hash: Option<String>,
pub branch: Option<String>, // None if detached HEAD
pub repository_url: Option<String>,
}
pub struct BranchAncestry {
pub branches: Vec<String>, // Ordered by priority
pub default_branch: Option<String>,
}
pub struct RemoteDistance {
pub remote_sha: Option<String>,
pub ahead: usize,
pub behind: usize,
}

Adopt Codex’s three-level fallback chain verbatim — it handles the most edge cases:

  1. git symbolic-ref --quiet refs/remotes/{remote}/HEAD (fast, local)
  2. git remote show {remote} + parse HEAD branch: line (slow, network)
  3. Check for refs/heads/main then refs/heads/master locally (offline fallback)

Remote prioritization: configurable via OPENOXIDE_PRIMARY_REMOTE env var, defaulting to origin.

Follow Codex’s branch.rs pattern with one improvement — cache the merge-base SHA per session so repeated review operations do not re-compute it:

pub async fn merge_base_with_head(
repo_root: &Path,
branch: &str,
) -> Result<Option<String>, GitError> {
// 1. resolve HEAD
// 2. resolve branch ref
// 3. check upstream: if remote is ahead, prefer upstream
// 4. git merge-base HEAD {preferred_ref}
}

Use the notify crate for cross-platform file watching, scoped to .git/HEAD only. Emit BranchChanged { old: Option<String>, new: Option<String> } events through a tokio::broadcast channel. Re-evaluate the current branch on each event using git rev-parse --abbrev-ref HEAD.

Follow Codex’s pipeline: build branch ancestry, find closest SHA, diff against it. Include untracked file diffs via parallel git diff --no-index -- /dev/null {file} calls. Cache the closest SHA within a turn to avoid re-computation on repeated diff requests.

All git subprocess calls go through a shared executor with a configurable timeout (default 5 seconds). Use tokio::time::timeout wrapping tokio::process::Command. Set GIT_OPTIONAL_LOCKS=0 and kill_on_drop(true) on all commands.

CratePurpose
openoxide-gitAll git CLI wrappers, branch info, merge-base
notifyCross-platform file watching for .git/HEAD
tokioAsync subprocess execution with timeout
  1. Shell out to git, do not use git2 for branch operations. The git2 crate (libgit2 bindings) does not support all ref formats and has historically lagged behind the git CLI for features like --porcelain=2. Shell execution also inherits the user’s git configuration (aliases, credential helpers).
  2. Cache branch ancestry per turn, not per session. Branch state can change between turns if the user runs git commands in another terminal.
  3. Make merge-base failure non-fatal. If merge-base cannot be computed (no common ancestor, empty repo, no remotes), fall back to diffing against HEAD and warn the user.
  4. Expose branch info to the TUI status line via the same tokio::broadcast channel used for branch change events.
  5. Run collect_git_info() calls in parallel via tokio::join! following Codex’s pattern. Three sequential git commands with 5-second timeouts would be 15 seconds worst case; parallel execution caps at 5 seconds.
  6. Prefer upstream when remote is ahead for merge-base computation, following Codex’s resolve_upstream_if_remote_ahead() pattern. This ensures review diffs compare against the latest remote state rather than a stale local copy.