use std::{
fmt::Debug,
path::{Path, PathBuf},
process::{Child, Command, Output, Stdio},
str::FromStr,
};
use miette::{Context, IntoDiagnostic, bail};
use semver::Version;
use tracing::{debug, info, instrument, warn};
use crate::{Task, cli::Cli, current_span, git::git_file::GitFiles};
#[derive(Debug)]
pub struct NoRootDirSet;
#[derive(Debug, Default)]
pub struct GitBuilder<T: Debug> {
root_directory: T,
}
impl GitBuilder<NoRootDirSet> {
pub fn new() -> Self {
Self {
root_directory: NoRootDirSet,
}
}
}
impl std::fmt::Display for GitBuilder<NoRootDirSet> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "No root directory set for GitBuilder.")
}
}
impl std::error::Error for GitBuilder<NoRootDirSet> {}
impl<T: Debug> GitBuilder<T> {
pub fn root_directory(self, path: PathBuf) -> GitBuilder<PathBuf> {
GitBuilder {
root_directory: path,
}
}
#[instrument]
pub fn find_root_directory(self) -> Result<GitBuilder<PathBuf>, Self> {
let mut git = Git::<NoRootDirSet>::command(true);
git.arg("rev-parse").arg("--show-toplevel");
let out = match git.output() {
Ok(o) => o.stdout(),
Err(_) => {
tracing::error!("Could not find git root dir.");
return Err(self);
}
};
let path = PathBuf::from_str(&out).map_err(|_| self)?;
Ok(GitBuilder {
root_directory: path,
})
}
}
impl GitBuilder<PathBuf> {
pub fn build(self) -> Git<PathBuf> {
Git {
root_directory: self.root_directory,
}
}
}
#[derive(Debug)]
pub struct Git<T: Debug> {
root_directory: T,
}
impl Git<NoRootDirSet> {
#[instrument(name = "Git::command")]
fn command(quiet: bool) -> Command {
let mut cmd = Command::new("git");
if !quiet {
cmd.stdout(Stdio::inherit());
}
cmd
}
}
impl Git<PathBuf> {
#[instrument(name = "Git::command", skip_all)]
fn command(&self, quiet: bool) -> Command {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(self.root_directory.clone().into_os_string());
tracing::trace!("Command: {:#?}", &cmd);
if !quiet {
cmd.stdout(Stdio::inherit());
}
cmd
}
pub fn root_directory(&self) -> &Path {
&self.root_directory
}
#[instrument(skip_all)]
pub fn add_cargo_files(&self) -> miette::Result<()> {
let mut git = self.command(false);
let cargo_toml = "Cargo.toml";
let all_cargo_toml = "./**/Cargo.toml";
let cargo_lock = "Cargo.lock";
info!("Staging cargo files: {}, {}", cargo_toml, cargo_lock);
git.args(["add", "-v", cargo_toml, cargo_lock, all_cargo_toml]);
tracing::debug!("Running: {:?}", git);
git.output().map(|_| ()).into_diagnostic()
}
}
impl Git<PathBuf> {
#[instrument(skip_all)]
pub fn dirty_files(&self) -> miette::Result<GitFiles> {
let mut git_status = self.command(true);
git_status.args(["status", "--short"]);
tracing::debug!("Running: {:?}", git_status);
let stdout = git_status.output().into_diagnostic()?.stdout();
if stdout.lines().count() == 0 {
return Ok(GitFiles::new());
};
match GitFiles::parse(stdout) {
Some(files) => Ok(files),
None => Ok(GitFiles::new()),
}
}
#[instrument(skip_all)]
pub fn commit(&self, cli_args: &Cli, new_version: &Version) -> miette::Result<()> {
let mut git = self.command(cli_args.suppress.includes_git());
info!("Creating commit");
git.args(["commit"]);
if cli_args.dry_run() {
git.arg("--dry-run");
}
match cli_args.git_message() {
Some(msg) => {
git.args(["--message", &msg]);
}
None => {
git.args(["--message", &new_version.to_string()]);
}
}
tracing::debug!("Running: {:?}", git);
let _stdout = git.output().into_diagnostic()?;
self.dirty_files().context("After Commit")?;
Ok(())
}
#[instrument(skip_all)]
pub fn tag(
&self,
cli_args: &Cli,
version: &Version,
args: Option<Vec<&str>>,
) -> miette::Result<()> {
let mut git = self.command(cli_args.suppress.includes_git());
git.arg("tag");
if let Some(a) = args {
git.args(a);
}
git.args([&self.generate_tag(version)]);
tracing::debug!("Running: {:?}", git);
let output = git.output().into_diagnostic()?;
if !output.status.success() {
tracing::debug!("stderr: {}", output.stderr());
bail!("Failed to tag repository.")
}
Ok(())
}
#[instrument(skip_all)]
pub fn generate_tag(&self, version: &Version) -> String {
let tag = version.to_string();
debug! {"Tag: {tag}", };
tag
}
#[instrument(skip_all, fields(dry_run))]
pub fn push(&self, cli_args: &Cli, version: &Version) -> miette::Result<Vec<(Task, Child)>> {
current_span!().record("dry_run", cli_args.dry_run());
let tag_string = String::from("tags/") + &self.generate_tag(version);
let join = self
.remotes()?
.iter()
.map(|remote| {
let task = Task::Push(remote.clone());
info!("Pushing to remote: {remote}");
let mut git_push = self.command(cli_args.suppress.includes_git());
git_push.arg("push");
if cli_args.dry_run() {
git_push.arg("--dry-run");
}
git_push.args([remote.as_str(), &tag_string, "--porcelain"]);
tracing::debug!("Running: {:?}", git_push);
(task, git_push.spawn().into_diagnostic())
})
.collect::<Vec<_>>();
let mut ret = vec![];
for (t, c) in join {
ret.push((t, c?));
}
Ok(ret)
}
#[instrument(skip_all)]
pub fn remotes(&self) -> miette::Result<Vec<String>> {
let mut git = self.command(true);
git.args(["remote"]);
tracing::debug!("Running: {:?}", git);
let remotes: Vec<String> = git
.output()
.into_diagnostic()?
.stdout()
.lines()
.map(String::from)
.collect();
let mut branch_remotes = Vec::new();
for line in self.branch(vec!["--remotes"])?.lines() {
let valid_remote = match line.split_once('/') {
Some((remote, _branch)) => remote.trim().to_string(),
None => {
warn!("Ensure you only run command on a branch with a remote.");
bail!("Failed to find remote for current branch.")
}
};
assert!(remotes.contains(&valid_remote));
branch_remotes.push(valid_remote);
}
info!("Remotes: {:?}", branch_remotes);
assert!(!branch_remotes.is_empty());
assert!(remotes.len() >= branch_remotes.len());
if branch_remotes.is_empty() {
warn!("Ensure you only run command on a branch with a remote.");
bail!("Failed to find remote for current branch.")
}
Ok(branch_remotes)
}
#[instrument(skip_all)]
pub fn branch(&self, args: Vec<&str>) -> miette::Result<String> {
let mut git = self.command(true);
git.arg("branch");
args.iter().for_each(|&arg| {
git.arg(arg);
});
tracing::debug!("Running: {:?}", git);
git.output().map(|output| output.stdout()).into_diagnostic()
}
}
#[allow(dead_code)]
pub trait OutputExt {
fn stderr(&self) -> String;
fn stdout(&self) -> String;
}
impl OutputExt for Output {
fn stderr(&self) -> String {
String::from_iter(self.stderr.iter().map(|&c| char::from(c)))
}
fn stdout(&self) -> String {
String::from_iter(self.stdout.iter().map(|&c| char::from(c)))
}
}