Workspace Tier 1: merge, move, revert, content diffs#3
Merged
Conversation
…ized_walk
Tier 1 from the SWE-agent capability map: the ops an agent is
genuinely worse without. Building on Exgit.Workspace.
Adds:
* `Exgit.FS.merge_trees/5` — pure path-level three-way tree merge
primitive, returning {:ok, merged_tree, conflicts, repo}.
Strategies: :abort (default) | :ours | :theirs. Conflict types:
:both_modified, :both_added, :modify_delete. Used by Workspace.merge
but composable on its own for callers building higher-level merge
machinery.
* `Workspace.merge/3` — agent-shaped path-level merge. Accepts
another %Workspace{} (typical), a tree/commit SHA, a ref name, or
:pristine. For workspace sources, objects reachable from the
source's working tree are imported into the target's repo before
merging — so two workspaces forked from a common ancestor can
re-converge even though each has accumulated its own object-store
state. Returns {:ok, ws} | {:conflict, conflicts, ws} | {:error, _}.
* `Workspace.move/3` — rename a file, preserving mode. Atomic from
the caller's view (one return, one observable head_tree advance).
Refuses to move directories in v1; refuses to overwrite an
existing dir.
* `Workspace.revert/2` — undo edits to a single path. Restores from
base_ref content if it exists there; rms from head_tree if the
agent had added the path. No-op on pristine.
* `Workspace.diff/2` (replaces diff/1, which delegates) — opts:
- `:against` — compare against a snapshot/ref/sha rather than
base_ref. Critical for "what's changed since my checkpoint."
- `:content` — return rich entries with before/after blob bytes
instead of just paths.
* `Workspace.materialized_walk/1` — convenience: materialize a lazy
repo and return the walk stream in one call, threading the
materialized workspace back so cache growth is captured.
Tests: 10 new merge_trees tests, 21 new workspace tests (move/revert/
diff opts/merge/materialized_walk). Full suite 729 tests / 0 failures;
extended tier 797 tests / 0 failures. Format, credo --strict, dialyzer
all green.
Builds on PR #2 (Exgit.Workspace base). Once #2 merges, this rebases
to main cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this enables
The agent ops Tier 1 from the capability map — the things an SWE agent is genuinely worse without. Built on top of
Exgit.Workspacefrom #2.Workspace.merge(target, source, opts)Workspace.move(ws, from, to)head_treehistoryWorkspace.revert(ws, path)Workspace.diff(ws, content: true)Workspace.diff(ws, against: snapshot)base_refWorkspace.materialized_walk(ws)Merge: agent-shaped, path-level three-way
Real
git mergedoes text-level merge with conflict markers in files. For agents that's the wrong shape — agents rewrite files, they don't patch hunks; markers in a file just become a parse-and-rewrite step. The agent-shaped version is path-level with structured conflicts:[{:both_modified | :both_added | :modify_delete, path}]— the agent re-reads both versions and writes a fresh resolution.:strategy: :ours | :theirsfor auto-resolution when conflict semantics are clear-cut.Cross-store object import. Each
%Workspace{}carries an independent object_store. When two forks each write, ws_a's objects aren't in ws_b's store and vice versa. Naïve merge-by-snapshot fails with:not_found. Somerge/3accepts a workspace and copies objects reachable from the source's working tree into the target's repo before merging — agent-loop forks reconverge cleanly without manual plumbing.The signature still accepts a tree-sha or ref string for the case where the source is known to be in the target's repo (e.g. merging from a local branch). Two clauses, one mental model.
What's in the FS layer vs the workspace layer
Exgit.FS.merge_trees/5is the pure tree-merge primitive —(repo, base, ours, theirs, opts) → {:ok, merged_tree, conflicts, repo}. No workspace state. Used byWorkspace.mergebut composable on its own.Workspace.mergeis the workspace-aware wrapper: handles object import for cross-fork merges, resolves the merge base frombase_ref, threadshead_tree.Test coverage
merge_trees/5tests infs_test.exs: clean merges, every conflict variant, both-sides-add-same-content (no conflict), both-sides-delete-same-path (no conflict), strategy: :abort/:ours/:theirs, mixed clean+conflict scenarios.workspace_test.exs: move/revert/merge/diff variants, materialized_walk, plus a regression test for the cross-store import path.Full suite: 729 tests, 0 failures. Extended tier (
--include slow --include real_git): 797 tests, 0 failures.Test plan
mix format --check-formattedmix compile --warnings-as-errorsMIX_ENV=dev mix credo --strictMIX_ENV=dev mix dialyzer— 0 warningsmix test --warnings-as-errorsmix test --warnings-as-errors --include slow --include real_gitOut of scope (deferred to Tier 2)
Workspace.update/2(pull-and-merge upstream)Workspace.cherry_pick/2Workspace.apply_patch/2(unified-diff patch application)These are the next chunk; Tier 1 was scoped to "things needed for the basic edit-and-coordinate loop."
Branch base
Targets
exgit-workspace(PR #2) since it builds directly on the workspace primitives. Once #2 merges to main, GitHub auto-rebases this onto main.🤖 Generated with Claude Code