-
Notifications
You must be signed in to change notification settings - Fork 46
Description
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
-
Configure gtr to copy a directory containing symlinks:
git gtr config set gtr.copy.includeDirs test-deps --local -
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
-
Verify source is a symlink:
ls -la test-deps/.bin/my-cli # lrwxr-xr-x my-cli -> ../real-package/index.js -
Create a worktree:
git gtr new my-feature
-
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; thenThe 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; thenFlags:
-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'