Skip to content

Commit c0cc510

Browse files
committed
feat: improve error handling with contextual error messages
- Migrate from Box<dyn Error> to anyhow for better error handling - Add contextual error messages throughout the codebase using anyhow::Context - Use bail! macro for cleaner error creation - Provide more helpful error messages for git command failures - Add specific context for team member and mob session operations - Update test expectations to match new error message format Users now get more informative error messages with context about what operation failed and why.
1 parent 055d083 commit c0cc510

12 files changed

Lines changed: 76 additions & 44 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ name = "git-mob"
2323
path = "src/main.rs"
2424

2525
[dependencies]
26+
anyhow = "1.0"
2627
clap = { version = "4.5.39", features = ["derive"] }
2728
inquire = "0.7.5"
2829
path-clean = "1.0.1"

src/commands/mob.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Result;
22
use crate::repositories::{MobSessionRepo, TeamMemberRepo};
3+
use anyhow::bail;
34
use clap::{Parser, arg};
45
use inquire::MultiSelect;
56
use std::io::Write;
@@ -72,9 +73,7 @@ impl Mob {
7273
Some([]) => {
7374
let team_members = team_member_repo.list(false)?;
7475
if team_members.is_empty() {
75-
return Err(
76-
"No team member(s) found. At least one team member must be added".into(),
77-
);
76+
bail!("No team member(s) found. At least one team member must be added");
7877
}
7978

8079
let result = MultiSelect::new("Select active co-author(s):", team_members)
@@ -100,7 +99,7 @@ impl Mob {
10099
mob_repo.add_coauthor(&team_member)?;
101100
coauthors.push(team_member);
102101
}
103-
None => return Err(format!("No team member found with key: {key}").into()),
102+
None => bail!("No team member found with key: {key}"),
104103
}
105104
}
106105

src/commands/setup.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::Result;
2+
use anyhow::bail;
23
use clap::Parser;
34
use path_clean::PathClean;
45
use std::{
@@ -40,11 +41,10 @@ impl Setup {
4041
let hooks_dir = match Self::get_hooks_dir("--global")? {
4142
Some(hooks_dir) => hooks_dir,
4243
None => {
43-
let new_hooks_dir = env::home_dir()
44-
.ok_or("Failed to get home directory")?
45-
.join(".git")
46-
.join("hooks")
47-
.clean();
44+
let Some(home_dir) = env::home_dir() else {
45+
bail!("Failed to get home directory");
46+
};
47+
let new_hooks_dir = home_dir.join(".git").join("hooks").clean();
4848

4949
Self::set_global_hooks_dir(out, &new_hooks_dir)?;
5050

@@ -73,7 +73,7 @@ impl Setup {
7373
fn handle_local(&self, out: &mut impl Write) -> Result<()> {
7474
let hooks_dir = match Self::get_hooks_dir("--local")? {
7575
Some(hooks_dir) => hooks_dir,
76-
None => return Err("Local githooks directory is not set".into()),
76+
None => bail!("Local githooks directory is not set"),
7777
};
7878

7979
let prepare_commit_msg_path = hooks_dir.join("prepare-commit-msg").clean();
@@ -108,7 +108,9 @@ impl Setup {
108108
return Ok(Some(hooks_dir));
109109
}
110110

111-
let mut expanded_hooks_dir = env::home_dir().ok_or("Failed to get home directory")?;
111+
let Some(mut expanded_hooks_dir) = env::home_dir() else {
112+
bail!("Failed to get home directory");
113+
};
112114
expanded_hooks_dir.extend(hooks_dir.components().skip(1));
113115
Ok(Some(expanded_hooks_dir.clean()))
114116
}
@@ -120,7 +122,7 @@ impl Setup {
120122
.status()?;
121123

122124
if !status.success() {
123-
return Err(format!("Failed to set global githooks directory to {path_str}").into());
125+
bail!("Failed to set global githooks directory to {path_str}");
124126
}
125127

126128
writeln!(out, "Set global githooks directory: {path_str}")?;

src/commands/team_member.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Result;
22
use crate::repositories::TeamMemberRepo;
3+
use anyhow::bail;
34
use clap::{Parser, arg};
45
use std::io::Write;
56

@@ -32,7 +33,7 @@ impl TeamMember {
3233
if let Some(key) = self.delete.as_deref() {
3334
match team_member_repo.get(key)? {
3435
Some(_) => team_member_repo.remove(key)?,
35-
None => return Err(format!("No team member found with key: {key}").into()),
36+
None => bail!("No team member found with key: {key}"),
3637
}
3738
}
3839
if self.list {

src/helpers.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::Result;
2+
use anyhow::Context;
23
use std::process::Command;
34

45
pub struct CmdOutput {
@@ -19,7 +20,9 @@ pub struct StdCommandRunner;
1920

2021
impl CommandRunner for StdCommandRunner {
2122
fn execute(&self, program: &str, args: &[&str]) -> Result<CmdOutput> {
22-
let output = Command::new(program).args(args).output()?;
23+
let output = Command::new(program).args(args).output().with_context(|| {
24+
format!("Failed to execute command: {} {}", program, args.join(" "))
25+
})?;
2326

2427
Ok(CmdOutput {
2528
stdout: output.stdout,

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
1+
pub type Result<T> = anyhow::Result<T>;
22

33
pub mod cli;
44
mod commands;

src/repositories/mob_session_repo.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Result;
22
use crate::helpers::{CmdOutput, CommandRunner};
3+
use anyhow::{Context, bail};
34

45
#[cfg(test)]
56
use mockall::{automock, predicate::*};
@@ -23,8 +24,8 @@ impl<Cmd: CommandRunner> GitConfigMobRepo<Cmd> {
2324

2425
fn git_config_error<T>(output: &CmdOutput) -> Result<T> {
2526
match output.status_code {
26-
Some(code) => Err(format!("Git config command exited with status code: {code}").into()),
27-
None => Err("Git config command terminated by signal".into()),
27+
Some(code) => bail!("Git config command exited with status code: {code}"),
28+
None => bail!("Git config command terminated by signal"),
2829
}
2930
}
3031
}
@@ -35,10 +36,12 @@ impl<Cmd: CommandRunner> MobSessionRepo for GitConfigMobRepo<Cmd> {
3536

3637
let output = self
3738
.command_runner
38-
.execute("git", &["config", "--global", "--get-all", &full_key])?;
39+
.execute("git", &["config", "--global", "--get-all", &full_key])
40+
.context("Failed to list coauthors from git config")?;
3941

4042
match output.status_code {
41-
Some(Self::EXIT_CODE_SUCCESS) => Ok(String::from_utf8(output.stdout)?
43+
Some(Self::EXIT_CODE_SUCCESS) => Ok(String::from_utf8(output.stdout)
44+
.context("Failed to parse git config output as UTF-8")?
4245
.lines()
4346
.map(|x| x.into())
4447
.collect()),
@@ -52,7 +55,8 @@ impl<Cmd: CommandRunner> MobSessionRepo for GitConfigMobRepo<Cmd> {
5255

5356
let output = self
5457
.command_runner
55-
.execute("git", &["config", "--global", "--add", &full_key, coauthor])?;
58+
.execute("git", &["config", "--global", "--add", &full_key, coauthor])
59+
.with_context(|| format!("Failed to add coauthor '{coauthor}' to git config"))?;
5660

5761
match output.status_code {
5862
Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
@@ -68,7 +72,8 @@ impl<Cmd: CommandRunner> MobSessionRepo for GitConfigMobRepo<Cmd> {
6872

6973
let output = self
7074
.command_runner
71-
.execute("git", &["config", "--global", "--remove-section", &section])?;
75+
.execute("git", &["config", "--global", "--remove-section", &section])
76+
.context("Failed to clear mob session from git config")?;
7277

7378
match output.status_code {
7479
Some(Self::EXIT_CODE_SUCCESS) => Ok(()),

src/repositories/team_member_repo.rs

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Result;
22
use crate::helpers::{CmdOutput, CommandRunner};
3+
use anyhow::{Context, bail};
34

45
#[cfg(test)]
56
use mockall::{automock, predicate::*};
@@ -24,8 +25,8 @@ impl<Cmd: CommandRunner> GitConfigTeamMemberRepo<Cmd> {
2425

2526
fn git_config_error<T>(output: &CmdOutput) -> Result<T> {
2627
match output.status_code {
27-
Some(code) => Err(format!("Git config command exited with status code: {code}").into()),
28-
None => Err("Git config command terminated by signal".into()),
28+
Some(code) => bail!("Git config command exited with status code: {code}"),
29+
None => bail!("Git config command terminated by signal"),
2930
}
3031
}
3132
}
@@ -35,23 +36,28 @@ impl<Cmd: CommandRunner> TeamMemberRepo for GitConfigTeamMemberRepo<Cmd> {
3536
let section = Self::COAUTHORS_SECTION;
3637
let search_regex = format!("^{section}\\.");
3738

38-
let output = self.command_runner.execute(
39-
"git",
40-
&["config", "--global", "--get-regexp", &search_regex],
41-
)?;
39+
let output = self
40+
.command_runner
41+
.execute(
42+
"git",
43+
&["config", "--global", "--get-regexp", &search_regex],
44+
)
45+
.context("Failed to list team members from git config")?;
4246

4347
match output.status_code {
44-
Some(Self::EXIT_CODE_SUCCESS) => String::from_utf8(output.stdout)?
48+
Some(Self::EXIT_CODE_SUCCESS) => String::from_utf8(output.stdout)
49+
.context("Failed to parse git config output as UTF-8")?
4550
.lines()
4651
.map(|x| {
4752
let delimiter = if show_keys {
4853
format!("{section}.")
4954
} else {
5055
" ".to_owned()
5156
};
52-
x.split_once(&delimiter)
53-
.ok_or(format!("Failed to split string: '{x}'").into())
54-
.map(|(_, team_member)| team_member.to_owned())
57+
let Some((_, team_member)) = x.split_once(&delimiter) else {
58+
bail!("Failed to split git config line: '{x}'");
59+
};
60+
Ok(team_member.to_owned())
5561
})
5662
.collect(),
5763

@@ -65,12 +71,16 @@ impl<Cmd: CommandRunner> TeamMemberRepo for GitConfigTeamMemberRepo<Cmd> {
6571

6672
let output = self
6773
.command_runner
68-
.execute("git", &["config", "--global", &full_key])?;
74+
.execute("git", &["config", "--global", &full_key])
75+
.with_context(|| format!("Failed to get team member '{key}' from git config"))?;
6976

7077
match output.status_code {
71-
Some(Self::EXIT_CODE_SUCCESS) => {
72-
Ok(Some(String::from_utf8(output.stdout)?.trim().into()))
73-
}
78+
Some(Self::EXIT_CODE_SUCCESS) => Ok(Some(
79+
String::from_utf8(output.stdout)
80+
.context("Failed to parse git config output as UTF-8")?
81+
.trim()
82+
.into(),
83+
)),
7484
Some(Self::EXIT_CODE_CONFIG_INVALID_KEY) => Ok(None),
7585
_ => Self::git_config_error(&output),
7686
}
@@ -81,7 +91,8 @@ impl<Cmd: CommandRunner> TeamMemberRepo for GitConfigTeamMemberRepo<Cmd> {
8191

8292
let output = self
8393
.command_runner
84-
.execute("git", &["config", "--global", "--unset-all", &full_key])?;
94+
.execute("git", &["config", "--global", "--unset-all", &full_key])
95+
.with_context(|| format!("Failed to remove team member '{key}' from git config"))?;
8596

8697
match output.status_code {
8798
Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
@@ -94,11 +105,14 @@ impl<Cmd: CommandRunner> TeamMemberRepo for GitConfigTeamMemberRepo<Cmd> {
94105

95106
let output = self
96107
.command_runner
97-
.execute("git", &["config", "--global", &full_key, team_member])?;
108+
.execute("git", &["config", "--global", &full_key, team_member])
109+
.with_context(|| format!("Failed to add team member '{key}' to git config"))?;
98110

99111
match output.status_code {
100112
Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
101-
Some(Self::EXIT_CODE_CONFIG_INVALID_KEY) => Err(format!("Invalid key: {key}").into()),
113+
Some(Self::EXIT_CODE_CONFIG_INVALID_KEY) => {
114+
bail!("Invalid key: {key}")
115+
}
102116
_ => Self::git_config_error(&output),
103117
}
104118
}

tests/mob.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn test_mob_with_by_key_when_team_member_not_found(
4444
.assert()
4545
.failure()
4646
.stderr(predicate::str::diff(
47-
"Error: \"No team member found with key: jk\"\n",
47+
"Error: No team member found with key: jk\n",
4848
));
4949

5050
Ok(())
@@ -61,7 +61,7 @@ fn test_mob_with_multiselect_given_no_team_members_added(
6161
.assert()
6262
.failure()
6363
.stderr(predicate::str::diff(
64-
"Error: \"No team member(s) found. At least one team member must be added\"\n",
64+
"Error: No team member(s) found. At least one team member must be added\n",
6565
));
6666

6767
Ok(())

0 commit comments

Comments
 (0)