Skip to content

Conversation

@satococoa
Copy link
Owner

@satococoa satococoa commented Dec 25, 2025

Summary

  • add symlink hook type with validation and execution
  • add tests for symlink hook
  • update docs and init template with symlink examples

Testing

  • go test ./internal/... ./cmd/...

Summary by CodeRabbit

  • New Features

    • Added symlink hook support to share directories and assets (e.g., .bin, caches) between main and worktrees.
  • Documentation

    • Added "Symlink Hooks: Shared Assets" to README with examples and semantics.
    • Updated architecture docs and examples to include symlink hook usage alongside copy/command hooks.
  • Tests

    • Added tests covering symlink hook success and error scenarios (validation, creation, edge cases).

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 25, 2025 15:04
@coderabbitai
Copy link

coderabbitai bot commented Dec 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds explicit symlink hook support across docs, config, execution, and tests to enable creating symlinks (e.g., sharing .bin) between repository root and worktrees during post-create hooks.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/architecture.md, cmd/wtp/init.go
Documented a new "Symlink Hooks: Shared Assets" section and updated examples to show a symlink post_create hook (from/to) alongside existing copy and command hooks.
Configuration
internal/config/config.go, internal/config/config_test.go
Added HookTypeSymlink = "symlink" constant and validation: require from and to, disallow command for symlink hooks; tests extended to cover valid/invalid symlink configurations.
Hook Execution
internal/hooks/executor.go, internal/hooks/executor_test.go
Implemented executeSymlinkHookWithWriter and dispatch for symlink hooks: resolves paths, ensures safety (no traversal/outside base), creates parent dirs, prevents clobbering, writes log output, and creates appropriate link targets; added comprehensive tests including Windows symlink support checks.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I dug a little tunnel, neat and quick,
Linking .bin between trees with a nimble flick.
No copies piled up, just a graceful bind,
Shared paths hopping, leaving clutter behind. 🥕🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add symlink hook' accurately describes the main change: introducing a new symlink hook type with validation, execution, and tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0ec68 and b38f11b.

📒 Files selected for processing (2)
  • internal/hooks/executor.go
  • internal/hooks/executor_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/hooks/executor.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Follow standard Go style with tabs and gofmt; package names must be short and lowercase
Keep import groups tidy; use goimports for organization with local prefix following module path github.com/satococoa/wtp/v2
Adhere to linting rules in .golangci.yml (vet, staticcheck, gosec, mnd, lll=120)
Wrap errors with context; avoid ignoring errors in Go code
Use snake_case for Go filenames (e.g., remove.go, shell_integration.go)
Document exported items in Go when non-trivial; follow godoc conventions

Files:

  • internal/hooks/executor_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Unit tests must be placed alongside packages as *_test.go files in the same directory
Target 70% code coverage with unit tests focusing on fast feedback and mocked git interactions using table-driven test cases

Files:

  • internal/hooks/executor_test.go
🧠 Learnings (3)
📚 Learning: 2025-12-02T13:33:48.693Z
Learnt from: CR
Repo: satococoa/wtp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T13:33:48.693Z
Learning: Applies to **/*_test.go : Target 70% code coverage with unit tests focusing on fast feedback and mocked git interactions using table-driven test cases

Applied to files:

  • internal/hooks/executor_test.go
📚 Learning: 2025-12-02T13:33:48.693Z
Learnt from: CR
Repo: satococoa/wtp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T13:33:48.693Z
Learning: Run commands from inside a worktree to mimic real usage (e.g., `go run ../cmd/wtp add feature/new-feature`)

Applied to files:

  • internal/hooks/executor_test.go
📚 Learning: 2025-12-02T13:33:48.693Z
Learnt from: CR
Repo: satococoa/wtp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T13:33:48.693Z
Learning: Applies to test/e2e/**/*.go : End-to-end tests must be placed in `test/e2e` and exercise real git workflows using the built binary

Applied to files:

  • internal/hooks/executor_test.go
🧬 Code graph analysis (1)
internal/hooks/executor_test.go (2)
internal/config/config.go (4)
  • Config (13-17)
  • Hooks (25-27)
  • Hook (30-37)
  • HookTypeSymlink (51-51)
internal/hooks/executor.go (1)
  • NewExecutor (29-34)
🔇 Additional comments (5)
internal/hooks/executor_test.go (5)

39-52: LGTM!

The requireSymlinkSupport helper correctly handles the Windows-specific symlink privilege check and gracefully skips tests when symlinks aren't available. Good use of t.Helper() for proper test stack traces.


118-165: LGTM!

Comprehensive test for the symlink happy path. Correctly verifies:

  • Symlink creation using os.Lstat to detect the symlink mode bit
  • Absolute path resolution for the link target
  • Expected output messages

Good use of the requireSymlinkSupport helper for cross-platform compatibility.


167-194: LGTM!

Correctly tests the source validation error case. The absence of requireSymlinkSupport is appropriate here since the test validates that source existence is checked before any symlink operation is attempted.


196-230: LGTM!

Good test for the destination conflict error case. The test correctly sets up a scenario where the destination already exists and verifies the appropriate error is returned.


232-281: No action needed—path traversal validation correctly precedes source existence checking.

The implementation validates path boundaries via ensureWithinBase() (lines 131-137 for from, lines 140-146 for to) before checking source existence (line 148). The test will correctly fail with "escapes base directory" error for both path traversal cases, even though the source path doesn't physically exist.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new symlink hook type to wtp, enabling users to create symbolic links from the main worktree to new worktrees. This is useful for sharing large or mutable directories (e.g., .bin, .cache, node_modules) between worktrees without duplicating content.

Key changes:

  • Added HookTypeSymlink constant and validation logic in the config package
  • Implemented executeSymlinkHookWithWriter in the hooks executor with path validation and safety checks
  • Added comprehensive test coverage including platform-specific symlink support detection

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/config/config.go Added symlink hook type constant and validation rules requiring from and to fields
internal/config/config_test.go Added test cases for valid symlink hooks and validation error cases (missing fields, invalid field combinations)
internal/hooks/executor.go Implemented symlink hook execution with path resolution, security checks, and relative symlink creation
internal/hooks/executor_test.go Added requireSymlinkSupport helper and basic symlink hook test verifying link creation and target resolution
docs/architecture.md Added symlink hook example to configuration documentation
README.md Added symlink hook documentation with examples and use cases
cmd/wtp/init.go Added commented symlink hook example to the init template

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 178 to 183
linkTarget := srcPath
if relTarget, err := filepath.Rel(dstDir, srcPath); err == nil {
linkTarget = relTarget
}

if err := os.Symlink(linkTarget, dstPath); err != nil {
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The symlink implementation creates relative symlinks when possible, which differs from the copy hook behavior that always uses absolute paths. This inconsistency could lead to unexpected behavior. Consider whether symlinks should always be absolute (like copy operations) for consistency, or document this difference clearly. Relative symlinks can break if the symlink is moved, while absolute symlinks remain valid but are less portable.

Suggested change
linkTarget := srcPath
if relTarget, err := filepath.Rel(dstDir, srcPath); err == nil {
linkTarget = relTarget
}
if err := os.Symlink(linkTarget, dstPath); err != nil {
// Use an absolute path for the symlink target for consistency with copy hooks.
if err := os.Symlink(srcPath, dstPath); err != nil {

Copilot uses AI. Check for mistakes.
Comment on lines 118 to 168
func TestExecutePostCreateHooks_Symlink(t *testing.T) {
requireSymlinkSupport(t)

tempDir := t.TempDir()
repoRoot := filepath.Join(tempDir, "repo")
worktreeDir := filepath.Join(tempDir, "worktree")

err := os.MkdirAll(repoRoot, directoryPermissions)
require.NoError(t, err)
err = os.MkdirAll(worktreeDir, directoryPermissions)
require.NoError(t, err)

srcDir := filepath.Join(repoRoot, ".bin")
require.NoError(t, os.MkdirAll(srcDir, directoryPermissions))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "tool"), []byte("bin"), 0644))

cfg := &config.Config{
Hooks: config.Hooks{
PostCreate: []config.Hook{
{
Type: config.HookTypeSymlink,
From: ".bin",
To: ".bin",
},
},
},
}

executor := NewExecutor(cfg, repoRoot)
var buf bytes.Buffer
err = executor.ExecutePostCreateHooks(&buf, worktreeDir)
assert.NoError(t, err)

dstPath := filepath.Join(worktreeDir, ".bin")
info, err := os.Lstat(dstPath)
require.NoError(t, err)
assert.NotZero(t, info.Mode()&os.ModeSymlink)

linkTarget, err := os.Readlink(dstPath)
require.NoError(t, err)

resolvedTarget := linkTarget
if !filepath.IsAbs(resolvedTarget) {
resolvedTarget = filepath.Join(filepath.Dir(dstPath), resolvedTarget)
}
assert.Equal(t, filepath.Clean(srcDir), filepath.Clean(resolvedTarget))

output := buf.String()
assert.Contains(t, output, "Symlinking: .bin → .bin")
assert.Contains(t, output, "✓ Hook 1 completed")
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the symlink hook's error handling cases. The test suite should include tests for: source path doesn't exist, destination path already exists, and path traversal attempts. The copy hook has similar test coverage (e.g., TestExecutePostCreateHooks_CopyNonExistentFile), and symlink hooks should have equivalent coverage.

Copilot uses AI. Check for mistakes.
@satococoa satococoa merged commit 2586e1d into main Jan 15, 2026
7 checks passed
@satococoa satococoa deleted the feature/symlink-hook branch January 15, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants