1use std::{
2 borrow::Cow,
3 process::{Command, Output},
4};
5
6use crate::error::AppError;
7#[cfg(test)]
8use mockall::automock;
9
10#[derive(Debug, PartialEq)]
11pub(crate) enum Local<'a> {
12 Branch(Cow<'a, str>),
13 NotBranch,
14}
15
16#[derive(Debug)]
17pub(crate) struct Url {
18 pub(crate) protocol: String,
19 pub(crate) domain: String,
20 pub(crate) path: String,
21}
22
23#[derive(Default)]
24pub struct Git;
25
26pub(crate) enum GitCommand<'a> {
27 IsValidRepository,
28 LocalBranch,
29 DefaultRemote,
30 TrackedRemote(&'a str),
31 UpstreamBranch(&'a str),
32 DefaultBranch(&'a str),
33 IsValidRemote(&'a str),
34 CurrentTag,
35 CurrentCommit,
36 CurrentWorkingDirectory,
37}
38
39pub enum GitOutput {
40 Ok(String),
41 Err(String),
42}
43
44#[cfg_attr(test, automock)]
45pub trait GitTrait {
46 fn is_valid_repository(&self) -> Result<GitOutput, AppError>;
47 fn get_local_branch(&self) -> Result<GitOutput, AppError>;
48 fn get_default_remote(&self) -> Result<GitOutput, AppError>;
49 fn get_tracked_remote(&self, tracked: &str) -> Result<GitOutput, AppError>;
50 fn get_upstream_branch(&self, branch: &str) -> Result<GitOutput, AppError>;
51 fn get_default_branch(&self, remote: &str) -> Result<GitOutput, AppError>;
52 fn is_valid_remote(&self, remote: &str) -> Result<GitOutput, AppError>;
53 fn get_current_tag(&self) -> Result<GitOutput, AppError>;
54 fn get_current_commit(&self) -> Result<GitOutput, AppError>;
55 fn get_current_working_directory(&self) -> Result<GitOutput, AppError>;
56}
57
58impl GitTrait for Git {
59 fn is_valid_repository(&self) -> Result<GitOutput, AppError> {
60 execute(command(GitCommand::IsValidRepository)?)
61 }
62
63 fn get_local_branch(&self) -> Result<GitOutput, AppError> {
64 execute(command(GitCommand::LocalBranch)?)
65 }
66
67 fn get_default_remote(&self) -> Result<GitOutput, AppError> {
68 execute(command(GitCommand::DefaultRemote)?)
69 }
70
71 fn get_tracked_remote(&self, tracked: &str) -> Result<GitOutput, AppError> {
72 execute(command(GitCommand::TrackedRemote(tracked))?)
73 }
74
75 fn get_upstream_branch(&self, branch: &str) -> Result<GitOutput, AppError> {
76 execute(command(GitCommand::UpstreamBranch(branch))?)
77 }
78
79 fn get_default_branch(&self, remote: &str) -> Result<GitOutput, AppError> {
80 execute(command(GitCommand::DefaultBranch(remote))?)
81 }
82
83 fn is_valid_remote(&self, remote: &str) -> Result<GitOutput, AppError> {
84 execute(command(GitCommand::IsValidRemote(remote))?)
85 }
86
87 fn get_current_tag(&self) -> Result<GitOutput, AppError> {
88 execute(command(GitCommand::CurrentTag)?)
89 }
90
91 fn get_current_commit(&self) -> Result<GitOutput, AppError> {
92 execute(command(GitCommand::CurrentCommit)?)
93 }
94
95 fn get_current_working_directory(&self) -> Result<GitOutput, AppError> {
96 execute(command(GitCommand::CurrentWorkingDirectory)?)
97 }
98}
99
100fn command(git_command: GitCommand) -> Result<Output, std::io::Error> {
101 match git_command {
102 GitCommand::IsValidRepository => Command::new("git")
103 .arg("rev-parse")
104 .arg("--is-inside-work-tree")
105 .output(),
106 GitCommand::LocalBranch => Command::new("git")
107 .arg("symbolic-ref")
108 .arg("-q")
109 .arg("--short")
110 .arg("HEAD")
111 .output(),
112 GitCommand::DefaultRemote => Command::new("git")
113 .arg("config")
114 .arg("open.default.remote")
115 .output(),
116 GitCommand::TrackedRemote(branch) => Command::new("git")
117 .arg("config")
118 .arg(format!("branch.{}.remote", branch))
119 .output(),
120 GitCommand::UpstreamBranch(branch) => Command::new("git")
121 .arg("config")
122 .arg(format!("branch.{}.merge", branch))
123 .output(),
124 GitCommand::DefaultBranch(remote) => Command::new("git")
125 .arg("rev-parse")
126 .arg("--abbrev-ref")
127 .arg(format!("{}/HEAD", remote))
128 .output(),
129 GitCommand::IsValidRemote(remote) => Command::new("git")
130 .arg("ls-remote")
131 .arg("--get-url")
132 .arg(remote)
133 .output(),
134 GitCommand::CurrentTag => Command::new("git")
135 .arg("describe")
136 .arg("--tags")
137 .arg("--exact-match")
138 .output(),
139 GitCommand::CurrentCommit => Command::new("git").arg("rev-parse").arg("HEAD").output(),
140 GitCommand::CurrentWorkingDirectory => Command::new("git")
141 .arg("rev-parse")
142 .arg("--show-prefix")
143 .output(),
144 }
145}
146
147fn execute(output: Output) -> Result<GitOutput, AppError> {
148 if output.status.success() {
149 Ok(GitOutput::Ok(trim(&output.stdout)?))
150 } else {
151 Ok(GitOutput::Err(trim(&output.stderr)?))
152 }
153}
154
155fn trim(bytes: &[u8]) -> Result<String, AppError> {
156 let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim());
157
158 if utf_8_string.ends_with('\n') {
159 utf_8_string.pop();
160 if utf_8_string.ends_with('\r') {
161 utf_8_string.pop();
162 }
163 }
164
165 Ok(utf_8_string)
166}
167
168impl Url {
169 pub(crate) fn new(protocol: &str, domain: &str, path: &str) -> Self {
170 Self {
171 protocol: protocol.into(),
172 domain: domain.into(),
173 path: path.into(),
174 }
175 }
176}