fix(task): handle dots in monorepo directory names correctly#6571
Conversation
Previously, the `display_name()` method would strip extensions from the entire task name by splitting on the last dot. This caused issues with monorepo tasks where directory names contained dots (e.g., "//projects/my.app:build" would become "//projects/my"). Now the method only strips extensions from the task name portion after the last colon, preserving dots in the path prefix. This allows monorepo subdirectories to have dots in their names without breaking task display and matching. Also updated `is_match()` to handle colon-separated names consistently. Includes comprehensive e2e test for monorepo tasks with dots in directory names, covering: - Single dots (my.app) - Multiple dots (my.service.api) - Version-like dots (feature.v2.beta) - Cross-directory dependencies - Wildcard matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull Request Overview
Fixes a bug where monorepo task names with dots in directory paths were incorrectly truncated when displayed, ensuring full task names are preserved while only stripping file extensions from the task portion.
- Modified
Task::display_name()to only strip file extensions after the last colon separator - Updated
Task::is_match()to handle colon-separated task names consistently for pattern matching - Added comprehensive e2e test covering various edge cases with dots in directory names
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/task/mod.rs | Improved task name parsing to handle dots in monorepo directory paths correctly |
| e2e/tasks/test_task_monorepo_dots_in_dir | Added comprehensive e2e test for various dot patterns in directory names |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| let task_without_ext = task_part.rsplitn(2, '.').last().unwrap_or_default(); | ||
| format!("{}:{}", prefix, task_without_ext) | ||
| } else { | ||
| // No colon separator (e.g., "build.sh") | ||
| // Strip extension from the whole name | ||
| self.name | ||
| .rsplitn(2, '.') | ||
| .last() | ||
| .unwrap_or_default() |
There was a problem hiding this comment.
The logic is incorrect. rsplitn(2, '.') followed by last() returns the part before the last dot, not after removing the extension. For 'build.sh', this returns 'build.sh' instead of 'build'. Use rsplitn(2, '.').nth(1).unwrap_or(task_part) to get the part before the extension.
| let task_without_ext = task_part.rsplitn(2, '.').last().unwrap_or_default(); | |
| format!("{}:{}", prefix, task_without_ext) | |
| } else { | |
| // No colon separator (e.g., "build.sh") | |
| // Strip extension from the whole name | |
| self.name | |
| .rsplitn(2, '.') | |
| .last() | |
| .unwrap_or_default() | |
| let task_without_ext = task_part.rsplitn(2, '.').nth(1).unwrap_or(task_part); | |
| format!("{}:{}", prefix, task_without_ext) | |
| } else { | |
| // No colon separator (e.g., "build.sh") | |
| // Strip extension from the whole name | |
| self.name | |
| .rsplitn(2, '.') | |
| .nth(1) | |
| .unwrap_or(&self.name) |
| let name_ext_stripped = task_part.rsplitn(2, '.').last().unwrap_or_default(); | ||
| let pat_ext_stripped = if let Some((_, pat_task)) = pat.rsplit_once(':') { | ||
| pat_task.rsplitn(2, '.').last().unwrap_or_default() | ||
| } else { | ||
| pat.rsplitn(2, '.').last().unwrap_or_default() | ||
| }; |
There was a problem hiding this comment.
Same logic error as in display_name(). The rsplitn(2, '.').last() pattern returns the original string instead of stripping the extension. Should use rsplitn(2, '.').nth(1).unwrap_or(original) to properly remove extensions.
| self.name.rsplitn(2, '.').last().unwrap_or_default(), | ||
| pat.rsplitn(2, '.').last().unwrap_or_default(), |
There was a problem hiding this comment.
Same logic error repeated in the else branch. These lines also incorrectly use rsplitn(2, '.').last() which doesn't strip extensions properly.
The previous implementation of is_match() only compared the task name portion after the colon, ignoring the path prefix. This caused false positive matches where `//projects/frontend:build` would match `//projects/backend:build`. Now the method preserves the full path prefix when comparing task names, only stripping file extensions from the task portion. This ensures tasks from different projects/directories don't incorrectly match each other. Added test case to verify no false positive matches occur between tasks from different directories with the same task name. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.10.4 x -- echo |
18.1 ± 0.4 | 17.6 | 22.6 | 1.00 |
mise x -- echo |
18.2 ± 0.3 | 17.7 | 19.2 | 1.00 ± 0.02 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.10.4 env |
17.5 ± 0.3 | 17.0 | 20.1 | 1.00 |
mise env |
17.7 ± 0.4 | 17.1 | 20.7 | 1.01 ± 0.03 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.10.4 hook-env |
17.2 ± 0.3 | 16.8 | 19.0 | 1.00 |
mise hook-env |
17.3 ± 0.3 | 16.8 | 18.4 | 1.01 ± 0.02 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.10.4 ls |
15.2 ± 0.2 | 14.7 | 16.4 | 1.00 |
mise ls |
15.4 ± 0.3 | 14.9 | 17.2 | 1.01 ± 0.02 |
xtasks/test/perf
| Command | mise-2025.10.4 | mise | Variance |
|---|---|---|---|
| install (cached) | 199ms | ✅ 106ms | +87% |
| ls (cached) | 63ms | 63ms | +0% |
| bin-paths (cached) | 70ms | 70ms | +0% |
| task-ls (cached) | 481ms | 469ms | +2% |
✅ Performance improvement: install cached is 87%
The previous fix prevented simple patterns from matching monorepo tasks. When a pattern lacked a colon (e.g., "build"), it would only compare against the stripped pattern, not the full task name with its prefix. This broke the ability to run monorepo tasks using just their task name. Now the matching logic properly handles: 1. Simple pattern (e.g., "build") matches monorepo tasks by comparing only the task portion after the colon 2. Full pattern (e.g., "//projects/my.app:build") matches only tasks with the exact path prefix 3. Extensions are stripped for all comparisons Added test case to verify simple pattern matching works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
| let display_name = if let Some((prefix, task_part)) = self.name.rsplit_once(':') { | ||
| // Has a colon separator (e.g., "//projects/my.app:build.sh") | ||
| // Strip extension from the task part only | ||
| let task_without_ext = task_part.rsplitn(2, '.').last().unwrap_or_default(); |
### 📦 Registry - add jules by @alefteris in [#6568](#6568) ### 🐛 Bug Fixes - **(docs)** improve favicon support for Safari by @jdx in [#6567](#6567) - **(github)** download assets via API to respect GITHUB_TOKEN by @roele in [#6496](#6496) - **(task)** load toml tasks in `task_config.includes` in system/global config and monorepo subdirs by @risu729 in [#6545](#6545) - **(task)** handle dots in monorepo directory names correctly by @jdx in [#6571](#6571) ### 📚 Documentation - **(readme)** add GitHub Issues & Discussions section by @rsyring in [#6573](#6573) - **(tasks)** create dedicated monorepo tasks documentation by @jdx in [#6561](#6561) - **(tasks)** enhance monorepo documentation with tool comparisons by @jdx in [#6563](#6563)
Summary
Fixes a bug where monorepo task names with dots in directory paths were incorrectly truncated when displayed. For example,
//projects/my.app:buildwould appear as//projects/myinstead of the full path.Changes
Task::display_name()to only strip file extensions from the task name portion after the last colon (:)Task::is_match()to handle colon-separated task names consistentlytest_task_monorepo_dots_in_dircovering various edge casesTest Coverage
The new test covers:
my.app)my.service.api)feature.v2.beta)//...:taskpatternsExample
Before:
//projects/my.app:build→ displayed as//projects/myAfter:
//projects/my.app:build→ displayed as//projects/my.app:build🤖 Generated with Claude Code
Note
Fixes display and pattern matching for monorepo tasks with dots by stripping extensions only from the task segment and adds comprehensive e2e tests.
Task::display_name(): Strip file extensions only after the last:so monorepo paths with dots (e.g.,//projects/my.app:build.sh) display correctly.Task::is_match(): Align matching logic to handle colon-separated names and extension stripping, supporting simple patterns (e.g.,build) against monorepo tasks.e2e/tasks/test_task_monorepo_dots_in_dircovering directories with dots, cross-project dependencies, wildcard//...:taskmatching, and false-positive checks.Written by Cursor Bugbot for commit 6f765fc. This will update automatically on new commits. Configure here.