From 8a8bc4c7817d8aa12d758a651b56dd2c3ced197b Mon Sep 17 00:00:00 2001 From: Ryutaro Matsumoto Date: Fri, 13 Feb 2026 13:56:24 +0900 Subject: [PATCH 1/6] perf: use CoW (copy-on-write) cloning for directory copies Add _fast_copy_dir() that leverages filesystem-level cloning (macOS APFS cp -c, Linux cp --reflink=auto) for near-instant directory copies on supported filesystems, with automatic fallback to standard cp -RP on ext4/NTFS/others. Co-Authored-By: Claude Opus 4.6 --- lib/copy.sh | 30 ++++++++++++++++++++++++++++-- tests/copy_safety.bats | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/lib/copy.sh b/lib/copy.sh index 7a2027c..3ca134d 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -75,6 +75,32 @@ merge_copy_patterns() { fi } +# Copy a directory using CoW (copy-on-write) when available, falling back to standard cp. +# macOS APFS: cp -cRP (clone); Linux Btrfs/XFS: cp --reflink=auto -RP +# Callers must guard the return value with `if` or `|| true` (set -e safe). +# Usage: _fast_copy_dir src dest +_fast_copy_dir() { + local src="$1" dest="$2" + local os + os=$(detect_os) + + case "$os" in + darwin) + # Try CoW clone first; if unsupported, fall back to regular copy + if cp -cRP "$src" "$dest" 2>/dev/null; then + return 0 + fi + cp -RP "$src" "$dest" + ;; + linux) + cp --reflink=auto -RP "$src" "$dest" + ;; + *) + cp -RP "$src" "$dest" + ;; + esac +} + # Copy a single file to destination, handling exclusion, path preservation, and dry-run # Usage: _copy_pattern_file file dst_root excludes preserve_paths dry_run # Returns: 0 if file was copied (or would be in dry-run), 1 if skipped/failed @@ -307,8 +333,8 @@ copy_directories() { dest_parent=$(dirname "$dest_dir") mkdir -p "$dest_parent" - # Copy directory (cp -RP preserves symlinks as symlinks) - if cp -RP "$dir_path" "$dest_parent/" 2>/dev/null; then + # Copy directory using CoW when available (preserves symlinks as symlinks) + if _fast_copy_dir "$dir_path" "$dest_parent/"; then log_info "Copied directory $dir_path" copied_count=$((copied_count + 1)) _apply_directory_excludes "$dest_parent" "$dir_path" "$excludes" diff --git a/tests/copy_safety.bats b/tests/copy_safety.bats index 2ce9a4b..f1b866a 100644 --- a/tests/copy_safety.bats +++ b/tests/copy_safety.bats @@ -2,6 +2,7 @@ setup() { load test_helper + source "$PROJECT_ROOT/lib/platform.sh" source "$PROJECT_ROOT/lib/copy.sh" } @@ -82,3 +83,41 @@ setup() { excludes=$(printf '%s\n' "*.log" "dist/*") ! is_excluded "src/app.js" "$excludes" } + +# --- _fast_copy_dir tests --- + +@test "_fast_copy_dir copies directory contents" { + local src dst + src=$(mktemp -d) + dst=$(mktemp -d) + mkdir -p "$src/mydir/sub" + echo "hello" > "$src/mydir/sub/file.txt" + + _fast_copy_dir "$src/mydir" "$dst/" + + [ -f "$dst/mydir/sub/file.txt" ] + [ "$(cat "$dst/mydir/sub/file.txt")" = "hello" ] + rm -rf "$src" "$dst" +} + +@test "_fast_copy_dir preserves symlinks" { + local src dst + src=$(mktemp -d) + dst=$(mktemp -d) + mkdir -p "$src/mydir" + echo "target" > "$src/mydir/real.txt" + ln -s real.txt "$src/mydir/link.txt" + + _fast_copy_dir "$src/mydir" "$dst/" + + [ -L "$dst/mydir/link.txt" ] + [ "$(readlink "$dst/mydir/link.txt")" = "real.txt" ] + rm -rf "$src" "$dst" +} + +@test "_fast_copy_dir fails on nonexistent source" { + local dst + dst=$(mktemp -d) + ! _fast_copy_dir "/nonexistent/path" "$dst/" + rm -rf "$dst" +} From 8e7e89d7180ed7bb5d2b13a90257133425dc43d6 Mon Sep 17 00:00:00 2001 From: Ryutaro Matsumoto Date: Fri, 13 Feb 2026 14:33:46 +0900 Subject: [PATCH 2/6] fix: address CodeRabbit review feedback for CoW copy - Clean up partial clone output before fallback in Darwin _fast_copy_dir - Cache detect_os result to avoid repeated subshell calls - Use teardown function in copy_safety tests for reliable tmpdir cleanup Co-Authored-By: Claude Opus 4.6 --- lib/copy.sh | 12 ++++++++++-- tests/copy_safety.bats | 26 ++++++++++++++------------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/copy.sh b/lib/copy.sh index 3ca134d..1eba238 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -79,10 +79,15 @@ merge_copy_patterns() { # macOS APFS: cp -cRP (clone); Linux Btrfs/XFS: cp --reflink=auto -RP # Callers must guard the return value with `if` or `|| true` (set -e safe). # Usage: _fast_copy_dir src dest +# Cached OS value for _fast_copy_dir; set on first call. +_fast_copy_os="" + _fast_copy_dir() { local src="$1" dest="$2" - local os - os=$(detect_os) + if [ -z "$_fast_copy_os" ]; then + _fast_copy_os=$(detect_os) + fi + local os="$_fast_copy_os" case "$os" in darwin) @@ -90,6 +95,9 @@ _fast_copy_dir() { if cp -cRP "$src" "$dest" 2>/dev/null; then return 0 fi + # Clean up any partial clone output before fallback + local _clone_target="${dest%/}/$(basename "$src")" + [ -e "$_clone_target" ] && rm -rf "$_clone_target" cp -RP "$src" "$dest" ;; linux) diff --git a/tests/copy_safety.bats b/tests/copy_safety.bats index f1b866a..81ae856 100644 --- a/tests/copy_safety.bats +++ b/tests/copy_safety.bats @@ -6,6 +6,12 @@ setup() { source "$PROJECT_ROOT/lib/copy.sh" } +teardown() { + if [ -n "${_test_tmpdir:-}" ]; then + rm -rf "$_test_tmpdir" + fi +} + # --- _is_unsafe_path tests --- @test "absolute path is unsafe" { @@ -87,9 +93,9 @@ setup() { # --- _fast_copy_dir tests --- @test "_fast_copy_dir copies directory contents" { - local src dst - src=$(mktemp -d) - dst=$(mktemp -d) + _test_tmpdir=$(mktemp -d) + local src="$_test_tmpdir/src" dst="$_test_tmpdir/dst" + mkdir -p "$src" "$dst" mkdir -p "$src/mydir/sub" echo "hello" > "$src/mydir/sub/file.txt" @@ -97,13 +103,12 @@ setup() { [ -f "$dst/mydir/sub/file.txt" ] [ "$(cat "$dst/mydir/sub/file.txt")" = "hello" ] - rm -rf "$src" "$dst" } @test "_fast_copy_dir preserves symlinks" { - local src dst - src=$(mktemp -d) - dst=$(mktemp -d) + _test_tmpdir=$(mktemp -d) + local src="$_test_tmpdir/src" dst="$_test_tmpdir/dst" + mkdir -p "$src" "$dst" mkdir -p "$src/mydir" echo "target" > "$src/mydir/real.txt" ln -s real.txt "$src/mydir/link.txt" @@ -112,12 +117,9 @@ setup() { [ -L "$dst/mydir/link.txt" ] [ "$(readlink "$dst/mydir/link.txt")" = "real.txt" ] - rm -rf "$src" "$dst" } @test "_fast_copy_dir fails on nonexistent source" { - local dst - dst=$(mktemp -d) - ! _fast_copy_dir "/nonexistent/path" "$dst/" - rm -rf "$dst" + _test_tmpdir=$(mktemp -d) + ! _fast_copy_dir "/nonexistent/path" "$_test_tmpdir/" } From ddc2f72357ae14dc21ddc37e83481c7f4056af25 Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Tue, 17 Feb 2026 10:27:24 -0800 Subject: [PATCH 3/6] fix: hoist case out of heredoc for Bash 3.2 compatibility Bash 3.2 cannot parse case/esac inside $() inside heredocs, causing copy_directories() to silently fail on macOS system Bash. Move the find command into a variable before the heredoc. --- lib/copy.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/copy.sh b/lib/copy.sh index 1eba238..973d074 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -329,6 +329,13 @@ copy_directories() { # Find directories matching the pattern # Use -path for patterns with slashes (e.g., vendor/bundle), -name for basenames + # Note: case inside $() inside heredocs breaks Bash 3.2, so compute first + local find_results + case "$pattern" in + */*) find_results=$(find . -type d -path "./$pattern" 2>/dev/null) ;; + *) find_results=$(find . -type d -name "$pattern" 2>/dev/null) ;; + esac + while IFS= read -r dir_path; do [ -z "$dir_path" ] && continue dir_path="${dir_path#./}" @@ -350,7 +357,7 @@ copy_directories() { log_warn "Failed to copy directory $dir_path" fi done </dev/null ;; *) find . -type d -name "$pattern" 2>/dev/null ;; esac) +$find_results EOF done < Date: Tue, 17 Feb 2026 11:01:14 -0800 Subject: [PATCH 4/6] feat: add GitHub Actions workflow for verifying completion files - Introduced a new job in the lint workflow to check if completion files are up to date. - Updated the generate-completions.sh script to include a new option for color output mode. --- .github/workflows/lint.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f7dfeac..8507681 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,6 +20,15 @@ jobs: run: | shellcheck bin/gtr bin/git-gtr lib/*.sh lib/commands/*.sh adapters/editor/*.sh adapters/ai/*.sh + completions: + name: Completions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Verify completion files are up to date + run: ./scripts/generate-completions.sh --check + test: name: Tests runs-on: ubuntu-latest From 6cc740a76f10407287bb2badc6d1f7008902883a Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Tue, 17 Feb 2026 11:14:31 -0800 Subject: [PATCH 5/6] fix: guard set -e footgun in _fast_copy_dir and reset cache in tests - Use if/fi instead of && for partial-clone cleanup to avoid exit code 1 leaking under set -e when target doesn't exist - Reset _fast_copy_os in test setup for deterministic test isolation --- lib/copy.sh | 2 +- tests/copy_safety.bats | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/copy.sh b/lib/copy.sh index 973d074..599115c 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -97,7 +97,7 @@ _fast_copy_dir() { fi # Clean up any partial clone output before fallback local _clone_target="${dest%/}/$(basename "$src")" - [ -e "$_clone_target" ] && rm -rf "$_clone_target" + if [ -e "$_clone_target" ]; then rm -rf "$_clone_target"; fi cp -RP "$src" "$dest" ;; linux) diff --git a/tests/copy_safety.bats b/tests/copy_safety.bats index 81ae856..377045e 100644 --- a/tests/copy_safety.bats +++ b/tests/copy_safety.bats @@ -2,6 +2,7 @@ setup() { load test_helper + _fast_copy_os="" source "$PROJECT_ROOT/lib/platform.sh" source "$PROJECT_ROOT/lib/copy.sh" } From f59e48c7eb4502f0011edd2bd66e96255564a99b Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Tue, 17 Feb 2026 11:21:47 -0800 Subject: [PATCH 6/6] fix: split local declaration to satisfy ShellCheck SC2155 --- lib/copy.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/copy.sh b/lib/copy.sh index 599115c..6446400 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -96,7 +96,8 @@ _fast_copy_dir() { return 0 fi # Clean up any partial clone output before fallback - local _clone_target="${dest%/}/$(basename "$src")" + local _clone_target + _clone_target="${dest%/}/$(basename "$src")" if [ -e "$_clone_target" ]; then rm -rf "$_clone_target"; fi cp -RP "$src" "$dest" ;;