use core::{
str::FromStr,
};
use std::{
collections::HashSet,
env,
ffi::OsStr,
io::{Error, ErrorKind},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use {
crate::Result,
dia_semver::Semver,
};
mod remote;
pub use self::remote::*;
const APP: &str = "git";
const CMD_BRANCH: &str = "branch";
const CMD_COMMIT: &str = "commit";
const CMD_PUSH: &str = "push";
const CMD_REMOTE: &str = "remote";
const CMD_TAG: &str = "tag";
const OPTION_ALL: &str = "--all";
const OPTION_ANNOTATE: &str = "--annotate";
const OPTION_MESSAGE: &str = "--message";
const OPTION_SHOW_CURRENT: &str = "--show-current";
const OPTION_TAGS: &str = "--tags";
const OPTION_VERBOSE: &str = "--verbose";
#[derive(Debug)]
pub struct Git {
path: PathBuf,
}
impl Git {
pub fn make<P>(path: Option<P>) -> Result<Self> where P: AsRef<Path> {
Ok(Self {
path: match path {
Some(path) => path.as_ref().to_path_buf(),
None => env::current_dir()?,
},
})
}
fn new_cmd<S, S2>(&self, cmd: S, args: Option<&[S2]>) -> Command where S: AsRef<OsStr>, S2: AsRef<OsStr> {
let mut result = Command::new(APP);
result.current_dir(&self.path);
result.arg(cmd);
if let Some(args) = args {
for a in args {
result.arg(a);
}
}
result
}
pub fn new_cmd_for_committing_all_files_with_a_message<S>(&self, msg: S) -> Command where S: AsRef<str> {
self.new_cmd(CMD_COMMIT, Some(&[OPTION_ALL, OPTION_MESSAGE, msg.as_ref()]))
}
pub fn new_cmd_for_adding_an_annotated_tag_with_a_message<S>(&self, tag: &Semver, msg: S) -> Command where S: AsRef<str> {
let tag = tag.to_string();
self.new_cmd(CMD_TAG, Some(&[tag.as_str(), OPTION_ANNOTATE, OPTION_MESSAGE, msg.as_ref()]))
}
pub fn new_cmd_for_pushing_to_a_remote(&self, remote: &Remote) -> Command {
self.new_cmd(CMD_PUSH, Some(&[remote.name()]))
}
pub fn new_cmd_for_pushing_tags_to_a_remote(&self, remote: &Remote) -> Command {
self.new_cmd(CMD_PUSH, Some(&[remote.name(), OPTION_TAGS]))
}
fn run_cmd<S, S2>(&self, cmd: S, args: Option<&[S2]>) -> Result<String> where S: AsRef<OsStr>, S2: AsRef<OsStr> {
let mut cmd = self.new_cmd(cmd, args);
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null());
let output = cmd.output()?;
if output.status.success() {
Ok(String::from_utf8(output.stdout).map_err(|e| Error::new(ErrorKind::Other, e))?)
} else {
Err(Error::new(ErrorKind::Other, format!("{app} returned: {status}", app=APP, status=output.status)))
}
}
pub fn current_branch(&self) -> Result<String> {
let output = self.run_cmd(CMD_BRANCH, Some(&[OPTION_SHOW_CURRENT]))?;
let output = output.trim();
if output.lines().count() == 1 {
Ok(output.to_string())
} else {
Err(Error::new(ErrorKind::Other, format!("{app} returned: {output}", app=APP, output=output)))
}
}
pub fn find_last_version_with_build_metadata(&self, build_metadata: Option<&str>) -> Result<Option<Semver>> {
let output = self.run_cmd::<_, &str>(CMD_TAG, None)?;
let mut result = None;
for line in output.lines() {
if let Ok(version) = Semver::from_str(line.trim()) {
if version.build_metadata() == build_metadata {
result = match result {
None => Some(version),
Some(other) => Some(version.max(other)),
};
}
}
}
Ok(result)
}
pub fn remotes(&self) -> Result<Vec<Remote>> {
let output = self.run_cmd(CMD_REMOTE, Some(&[OPTION_VERBOSE]))?;
let mut set = HashSet::with_capacity(9);
for line in output.lines() {
let mut parts = line.split_whitespace();
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(name), Some(url), Some("(fetch)" | "(push)"), None) => set.insert(Remote::new(name.to_string(), url.to_string())),
_ => continue,
};
}
let mut result: Vec<_> = set.into_iter().collect();
result.sort();
Ok(result)
}
}