Skip to content

copy_directories dereferences symlinks, breaking dependency manager binaries #45

@damianlewis

Description

@damianlewis

Description

The copy_directories function in lib/copy.sh uses cp -r which dereferences symlinks instead of preserving them. This corrupts directories that rely on symlinks, such as dependency manager binary directories (.bin/, bin/, etc.), causing tools to fail with path resolution errors.

Environment

  • gtr version: 2.0.0
  • Git version: 2.50.1
  • OS: macOS
  • Shell: zsh

Affected Use Cases

Any directory copied via gtr.copy.includeDirs that contains symlinks:

Directory Symlink Example Ecosystem
node_modules/.bin/ vite → ../vite/bin/vite.js Node.js
.venv/bin/ python → /usr/bin/python3 Python
vendor/bin/ phpunit → ../phpunit/phpunit/phpunit PHP (Composer)
vendor/bundle/bin/ Gem executable symlinks Ruby (Bundler)

Steps to Reproduce

  1. Configure gtr to copy a directory containing symlinks:

    git gtr config set gtr.copy.includeDirs test-deps --local
  2. Create a test directory with symlinks:

    mkdir -p test-deps/real-package
    echo "console.log('test')" > test-deps/real-package/index.js
    mkdir -p test-deps/.bin
    ln -s ../real-package/index.js test-deps/.bin/my-cli
  3. Verify source is a symlink:

    ls -la test-deps/.bin/my-cli
    # lrwxr-xr-x  my-cli -> ../real-package/index.js
  4. Create a worktree:

    git gtr new my-feature
  5. Verify symlinks are corrupted in worktree:

    ls -la /path/to/worktree/test-deps/.bin/my-cli
    # -rw-r--r--  my-cli  (regular file, not symlink!)

Expected Behavior

Symlinks should be preserved as symlinks in the destination directory.

Actual Behavior

Symlinks are dereferenced and copied as regular files. When these files contain relative imports/requires, they fail because the paths resolve from the wrong location.

Example error (Node.js):

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/path/to/worktree/node_modules/dist/node/cli.js'

Root Cause

In lib/copy.sh line 288:

if cp -r "$dir_path" "$dest_parent/" 2>/dev/null; then

The cp -r command follows symlinks by default, copying the target file contents instead of the symlink itself.

Proposed Fix

Use cp -RP to preserve symlinks:

- if cp -r "$dir_path" "$dest_parent/" 2>/dev/null; then
+ if cp -RP "$dir_path" "$dest_parent/" 2>/dev/null; then

Flags:

  • -R: Recursive copy (POSIX-compliant)
  • -P: Never follow symbolic links in source; copy symlinks as symlinks

Portability

Platform cp -RP Support
macOS
Linux (GNU coreutils)
FreeBSD
OpenBSD

Note: cp -a is an alternative (-RPp) but is not available on OpenBSD. Using cp -RP ensures maximum portability.

Workaround

Until fixed, users can regenerate symlinks via a post-create hook:

# Node.js
git gtr config add gtr.hook.postCreate 'cd $WORKTREE_PATH && npm install'

# Python
git gtr config add gtr.hook.postCreate 'cd $WORKTREE_PATH && python -m venv .venv --upgrade'

# PHP
git gtr config add gtr.hook.postCreate 'cd $WORKTREE_PATH && composer install'

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions