git_mob_tool/repositories/
mob_session_repo.rs1use crate::Result;
2use crate::helpers::{CmdOutput, CommandRunner};
3
4#[cfg(test)]
5use mockall::{automock, predicate::*};
6#[cfg_attr(test, automock)]
7pub trait MobSessionRepo {
8 fn list_coauthors(&self) -> Result<Vec<String>>;
9 fn add_coauthor(&self, coauthor: &str) -> Result<()>;
10 fn clear(&self) -> Result<()>;
11}
12
13pub struct GitConfigMobRepo<Cmd> {
14 pub command_runner: Cmd,
15}
16
17impl<Cmd: CommandRunner> GitConfigMobRepo<Cmd> {
18 const COAUTHORS_MOB_SECTION: &'static str = "coauthors-mob";
19 const COAUTHOR_MOB_KEY: &'static str = "entry";
20
21 const EXIT_CODE_SUCCESS: i32 = 0;
22 const EXIT_CODE_CONFIG_INVALID_KEY: i32 = 1;
23
24 fn git_config_error<T>(output: &CmdOutput) -> Result<T> {
25 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()),
28 }
29 }
30}
31
32impl<Cmd: CommandRunner> MobSessionRepo for GitConfigMobRepo<Cmd> {
33 fn list_coauthors(&self) -> Result<Vec<String>> {
34 let full_key = format!("{}.{}", Self::COAUTHORS_MOB_SECTION, Self::COAUTHOR_MOB_KEY);
35
36 let output = self
37 .command_runner
38 .execute("git", &["config", "--global", "--get-all", &full_key])?;
39
40 match output.status_code {
41 Some(Self::EXIT_CODE_SUCCESS) => Ok(String::from_utf8(output.stdout)?
42 .lines()
43 .map(|x| x.into())
44 .collect()),
45
46 Some(Self::EXIT_CODE_CONFIG_INVALID_KEY) => Ok(vec![]),
47 _ => Self::git_config_error(&output),
48 }
49 }
50 fn add_coauthor(&self, coauthor: &str) -> Result<()> {
51 let full_key = format!("{}.{}", Self::COAUTHORS_MOB_SECTION, Self::COAUTHOR_MOB_KEY);
52
53 let output = self
54 .command_runner
55 .execute("git", &["config", "--global", "--add", &full_key, coauthor])?;
56
57 match output.status_code {
58 Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
59 _ => Self::git_config_error(&output),
60 }
61 }
62 fn clear(&self) -> Result<()> {
63 if self.list_coauthors()?.is_empty() {
64 return Ok(());
65 }
66
67 let section = Self::COAUTHORS_MOB_SECTION.to_owned();
68
69 let output = self
70 .command_runner
71 .execute("git", &["config", "--global", "--remove-section", §ion])?;
72
73 match output.status_code {
74 Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
75 _ => Self::git_config_error(&output),
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use crate::helpers::MockCommandRunner;
83
84 use super::*;
85
86 fn create_mock_command_runner(
87 program: &str,
88 args: &[&str],
89 stdout: Vec<u8>,
90 stderr: Vec<u8>,
91 status_code: Option<i32>,
92 ) -> MockCommandRunner {
93 let cloned_program = program.to_string();
94 let cloned_args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
95
96 let mut mock_cmd_runner = MockCommandRunner::new();
97 mock_cmd_runner
98 .expect_execute()
99 .once()
100 .withf(move |program, args| program == cloned_program && args == cloned_args)
101 .returning(move |_, _| {
102 Ok(CmdOutput {
103 stdout: stdout.clone(),
104 stderr: stderr.clone(),
105 status_code,
106 })
107 });
108 mock_cmd_runner
109 }
110
111 #[test]
112 fn test_list_coauthors() -> Result<()> {
113 let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
114 let stdout =
115 b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into();
116 let stderr = vec![];
117 let status_code = Some(0);
118 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
119 let mob_repo = GitConfigMobRepo { command_runner };
120
121 let result = mob_repo.list_coauthors()?;
122
123 assert_eq!(
124 result,
125 vec![
126 "Leo Messi <leo.messi@example.com>",
127 "Emi Martinez <emi.martinez@example.com>"
128 ]
129 );
130
131 Ok(())
132 }
133
134 #[test]
135 fn test_list_coauthors_when_mob_session_empty() -> Result<()> {
136 let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
137 let stdout = vec![];
138 let stderr = vec![];
139 let status_code = Some(1);
140 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
141 let mob_repo = GitConfigMobRepo { command_runner };
142
143 let result = mob_repo.list_coauthors()?;
144
145 assert_eq!(result, Vec::<String>::new());
146
147 Ok(())
148 }
149
150 #[test]
151 fn test_list_coauthors_when_unexpected_error() -> Result<()> {
152 let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
153 let stdout = vec![];
154 let stderr = b"uh-oh!".into();
155 let status_code = Some(129);
156 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
157 let mob_repo = GitConfigMobRepo { command_runner };
158
159 let result = mob_repo.list_coauthors();
160
161 assert!(
162 result
163 .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
164 );
165
166 Ok(())
167 }
168
169 #[test]
170 fn test_list_coauthors_when_terminated_by_signal() -> Result<()> {
171 let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
172 let stdout = vec![];
173 let stderr = vec![];
174 let status_code = None;
175 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
176 let mob_repo = GitConfigMobRepo { command_runner };
177
178 let result = mob_repo.list_coauthors();
179
180 assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
181
182 Ok(())
183 }
184
185 #[test]
186 fn test_add_coauthor() -> Result<()> {
187 let coauthor = "Leo Messi <leo.messi@example.com>";
188 let args = &[
189 "config",
190 "--global",
191 "--add",
192 "coauthors-mob.entry",
193 coauthor,
194 ];
195 let stdout = vec![];
196 let stderr = vec![];
197 let status_code = Some(0);
198 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
199 let mob_repo = GitConfigMobRepo { command_runner };
200
201 mob_repo.add_coauthor(coauthor)?;
202
203 Ok(())
204 }
205
206 #[test]
207 fn test_add_coauthor_when_unexpected_error() -> Result<()> {
208 let coauthor = "Leo Messi <leo.messi@example.com>";
209 let args = &[
210 "config",
211 "--global",
212 "--add",
213 "coauthors-mob.entry",
214 coauthor,
215 ];
216 let stdout = vec![];
217 let stderr = b"uh-oh!".into();
218 let status_code = Some(129);
219 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
220 let mob_repo = GitConfigMobRepo { command_runner };
221
222 let result = mob_repo.add_coauthor(coauthor);
223
224 assert!(
225 result
226 .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
227 );
228
229 Ok(())
230 }
231
232 #[test]
233 fn test_add_coauthor_when_terminated_by_signal() -> Result<()> {
234 let coauthor = "Leo Messi <leo.messi@example.com>";
235 let args = &[
236 "config",
237 "--global",
238 "--add",
239 "coauthors-mob.entry",
240 coauthor,
241 ];
242 let stdout = vec![];
243 let stderr = vec![];
244 let status_code = None;
245 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
246 let mob_repo = GitConfigMobRepo { command_runner };
247
248 let result = mob_repo.add_coauthor(coauthor);
249
250 assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
251
252 Ok(())
253 }
254
255 #[test]
256 fn test_clear() -> Result<()> {
257 let mut command_runner = MockCommandRunner::new();
258 command_runner
259 .expect_execute()
260 .once()
261 .withf(|program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
262 .returning(|_, _| {
263 Ok(CmdOutput {
264 stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
265 stderr: vec![],
266 status_code: Some(0),
267 })
268 });
269 command_runner
270 .expect_execute()
271 .once()
272 .withf(|program, args| {
273 program == "git"
274 && args == ["config", "--global", "--remove-section", "coauthors-mob"]
275 })
276 .returning(|_, _| {
277 Ok(CmdOutput {
278 stdout: vec![],
279 stderr: vec![],
280 status_code: Some(0),
281 })
282 });
283 let mob_repo = GitConfigMobRepo { command_runner };
284
285 mob_repo.clear()?;
286
287 Ok(())
288 }
289
290 #[test]
291 fn test_clear_when_mob_session_empty() -> Result<()> {
292 let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
293 let stdout = vec![];
294 let stderr = vec![];
295 let status_code = Some(1);
296 let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
297 let mob_repo = GitConfigMobRepo { command_runner };
298
299 mob_repo.clear()?;
300
301 Ok(())
302 }
303
304 #[test]
305 fn test_clear_when_unexpected_error() -> Result<()> {
306 let mut command_runner = MockCommandRunner::new();
307 command_runner
308 .expect_execute()
309 .once()
310 .withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
311 .returning( |_, _| {
312 Ok(CmdOutput {
313 stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
314 stderr: vec![],
315 status_code: Some(0),
316 })
317 });
318 command_runner
319 .expect_execute()
320 .once()
321 .withf(|program, args| {
322 program == "git"
323 && args == ["config", "--global", "--remove-section", "coauthors-mob"]
324 })
325 .returning(|_, _| {
326 Ok(CmdOutput {
327 stdout: vec![],
328 stderr: b"uh-oh!".into(),
329 status_code: Some(129),
330 })
331 });
332 let mob_repo = GitConfigMobRepo { command_runner };
333
334 let result = mob_repo.clear();
335
336 assert!(
337 result
338 .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
339 );
340
341 Ok(())
342 }
343
344 #[test]
345 fn test_clear_when_terminated_by_signal() -> Result<()> {
346 let mut command_runner = MockCommandRunner::new();
347 command_runner
348 .expect_execute()
349 .once()
350 .withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
351 .returning( |_, _| {
352 Ok(CmdOutput {
353 stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
354 stderr: vec![],
355 status_code: Some(0),
356 })
357 });
358 command_runner
359 .expect_execute()
360 .once()
361 .withf(|program, args| {
362 program == "git"
363 && args == ["config", "--global", "--remove-section", "coauthors-mob"]
364 })
365 .returning(|_, _| {
366 Ok(CmdOutput {
367 stdout: vec![],
368 stderr: vec![],
369 status_code: None,
370 })
371 });
372 let mob_repo = GitConfigMobRepo { command_runner };
373 let result = mob_repo.clear();
374
375 assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
376
377 Ok(())
378 }
379}