Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ testdir
src-go/git-cross-go
src-rust/git-cross-rust
src-rust/target

# git-cross state directory
.cross/
278 changes: 198 additions & 80 deletions AGENTS.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-03-28

### Added
- **Context-aware `cross diff`** - Auto-detects current patch from CWD when no path argument given
- From inside `vendor/lib`: shows only that patch's diff
- From repo root: shows all diffs (Go/Rust) or requires explicit path (Just/Shell)
- From subdirectory of a patch: resolves to parent patch
- Implemented across all three implementations (Just, Go, Rust)
- New test `test/016_diff_context.sh` with 7 scenarios
- **Expanded test coverage** for Go and Rust CLIs
- `diff`, `replay`, `remove`, `prune` commands now tested in `test/008_rust_cli.sh` and `test/009_go_cli.sh`
- Output assertions for `list` and `status` commands
- Re-enabled `test/006_push.sh` with 4 test scenarios (basic, custom message, named branch, force push)
- Graceful skip on emulated ARM64 platforms where compiled binaries crash
- **P3 TODO item**: Auto-generate GitHub Release with changelog on version tags

### Fixed
- **Rust `exec` error handling** - Exit status is now properly propagated instead of silently discarded
- **Rust dead code cleanup** - Removed unused `--dry` flag and `shell-words` dependency
- **Go `go.mod` version** - Downgraded from unreleased 1.25.5 to 1.23 (matches CI workflow)
- **Justfile.cross `_resolve_context2`** - Fixed jq `startswith` logic (was checking wrong direction for parent path matching) and fixed `{{path}}` vs `$path` template/variable confusion
- **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
- **Justfile.cross push warning** - Changed stale "WORK IN PROGRESS" message to accurate "experimental" notice
- **Test 003** - Added missing `mkdir -p` for `src/lib2` upstream directory
- **Test 006** - Fixed Just positional parameter passing (bypasses `*ARGS` empty-string loss)
- **Test 017** - Fixed Go binary path (`git-cross` → `git-cross-go`) and Rust binary path (`release` → `debug`)

### Changed
- **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

## [0.2.1] - 2026-01-06

### Added
Expand Down
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import? "git.just"
[no-cd]
@cross *ARGS:
REPO_DIR=$(git rev-parse --show-toplevel) \
USER_CWD=${USER_CWD:-$(pwd)} \
just --justfile "{{source_dir()}}/Justfile.cross" {{ARGS}}

# keep compatibility with `just test-cross`
Expand Down
46 changes: 34 additions & 12 deletions Justfile.cross
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,28 @@ _resolve_context2 path="": check-initialized

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

# 2. Remove from Crossfile
# 2. Remove from Crossfile (match patch lines where last field is the local_path)
just cross _log info "Removing from Crossfile..."
if test -f "{{CROSSFILE}}"
set tmp (mktemp)
grep -v "patch" "{{CROSSFILE}}" > "$tmp"
grep "patch" "{{CROSSFILE}}" | grep -v "$l_path" >> "$tmp"
while read -l line
# Only remove lines that are patch commands with this exact local_path as last field
set fields (string split ' ' -- (string trim $line))
set is_patch false
for f in $fields
test "$f" = "patch" && set is_patch true
end
if test "$is_patch" = true -a (count $fields) -gt 0 -a "$fields[-1]" = "$l_path"
continue # skip this line
end
echo "$line" >> "$tmp"
end < "{{CROSSFILE}}"
mv "$tmp" "{{CROSSFILE}}"
end

Expand Down Expand Up @@ -350,8 +371,9 @@ patch remote_spec local_path="": check-deps
end
end

# calculate hash/id
set hash (echo $l_path | md5sum | cut -d' ' -f1 | cut -c1-8)
# calculate hash/id — must match Go/Rust: SHA256(remote\0remote_path\0branch)[:8]
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)
set hash (echo $hash | cut -d' ' -f1 | string sub -l 8)
set wt ".git/cross/worktrees/$remote"_"$hash"

# setup worktree
Expand Down Expand Up @@ -624,7 +646,7 @@ push path="" branch="" force="false" yes="false" message="": check-initialized
|| { just cross _log error "Error: Could not resolve metadata for '$path'."; exit 1; }

pushd "{{REPO_DIR}}"
just cross _log warn "The 'push' command is currently WORK IN PROGRESS."
just cross _log warn "The 'push' command is experimental. Verify upstream changes after use."
if not test -d $worktree
just cross _log error "Error: Worktree not found. Run 'just patch' first."
exit 1
Expand Down Expand Up @@ -812,7 +834,7 @@ wt path="":
if test -n "{{path}}"
just cross _open_shell worktree "{{path}}"
else
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
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}')
test -z "$selected" && exit 0
just cross _resolve_context2 "$selected" | source || exit 1
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$worktree)
Expand All @@ -829,7 +851,7 @@ cd path="":
if test -n "{{path}}"
just cross _open_shell local_path "{{path}}"
else
set -l selected (just cross list | tail -n +3 | fzf --height 40% 2>/dev/null | awk '{print $NF}')
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}')
test -z "$selected" && exit 0
just cross _resolve_context2 "$selected" | source || exit 1
set -l rel_dir (realpath -m --relative-to=$PWD {{REPO_DIR}}/$local_path)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

Expand Down
26 changes: 19 additions & 7 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## Summary

**Status:** v0.2.1 released with prune command and sync fixes
**Status:** v0.3.0 — context-aware diff, fzf improvements, bug fixes, expanded test coverage
**Critical Issues:** 0 (all P0 issues resolved)
**Pending Enhancements:** 2 (single-file patch, fzf improvements)
**Pending Enhancements:** 1 (single-file patch)

## Core Implementation Status

Expand Down Expand Up @@ -80,12 +80,24 @@
- [ ] **Single file patch capability** - Review and propose implementation (tool and test) to be able to patch even single file. If not easily possible without major refactoring, evaluate new command "patch-file".
- **Effort:** 4-6 hours (includes research)

- [ ] **Improve interactive `fzf` selection** in native implementations - Better UI, preview panes, multi-select for batch operations.
- **Effort:** 3-5 hours
- [x] **Improve interactive `fzf` selection** in native implementations - Added `--header`, `--border`, `--select-1`, `--exit-0`, custom prompts.
- **Effort:** 1 hour (completed)

### P3: Low Priority (UX Improvements)

- [ ] **Context-aware `cross diff` command** - Smart diff behavior based on current working directory
- [ ] **Auto-generate GitHub Release with changelog on version tags** - Automatically create a GitHub Release with generated changelog when a `v*.*` tag (major/minor) is pushed.
- **Current State:** `release.yml` triggers on `v*` tags and runs GoReleaser (which creates a release for Go binaries) + Rust matrix builds. Rust artifacts are uploaded to the GoReleaser-created release via `softprops/action-gh-release`. GoReleaser generates its own changelog from commits.
- **Desired Improvements:**
- Generate a meaningful changelog (not just raw commits) grouped by category (features, fixes, etc.)
- Include links to CHANGELOG.md entries if maintained
- Only create full releases for major/minor tags (`v*.*.0`); patch tags (`v*.*.N`) can be lighter
- Consider using `git-cliff` or GoReleaser's built-in changelog templates for better formatting
- Add a release-notes template that summarizes: binary download links, notable changes, upgrade instructions
- **Effort:** 2-3 hours
- **Files:** `.github/workflows/release.yml`, `.goreleaser.yaml` (if exists), optionally `cliff.toml`
- **Status:** Documented for future implementation

- [x] **Context-aware `cross diff` command** - Smart diff behavior based on current working directory
- **Issue:** Currently `cross diff` shows diffs for ALL patches regardless of PWD
- **Desired Behavior:**
- When executed inside a patched local_path: Show diff only for that specific patch
Expand Down Expand Up @@ -126,8 +138,8 @@
- CWD inside nested subdirectory of patch (needs parent resolution)
- Multiple patches in nested directories (resolve closest parent)
- Symlinked directories (should follow symlinks)
- **Priority Rationale:** Low priority - UX improvement, not a bug
- **Status:** Documented for future implementation
- **Status:** COMPLETE — Implemented across all three implementations. Test coverage in `test/016_diff_context.sh`.
- Also fixed `_resolve_context2` jq query (startswith was inverted) and `USER_CWD` propagation through Just wrapper.

### Completed Enhancements

Expand Down
4 changes: 3 additions & 1 deletion src-go/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/epcim/git-cross

go 1.25.5
go 1.24.0

toolchain go1.24.4

require (
github.com/fatih/color v1.18.0
Expand Down
Loading
Loading