Skip to content

feat(workspace): classify git errors into user-friendly messages (T-095)#93

Merged
mpiton merged 6 commits intomainfrom
feat/t-095-worktree-error-handling
Mar 31, 2026
Merged

feat(workspace): classify git errors into user-friendly messages (T-095)#93
mpiton merged 6 commits intomainfrom
feat/t-095-worktree-error-handling

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Mar 31, 2026

Summary

  • Add classify_git_error() function that parses git stderr patterns (branch not found, permission denied, worktree already checked out, not a git repository) into specific, human-readable AppError messages
  • Handle both English and French git locales for all error patterns
  • Add repo path validation in create_worktree() (existence check + .git check) before running any git commands
  • Improve create_dir_all error to explicitly mention permission denied when applicable

Test plan

  • 5 unit tests for classify_git_error (branch not found, permission denied, not a repo, already checked out, generic fallback)
  • 2 integration tests for create_worktree validation (invalid repo path, missing repo path)
  • 1 integration test for permission denied during worktree creation (filesystem permissions)
  • Enhanced existing test_create_worktree_branch_not_found to verify user-friendly message content
  • All 16 worktree tests pass
  • clippy clean with -D warnings

Summary by cubic

Converts git errors into clear, actionable messages, validates repos with git rev-parse, and hides raw stderr from the UI. Improves worktree creation reliability and satisfies Linear T-095.

  • New Features

    • Added classify_git_error() to map common git failures (missing branch, permission denied, already checked out/locked, not a repo) to specific AppError messages in English and French.
    • Validates repo_local_path with tokio::fs::metadata() and git rev-parse --git-dir to support regular and bare repos.
    • Clear permission-denied errors when creating worktree directories.
  • Bug Fixes

    • No raw stderr in user-visible messages; details are logged via tracing. Classified errors now propagate from run_git (including rev-parse).
    • Tightened detection and tests: removed broad "unable to access", replaced Path::exists() with tokio::fs::metadata(), added a Unix-only guard, narrowed French "not a repo" matching, and fixed the permission test to make the parent dir read-only so create_dir_all hits PermissionDenied.

Written for commit 0a119ab. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Clearer, localized user-facing error messages for git failures (missing branch/ref, permission denied, worktree already checked out/locked, not-a-repo). Repository path is validated before worktree operations to avoid misleading errors; permission errors for parent-directory creation are reported specifically. Raw git stderr is no longer exposed to users.
  • Tests

    • Expanded automated tests covering error classifications, repository-path validation, and permission-failure scenarios.

Add classify_git_error() to parse git stderr patterns (branch not found,
permission denied, already checked out, not a git repo) into specific,
human-readable AppError messages. Handles both English and French git
locales. Add repo path validation in create_worktree() before running
git commands. Includes 8 new tests for error classification and edge
cases.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds git stderr classification and stronger repo-path validation in the worktree module: introduces classify_git_error, routes non-success git runs through it, validates repo_local_path and .git before running git, and refines permission-denied error mapping. Tests expanded for new behaviors.

Changes

Cohort / File(s) Summary
Worktree error classification & validation
src-tauri/src/workspace/worktree.rs
Added classify_git_error() to map localized git stderr patterns to AppError variants; updated run_git() to return classified errors on non-zero exit; pre-validate repo_local_path via tokio::fs::metadata and git rev-parse --git-dir in create_worktree(); refined create_dir_all permission error mapping; added unit and integration tests for classification and validation cases.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant WorktreeModule
    participant Filesystem
    participant GitProcess

    Caller->>WorktreeModule: create_worktree(repo_local_path, ...)
    WorktreeModule->>Filesystem: tokio::fs::metadata(repo_local_path)
    alt stat error (NotFound / PermissionDenied / other)
        Filesystem-->>WorktreeModule: error
        WorktreeModule-->>Caller: AppError::Workspace / AppError::Git
    else stat ok
        WorktreeModule->>GitProcess: git rev-parse --git-dir
        alt rev-parse fails
            GitProcess-->>WorktreeModule: stderr
            WorktreeModule->>WorktreeModule: classify_git_error(stderr, args_display)
            WorktreeModule-->>Caller: mapped AppError
        else rev-parse ok
            WorktreeModule->>GitProcess: git fetch / git worktree add ...
            GitProcess-->>WorktreeModule: stdout, stderr, exit_code
            alt exit_code == 0
                WorktreeModule-->>Caller: success
            else
                WorktreeModule->>WorktreeModule: classify_git_error(stderr, args_display)
                WorktreeModule-->>Caller: mapped AppError
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇

I sniffed the stderr, found the clue,
Translated garble into errors true.
I checked each path, I banged each door,
Now worktrees falter here no more. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding git error classification for user-friendly messages. It is concise, specific, and directly reflects the core feature addition in the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/t-095-worktree-error-handling

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR improves the create_worktree flow by (1) adding a new classify_git_error() function that maps raw git stderr patterns to structured, user-friendly AppError messages in both English and French, (2) replacing the previous raw-stderr exposure in run_git with this classification layer, and (3) adding upfront validation of repo_local_path using async tokio::fs::metadata and git rev-parse --git-dir before any worktree operations begin. The previous threads' concerns about synchronous Path::exists() calls and the "unable to access" network-error misclassification are both fully addressed.

Key changes:

  • classify_git_error() handles: branch-not-found (with ref-name extraction), permission denied, worktree-already-checked-out/locked, not-a-git-repository, and a generic fallback that hides raw stderr from users while logging via tracing::warn.
  • create_worktree() now eagerly validates repo_local_path with tokio::fs::metadata (distinguishing NotFound vs. PermissionDenied) and git rev-parse --git-dir (supports bare repos).
  • create_dir_all errors now distinguish PermissionDenied from other I/O failures.
  • 8 new unit/integration tests including a Unix-only permission scenario.

Two minor P2 issues found:

  • The French pattern \"est déjà\" is too broad and can match unrelated French git messages, causing incorrect "Worktree is already in use" classification.
  • The test_worktree_error_permission integration test lacks a root-user guard, which can cause spurious CI failures in Docker-based environments.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 with no blocking defects on the happy path.

Both findings are P2: the broad French "est déjà" pattern is an edge-case misclassification affecting only French-locale users in non-worktree error scenarios, and the root-user test guard is a CI reliability nit. Core logic — async validation, error classification, and test coverage — is solid and the previous P1 concerns have been fully resolved.

src-tauri/src/workspace/worktree.rs — minor French locale pattern and CI test robustness

Important Files Changed

Filename Overview
src-tauri/src/workspace/worktree.rs Adds classify_git_error() for human-friendly error mapping and repo-path validation via tokio::fs::metadata + git rev-parse; two minor issues: overly broad French "est déjà" pattern for worktree classification and missing root-user guard in the permission integration test.

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src-tauri/src/workspace/worktree.rs
Line: 92-98

Comment:
**`est déjà` French pattern is overly broad**

The string `"est déjà"` means "is already" in French and appears in many unrelated git messages, not just worktree conflicts. Any French git error containing those two words would be misclassified as "Worktree is already in use by another working copy." — for example:

```
fatal: La branche 'main' est déjà fusionnée.
# → incorrectly classified as Workspace("Worktree is already in use…")
```

The English side uses the specific phrases `"is already checked out"` and `"already locked"`. The French equivalent of "is already checked out" is `"est déjà extrait"` and of "already locked" is `"est déjà verrouillé"`. Using these tighter substrings avoids false-positive matches:

```suggestion
    if stderr_lower.contains("is already checked out")
        || stderr_lower.contains("already locked")
        || stderr_lower.contains("est d\u{00e9}j\u{00e0} extrait")
        || stderr_lower.contains("est d\u{00e9}j\u{00e0} verrouill\u{00e9}")
    {
        warn!(stderr = stderr.trim(), "git worktree already in use");
        return AppError::Workspace("Worktree is already in use by another working copy.".into());
    }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src-tauri/src/workspace/worktree.rs
Line: 631-662

Comment:
**Permission test spuriously passes (and then fails) when run as root**

Many CI environments run tests inside containers where the process is `root`. Because `root` ignores filesystem permission bits, `create_dir_all` will succeed on a `0o555` directory and `create_worktree` will return `Ok(...)`, causing `assert!(result.is_err())` on line 656 to panic and the test to fail with a misleading message.

A standard guard for this class of test is to skip it when running as root:

```rust
#[cfg(unix)]
#[tokio::test]
async fn test_worktree_error_permission() {
    use std::os::unix::fs::PermissionsExt;

    // Root bypasses permission checks; skip to avoid a spurious failure in CI.
    if unsafe { libc::getuid() } == 0 {
        return;
    }

    // … rest of the test unchanged …
}
```

Alternatively, use the `nix` crate's `Uid::current().is_root()` if `libc` is not already a dependency. Without this guard, the test is liable to fail in Docker-based CI pipelines.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (6): Last reviewed commit: "fix(workspace): test create_dir_all perm..." | Re-trigger Greptile

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/workspace/worktree.rs">

<violation number="1" location="src-tauri/src/workspace/worktree.rs:84">
P2: `"unable to access"` is too broad for permission detection and can misclassify network/auth git failures as filesystem permission errors.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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-tauri/src/workspace/worktree.rs`:
- Around line 600-628: The test function test_worktree_error_permission uses
std::os::unix::fs::PermissionsExt and must be gated to Unix-only targets; add a
platform guard by placing #[cfg(unix)] immediately above the #[tokio::test]
attribute on the test_worktree_error_permission function so the test (and its
use of PermissionsExt) is only compiled and run on Unix platforms.
- Around line 81-89: The permission-error classifier in run_git() is too
broad—remove the check for the string "unable to access" from the condition that
builds AppError::Git("Permission denied...") so only true filesystem permission
phrases remain (e.g., "permission denied", "permission non accord"); update the
conditional that currently checks stderr_lower.contains("unable to access") to
no longer include that clause and ensure run_git() instead falls through to the
generic error path for network/TLS/authentication failures so those produce an
appropriate non-permission error message.
- Around line 172-184: The current preflight uses Path::exists() which collapses
PermissionDenied into false; replace the two exists() checks with
std::fs::metadata calls (or repo_local_path.metadata() and
repo_local_path.join(".git").metadata()) and pattern-match the io::ErrorKind to
preserve NotFound vs PermissionDenied: when metadata() returns Ok proceed, when
Err(kind==NotFound) return the existing AppError::Git "does not exist"/"not a
valid git repository" messages, and when Err(kind==PermissionDenied) return a
distinct error (or map to AppError::Git with a PermissionDenied-specific
message) so unreadable repos are not reported as missing; update the logic
around repo_local_path and repo_local_path.join(".git") accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4072c966-f16b-471f-a33f-8bdf6aaf5a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 2b54f2e and 8b57c53.

📒 Files selected for processing (1)
  • src-tauri/src/workspace/worktree.rs

- Remove "unable to access" from permission classifier (too broad,
  covers network/DNS/TLS errors, not just permissions)
- Replace sync Path::exists() with tokio::fs::metadata() to distinguish
  NotFound from PermissionDenied in async context
- Add #[cfg(unix)] guard to permission test for cross-platform compat
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src-tauri/src/workspace/worktree.rs (1)

532-593: Add one French fixture to lock in the locale support.

Lines 63-65, 82, 93, and 103 add French-language classifiers, but this new test block only exercises English stderr. A small French sample for each mapped class would keep those branches from regressing silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 532 - 593, Add
French-language stderr fixtures and corresponding assertions alongside the
existing English tests to ensure locale support doesn't regress: for
classify_git_error add tests mirroring test_classify_git_error_branch_not_found,
test_classify_git_error_permission_denied, test_classify_git_error_not_a_repo,
test_classify_git_error_already_checked_out and
test_classify_git_error_generic_fallback but using French stderr samples (e.g.
"introuvable", "Permission refusée", "n'est pas un dépôt git", "est déjà
extrait", etc.), call classify_git_error with the same command strings, assert
the same AppError variants (AppError::Git or AppError::Workspace) and the same
substring expectations on err.to_string() to lock in French classifier behavior
for the functions/classify_git_error and the test names (e.g. append _fr to each
test).
🤖 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-tauri/src/workspace/worktree.rs`:
- Around line 200-206: The current preflight in worktree.rs rejects repositories
by checking tokio::fs::metadata(repo_local_path.join(".git")), which incorrectly
fails for bare repositories; replace that filesystem-only check with a Git-aware
validation: run "git rev-parse --git-dir" (e.g. via tokio::process::Command with
"-C" repo_local_path) and treat a successful exit as a valid repo, or
alternatively accept a repo root that contains bare-repo markers (HEAD plus
objects/refs) instead of requiring a .git subdirectory; update the logic that
currently matches on tokio::fs::metadata and use the git-aware check
(referencing repo_local_path and the metadata-matching branch) so bare
repositories are allowed.

---

Nitpick comments:
In `@src-tauri/src/workspace/worktree.rs`:
- Around line 532-593: Add French-language stderr fixtures and corresponding
assertions alongside the existing English tests to ensure locale support doesn't
regress: for classify_git_error add tests mirroring
test_classify_git_error_branch_not_found,
test_classify_git_error_permission_denied, test_classify_git_error_not_a_repo,
test_classify_git_error_already_checked_out and
test_classify_git_error_generic_fallback but using French stderr samples (e.g.
"introuvable", "Permission refusée", "n'est pas un dépôt git", "est déjà
extrait", etc.), call classify_git_error with the same command strings, assert
the same AppError variants (AppError::Git or AppError::Workspace) and the same
substring expectations on err.to_string() to lock in French classifier behavior
for the functions/classify_git_error and the test names (e.g. append _fr to each
test).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f6b44a3-1caa-4310-a335-cda4f7f00ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 8b57c53 and 967aa08.

📒 Files selected for processing (1)
  • src-tauri/src/workspace/worktree.rs

Replace .git metadata check with git rev-parse --git-dir to correctly
accept both regular and bare repositories.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/workspace/worktree.rs">

<violation number="1" location="src-tauri/src/workspace/worktree.rs:204">
P2: This `map_err(|_| ...)` masks all `git rev-parse` failures as "not a valid git repository", hiding specific errors like permission denied. Propagate the original `run_git` error instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src-tauri/src/workspace/worktree.rs (1)

632-647: This test never exercises the new create_dir_all permission mapping.

wt_parent already exists before create_worktree() runs, so create_dir_all(parent) is a no-op even after chmod 0555; the failure is deferred to git worktree add instead. That means the explicit PermissionDenied branch on Lines 223-230 can regress without this test failing. Make test-repo/ non-writable and leave worktrees/ absent so the permission error happens at create_dir_all().

🧪 Adjust the fixture so it hits the intended branch
-        // Create the worktrees dir, then make it non-writable so git cannot
-        // create the pr-42 subdirectory inside it.
-        let wt_parent = base_dir.join("test-repo").join("worktrees");
-        tokio::fs::create_dir_all(&wt_parent).await.unwrap();
-
-        let mut perms = tokio::fs::metadata(&wt_parent).await.unwrap().permissions();
+        // Create the repo dir, then make it non-writable so create_dir_all()
+        // cannot create the `worktrees` child.
+        let repo_dir = base_dir.join("test-repo");
+        tokio::fs::create_dir_all(&repo_dir).await.unwrap();
+
+        let mut perms = tokio::fs::metadata(&repo_dir).await.unwrap().permissions();
         perms.set_mode(0o555); // r-xr-xr-x — no write
-        tokio::fs::set_permissions(&wt_parent, perms.clone())
+        tokio::fs::set_permissions(&repo_dir, perms.clone())
             .await
             .unwrap();

         let result = create_worktree(&local, "feature-42", 42, "test-repo", &base_dir).await;

         // Restore permissions for cleanup
         perms.set_mode(0o755);
-        tokio::fs::set_permissions(&wt_parent, perms).await.unwrap();
+        tokio::fs::set_permissions(&repo_dir, perms).await.unwrap();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 632 - 647, The test
currently creates worktrees/ before calling create_worktree so create_dir_all is
a no-op; change the fixture to make the test-repo directory non-writable (set
permissions on base_dir.join("test-repo") to 0o555) and ensure the worktrees/
directory does NOT exist so that create_dir_all(&wt_parent) inside
create_worktree triggers the PermissionDenied branch (lines handling
PermissionDenied). Keep the call to create_worktree(&local, "feature-42", 42,
"test-repo", &base_dir).await, then restore permissions on test-repo to writable
for cleanup; reference wt_parent, base_dir, create_worktree, and the
PermissionDenied branch when making edits.
🤖 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-tauri/src/workspace/worktree.rs`:
- Around line 84-87: The code in worktree.rs currently interpolates raw git
stderr into AppError::Git (e.g., the return using AppError::Git(format!(...
stderr.trim() ...))) which can leak sensitive repo URLs and paths to the
frontend via workspace_open; instead, log the raw stderr locally (e.g., using
the existing logger) and return a sanitized, generic error string in
AppError::Git such as "Permission denied during git operation; check repository
and file permissions" (omit stderr content), applying the same change to the
other similar returns (the AppError::Git occurrences at the other noted
locations); keep AppError::Git as the error variant but remove any raw stderr
interpolation so only backend logs contain the detailed git output.
- Around line 202-209: The current map_err on run_git(...) throws away the
original error; change it to inspect the error returned by run_git (capture it
as e) and only convert to AppError::Git with the repo_local_path when the error
indicates the specific "not a git repository" condition (e.g., inspect an error
variant or stderr/message containing "not a git repository"); otherwise return
or propagate the original error unchanged. Update the call around
run_git(&["rev-parse"...], repo_local_path).await to match on the captured error
and only produce AppError::Git(...) for the specific case, leaving other errors
(timeouts, git missing, permission issues) intact.

---

Nitpick comments:
In `@src-tauri/src/workspace/worktree.rs`:
- Around line 632-647: The test currently creates worktrees/ before calling
create_worktree so create_dir_all is a no-op; change the fixture to make the
test-repo directory non-writable (set permissions on base_dir.join("test-repo")
to 0o555) and ensure the worktrees/ directory does NOT exist so that
create_dir_all(&wt_parent) inside create_worktree triggers the PermissionDenied
branch (lines handling PermissionDenied). Keep the call to
create_worktree(&local, "feature-42", 42, "test-repo", &base_dir).await, then
restore permissions on test-repo to writable for cleanup; reference wt_parent,
base_dir, create_worktree, and the PermissionDenied branch when making edits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 510322b3-57e8-4fe6-b626-d5b4e2b8b410

📥 Commits

Reviewing files that changed from the base of the PR and between 967aa08 and 7427012.

📒 Files selected for processing (1)
  • src-tauri/src/workspace/worktree.rs

Comment on lines +202 to +209
run_git(&["rev-parse".into(), "--git-dir".into()], repo_local_path)
.await
.map_err(|_| {
AppError::Git(format!(
"Path is not a valid git repository: {}",
repo_local_path.display()
))
})?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t collapse every rev-parse failure into “not a valid git repository”.

This map_err(|_| ...) discards the classifier you just added. A timeout, an unreadable .git, or even “git not found” will all be reported as a bad repository path, which sends users to the wrong remediation. Let run_git() propagate other failures unchanged, and only rewrite the specific not-a-repo case if you still want the path in the message.

🔎 Minimal fix
-    run_git(&["rev-parse".into(), "--git-dir".into()], repo_local_path)
-        .await
-        .map_err(|_| {
-            AppError::Git(format!(
-                "Path is not a valid git repository: {}",
-                repo_local_path.display()
-            ))
-        })?;
+    run_git(&["rev-parse".into(), "--git-dir".into()], repo_local_path).await?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run_git(&["rev-parse".into(), "--git-dir".into()], repo_local_path)
.await
.map_err(|_| {
AppError::Git(format!(
"Path is not a valid git repository: {}",
repo_local_path.display()
))
})?;
run_git(&["rev-parse".into(), "--git-dir".into()], repo_local_path).await?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 202 - 209, The current
map_err on run_git(...) throws away the original error; change it to inspect the
error returned by run_git (capture it as e) and only convert to AppError::Git
with the repo_local_path when the error indicates the specific "not a git
repository" condition (e.g., inspect an error variant or stderr/message
containing "not a git repository"); otherwise return or propagate the original
error unchanged. Update the call around run_git(&["rev-parse"...],
repo_local_path).await to match on the captured error and only produce
AppError::Git(...) for the specific case, leaving other errors (timeouts, git
missing, permission issues) intact.

…rs (T-095)

- Remove raw stderr from user-facing error messages, log it via tracing
  instead to prevent leaking filesystem paths and URLs to the frontend
- Propagate classified error from run_git for rev-parse instead of
  masking all failures as "not a valid git repository"
- Fix French locale detection for "not a git repository" pattern
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src-tauri/src/workspace/worktree.rs (1)

213-218: Consider using async metadata check for consistency.

This synchronous wt_path.exists() call is inconsistent with the async tokio::fs::metadata approach used for repo_local_path validation. While unlikely to cause issues in practice (subsequent operations would fail), using async file operations throughout would be more consistent with the PR's approach.

♻️ Optional refactor for consistency
-    if wt_path.exists() {
-        return Err(AppError::Workspace(format!(
-            "worktree already exists at {}",
-            wt_path.display()
-        )));
-    }
+    if tokio::fs::try_exists(&wt_path).await.unwrap_or(false) {
+        return Err(AppError::Workspace(format!(
+            "worktree already exists at {}",
+            wt_path.display()
+        )));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 213 - 218, Replace the
synchronous wt_path.exists() call with an async tokio::fs::metadata check to
match the repo_local_path validation: call tokio::fs::metadata(&wt_path).await
and treat Ok(_) as "worktree exists" (return the same AppError::Workspace),
treat an Err whose kind is NotFound as "does not exist" and continue, and map
any other Err to an appropriate AppError::Workspace with the error details;
update the surrounding async function to await this call and keep references to
wt_path and AppError::Workspace intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/workspace/worktree.rs`:
- Around line 213-218: Replace the synchronous wt_path.exists() call with an
async tokio::fs::metadata check to match the repo_local_path validation: call
tokio::fs::metadata(&wt_path).await and treat Ok(_) as "worktree exists" (return
the same AppError::Workspace), treat an Err whose kind is NotFound as "does not
exist" and continue, and map any other Err to an appropriate AppError::Workspace
with the error details; update the surrounding async function to await this call
and keep references to wt_path and AppError::Workspace intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 383c1f97-8e56-4e7e-b051-098e51fb3536

📥 Commits

Reviewing files that changed from the base of the PR and between 7427012 and 1e29bee.

📒 Files selected for processing (1)
  • src-tauri/src/workspace/worktree.rs

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/workspace/worktree.rs">

<violation number="1" location="src-tauri/src/workspace/worktree.rs:102">
P2: The French not-a-repository matcher is too broad and can misclassify unrelated git errors as local repo-path errors.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Replace broad "dépôt git" pattern with specific "n'est un dépôt git"
and "pas un dépôt git" to avoid misclassifying unrelated git errors.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src-tauri/src/workspace/worktree.rs (2)

522-588: Add a French fixture for the localized matcher paths.

The classifier now relies on exact French substrings, but every unit test here is English-only. A couple of French regression cases would keep that support from regressing silently.

🇫🇷 Suggested regression cases
+    #[test]
+    fn test_classify_git_error_branch_not_found_french() {
+        let stderr = "fatal: impossible de trouver la référence distante nonexistent-branch\n";
+        let err = classify_git_error(stderr, "fetch origin -- nonexistent-branch");
+        let msg = err.to_string();
+        assert!(msg.contains("not found"), "missing 'not found' in: {msg}");
+        assert!(
+            msg.contains("nonexistent-branch"),
+            "missing branch name in: {msg}"
+        );
+    }
+
+    #[test]
+    fn test_classify_git_error_not_a_repo_french() {
+        let stderr = "fatal: ce n'est pas un dépôt git (ou un des répertoires parents): .git\n";
+        let err = classify_git_error(stderr, "rev-parse --git-dir");
+        assert!(
+            err.to_string().contains("not a valid git repository"),
+            "missing repo validation message: {err}"
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 522 - 588, Tests exercise
classify_git_error only with English stderr messages, but the classifier also
matches exact French substrings and needs regression coverage; add one or two
unit tests (e.g., test_classify_git_error_branch_not_found_fr and
test_classify_git_error_permission_denied_fr) that pass French stderr strings
into classify_git_error (using the same command strings as the English tests)
and assert the same AppError variant and user-facing message fragments
(translated equivalents like "introuvable" or "permission refusée" / "permission
denied" in lowercased checks), and ensure the generic fallback test still
verifies that raw French stderr is not leaked in the user-facing message. Ensure
you reference classify_git_error and the existing test names when adding the new
fixtures so they live next to the corresponding English tests.

63-79: Mirror the warn! logging in these early-return classifier branches.

These two paths sanitize the user-facing message, but they are also the only classified branches that currently drop the backend stderr/context entirely.

🪵 Minimal follow-up
     if stderr_lower.contains("couldn't find remote ref")
         || stderr_lower.contains("remote ref does not exist")
         || stderr_lower.contains("impossible de trouver la r\u{00e9}f\u{00e9}rence distante")
     {
+        warn!(
+            command = args_display,
+            stderr = stderr.trim(),
+            "git branch not found"
+        );
         // Extract the ref name from the last word of the matching line.
         let branch = stderr
             .lines()
@@
     if stderr_lower.contains("not a git repository")
         || stderr_lower.contains("n'est un d\u{00e9}p\u{00f4}t git")
         || stderr_lower.contains("pas un d\u{00e9}p\u{00f4}t git")
     {
+        warn!(
+            command = args_display,
+            stderr = stderr.trim(),
+            "git repository validation failed"
+        );
         return AppError::Git(
             "Path is not a valid git repository. Check the repository configuration.".into(),
         );
     }

Also applies to: 100-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/workspace/worktree.rs` around lines 63 - 79, The early-return
branch in worktree.rs that matches stderr_lower ("couldn't find remote ref" /
"remote ref does not exist" / "impossible de trouver la référence distante")
returns an AppError::Git without logging the underlying stderr; update this path
to call warn! with the raw stderr (and the extracted branch name) before
returning so the backend context is preserved (mirror the existing warn! pattern
used elsewhere), and make the analogous change to the other similar classifier
branch (around the 100-107 area) so both branches log warn!(target: ... or
similar) including stderr and branch to aid debugging while still returning the
sanitized AppError::Git.
🤖 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-tauri/src/workspace/worktree.rs`:
- Around line 640-647: The test sets up wt_parent ahead of calling
create_worktree(), so create_dir_all(&parent) in create_worktree() is a no-op
and the new permission-handling branch is never executed; change the test to
ensure the directory does not exist before calling create_worktree() (or create
a parent directory and make the parent of wt_parent readonly) so that
create_dir_all(parent) in create_worktree() actually runs and fails due to
permissions—target symbols: wt_parent, create_worktree(),
create_dir_all(parent), and the pre-git permission handling branch referenced
around the git worktree add path.

---

Nitpick comments:
In `@src-tauri/src/workspace/worktree.rs`:
- Around line 522-588: Tests exercise classify_git_error only with English
stderr messages, but the classifier also matches exact French substrings and
needs regression coverage; add one or two unit tests (e.g.,
test_classify_git_error_branch_not_found_fr and
test_classify_git_error_permission_denied_fr) that pass French stderr strings
into classify_git_error (using the same command strings as the English tests)
and assert the same AppError variant and user-facing message fragments
(translated equivalents like "introuvable" or "permission refusée" / "permission
denied" in lowercased checks), and ensure the generic fallback test still
verifies that raw French stderr is not leaked in the user-facing message. Ensure
you reference classify_git_error and the existing test names when adding the new
fixtures so they live next to the corresponding English tests.
- Around line 63-79: The early-return branch in worktree.rs that matches
stderr_lower ("couldn't find remote ref" / "remote ref does not exist" /
"impossible de trouver la référence distante") returns an AppError::Git without
logging the underlying stderr; update this path to call warn! with the raw
stderr (and the extracted branch name) before returning so the backend context
is preserved (mirror the existing warn! pattern used elsewhere), and make the
analogous change to the other similar classifier branch (around the 100-107
area) so both branches log warn!(target: ... or similar) including stderr and
branch to aid debugging while still returning the sanitized AppError::Git.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1d78f29-e1d3-4252-956e-d02cf4e4ea95

📥 Commits

Reviewing files that changed from the base of the PR and between 1e29bee and 49f40cc.

📒 Files selected for processing (1)
  • src-tauri/src/workspace/worktree.rs

Make repo_root (not worktrees/) read-only so create_dir_all actually
fails with PermissionDenied instead of being a no-op.
@mpiton mpiton merged commit 4cffef9 into main Mar 31, 2026
7 checks passed
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.

1 participant