git_view/
git.rs

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}