Repository navigation
Implement worktree-link CLI tool - #1
Conversation
Rust CLI tool that reads .worktreelinks config files with gitignore-style glob patterns and creates symlinks between git worktrees. Implements all features from the spec: - CLI argument parsing with clap (source, target, --config, --dry-run, --force, --verbose, --unlink) - .worktreelinks config file parser (comments, blank lines, glob patterns) - Pattern matching using the ignore crate's OverrideBuilder - Directory-level symlink collapsing (node_modules linked as a whole) - Symlink creation with absolute paths - --force mode to overwrite existing files/links - --unlink mode to remove previously created symlinks - --dry-run mode to preview changes - --verbose mode with tracing-based structured logging - .git directory exclusion for safety - Colored terminal output for link/skip/unlink/overwrite actions https://claude.ai/code/session_01HVMBXoJvcrU1ouT2MrZNMC
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
ウォークスルーこのプルリクエストは、GitIgnoreパターンに基づいてソースディレクトリからターゲットディレクトリへのシンボリックリンクを管理する新しいRustコマンドラインツール「worktree-link」を導入します。CLIパーサー、設定ファイルローダー、ターゲット収集ウォーカー、リンク作成・削除ロジックを含むモジュール群で構成されています。 変更内容
シーケンス図sequenceDiagram
participant User
participant CLI as CLI Parser
participant Config as Config Loader
participant Walker as Target Walker
participant Linker as Link Manager
participant FS as Filesystem
User->>CLI: worktree-link --source <src> --target <tgt> --config <cfg>
CLI->>Config: load patterns from config file
Config->>FS: read config file
FS-->>Config: patterns (lines)
Config-->>CLI: parsed patterns
CLI->>Walker: collect_targets(source, patterns)
Walker->>FS: walk source with gitignore-like filters
FS-->>Walker: matched paths
Walker-->>CLI: sorted target list
CLI->>Linker: create_link for each target
loop per matched target
Linker->>FS: create or overwrite symlink
FS-->>Linker: LinkAction (Created/Overwritten/Skipped)
end
Linker-->>CLI: all actions
CLI->>User: summary (created/overwritten/skipped)
sequenceDiagram
participant User
participant CLI as CLI Parser
participant Linker as Link Manager
participant FS as Filesystem
User->>CLI: worktree-link --source <src> --target <tgt> --unlink
CLI->>Linker: unlink_targets(source, target)
Linker->>FS: walk target directory (non-following)
FS-->>Linker: all symlinks
loop per symlink pointing to source
Linker->>FS: remove symlink
FS-->>Linker: UnlinkAction (Removed/Skipped)
end
Linker-->>CLI: all actions (sorted)
CLI->>User: summary (removed/skipped)
推定コードレビュー難易度🎯 4 (複雑) | ⏱️ ~45 分 詩
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce6dfc2e5f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main.rs (1)
52-55: source/target が入れ子の場合のガードを追加したいです。同一ディレクトリは弾いていますが、どちらかがもう片方の配下だと自己リンクや意図しない走査対象が生まれやすいので、早めに拒否するのが安全です。
✅ 例: 入れ子禁止の追加
if source == target { bail!("Source and target cannot be the same directory"); } + if target.starts_with(&source) || source.starts_with(&target) { + bail!("Source and target must not be nested"); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 52 - 55, source と target が同一ディレクトリのみでなく一方が他方の配下(入れ子)になっているケースを拒否するチェックを追加してください: source と target を fs::canonicalize して正規化し(エラーが出れば bail! で失敗させるか適切にハンドル)、同一であるか既にある判定に加えて canonical_source.starts_with(&canonical_target) または canonical_target.starts_with(&canonical_source) を評価し、どちらかが true なら現在の bail!("Source and target cannot be the same directory") と同様に早期に拒否するようにしてください(関数内の source/target 変数を使って実装してください)。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/walker.rs`:
- Around line 7-53: The code currently walks the entire tree and only prunes
matched directories later; change collect_targets to stop descending into
matched directories by using the WalkBuilder.filter_entry closure to check
overrides.matched and return false for directories that match. Wrap overrides in
a shareable container (e.g. Arc) so the closure can call overrides.matched(path,
is_dir) and if it returns Match::Whitelist(_) and is_dir==true return false (but
still allow the entry itself so you can push it into targets), while keeping the
existing ".git" check; update the WalkBuilder.filter_entry closure accordingly.
---
Nitpick comments:
In `@src/main.rs`:
- Around line 52-55: source と target
が同一ディレクトリのみでなく一方が他方の配下(入れ子)になっているケースを拒否するチェックを追加してください: source と target を
fs::canonicalize して正規化し(エラーが出れば bail! で失敗させるか適切にハンドル)、同一であるか既にある判定に加えて
canonical_source.starts_with(&canonical_target) または
canonical_target.starts_with(&canonical_source) を評価し、どちらかが true なら現在の
bail!("Source and target cannot be the same directory")
と同様に早期に拒否するようにしてください(関数内の source/target 変数を使って実装してください)。
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (7)
.gitignoresrc/cli.rssrc/config.rssrc/error.rssrc/linker.rssrc/main.rssrc/walker.rs
…detection, nested guard - linker.rs: Stop canonicalizing source_path in create_link() to preserve symlinks as-is. Previously, canonicalize resolved symlinks to their ultimate target, breaking the link/unlink round-trip when source entries were themselves symlinks (P1 review). - walker.rs: Use filter_entry to prune matched directories immediately instead of walking the entire subtree and collapsing afterward. This avoids unnecessary I/O for large directories like node_modules (Major review). Also added proper unit tests for collect_targets. - linker.rs: Rewrite unlink_targets to walk the target directory looking for symlinks that point into source, instead of deriving candidates from the source tree. This catches stale symlinks whose source-side originals have been deleted or renamed (P2 review). - main.rs: Add guard rejecting nested source/target directories to prevent self-linking and unintended traversal (Nitpick review). https://claude.ai/code/session_01HVMBXoJvcrU1ouT2MrZNMC
|
@codex review |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/walker.rs (1)
44-71:filter_entryによるディレクトリ枝刈りが正しく実装されている以前のレビューで指摘されていた「マッチしたディレクトリに不要に走査する」問題が修正されています。
filter_entryでマッチしたディレクトリをmatched_dirsに記録しつつfalseを返すことで、走査が正しく打ち切られています。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/walker.rs` around lines 44 - 71, The filter_entry closure correctly prunes matched directories by checking overrides_clone.matched(path, true), pushing the directory into matched_dirs_clone via matched_dirs_clone.lock().unwrap().push(path.to_path_buf()), and returning false to stop descent; no functional change required—leave filter_entry, matched_dirs_clone and the overrides_clone.matched call as-is (optionally add a brief comment above the matched_dirs_clone.lock() call to explain the lock/unwrap and early-return behavior for future readers).
🧹 Nitpick comments (4)
src/walker.rs (2)
163-169: テスト用一時ディレクトリのクリーンアップが不完全
tempdirヘルパーはテスト開始時に前回の残骸を削除しますが、テスト終了後のクリーンアップがありません。テストが失敗した場合やテスト名が変わった場合に、ゴミが残ります。tempfileクレートのTempDirを使うと RAII でスコープ終了時に自動削除されます。♻️ tempfile クレートを使った改善案
Cargo.tomlの[dev-dependencies]にtempfileを追加した上で:+ use tempfile::TempDir; + #[test] fn collect_targets_skips_unmatched() { - let dir = tempdir("collect_skip"); + let dir = TempDir::new().unwrap(); + let dir = dir.path(); fs::write(dir.join(".env"), "SECRET=1").unwrap();各テストで同様のパターンを適用すれば、テスト後にディレクトリが自動削除されます。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/walker.rs` around lines 163 - 169, The tempdir helper currently creates and canonicalizes a PathBuf but never removes the directory after tests; replace uses of the tempdir(name: &str) -> PathBuf helper with tempfile::TempDir so directories are removed via RAII: add tempfile to [dev-dependencies] in Cargo.toml, change the helper (or each test) to call tempfile::Builder::new().prefix(&format!("worktree-link-test-{name}")).tempdir().unwrap() (keep references to the TempDir handle in the test scope) instead of calling tempdir(), and update any places that relied on the returned PathBuf (use tempdir.path() for file operations) so cleanup happens automatically when TempDir is dropped.
32-33: シーケンシャルWalkでのArc<Mutex<>>は型制約のためだけに存在する点にコメントを追加すると良い
WalkBuilder::build()はシングルスレッドのWalkイテレータを返しますが、filter_entryのシグネチャがSend + 'staticを要求するためArc<Mutex<>>が必要になっています。実行時にはロック競合は発生しません。この理由をコードコメントに残しておくと、将来のメンテナが不要な同期に見えて混乱するのを防げます。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/walker.rs` around lines 32 - 33, Add a brief code comment next to the matched_dirs declaration (the Arc<std::sync::Mutex<Vec<PathBuf>>> bound used by Walk) explaining that WalkBuilder::build() returns a single-threaded Walk iterator but filter_entry requires its closure to be Send + 'static, so the Arc<Mutex<...>> exists only to satisfy the type/lifetime/thread-safety constraints and not because concurrent access occurs at runtime (no lock contention expected); reference Walk, WalkBuilder::build(), and filter_entry in the comment so future maintainers understand the reason.README.md (1)
95-109: 対応プラットフォームについての記載がない
src/linker.rsのシンボリックリンク作成は Unix 専用で、非 Unix 環境ではbail!でエラーになります。README の「Safety」セクションまたは別セクションに、現在 Unix(Linux / macOS)のみ対応であることを明記するのが望ましいです。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 95 - 109, The README lacks a platforms note: in src/linker.rs the symlink creation path is Unix-only and returns an error via bail! on non-Unix systems, so update the README (in the Safety section or a new "Supported platforms" section) to state the tool currently supports Unix-like systems (Linux and macOS) only; reference the Unix-only symlink creation in src/linker.rs (the function that performs symlink creation / erroring with bail!) and mention that Windows is not supported yet and will error at runtime unless implemented.src/linker.rs (1)
229-232: 非 Unix 環境でのフォールバックはbail!で即座に失敗するWindows 環境では
std::os::windows::fs::symlink_dir/symlink_fileを使えば対応可能ですが、現状 Unix 専用ツールとして割り切るのであればこれで問題ありません。README に「Unix のみ対応」と明記することを推奨します。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/linker.rs` around lines 229 - 232, The non-Unix fallback in the symlink function currently bails immediately; implement Windows support instead of failing by using std::os::windows::fs::symlink_dir and symlink_file: in src/linker.rs replace the #[cfg(not(unix))] symlink stub with a Windows-specific implementation that checks source.metadata()?.is_dir() and calls symlink_dir(source, target) for directories or symlink_file(source, target) for files, propagating errors as anyhow::Error; keep the function signature fn symlink(source: &Path, target: &Path) -> Result<()> and ensure you import std::os::windows::fs::{symlink_dir, symlink_file} and use anyhow::Context on IO calls for clearer errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 20-22: README の使用例の fenced code block に言語指定が無いため markdownlint
MD040 を引いています。該当ブロック(内容: "worktree-link [OPTIONS] <SOURCE> [TARGET]") を見つけて、開閉の
``` に続けて言語タグ(例: text または console)を追加してください(例: ```text)。これで静的解析の警告が解消されます。
- Around line 50-53:
ドキュメントのサンプルが誤解を招くため、同じコマンド群の中で「--dry-run」例と実行例が連続して見えないように修正してください。具体的には README
の該当ブロックで `worktree-link --dry-run /path/to/main` と `worktree-link /path/to/main`
を分離し、どちらかに「これは確認のみで実際の変更は行わない」などの説明コメントを追加するか、別の見出し/サブセクションに移してワークフロー(確認→実行)であることを明示してください(参照シンボル:
the two example commands `worktree-link --dry-run /path/to/main` と
`worktree-link /path/to/main`)。
In `@src/linker.rs`:
- Around line 140-165: The current closure passed to walk_symlinks inside
unlink_targets propagates errors with `?`, aborting the whole walk; change it to
handle per-entry failures by catching errors from `fs::read_link` and
`remove_entry` (and any other fallible calls) instead of using `?`, log a
warning (e.g., via `warn!`) and push an `UnlinkAction::Skipped { target:
entry_path.clone(), reason: format!("...: {}", err) }` so processing continues
for remaining entries; preserve the existing success path (push
`UnlinkAction::Removed(entry_path)` and `info!`), and ensure `dry_run` behavior
remains unchanged.
- Around line 145-155: fs::read_link(&entry_path) can return a
relative/non-normalized path so starts_with(source_dir) may misfire;
canonicalize the link target (resolve relative targets against
entry_path.parent()) before comparing to source_dir. Replace the direct
read_link() -> starts_with check with code that: (1) obtains link_dest_raw =
fs::read_link(&entry_path), (2) builds an absolute path for the target by if
link_dest_raw.is_absolute() { fs::canonicalize(&link_dest_raw) } else {
fs::canonicalize(entry_path.parent().unwrap().join(&link_dest_raw)) }, (3) use
that canonicalized path in the starts_with(source_dir) check and in the
UnlinkAction::Skipped reason, and propagate/contextualize any canonicalize
errors similarly to the existing with_context calls.
---
Duplicate comments:
In `@src/walker.rs`:
- Around line 44-71: The filter_entry closure correctly prunes matched
directories by checking overrides_clone.matched(path, true), pushing the
directory into matched_dirs_clone via
matched_dirs_clone.lock().unwrap().push(path.to_path_buf()), and returning false
to stop descent; no functional change required—leave filter_entry,
matched_dirs_clone and the overrides_clone.matched call as-is (optionally add a
brief comment above the matched_dirs_clone.lock() call to explain the
lock/unwrap and early-return behavior for future readers).
---
Nitpick comments:
In `@README.md`:
- Around line 95-109: The README lacks a platforms note: in src/linker.rs the
symlink creation path is Unix-only and returns an error via bail! on non-Unix
systems, so update the README (in the Safety section or a new "Supported
platforms" section) to state the tool currently supports Unix-like systems
(Linux and macOS) only; reference the Unix-only symlink creation in
src/linker.rs (the function that performs symlink creation / erroring with
bail!) and mention that Windows is not supported yet and will error at runtime
unless implemented.
In `@src/linker.rs`:
- Around line 229-232: The non-Unix fallback in the symlink function currently
bails immediately; implement Windows support instead of failing by using
std::os::windows::fs::symlink_dir and symlink_file: in src/linker.rs replace the
#[cfg(not(unix))] symlink stub with a Windows-specific implementation that
checks source.metadata()?.is_dir() and calls symlink_dir(source, target) for
directories or symlink_file(source, target) for files, propagating errors as
anyhow::Error; keep the function signature fn symlink(source: &Path, target:
&Path) -> Result<()> and ensure you import std::os::windows::fs::{symlink_dir,
symlink_file} and use anyhow::Context on IO calls for clearer errors.
In `@src/walker.rs`:
- Around line 163-169: The tempdir helper currently creates and canonicalizes a
PathBuf but never removes the directory after tests; replace uses of the
tempdir(name: &str) -> PathBuf helper with tempfile::TempDir so directories are
removed via RAII: add tempfile to [dev-dependencies] in Cargo.toml, change the
helper (or each test) to call
tempfile::Builder::new().prefix(&format!("worktree-link-test-{name}")).tempdir().unwrap()
(keep references to the TempDir handle in the test scope) instead of calling
tempdir(), and update any places that relied on the returned PathBuf (use
tempdir.path() for file operations) so cleanup happens automatically when
TempDir is dropped.
- Around line 32-33: Add a brief code comment next to the matched_dirs
declaration (the Arc<std::sync::Mutex<Vec<PathBuf>>> bound used by Walk)
explaining that WalkBuilder::build() returns a single-threaded Walk iterator but
filter_entry requires its closure to be Send + 'static, so the Arc<Mutex<...>>
exists only to satisfy the type/lifetime/thread-safety constraints and not
because concurrent access occurs at runtime (no lock contention expected);
reference Walk, WalkBuilder::build(), and filter_entry in the comment so future
maintainers understand the reason.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (5)
LICENSEREADME.mdsrc/linker.rssrc/main.rssrc/walker.rs
✅ Files skipped from review due to trivial changes (1)
- LICENSE
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81e79de1a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- README: Add language specifier to fenced code block (MD040) - README: Clarify dry-run → actual run workflow in examples - linker.rs: Make walk_symlinks best-effort — warn and skip on permission errors and individual entry failures instead of aborting - linker.rs: Resolve relative symlink targets before starts_with comparison in unlink_targets (fs::read_link can return relative paths) - linker.rs: Guard --force from deleting through symlinked parents to prevent destroying source data when a parent is already a symlink - main.rs: Move config parsing inside link branch so --unlink works without a .worktreelinks file (unlink only scans target for symlinks) https://claude.ai/code/session_01HVMBXoJvcrU1ouT2MrZNMC
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 345002db14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- Add has_symlink_parent check to the non-overwrite create path so links are never created through symlinked parent directories - Skip .git directories in walk_symlinks to protect repository internals during --unlink https://claude.ai/code/session_01HVMBXoJvcrU1ouT2MrZNMC
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a93d05b4a3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nlink Resolve relative symlink targets with fs::canonicalize to correctly handle `..` segments before comparing with source_dir via starts_with. Falls back to non-canonicalized path for broken (dangling) symlinks to avoid errors. 🤖 Generated with AI
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/linker.rs`:
- Around line 83-145: Add an explicit validation at the start of create_link to
ensure source_path.is_absolute() is true; if not, return an Err with a clear
context/error (e.g., using anyhow::bail or Err(anyhow!(...)) so callers get
"source_path must be absolute" rather than accidentally creating a relative
symlink. Place this guard before any filesystem operations (before the existing
debug! and checks), referencing the create_link function and the source_path
parameter.
- Around line 178-196: The current logic may leave `..` segments in `resolved`
when `fs::canonicalize(&resolved)` fails, causing
`resolved.starts_with(source_dir)` to mis-evaluate; update the code around the
`resolved` handling (the variables `link_dest`, `entry_path`, `resolved`, and
the `fs::canonicalize` fallback) to perform lexical normalization when
canonicalization fails: after the relative join fallback, run a small
normalization routine that collapses `.` segments and resolves `..` against the
preceding components (ensuring the path remains absolute by preserving the
leading root or drive), and then use that normalized `resolved` for the
`starts_with(source_dir)` check; implement this normalization as a helper
function (e.g., `lexical_normalize_path`) and call it in place of leaving the
un-normalized `resolved`.
There was a problem hiding this comment.
Pull request overview
Implements a new Rust CLI (worktree-link) that reads .worktreelinks (gitignore-style patterns), finds matching paths in a source worktree, and creates/removes symlinks in a target worktree with safety flags and colored output.
Changes:
- Added CLI argument parsing and main orchestration for link/unlink modes (
--dry-run,--force,--verbose,--unlink). - Implemented
.worktreelinksparsing and filesystem walking with gitignore-compatible matching and directory “collapse” behavior. - Implemented symlink creation/removal logic with basic safety guards, plus docs, CI, and crate metadata.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/cli.rs |
Defines clap-based CLI interface and flags. |
src/config.rs |
Parses .worktreelinks patterns (comments/blank lines). |
src/walker.rs |
Walks source tree and collects matched files/dirs using ignore overrides. |
src/linker.rs |
Creates symlinks (with --force/--dry-run) and unlinks symlinks pointing into source. |
src/main.rs |
Wires CLI → config → walker → linker; sets up tracing and output. |
src/error.rs |
Adds typed error enum (currently unused). |
README.md |
Documents installation, usage, config format, and behavior. |
Cargo.toml / Cargo.lock |
Adds Rust crate definition and dependencies. |
.github/workflows/ci.yml |
Adds Rust CI (check/test/fmt/clippy). |
.gitignore |
Ignores /target. |
LICENSE |
Adds MIT license text. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… clippy CI - Canonicalize source_dir in unlink_targets so starts_with comparison works correctly against fully-resolved link destinations - Add [dry-run] prefix to info! logs in create_link to distinguish dry-run from actual operations - Add -- -D warnings to clippy CI step to fail on clippy lint warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RUSTFLAGS=-Dwarnings already covers clippy lints since clippy wraps rustc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion, cleanup - Add is_absolute() guard to create_link for defensive input validation - Use lexical path normalization as fallback when fs::canonicalize fails on dangling symlinks, so `..` segments are still resolved correctly - Remove unused error module (anyhow is used throughout) - Document Unix-only platform support in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/main.rs (1)
119-121:strip_prefixのエラーコンテキストにパスを含めてください(任意)この
?が実際に発動することはほぼありませんが、万一発動した際のデバッグ性を高めるためパスを含めると有用です。🔧 改善案
- let rel = source_path - .strip_prefix(&source) - .with_context(|| "Path is not relative to source")?; + let rel = source_path + .strip_prefix(&source) + .with_context(|| format!("Path {} is not relative to source {}", source_path.display(), source.display()))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 119 - 121, 現在の strip_prefix 呼び出しは固定メッセージ "Path is not relative to source" のみを返しており、失敗時のデバッグ情報が不足しています。source_path と source の情報をエラーコンテキストに含めるように with_context クロージャ内のメッセージを具体化してください(たとえば source_path.display() と source.display() を組み込んだ説明的な文字列を返す)ことで、source_path.strip_prefix(&source) の失敗時にどのパスが問題だったか分かるようにしてください。src/linker.rs (1)
299-314:normalize_lexicallyがルートより上への..でルートコンポーネントを誤ってポップする可能性があるシンボリックリンクのエントリが浅いパス(例:
/a/link)にあり、そのlink_destが../../source/fooのように深さを超える..を含む場合、entry_path.parent().join(link_dest)が/a/../../source/fooとなり、normalize_lexicallyはRootDirコンポーネントまでポップしてsource/fooという相対パスを返します。その結果、starts_with(&canonical_source)が常にfalseとなり、対象シンボリックリンクが削除されないフォールスネガティブが発生します。実際のワークツリーは通常ホームディレクトリ以下に存在するためほぼ影響はありませんが、ルートに近い浅いパスでは発生し得ます。
🔧 修正案:ルートコンポーネントをポップしない
Component::ParentDir => { - if !parts.is_empty() { + let can_pop = parts.last().map(|c| { + !matches!(c, Component::RootDir | Component::Prefix(_)) + }).unwrap_or(false); + if can_pop { parts.pop(); } }
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 109-111: README の「Existing files/symlinks are never
overwritten...」の記述が実際と異なるため修正してください:linker.rs の remove_entry 実装は --force 時に
std::fs::remove_dir_all を呼んで通常ディレクトリを再帰削除するので、README の該当箇所("files/symlinks"
の表現)を「既存のファイル・シンボリックリンクは上書きされないが、--force
を指定すると実ディレクトリも再帰削除され得る(取り扱いに注意)」のように正確に書き換えて、参照として linker.rs の remove_entry と
remove_dir_all の挙動を明示してください。
In `@src/linker.rs`:
- Around line 222-234: The info log and action push after the removal are
unconditional, so when dry_run is true we still log "unlinked:" and push
UnlinkAction::Removed; update the unlinking logic in unlink_targets to branch on
the dry_run flag: when dry_run is true, log with a "[dry-run] " prefix (e.g.
"[dry-run] unlinked: {}", using entry_path) and push UnlinkAction::Skipped {
target: entry_path, reason: "dry-run" } (or similar), and when dry_run is false
keep the current behavior of calling remove_entry, logging "unlinked: {}", and
pushing UnlinkAction::Removed(entry_path); reference remove_entry,
UnlinkAction::Removed, UnlinkAction::Skipped, dry_run, and entry_path to locate
the changes.
---
Nitpick comments:
In `@src/main.rs`:
- Around line 119-121: 現在の strip_prefix 呼び出しは固定メッセージ "Path is not relative to
source" のみを返しており、失敗時のデバッグ情報が不足しています。source_path と source の情報をエラーコンテキストに含めるように
with_context クロージャ内のメッセージを具体化してください(たとえば source_path.display() と
source.display() を組み込んだ説明的な文字列を返す)ことで、source_path.strip_prefix(&source)
の失敗時にどのパスが問題だったか分かるようにしてください。
- Update README safety section to clarify --force removes directories recursively, not just files/symlinks - Add [dry-run] prefix to unlink info! log for consistency with create_link logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set up cargo-dist for automated release builds and Homebrew formula generation. On tag push, builds binaries for macOS (arm64/x86_64) and Linux (x86_64/arm64), creates GitHub Release, and publishes Homebrew formula to this repo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Configure cargo-release for streamlined version bumping and tagging. Disable crates.io publish; distribution is via GitHub Releases only. Usage: cargo release patch --execute Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
README.md (1)
48-53:⚠️ Potential issue | 🟡 Minor
--forceの説明が本文と不整合です。
オプション表では “files/symlinks” のみ対象に見えますが、本文ではディレクトリも再帰削除される旨が記載されています。表側もディレクトリ削除を明記しておくと誤解が減ります。📝 修正案
-| `-f, --force` | Overwrite existing files/symlinks | `false` | +| `-f, --force` | Overwrite existing files, symlinks, and directories (directories removed recursively) | `false` |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 48 - 53, Update the `-f, --force` option description in the README table to match the body text by explicitly mentioning that directories may be overwritten/recursively removed (e.g., change "Overwrite existing files/symlinks" to "Overwrite existing files, symlinks and directories (may remove directories recursively)"), ensuring the `-f, --force` row text aligns with the documented behavior elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/linker.rs`:
- Around line 300-318: normalize_lexically currently pops components even past a
root/prefix which can convert absolute paths to relative and drop leading `..`;
update normalize_lexically so it preserves leading RootDir/Prefix components and
preserves leading ParentDir components for relative paths: when handling
Component::ParentDir, only pop if the last pushed component is a normal path
segment (not RootDir/Prefix) and if parts is non-empty; if parts is empty and
the input path is relative (no leading RootDir/Prefix was pushed) then push the
ParentDir component instead of dropping it; ensure you continue to push RootDir
and Prefix components unchanged so starts_with checks (like in unlink_targets)
remain valid.
---
Duplicate comments:
In `@README.md`:
- Around line 48-53: Update the `-f, --force` option description in the README
table to match the body text by explicitly mentioning that directories may be
overwritten/recursively removed (e.g., change "Overwrite existing
files/symlinks" to "Overwrite existing files, symlinks and directories (may
remove directories recursively)"), ensuring the `-f, --force` row text aligns
with the documented behavior elsewhere.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
.github/workflows/release.ymlis excluded by!**/*.ymlCargo.tomlis excluded by!**/*.tomldist-workspace.tomlis excluded by!**/*.toml
📒 Files selected for processing (2)
README.mdsrc/linker.rs
Add tap and publish-jobs configuration to dist-workspace.toml to generate publish-homebrew-formula job that commits and pushes worktree-link.rb to the tap repository on release. Requires HOMEBREW_TAP_TOKEN secret. 🤖 Generated with AI
Pin cargo-dist and cargo-release versions via mise so team members can install exact versions with mise install. 🤖 Generated with AI
Guard against ParentDir components popping RootDir or Prefix components, which would turn absolute paths into relative ones and break the starts_with comparison in unlink_targets. 🤖 Generated with AI
🤖 Generated with AI
🤖 Generated with AI
Skip fmt/clippy/test when no .rs or .toml files are staged, e.g. for README-only commits. 🤖 Generated with AI
Tests are run in CI; pre-commit only needs fmt and clippy. 🤖 Generated with AI
🤖 Generated with AI
Fix glob patterns to **/*.rs so hooks trigger for files in subdirectories. Add -D warnings to clippy to fail on lint warnings. 🤖 Generated with AI
Add doc comment to config parser explaining inline # is not stripped (matching .gitignore semantics). Remove manual [default: current directory] from cli.rs to avoid duplication with clap auto-generated default. 🤖 Generated with AI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
worktree-linkRust CLI tool that reads.worktreelinksconfig files with gitignore-style glob patterns and creates symlinks between git worktreescli.rs(clap args),config.rs(parser),walker.rs(pattern matching viaignorecrate),linker.rs(symlink create/remove),error.rs(error types),main.rs(orchestration)Features
node_modules,.env.*,packages/*/node_modules, etc.)node_modulesare symlinked as a unit, not file-by-file--dry-run— preview without making changes--force— overwrite existing files/symlinks--unlink— remove symlinks pointing back to the source--verbose— structured logging viatracing.gitexclusion — always skipped for safetyTest plan
cargo test— 5 tests for config parsing + directory collapsing).gitand non-matching files are correctly excludedhttps://claude.ai/code/session_01HVMBXoJvcrU1ouT2MrZNMC