1use std::sync::Arc;
14
15use crate::cmds::merge_request;
16use crate::cmds::merge_request::SummaryOptions;
17use crate::error;
18use crate::error::AddContext;
19use crate::error::GRError;
20use crate::io::CmdInfo;
21use crate::io::ShellResponse;
22use crate::io::TaskRunner;
23use crate::remote::RemoteURL;
24use crate::Result;
25
26pub fn status(exec: Arc<impl TaskRunner<Response = ShellResponse>>) -> Result<CmdInfo> {
31 let cmd_params = ["git", "status", "--short"];
32 let response = exec.run(cmd_params)?;
33 handle_git_status(&response)
34}
35
36fn handle_git_status(response: &ShellResponse) -> Result<CmdInfo> {
37 let modified = response
38 .body
39 .split('\n')
40 .filter(|s| {
41 let fields = s.split(' ').collect::<Vec<&str>>();
42 if fields.len() == 3 && fields[1] == "M" {
43 return true;
44 }
45 false
46 })
47 .count();
48 if modified > 0 {
49 return Ok(CmdInfo::StatusModified(true));
50 }
51 Ok(CmdInfo::StatusModified(false))
52}
53
54pub fn current_branch(runner: Arc<impl TaskRunner<Response = ShellResponse>>) -> Result<CmdInfo> {
56 let cmd_params = ["git", "rev-parse", "--abbrev-ref", "HEAD"];
60 let response = runner.run(cmd_params).err_context(format!(
61 "Failed to get current branch. Command: {}",
62 cmd_params.join(" ")
63 ))?;
64 Ok(CmdInfo::Branch(response.body))
65}
66
67pub fn fetch(exec: Arc<impl TaskRunner>, remote_alias: String) -> Result<CmdInfo> {
73 let cmd_params = ["git", "fetch", &remote_alias];
74 exec.run(cmd_params).err_context(format!(
75 "Failed to git fetch. Command: {}",
76 cmd_params.join(" ")
77 ))?;
78 Ok(CmdInfo::Ignore)
79}
80
81pub fn add(exec: &impl TaskRunner) -> Result<CmdInfo> {
82 let cmd_params = ["git", "add", "-u"];
83 exec.run(cmd_params).err_context(format!(
84 "Failed to git add changes. Command: {}",
85 cmd_params.join(" ")
86 ))?;
87 Ok(CmdInfo::Ignore)
88}
89
90pub fn commit(exec: &impl TaskRunner, message: &str) -> Result<CmdInfo> {
91 let cmd_params = ["git", "commit", "-m", message];
92 exec.run(cmd_params).err_context(format!(
93 "Failed to git commit changes. Command: {}",
94 cmd_params.join(" ")
95 ))?;
96 Ok(CmdInfo::Ignore)
97}
98
99pub fn remote_url(exec: &impl TaskRunner<Response = ShellResponse>) -> Result<CmdInfo> {
101 let cmd_params = ["git", "remote", "get-url", "--all", "origin"];
102 let response = exec.run(cmd_params)?;
103 handle_git_remote_url(&response)
104}
105
106fn handle_git_remote_url(response: &ShellResponse) -> Result<CmdInfo> {
107 let fields = response.body.split(':').collect::<Vec<&str>>();
108 match fields.len() {
109 2 => {
111 let domain: Vec<&str> = fields[0].split('@').collect();
112 if domain.len() == 2 {
113 let remote_path_partial: Vec<&str> = fields[1].split(".git").collect();
114 return Ok(CmdInfo::RemoteUrl(RemoteURL::new(
115 domain[1].to_string(),
116 remote_path_partial[0].to_string(),
117 )));
118 }
119 let remote_path_partial = fields[1].split('/').skip(2).collect::<Vec<&str>>();
121 let host = remote_path_partial[0];
122 let project = remote_path_partial[2].split(".git").collect::<Vec<&str>>();
123 let project_path = format!("{}/{}", remote_path_partial[1], project[0]);
124 Ok(CmdInfo::RemoteUrl(RemoteURL::new(
125 host.to_string(),
126 project_path,
127 )))
128 }
129 3 => {
131 let domain: Vec<&str> = fields[1].split('@').collect();
132 let remote_path_partial = fields[2].split('/').skip(1).collect::<Vec<&str>>();
133 let remote_path = remote_path_partial
134 .join("/")
135 .strip_suffix(".git")
136 .unwrap() .to_string();
138 Ok(CmdInfo::RemoteUrl(RemoteURL::new(
139 domain[1].to_string(),
140 remote_path,
141 )))
142 }
143 _ => {
144 let trace = format!("git configuration error: {}", response.body);
145 Err(error::gen(trace))
146 }
147 }
148}
149
150pub fn commit_summary(
156 runner: Arc<impl TaskRunner<Response = ShellResponse>>,
157 commit: &Option<String>,
158) -> Result<CmdInfo> {
159 let mut cmd_params = vec!["git", "log", "--format=%s", "-n1"];
160 if let Some(commit) = commit {
161 cmd_params.push(commit);
162 }
163 let response = runner.run(cmd_params)?;
164 Ok(CmdInfo::CommitSummary(response.body))
165}
166
167pub fn outgoing_commits(
168 runner: &impl TaskRunner<Response = ShellResponse>,
169 remote: &str,
170 default_branch: &str,
171 summary_options: &merge_request::SummaryOptions,
172) -> Result<String> {
173 let cmd = match summary_options {
174 SummaryOptions::Short => vec![
175 "git".to_string(),
176 "log".to_string(),
177 format!("{}/{}..", remote, default_branch),
178 "--reverse".to_string(),
179 "--pretty=format:%s - %h %d".to_string(),
180 ],
181 SummaryOptions::Long => vec![
182 "git".to_string(),
183 "log".to_string(),
184 format!("{}/{}..", remote, default_branch),
185 "--reverse".to_string(),
186 "--pretty=format:%s - %h %d%n%b".to_string(),
187 ],
188 SummaryOptions::None => Err(GRError::ApplicationError(
191 "Invalid summary. It needs to be Short or Long, but never None".to_string(),
192 ))?,
193 };
194 let response = runner.run(cmd)?;
195 Ok(response.body)
196}
197
198pub fn patch<S: Into<String>, T: Into<String>>(
199 runner: &impl TaskRunner<Response = ShellResponse>,
200 current_branch: S,
201 target_branch: T,
202) -> Result<String> {
203 let cmd = vec![
204 "git".to_string(),
205 "diff".to_string(),
206 target_branch.into(),
207 current_branch.into(),
208 ];
209 let response = runner.run(cmd)?;
210 Ok(response.body)
211}
212
213pub fn push(runner: &impl TaskRunner, remote: &str, repo: &Repo, force: bool) -> Result<CmdInfo> {
214 let force_str = if force { "+" } else { "" };
215 let cmd = format!("git push {} {}{}", remote, force_str, repo.current_branch);
216 let cmd_params = cmd.split(' ').collect::<Vec<&str>>();
217 runner.run(cmd_params)?;
218 Ok(CmdInfo::Ignore)
219}
220
221pub fn rebase(runner: &impl TaskRunner, remote_alias: &str) -> Result<CmdInfo> {
222 let cmd = format!("git rebase {remote_alias}");
223 let cmd_params = cmd.split(' ').collect::<Vec<&str>>();
224 runner.run(cmd_params)?;
225 Ok(CmdInfo::Ignore)
226}
227
228pub fn commit_message(
229 runner: Arc<impl TaskRunner<Response = ShellResponse>>,
230 commit: &Option<String>,
231) -> Result<CmdInfo> {
232 let mut cmd_params = vec!["git", "log", "--pretty=format:%b", "-n1"];
233 if let Some(commit) = commit {
234 cmd_params.push(commit);
235 }
236 let response = runner.run(cmd_params)?;
237 Ok(CmdInfo::CommitMessage(response.body))
238}
239
240pub fn checkout(runner: &impl TaskRunner<Response = ShellResponse>, branch: &str) -> Result<()> {
241 let git_cmd = format!("git checkout origin/{branch} -b {branch}");
242 let cmd_params = ["/bin/sh", "-c", &git_cmd];
243 runner.run(cmd_params).err_context(format!(
244 "Failed to git checkout remote branch. Command: {}",
245 cmd_params.join(" ")
246 ))?;
247 Ok(())
248}
249
250#[derive(Clone, Debug, Default)]
252pub struct Repo {
253 current_branch: String,
254 dirty: bool,
255 title: String,
256 last_commit_message: String,
257}
258
259impl Repo {
260 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn with_current_branch(&mut self, branch: &str) {
265 self.current_branch = branch.to_string();
266 }
267
268 pub fn with_status(&mut self, dirty: bool) {
269 self.dirty = dirty;
270 }
271
272 pub fn with_title(&mut self, title: &str) {
273 self.title = title.to_string();
274 }
275
276 pub fn with_branch(&mut self, branch: &str) {
277 self.current_branch = branch.to_string();
278 }
279
280 pub fn with_last_commit_message(&mut self, message: &str) {
281 self.last_commit_message = message.to_string();
282 }
283
284 pub fn current_branch(&self) -> &str {
285 &self.current_branch
286 }
287
288 pub fn dirty(&self) -> bool {
289 self.dirty
290 }
291
292 pub fn title(&self) -> &str {
293 &self.title
294 }
295
296 pub fn last_commit_message(&self) -> &str {
297 &self.last_commit_message
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 use crate::test::utils::{get_contract, ContractType, MockRunner};
306
307 #[test]
308 fn test_git_repo_has_modified_files() {
309 let response = ShellResponse::builder()
310 .body(get_contract(
311 ContractType::Git,
312 "git_status_modified_files.txt",
313 ))
314 .build()
315 .unwrap();
316 let runner = Arc::new(MockRunner::new(vec![response]));
317 let cmd_info = status(runner).unwrap();
318 if let CmdInfo::StatusModified(dirty) = cmd_info {
319 assert!(dirty);
320 } else {
321 panic!("Expected CmdInfo::StatusModified");
322 }
323 }
324
325 #[test]
326 fn test_git_repo_has_untracked_and_modified_files_is_modified() {
327 let response = ShellResponse::builder()
328 .body(get_contract(
329 ContractType::Git,
330 "git_status_untracked_and_modified_files.txt",
331 ))
332 .build()
333 .unwrap();
334 let runner = Arc::new(MockRunner::new(vec![response]));
335 let cmd_info = status(runner).unwrap();
336 if let CmdInfo::StatusModified(dirty) = cmd_info {
337 assert!(dirty);
338 } else {
339 panic!("Expected CmdInfo::StatusModified");
340 }
341 }
342
343 #[test]
344 fn test_git_status_command_is_correct() {
345 let response = ShellResponse::builder().build().unwrap();
346 let runner = Arc::new(MockRunner::new(vec![response]));
347 status(runner.clone()).unwrap();
348 assert_eq!("git status --short", *runner.cmd());
350 }
351
352 #[test]
353 fn test_git_repo_is_clean() {
354 let response = ShellResponse::builder()
355 .body(get_contract(ContractType::Git, "git_status_clean_repo.txt"))
356 .build()
357 .unwrap();
358 let runner = Arc::new(MockRunner::new(vec![response]));
359 let cmd_info = status(runner).unwrap();
360 if let CmdInfo::StatusModified(dirty) = cmd_info {
361 assert!(!dirty);
362 } else {
363 panic!("Expected CmdInfo::StatusModified");
364 }
365 }
366
367 #[test]
368 fn test_git_repo_has_untracked_files_treats_repo_as_no_local_modifications() {
369 let response = ShellResponse::builder()
370 .body(get_contract(
371 ContractType::Git,
372 "git_status_untracked_files.txt",
373 ))
374 .build()
375 .unwrap();
376 let runner = Arc::new(MockRunner::new(vec![response]));
377 let cmd_info = status(runner).unwrap();
378 if let CmdInfo::StatusModified(dirty) = cmd_info {
379 assert!(!dirty);
380 } else {
381 panic!("Expected CmdInfo::StatusModified");
382 }
383 }
384
385 #[test]
386 fn test_git_remote_url_cmd_is_correct() {
387 let response = ShellResponse::builder()
388 .body("git@github.com:jordilin/mr.git".to_string())
389 .build()
390 .unwrap();
391 let runner = MockRunner::new(vec![response]);
392 remote_url(&runner).unwrap();
393 assert_eq!("git remote get-url --all origin", *runner.cmd());
394 }
395
396 #[test]
397 fn test_get_remote_git_url() {
398 let response = ShellResponse::builder()
399 .body("git@github.com:jordilin/mr.git".to_string())
400 .build()
401 .unwrap();
402 let runner = MockRunner::new(vec![response]);
403 let cmdinfo = remote_url(&runner).unwrap();
404 match cmdinfo {
405 CmdInfo::RemoteUrl(url) => {
406 assert_eq!("github.com", url.domain());
407 assert_eq!("jordilin/mr", url.path());
408 assert_eq!("jordilin_mr", url.config_encoded_project_path());
409 }
410 _ => panic!("Failed to parse remote url"),
411 }
412 }
413
414 #[test]
415 fn test_get_remote_https_url() {
416 let response = ShellResponse::builder()
417 .body("https://github.com/jordilin/gitar.git".to_string())
418 .build()
419 .unwrap();
420 let runner = MockRunner::new(vec![response]);
421 let cmdinfo = remote_url(&runner).unwrap();
422 match cmdinfo {
423 CmdInfo::RemoteUrl(url) => {
424 assert_eq!("github.com", url.domain());
425 assert_eq!("jordilin/gitar", url.path());
426 assert_eq!("jordilin_gitar", url.config_encoded_project_path());
427 }
428 _ => panic!("Failed to parse remote url"),
429 }
430 }
431
432 #[test]
433 fn test_get_remote_ssh_url() {
434 let response = ShellResponse::builder()
435 .body("ssh://git@gitlab-web:2222/testgroup/testsubproject.git".to_string())
436 .build()
437 .unwrap();
438 let runner = MockRunner::new(vec![response]);
439 let cmdinfo = remote_url(&runner).unwrap();
440 match cmdinfo {
441 CmdInfo::RemoteUrl(url) => {
442 assert_eq!("gitlab-web", url.domain());
443 assert_eq!("testgroup/testsubproject", url.path());
444 assert_eq!(
445 "testgroup_testsubproject",
446 url.config_encoded_project_path()
447 );
448 }
449 _ => panic!("Failed to parse remote url"),
450 }
451 }
452
453 #[test]
454 fn test_remote_url_no_remote() {
455 let response = ShellResponse::builder()
456 .status(1)
457 .body("error: No such remote 'origin'".to_string())
458 .build()
459 .unwrap();
460 let runner = MockRunner::new(vec![response]);
461 assert!(remote_url(&runner).is_err())
462 }
463
464 #[test]
465 fn test_empty_remote_url() {
466 let response = ShellResponse::builder().build().unwrap();
467 let runner = MockRunner::new(vec![response]);
468 assert!(remote_url(&runner).is_err())
469 }
470
471 #[test]
472 fn test_git_fetch_cmd_is_correct() {
473 let response = ShellResponse::builder().build().unwrap();
474 let runner = Arc::new(MockRunner::new(vec![response]));
475 fetch(runner.clone(), "origin".to_string()).unwrap();
476 assert_eq!("git fetch origin", *runner.cmd());
477 }
478
479 #[test]
480 fn test_gather_current_branch_cmd_is_correct() {
481 let response = ShellResponse::builder().build().unwrap();
482 let runner = Arc::new(MockRunner::new(vec![response]));
483 current_branch(runner.clone()).unwrap();
484 assert_eq!("git rev-parse --abbrev-ref HEAD", *runner.cmd());
485 }
486
487 #[test]
488 fn test_gather_current_branch_ok() {
489 let response = ShellResponse::builder()
490 .body(get_contract(ContractType::Git, "git_current_branch.txt"))
491 .build()
492 .unwrap();
493 let runner = Arc::new(MockRunner::new(vec![response]));
494 let cmdinfo = current_branch(runner).unwrap();
495 if let CmdInfo::Branch(branch) = cmdinfo {
496 assert_eq!("main", branch);
497 } else {
498 panic!("Expected CmdInfo::Branch");
499 }
500 }
501
502 #[test]
503 fn test_last_commit_summary_cmd_is_correct() {
504 let response = ShellResponse::builder()
505 .body("Add README".to_string())
506 .build()
507 .unwrap();
508 let runner = Arc::new(MockRunner::new(vec![response]));
509 commit_summary(runner.clone(), &None).unwrap();
510 assert_eq!("git log --format=%s -n1", *runner.cmd());
511 }
512
513 #[test]
514 fn test_last_commit_summary_get_last_commit() {
515 let response = ShellResponse::builder()
516 .body("Add README".to_string())
517 .build()
518 .unwrap();
519 let runner = MockRunner::new(vec![response]);
520 let title = commit_summary(Arc::new(runner), &None).unwrap();
521 if let CmdInfo::CommitSummary(title) = title {
522 assert_eq!("Add README", title);
523 } else {
524 panic!("Expected CmdInfo::LastCommitSummary");
525 }
526 }
527
528 #[test]
529 fn test_last_commit_summary_errors() {
530 let response = ShellResponse::builder()
531 .status(1)
532 .body("Could not retrieve last commit".to_string())
533 .build()
534 .unwrap();
535 let runner = Arc::new(MockRunner::new(vec![response]));
536 assert!(commit_summary(runner, &None).is_err());
537 }
538
539 #[test]
540 fn test_commit_summary_specific_sha_cmd_is_correct() {
541 let response = ShellResponse::builder()
542 .body("Add README".to_string())
543 .build()
544 .unwrap();
545 let runner = Arc::new(MockRunner::new(vec![response]));
546 commit_summary(runner.clone(), &Some("123456".to_string())).unwrap();
547 assert_eq!("git log --format=%s -n1 123456", *runner.cmd());
548 }
549
550 #[test]
551 fn test_git_push_cmd_is_correct() {
552 let response = ShellResponse::builder().build().unwrap();
553 let runner = MockRunner::new(vec![response]);
554 let mut repo = Repo::new();
555 repo.with_current_branch("new_feature");
556 push(&runner, "origin", &repo, false).unwrap();
557 assert_eq!("git push origin new_feature", *runner.cmd());
558 }
559
560 #[test]
561 fn test_git_push_cmd_fails() {
562 let response = ShellResponse::builder()
563 .status(1)
564 .body(get_contract(ContractType::Git, "git_push_failure.txt"))
565 .build()
566 .unwrap();
567 let runner = MockRunner::new(vec![response]);
568 let mut repo = Repo::new();
569 repo.with_current_branch("new_feature");
570 assert!(push(&runner, "origin", &repo, false).is_err());
571 }
572
573 #[test]
574 fn test_git_force_push_cmd_is_correct() {
575 let response = ShellResponse::builder().build().unwrap();
576 let runner = MockRunner::new(vec![response]);
577 let mut repo = Repo::new();
578 repo.with_current_branch("new_feature");
579 let force = true;
580 push(&runner, "origin", &repo, force).unwrap();
581 assert_eq!("git push origin +new_feature", *runner.cmd());
582 }
583
584 #[test]
585 fn test_repo_is_dirty_if_there_are_local_changes() {
586 let mut repo = Repo::new();
587 repo.with_status(true);
588 assert!(repo.dirty())
589 }
590
591 #[test]
592 fn test_repo_title_based_on_cmdinfo_lastcommit_summary() {
593 let mut repo = Repo::new();
594 repo.with_title("Add README");
595 assert_eq!(repo.title(), "Add README")
596 }
597
598 #[test]
599 fn test_repo_current_branch_based_on_cmdinfo_branch() {
600 let mut repo = Repo::new();
601 repo.with_current_branch("new_feature");
602 assert_eq!(repo.current_branch(), "new_feature")
603 }
604
605 #[test]
606 fn test_git_rebase_cmd_is_correct() {
607 let response = ShellResponse::builder().build().unwrap();
608 let runner = MockRunner::new(vec![response]);
609 rebase(&runner, "origin/main").unwrap();
610 assert_eq!("git rebase origin/main", *runner.cmd());
611 }
612
613 #[test]
614 fn test_git_rebase_fails_throws_error() {
615 let response = ShellResponse::builder()
616 .status(1)
617 .body(get_contract(
618 ContractType::Git,
619 "git_rebase_wrong_origin.txt",
620 ))
621 .build()
622 .unwrap();
623 let runner = MockRunner::new(vec![response]);
624 assert!(rebase(&runner, "origin/main").is_err())
625 }
626
627 #[test]
628 fn test_outgoing_commits_cmd_is_ok_short_summary() {
629 let response = ShellResponse::builder().build().unwrap();
630 let runner = MockRunner::new(vec![response]);
631 outgoing_commits(&runner, "origin", "main", &SummaryOptions::Short).unwrap();
632 let expected_cmd = "git log origin/main.. --reverse --pretty=format:%s - %h %d".to_string();
633 assert_eq!(expected_cmd, *runner.cmd());
634 }
635
636 #[test]
637 fn test_outgoing_commits_cmd_is_ok_long_summary() {
638 let response = ShellResponse::builder().build().unwrap();
639 let runner = MockRunner::new(vec![response]);
640 outgoing_commits(&runner, "origin", "main", &SummaryOptions::Long).unwrap();
641 let expected_cmd =
642 "git log origin/main.. --reverse --pretty=format:%s - %h %d%n%b".to_string();
643 assert_eq!(expected_cmd, *runner.cmd());
644 }
645
646 #[test]
647 fn test_outgoing_commits_cmd_error_no_summary_option() {
648 let response = ShellResponse::builder().build().unwrap();
649 let runner = MockRunner::new(vec![response]);
650 let result = outgoing_commits(&runner, "origin", "main", &SummaryOptions::None);
651 match result {
652 Err(err) => match err.downcast_ref::<GRError>() {
653 Some(GRError::ApplicationError(_)) => (),
654 _ => panic!("Expected ApplicationError"),
655 },
656 _ => panic!("Expected ApplicationError"),
657 }
658 }
659
660 #[test]
661 fn test_patch_cmd_is_ok() {
662 let response = ShellResponse::builder().build().unwrap();
663 let runner = MockRunner::new(vec![response]);
664 patch(&runner, "feature", "main").unwrap();
665 let expected_cmd = "git diff main feature".to_string();
666 assert_eq!(expected_cmd, *runner.cmd());
667 }
668
669 #[test]
670 fn test_last_commit_message_cmd_is_ok() {
671 let response = ShellResponse::builder().build().unwrap();
672 let runner = Arc::new(MockRunner::new(vec![response]));
673 commit_message(runner.clone(), &None).unwrap();
674 let expected_cmd = "git log --pretty=format:%b -n1".to_string();
675 assert_eq!(expected_cmd, *runner.cmd());
676 }
677
678 #[test]
679 fn test_commit_message_from_specific_commit_cmd_is_ok() {
680 let response = ShellResponse::builder().build().unwrap();
681 let runner = Arc::new(MockRunner::new(vec![response]));
682 commit_message(runner.clone(), &Some("123456".to_string())).unwrap();
683 let expected_cmd = "git log --pretty=format:%b -n1 123456".to_string();
684 assert_eq!(expected_cmd, *runner.cmd());
685 }
686
687 #[test]
688 fn test_git_add_changes_cmd_is_ok() {
689 let response = ShellResponse::builder().build().unwrap();
690 let runner = MockRunner::new(vec![response]);
691 add(&runner).unwrap();
692 let expected_cmd = "git add -u".to_string();
693 assert_eq!(expected_cmd, *runner.cmd());
694 }
695
696 #[test]
697 fn test_git_add_changes_cmd_is_err() {
698 let response = ShellResponse::builder()
699 .status(1)
700 .body("error: could not add changes".to_string())
701 .build()
702 .unwrap();
703 let runner = MockRunner::new(vec![response]);
704 assert!(add(&runner).is_err());
705 }
706
707 #[test]
708 fn test_git_commit_message_is_ok() {
709 let response = ShellResponse::builder()
710 .body("Add README".to_string())
711 .build()
712 .unwrap();
713 let runner = MockRunner::new(vec![response]);
714 commit(&runner, "Add README").unwrap();
715 let expected_cmd = "git commit -m Add README".to_string();
716 assert_eq!(expected_cmd, *runner.cmd());
717 }
718
719 #[test]
720 fn test_git_commit_message_is_err() {
721 let response = ShellResponse::builder()
722 .status(1)
723 .body("error: could not commit changes".to_string())
724 .build()
725 .unwrap();
726 let runner = MockRunner::new(vec![response]);
727 assert!(commit(&runner, "Add README").is_err());
728 }
729}