Skip to content

Conversation

@atrtde
Copy link
Owner

@atrtde atrtde commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • Added mntn sync --diff command to display git diffs before syncing, with fallback support for older git versions.
  • Bug Fixes

    • Fixed package registry command output to strip ANSI escape codes for cleaner display.
  • Documentation

    • Added examples for mntn sync --status and mntn sync --diff commands in the Git Integration and Sync Guide.

@atrtde atrtde self-assigned this Feb 10, 2026
Copilot AI review requested due to automatic review settings February 10, 2026 01:18
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

This PR introduces centralized error handling with anyhow and thiserror crates, standardizes error types across the codebase via a unified Result<T> type alias and AppError enum, adds a --diff flag to display git diffs before syncing, and implements ANSI escape code stripping for cleaner output.

Changes

Cohort / File(s) Summary
Error Handling Infrastructure
Cargo.toml, src/error.rs, src/main.rs
Added anyhow and thiserror dependencies; introduced centralized AppError enum with variants for IO, JSON, UTF-8, Inquire, command failures, and registry lookup errors; exported Result<T> type alias for uniform error propagation.
Encryption & Registry Error Standardization
src/encryption.rs, src/registry.rs
Updated function signatures to use crate's Result<T> instead of Box<dyn std::error::Error>; added contextual error messages via anyhow's Context trait; replaced plain string errors with AppError variants.
Utilities with New Error Handling
src/utils/system.rs
Added public strip_ansi_codes() function to remove ANSI escape sequences; refactored run_cmd, run_cmd_in_dir, and get_current_git_branch to return crate's Result<T>; introduced AppError::CommandFailure variant for command execution errors.
Sync Feature & CLI
src/cli.rs, src/tasks/sync.rs, README.md, CHANGELOG.md
Added --diff flag to SyncArgs; implemented show_git_diff() with colored output for staged and unstaged changes; added fallback logic for older git versions; updated documentation; marked v2.3.0 release notes.
Package Registry Cleaning
src/tasks/backup.rs
Updated execute return type to anyhow::Result<()>; integrated strip_ansi_codes() to sanitize package manager output before writing to disk.
Task Trait & All Implementations
src/tasks/core.rs, src/tasks/{biometric_sudo,clean,configs_registry,delete,encrypted_configs_registry,install,migrate,package_registry,purge,restore,use_profile,validate}.rs
Standardized Task trait's execute() method to return anyhow::Result<()> instead of Result<(), Box<dyn std::error::Error>>; all implementations updated for consistency; added bail! macro usage in use_profile.rs for early error exits.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant mntn as mntn CLI
    participant Git as Git
    participant Display as Output Display

    User->>mntn: mntn sync --diff
    mntn->>Git: git diff (unstaged changes)
    Git-->>mntn: Diff output (bytes)
    mntn->>Display: Print unstaged diff block
    Display-->>User: Show formatted diff
    
    mntn->>Git: git diff --cached (staged changes)<br/>or fallback for older git
    Git-->>mntn: Diff output (bytes)
    mntn->>Display: Print staged diff block
    Display-->>User: Show formatted diff
    
    mntn->>Git: Proceed with sync if not --diff mode
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: sync diff #8: Directly related—both PRs modify the sync code paths, add the --diff flag to SyncArgs/SyncTask, implement git-diff display functionality, and introduce the strip_ansi_codes utility in system.rs.

Poem

🐰 Errors now flow with anyhow's grace,
No more boxing errors all over the place!
A diff flag shows changes so clear,
And ANSI codes disappear—hooray, cheer! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions stripping ANSI codes from package registry outputs, but the changeset is significantly broader, including error handling refactoring (anyhow integration), git diff functionality, and new dependencies that represent the primary scope of changes. Revise the title to reflect the main change: consider 'refactor: migrate error handling to anyhow and add git diff support' or similar to accurately represent the overall scope of this comprehensive update.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch staging

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Cargo.toml (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Package version not bumped to match CHANGELOG.

Cargo.toml still has version = "2.2.0" while CHANGELOG.md documents a v2.3.0 release. This should be updated to "2.3.0" before release.

Proposed fix
-version = "2.2.0"
+version = "2.3.0"
🤖 Fix all issues with AI agents
In `@src/tasks/sync.rs`:
- Around line 256-271: run_staged_diff_with_fallback currently swallows all
errors when run_git_diff_bytes fails and always retries with "--cached"; change
it to only fallback when the error indicates an unsupported/unrecognized flag by
inspecting the error (e.g., stderr text or a specific error kind) returned from
run_git_diff_bytes, otherwise propagate the error; if you keep the fallback
path, log a warning (via your existing logger) that you are retrying with
"--cached" and include the original error details; update references in the
function run_staged_diff_with_fallback and use the error value returned by
run_git_diff_bytes to decide whether to retry.
- Around line 273-292: The function run_git_diff_bytes duplicates error handling
from run_cmd_impl and returns a std::io::Error-based error; instead, add a
run_cmd_bytes variant in src/utils/system.rs that mirrors run_cmd_impl but
returns Result<Vec<u8>, AppError> (reusing AppError::CommandFailure for non-zero
exit and UTF-8 stderr handling), then replace the body of run_git_diff_bytes to
call that new run_cmd_bytes helper so all command failures use
AppError::CommandFailure consistently.
🧹 Nitpick comments (2)
src/tasks/backup.rs (1)

174-174: Consider caching the compiled Regex in strip_ansi_codes.

The underlying strip_ansi_codes (in src/utils/system.rs) recompiles the regex on every invocation. Since this runs inside a par_iter, it compiles once per package manager — tolerable for now but a LazyLock/OnceLock static would be cleaner.

src/tasks/sync.rs (1)

294-295: Redundant inner import.

std::io::Write is already imported at Line 9 (use std::io::Write;). The inner use on Line 295 is unnecessary.

♻️ Remove redundant import
 fn print_diff_block(title: &str, content: &[u8]) -> anyhow::Result<()> {
-    use std::io::Write;
-
     println!("{}", title);

Comment on lines +256 to +271
fn run_staged_diff_with_fallback(staged_args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
match run_git_diff_bytes(staged_args, mntn_dir) {
Ok(output) => Ok(output),
Err(_) => {
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

run_staged_diff_with_fallback swallows all errors, not just unsupported-flag errors.

The Err(_) match arm catches every failure (permission denied, corrupt repo, disk errors, etc.) and silently retries with --cached. This could mask real problems. Consider matching only on errors likely caused by an unrecognized --staged flag, or at least logging a warning when falling back.

🛡️ Suggested approach: inspect stderr before falling back
 fn run_staged_diff_with_fallback(staged_args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
     match run_git_diff_bytes(staged_args, mntn_dir) {
         Ok(output) => Ok(output),
-        Err(_) => {
+        Err(e) => {
+            // Only fall back if the error looks like an unrecognized option
+            let msg = e.to_string();
+            if !msg.contains("unknown option") && !msg.contains("unrecognized") {
+                return Err(e);
+            }
             let mut cached_args = Vec::with_capacity(staged_args.len());
📝 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
fn run_staged_diff_with_fallback(staged_args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
match run_git_diff_bytes(staged_args, mntn_dir) {
Ok(output) => Ok(output),
Err(_) => {
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
}
}
}
fn run_staged_diff_with_fallback(staged_args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
match run_git_diff_bytes(staged_args, mntn_dir) {
Ok(output) => Ok(output),
Err(e) => {
// Only fall back if the error looks like an unrecognized option
let msg = e.to_string();
if !msg.contains("unknown option") && !msg.contains("unrecognized") {
return Err(e);
}
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
}
}
}
🤖 Prompt for AI Agents
In `@src/tasks/sync.rs` around lines 256 - 271, run_staged_diff_with_fallback
currently swallows all errors when run_git_diff_bytes fails and always retries
with "--cached"; change it to only fallback when the error indicates an
unsupported/unrecognized flag by inspecting the error (e.g., stderr text or a
specific error kind) returned from run_git_diff_bytes, otherwise propagate the
error; if you keep the fallback path, log a warning (via your existing logger)
that you are retrying with "--cached" and include the original error details;
update references in the function run_staged_diff_with_fallback and use the
error value returned by run_git_diff_bytes to decide whether to retry.

Comment on lines +273 to +292
fn run_git_diff_bytes(args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
let output = Command::new("git")
.args(args)
.current_dir(mntn_dir)
.output()?;

if !output.status.success() {
let stderr_len = output.stderr.len();
let stderr_msg = String::from_utf8(output.stderr)
.unwrap_or_else(|_| format!("<non-UTF-8 stderr data: {} bytes>", stderr_len));
return Err(std::io::Error::other(format!(
"Command 'git' failed with status {:?}: {}",
output.status.code(),
stderr_msg
))
.into());
}

Ok(output.stdout)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicated error handling and inconsistent error type.

run_git_diff_bytes reimplements the error-handling pattern from run_cmd_impl in src/utils/system.rs (Lines 36–60), but uses std::io::Error::other(...) instead of the new AppError::CommandFailure introduced in this PR. This defeats the purpose of centralized error handling.

Consider adding a run_cmd_bytes variant to src/utils/system.rs that returns Result<Vec<u8>> and reuses AppError::CommandFailure, then call it from here.

♻️ Quick fix: at minimum, use AppError::CommandFailure for consistency
     if !output.status.success() {
         let stderr_len = output.stderr.len();
         let stderr_msg = String::from_utf8(output.stderr)
             .unwrap_or_else(|_| format!("<non-UTF-8 stderr data: {} bytes>", stderr_len));
-        return Err(std::io::Error::other(format!(
-            "Command 'git' failed with status {:?}: {}",
-            output.status.code(),
-            stderr_msg
-        ))
-        .into());
+        return Err(crate::error::AppError::CommandFailure {
+            cmd: "git".to_string(),
+            status: output.status.code(),
+            stderr: stderr_msg,
+        }.into());
     }
📝 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
fn run_git_diff_bytes(args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
let output = Command::new("git")
.args(args)
.current_dir(mntn_dir)
.output()?;
if !output.status.success() {
let stderr_len = output.stderr.len();
let stderr_msg = String::from_utf8(output.stderr)
.unwrap_or_else(|_| format!("<non-UTF-8 stderr data: {} bytes>", stderr_len));
return Err(std::io::Error::other(format!(
"Command 'git' failed with status {:?}: {}",
output.status.code(),
stderr_msg
))
.into());
}
Ok(output.stdout)
}
fn run_git_diff_bytes(args: &[&str], mntn_dir: &Path) -> anyhow::Result<Vec<u8>> {
let output = Command::new("git")
.args(args)
.current_dir(mntn_dir)
.output()?;
if !output.status.success() {
let stderr_len = output.stderr.len();
let stderr_msg = String::from_utf8(output.stderr)
.unwrap_or_else(|_| format!("<non-UTF-8 stderr data: {} bytes>", stderr_len));
return Err(crate::error::AppError::CommandFailure {
cmd: "git".to_string(),
status: output.status.code(),
stderr: stderr_msg,
}.into());
}
Ok(output.stdout)
}
🤖 Prompt for AI Agents
In `@src/tasks/sync.rs` around lines 273 - 292, The function run_git_diff_bytes
duplicates error handling from run_cmd_impl and returns a std::io::Error-based
error; instead, add a run_cmd_bytes variant in src/utils/system.rs that mirrors
run_cmd_impl but returns Result<Vec<u8>, AppError> (reusing
AppError::CommandFailure for non-zero exit and UTF-8 stderr handling), then
replace the body of run_git_diff_bytes to call that new run_cmd_bytes helper so
all command failures use AppError::CommandFailure consistently.

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 improves output stability/cleanliness and modernizes error handling across the CLI by introducing a central AppError and switching task execution to anyhow::Result, while also adding a mntn sync --diff preview mode.

Changes:

  • Add ANSI-stripping utility and apply it to package manager backup outputs.
  • Introduce AppError + Result<T> and migrate utility/registry APIs to use it; migrate tasks to anyhow::Result<()>.
  • Add mntn sync --diff (and documentation/changelog updates) to show unstaged + staged git diffs.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/system.rs Adds strip_ansi_codes, migrates command helpers to AppError/Result<T>, updates related tests.
src/tasks/backup.rs Strips ANSI escape codes from captured package manager outputs before writing to files.
src/error.rs Introduces centralized application error enum (AppError) and Result<T> alias.
src/main.rs Wires the new error module into the crate module tree.
src/registry.rs Converts registry load/save and enable/disable to use AppError/Result<T> (typed errors).
src/tasks/core.rs Updates Task::execute() to return anyhow::Result<()>.
src/tasks/sync.rs Adds --diff support and implements combined staged/unstaged diff output with fallback behavior.
src/cli.rs Adds mntn sync --diff CLI flag.
README.md Documents mntn sync --status and mntn sync --diff.
CHANGELOG.md Adds v2.3.0 notes for --diff and ANSI stripping fix.
Cargo.toml Adds anyhow and thiserror dependencies.
Cargo.lock Updates lockfile for new dependencies and version bumps.
src/encryption.rs Migrates encryption functions to anyhow::Result and adds context-rich errors.
src/tasks/use_profile.rs Switches to anyhow::Result and uses bail! for missing profile.
src/tasks/validate.rs Switches task execution signature to anyhow::Result.
src/tasks/restore.rs Switches task execution signature to anyhow::Result.
src/tasks/purge.rs Switches task execution signature to anyhow::Result.
src/tasks/package_registry.rs Switches task execution signature to anyhow::Result.
src/tasks/migrate.rs Switches task execution signature to anyhow::Result.
src/tasks/install.rs Switches task execution signature to anyhow::Result across install helpers.
src/tasks/encrypted_configs_registry.rs Switches task execution signature to anyhow::Result.
src/tasks/delete.rs Switches task execution signature to anyhow::Result.
src/tasks/configs_registry.rs Switches task execution signature to anyhow::Result.
src/tasks/clean.rs Switches task execution signature to anyhow::Result.
src/tasks/biometric_sudo.rs Switches task execution signature to anyhow::Result and converts error propagation.

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

Comment on lines +259 to +268
Err(_) => {
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

run_staged_diff_with_fallback falls back to --cached on any error from git diff --staged, which can hide real failures and drops the original error details. Consider only retrying when stderr/exit status indicates --staged is unsupported, and otherwise returning the original error.

Suggested change
Err(_) => {
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
Err(e) => {
let msg = e.to_string();
let staged_unsupported =
(msg.contains("unknown option") || msg.contains("unrecognized option"))
&& msg.contains("--staged");
if staged_unsupported {
let mut cached_args = Vec::with_capacity(staged_args.len());
for arg in staged_args {
if *arg == "--staged" {
cached_args.push("--cached");
} else {
cached_args.push(*arg);
}
}
run_git_diff_bytes(&cached_args, mntn_dir)
} else {
Err(e)
}

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13
/// - Captures stdout and returns it if the command succeeds and output is valid UTF-8.
/// - Returns an `io::Error` if the command cannot be run.
/// - Returns an error if the command exits with non-zero status.
/// - Returns a `FromUtf8Error` if stdout isn't valid UTF-8.
pub fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
pub fn run_cmd(cmd: &str, args: &[&str]) -> Result<String> {
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The rustdoc above run_cmd still describes returning io::Error / FromUtf8Error, but the function now returns crate::error::Result<String> (an AppError). Update the docs to reflect the new error type/variants so callers know what to handle.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27
pub fn strip_ansi_codes(input: &str) -> String {
// Matches CSI (Control Sequence Introducer) ANSI escape codes.
let re = Regex::new(r"\x1B\[[0-?]*[ -/]*[@-~]").unwrap();
re.replace_all(input, "").to_string()
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

strip_ansi_codes recompiles the regex on every call, which is avoidable overhead (and this runs in a loop when backing up multiple package managers). Consider compiling the regex once (e.g., static via std::sync::LazyLock/once_cell) and reusing it.

Copilot uses AI. Check for mistakes.
Comment on lines 217 to +221
fn test_get_current_git_branch_returns_branch_name() {
if run_cmd("git", &["--version"]).is_err() {
return;
}

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This test exits early (silently passes) when git --version fails and later repeats that pattern for other git commands. That can mask real regressions on systems where git exists but one step fails; prefer skipping only when git is unavailable, and otherwise asserting so failures are visible.

Copilot uses AI. Check for mistakes.
@atrtde atrtde merged commit 8fe8191 into main Feb 10, 2026
17 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