Skip to content

Commit d757ba4

Browse files
epcimTest RunnerTest User
authored
Fixes addressed by pr (#14)
* updates/fixes opus 4.6 * AGENTS.md * update from fixes * update ignore * .git/cross to .cross/ * updates .cross dir * fixes - addressed by pr --------- Co-authored-by: Test Runner <test@runner.local> Co-authored-by: Test User <test@example.com>
1 parent 31ebe45 commit d757ba4

26 files changed

Lines changed: 1488 additions & 517 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@ testdir
1515

1616
# builds
1717
src-go/git-cross-go
18+
src-go/vendor/
1819
src-rust/git-cross-rust
1920
src-rust/target
21+
22+
# git-cross state directory
23+
.cross/

AGENTS.md

Lines changed: 199 additions & 80 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **P0: Sparse checkout broken on newer Git versions** - `git sparse-checkout set <path>` in `--no-cone` mode no longer reliably checks out directories without trailing `/`. Fixed across all three implementations (Go, Rust, Justfile.cross) by appending `/` to sparse-checkout patterns and using `git read-tree -mu HEAD` instead of bare `git checkout` (which can no-op after `--no-checkout`).
12+
- **P0: Go build "inconsistent vendoring" in CI** - Tests that build the Go binary now remove stale `vendor/` directory and pass `-mod=mod` as a direct flag. Added `src-go/vendor/` to `.gitignore` to prevent accidental commits.
13+
- **test/014_remove.sh robustness** - Go binary is now reused if already built; gracefully skips Go tests when Go toolchain is unavailable.
14+
15+
### Added
16+
- **AI-assisted coding / sandbox workflow documentation** - New README section explaining how git-cross integrates with AI coding tools and container-based development sandboxes (`sbx`, Docker sandbox). Covers subfolder scoping, `Crossfile` reproducibility, and bidirectional sync.
17+
- **test/018_sbx_sandbox.sh** - End-to-end test for the AI sandbox workflow: vendor setup, sandbox isolation (no `.git`), AI file modifications, diff review, upstream push, Crossfile replay, and sync.
18+
19+
## [0.3.0] - 2026-03-28
20+
21+
### Added
22+
- **Context-aware `cross diff`** - Auto-detects current patch from CWD when no path argument given
23+
- From inside `vendor/lib`: shows only that patch's diff
24+
- From repo root: shows all diffs (Go/Rust) or requires explicit path (Just/Shell)
25+
- From subdirectory of a patch: resolves to parent patch
26+
- Implemented across all three implementations (Just, Go, Rust)
27+
- New test `test/016_diff_context.sh` with 7 scenarios
28+
- **Expanded test coverage** for Go and Rust CLIs
29+
- `diff`, `replay`, `remove`, `prune` commands now tested in `test/008_rust_cli.sh` and `test/009_go_cli.sh`
30+
- Output assertions for `list` and `status` commands
31+
- Re-enabled `test/006_push.sh` with 4 test scenarios (basic, custom message, named branch, force push)
32+
- Graceful skip on emulated ARM64 platforms where compiled binaries crash
33+
- **P3 TODO item**: Auto-generate GitHub Release with changelog on version tags
34+
35+
### Fixed
36+
- **Rust `exec` error handling** - Exit status is now properly propagated instead of silently discarded
37+
- **Rust dead code cleanup** - Removed unused `--dry` flag and `shell-words` dependency
38+
- **Go `go.mod` version** - Downgraded from unreleased 1.25.5 to 1.23 (matches CI workflow)
39+
- **Justfile.cross `_resolve_context2`** - Fixed jq `startswith` logic (was checking wrong direction for parent path matching) and fixed `{{path}}` vs `$path` template/variable confusion
40+
- **Justfile.cross CWD propagation** - Added `USER_CWD` env var to preserve caller's working directory through Just's CWD changes, enabling reliable CWD-based patch auto-detection
41+
- **Justfile.cross push warning** - Changed stale "WORK IN PROGRESS" message to accurate "experimental" notice
42+
- **Test 003** - Added missing `mkdir -p` for `src/lib2` upstream directory
43+
- **Test 006** - Fixed Just positional parameter passing (bypasses `*ARGS` empty-string loss)
44+
- **Test 017** - Fixed Go binary path (`git-cross``git-cross-go`) and Rust binary path (`release``debug`)
45+
46+
### Changed
47+
- **fzf selection UX** - Added `--select-1`, `--exit-0`, and custom `--prompt` to Justfile fzf invocations; added `--header` and `--border` to Go/Rust fzf for consistent, cleaner selection UI
48+
1049
## [0.2.1] - 2026-01-06
1150

1251
### Added

Justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import? "git.just"
44
[no-cd]
55
@cross *ARGS:
66
REPO_DIR=$(git rev-parse --show-toplevel) \
7+
USER_CWD=${USER_CWD:-$(pwd)} \
78
just --justfile "{{source_dir()}}/Justfile.cross" {{ARGS}}
89

910
# keep compatibility with `just test-cross`

Justfile.cross

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ set export := true
33
set positional-arguments
44

55
CROSSFILE := "Crossfile"
6-
CROSSDIR := ".git/cross"
6+
CROSSDIR := ".cross"
77
METADATA := "$CROSSDIR/metadata.json"
88
JUST_DIR := env("JUST_DIR", source_dir())
99
REPO_DIR := env ("REPO_DIR", "$(git rev-parse --show-toplevel)")
@@ -107,17 +107,28 @@ _resolve_context2 path="": check-initialized
107107

108108
# Resolve git repo relative path of target
109109
if test -z "$path"
110-
set -x path "$(git rev-parse --show-prefix | sed 's,\/$,,')" # cwd, relative to git repo
110+
# Use USER_CWD (set by root Justfile cross recipe) to preserve caller's CWD
111+
if test -n "$USER_CWD" -a -n "$REPO_DIR"
112+
set -x path (realpath --relative-to="$REPO_DIR" "$USER_CWD" 2>/dev/null; or echo "")
113+
# "." means we're at repo root — treat as empty
114+
if test "$path" = "."
115+
set -x path ""
116+
end
117+
end
118+
if test -z "$path"
119+
set -x path "$(git rev-parse --show-prefix | sed 's,\/$,,')"
120+
end
111121
end
112122
if test -z "$path"
113123
just cross _log error "Provide path to 'patch' or change directory into it."
114124
exit 1
115125
end
116126
# Query metadata.json and export matching key as env variables
117-
# Find patch where local_path matches rel_target or is a parent of rel_target
118-
jq -r --arg path "{{path}}" '
127+
# Find patch whose local_path is a prefix of (or equal to) the given path
128+
# i.e. path starts with local_path — handles both exact match and subdirectory cases
129+
jq -r --arg path "$path" '
119130
.patches
120-
| map(. as $patch | select($patch.local_path | startswith($path)))
131+
| map(. as $patch | select($path | startswith($patch.local_path)))
121132
| map(. + {mlen:(.local_path|length)})
122133
| max_by(.mlen)
123134
| to_entries | map("set -x \(.key) \(.value|@sh)") | .[]
@@ -205,12 +216,22 @@ remove path: check-deps
205216
git worktree remove --force "$wt"
206217
end
207218

208-
# 2. Remove from Crossfile
219+
# 2. Remove from Crossfile (match patch lines where last field is the local_path)
209220
just cross _log info "Removing from Crossfile..."
210221
if test -f "{{CROSSFILE}}"
211222
set tmp (mktemp)
212-
grep -v "patch" "{{CROSSFILE}}" > "$tmp"
213-
grep "patch" "{{CROSSFILE}}" | grep -v "$l_path" >> "$tmp"
223+
while read -l line
224+
# Only remove lines that are patch commands with this exact local_path as last field
225+
set fields (string split ' ' -- (string trim $line))
226+
set is_patch false
227+
for f in $fields
228+
test "$f" = "patch" && set is_patch true
229+
end
230+
if test "$is_patch" = true -a (count $fields) -gt 0 -a "$fields[-1]" = "$l_path"
231+
continue # skip this line
232+
end
233+
echo "$line" >> "$tmp"
234+
end < "{{CROSSFILE}}"
214235
mv "$tmp" "{{CROSSFILE}}"
215236
end
216237

@@ -350,9 +371,10 @@ patch remote_spec local_path="": check-deps
350371
end
351372
end
352373

353-
# calculate hash/id
354-
set hash (echo $l_path | md5sum | cut -d' ' -f1 | cut -c1-8)
355-
set wt ".git/cross/worktrees/$remote"_"$hash"
374+
# calculate hash/id — must match Go/Rust: SHA256(remote\0remote_path\0branch)[:8]
375+
set hash (printf '%s\0%s\0%s' $remote $r_path $remote_branch | sha256sum 2>/dev/null; or printf '%s\0%s\0%s' $remote $r_path $remote_branch | shasum -a 256)
376+
set hash (echo $hash | cut -d' ' -f1 | string sub -l 8)
377+
set wt ".cross/worktrees/$remote"_"$hash"
356378

357379
# setup worktree
358380
just cross _log info "Setting up worktree at $wt..."
@@ -361,17 +383,27 @@ patch remote_spec local_path="": check-deps
361383
git fetch $remote $remote_branch
362384
git worktree add --no-checkout -B "cross/$remote/$remote_branch/$hash" $wt "$remote/$remote_branch" >/dev/null 2>&1
363385

364-
# Sparse checkout
386+
# Sparse checkout — trailing "/" ensures gitignore-style pattern
387+
# reliably matches the directory in --no-cone mode.
365388
git -C $wt sparse-checkout init --no-cone
366-
git -C $wt sparse-checkout set $r_path
367-
git -C $wt checkout
389+
git -C $wt sparse-checkout set "$r_path/"
390+
# read-tree explicitly populates worktree (bare checkout can no-op after --no-checkout)
391+
git -C $wt read-tree -mu HEAD
368392
end
369393

370394
# sync to local_path
371395
just cross _log info "Syncing files to $l_path..."
372396
mkdir -p $l_path
373397
rsync -av --delete --exclude .git $wt/$r_path/ $l_path/
374398

399+
# Ensure .cross/ is in .gitignore
400+
if not grep -qxF '.cross/' .gitignore 2>/dev/null
401+
if test -f .gitignore; and test -s .gitignore; and not string match -q '' (tail -c1 .gitignore)
402+
echo '' >> .gitignore
403+
end
404+
echo '.cross/' >> .gitignore
405+
end
406+
375407
# Add local_path to git
376408
git add $l_path
377409

@@ -494,6 +526,8 @@ sync *path="": check-initialized
494526
popd
495527

496528
# 3. Pull rebase from upstream
529+
# Refresh index to avoid false conflicts from rsync mtime changes
530+
git -C $worktree update-index --refresh -q 2>/dev/null; or true
497531
just cross _log info "Pulling from upstream..."
498532
if not git -C $worktree pull --rebase
499533
just cross _log error "Conflict detected in $worktree. Please resolve manually."
@@ -624,7 +658,7 @@ push path="" branch="" force="false" yes="false" message="": check-initialized
624658
|| { just cross _log error "Error: Could not resolve metadata for '$path'."; exit 1; }
625659

626660
pushd "{{REPO_DIR}}"
627-
just cross _log warn "The 'push' command is currently WORK IN PROGRESS."
661+
just cross _log warn "The 'push' command is experimental. Verify upstream changes after use."
628662
if not test -d $worktree
629663
just cross _log error "Error: Worktree not found. Run 'just patch' first."
630664
exit 1
@@ -715,9 +749,9 @@ push path="" branch="" force="false" yes="false" message="": check-initialized
715749
list: check-deps
716750
#!/usr/bin/env fish
717751
pushd "{{REPO_DIR}}" >/dev/null
718-
if test -f .git/cross/metadata.json
752+
if test -f .cross/metadata.json
719753
# Get unique remotes used by patches
720-
set used_remotes (jq -r '.patches[].remote' .git/cross/metadata.json | sort -u)
754+
set used_remotes (jq -r '.patches[].remote' .cross/metadata.json | sort -u)
721755

722756
if test (count $used_remotes) -gt 0
723757
just cross _log info "Configured Remotes:"
@@ -761,8 +795,8 @@ list: check-deps
761795
printf "%-20s %-30s %-20s\n" "REMOTE" "REMOTE PATH" "LOCAL PATH"
762796
printf "%s\n" (string repeat -n 70 "-")
763797

764-
if test -f .git/cross/metadata.json
765-
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path)"' .git/cross/metadata.json | while read -l remote rpath lpath
798+
if test -f .cross/metadata.json
799+
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path)"' .cross/metadata.json | while read -l remote rpath lpath
766800
printf "%-20s %-30s %-20s\n" $remote $rpath $lpath
767801
end
768802
else
@@ -812,7 +846,7 @@ wt path="":
812846
if test -n "{{path}}"
813847
just cross _open_shell worktree "{{path}}"
814848
else
815-
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
849+
set -l selected (just cross list | tail -n +3 | fzf --height 40% --select-1 --exit-0 --prompt "Select patch (wt)> " 2>/dev/null | awk '{print $NF}')
816850
test -z "$selected" && exit 0
817851
just cross _resolve_context2 "$selected" | source || exit 1
818852
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$worktree)
@@ -829,7 +863,7 @@ cd path="":
829863
if test -n "{{path}}"
830864
just cross _open_shell local_path "{{path}}"
831865
else
832-
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
866+
set -l selected (just cross list | tail -n +3 | fzf --height 40% --select-1 --exit-0 --prompt "Select patch (cd)> " 2>/dev/null | awk '{print $NF}')
833867
test -z "$selected" && exit 0
834868
just cross _resolve_context2 "$selected" | source || exit 1
835869
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$local_path)
@@ -851,8 +885,8 @@ status: check-deps
851885
printf "%-20s %-15s %-15s %-15s\n" "LOCAL PATH" "DIFF" "UPSTREAM" "CONFLICTS"
852886
printf "%s\n" (string repeat -n 70 "-")
853887

854-
if test -f .git/cross/metadata.json
855-
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path) \(.worktree)"' .git/cross/metadata.json | while read -l remote rpath local_path wt
888+
if test -f .cross/metadata.json
889+
jq -r '.patches[] | "\(.remote) \(.remote_path) \(.local_path) \(.worktree)"' .cross/metadata.json | while read -l remote rpath local_path wt
856890
set diff_stat "Clean"
857891
set upstream_stat "Synced"
858892
set conflict_stat "No"

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://github.com/epcim/git-cross/workflows/CI/badge.svg)](https://github.com/epcim/git-cross/actions/workflows/ci.yml)
44
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5-
[![Version](https://img.shields.io/badge/version-0.2.1-blue.svg)](https://github.com/epcim/git-cross/blob/main/CHANGELOG.md)
5+
[![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)](https://github.com/epcim/git-cross/blob/main/CHANGELOG.md)
66

77
**Git's CRISPR.** Minimalist approach for mixing "parts" of git repositories using `git worktree` + `rsync`.
88

@@ -160,6 +160,46 @@ If using `just`, you can override targets to add pre/post hooks:
160160
3. **Rsync**: Efficiently syncs changes between worktree and your source tree.
161161
4. **Crossfile**: A plain-text record of all active patches for easy sharing.
162162

163+
## AI-Assisted Coding and Sandbox Workflows
164+
165+
AI coding tools (Cursor, Copilot Workspace, Claude Code, Aider, etc.) frequently work in **subfolders** rather than the repository root. This is by design: the main `.git/` directory and full repository history are not shared with the AI tool's context, reducing noise and improving focus.
166+
167+
**git-cross fits this pattern naturally.** Vendored files are physical files in subfolders -- AI tools can read, modify, and reason about them directly without needing access to the upstream `.git` state.
168+
169+
### Docker Sandbox (`sbx`) Integration
170+
171+
Container-based development sandboxes (e.g. `docker sandbox`, `sbx`) create isolated environments where your code runs inside a container. These tools often support `git worktree` to share repository state without copying `.git/`:
172+
173+
```bash
174+
# 1. Set up git-cross in your main repo
175+
git cross use upstream https://github.com/example/lib.git
176+
git cross patch upstream:src vendor/lib
177+
178+
# 2. Create a sandbox scoped to the vendor subfolder
179+
sbx create --mount vendor/lib # AI tool sees only vendor/lib/
180+
181+
# 3. AI modifies files in the sandbox (vendor/lib/)
182+
# 4. Push changes back upstream from the host
183+
git cross push vendor/lib
184+
```
185+
186+
**Key properties that make this work:**
187+
- **Physical files** in subfolders (not gitlinks) -- sandbox tools mount them directly.
188+
- **Sparse checkout** -- only the needed subdirectory is present, keeping AI context small.
189+
- **`Crossfile` reproducibility** -- `git cross replay` reconstructs the vendored environment inside a fresh container or CI job.
190+
- **Bidirectional sync** -- AI-generated changes in the sandbox flow back upstream via `git cross push`.
191+
- **Hidden worktrees** -- the `.git/cross/worktrees/` directory stays on the host; the sandbox only sees clean working copies.
192+
193+
### Rules for AI-Assisted Development
194+
195+
When using AI tools with git-cross managed subfolders:
196+
197+
1. **Scope AI context to subfolders.** Share `vendor/<name>/` with the AI tool, not the repository root. The AI doesn't need `.git/`, `Crossfile`, or `.cross/`.
198+
2. **Use `git cross diff` to review AI changes** before pushing upstream. This compares the local subfolder against the worktree (upstream state).
199+
3. **Use `git cross sync` after upstream changes** to pull updates into the AI's working directory.
200+
4. **`Crossfile` is the source of truth.** When setting up a new sandbox or CI environment, `git cross replay` recreates all patches from scratch.
201+
5. **Worktrees enable container-aware git.** Tools like `sbx` that support `git worktree` can access the repository's git state without mounting the entire `.git/` directory into the container.
202+
163203
## Architecture
164204

165205
### Technical Implementation Analysis

0 commit comments

Comments
 (0)