mfgames-conventional-commit-rs/src/commands/version.rs

209 lines
6.9 KiB
Rust

use crate::config::Config;
use crate::trees::get_tree_skip;
use crate::versions::VersionBump;
use anyhow::Result;
use clap::Parser;
use conventional_commit::ConventionalCommit;
use git2::Repository;
use semver::Version;
use semver_bump_trait::SemverBump;
use slog::{debug, info};
use std::cmp::Ordering;
use std::str::FromStr;
use crate::tags::get_tag_map;
/// Gets the current version based on commits.
#[derive(Debug, Parser)]
pub struct VersionCommand {
/// The directory to perform the search.
#[clap(short, long)]
directory: Option<String>,
/// The name of the package to use.
#[clap(short, long)]
package: Option<String>,
}
impl VersionCommand {
pub async fn run(&self, log: slog::Logger) -> Result<()> {
// Figure out the path we're searching and which one we found.
let current_dir = &self.directory.clone();
let current_dir = current_dir.as_ref().unwrap();
info!(log, "searching from {:?}", current_dir);
// Load the repository so we can walk through it.
let repo = Repository::discover(current_dir)?;
let git_dir = &repo.workdir();
let git_dir = git_dir.unwrap();
info!(log, "git root at {:?}", git_dir);
// Load the configuration file.
let config_file = Config::get_git_config_file(git_dir);
info!(log, "config at {:?}", config_file);
let config = Config::load(&config_file)?;
debug!(log, "config {:?}", &config);
// Get the settings.
let package_name = &self.package.clone();
let package_name = package_name.as_ref().unwrap();
let package = config.get_package(package_name)?;
debug!(log, "package {:?}", &package);
// Load a map of all commits that are pointed to by a tag.
let tag_prefix = &package.tag_prefix.clone();
let tag_map = get_tag_map(&log, &repo, &tag_prefix)?;
// Figure out the head.
let head = repo.head()?;
let head = head.resolve()?;
let commit = head.peel_to_commit()?;
// We have to walk back through the revisions until we find a proper
// commit. If we encounter a merge commit, then we split our search to
// look down both parents until we find the commit or we reach the end.
//
// While we're collecting it, we figure out what is the proper operation
// to perform once we do find it.
//
// Failing everything, we use our fallback.
let mut version = Version::parse("0.0.1").unwrap();
let mut check_list = vec![(commit.id(), VersionBump::None, 0usize)];
let mut count = 0;
while !&check_list.is_empty() {
count += 1;
info!(
log,
"checking {:?} entries, round {}",
&check_list.len(),
count
);
let old_list = check_list.clone();
check_list.clear();
for (oid, bump, depth) in old_list {
// First check to see if we have a version for this commit.
info!(
log,
"checking oid {:?}, bump {:?}, depth {}", oid, bump, depth
);
if let Some(tag_version) = &tag_map.get(&oid) {
// We have a tag, so use our gathered operation to figure
// out the new version since we don't have to continue this.
let mut bump_version: Version =
tag_version.to_owned().clone();
match &bump {
VersionBump::Major => {
bump_version.mut_increment_major()
}
VersionBump::Minor => {
bump_version.mut_increment_minor()
}
VersionBump::Patch => {
bump_version.mut_increment_patch()
}
_ => {}
}
info!(
log,
"found tag {} + {:?} -> {}",
&tag_version,
&bump,
&bump_version
);
if &bump_version.cmp(&version) == &Ordering::Greater {
version = bump_version;
}
// We can skip this one.
continue;
}
// Parse the message to see if we need to modify the bump.
let message = commit.message().unwrap_or("");
let conv = ConventionalCommit::from_str(message)?;
let commit_bump = self.get_version_bump(&conv);
let commit = &repo.find_commit(oid)?;
let parent_count = commit.parent_count();
debug!(log, "commit {:?}", commit.id());
debug!(log, "message {:?}", message);
debug!(log, "type {:?}", conv.type_());
// See what files were changed by this commit. If there is no
// bump, then we don't have to do this step at all.
let tree_skip = get_tree_skip(
log.clone(),
&repo,
&commit,
&commit_bump,
&package,
)?;
let new_bump = match tree_skip {
true => {
debug!(log, "no bump");
bump
}
false => {
let tree_bump = match &bump > &commit_bump {
true => bump.clone(),
false => commit_bump.clone(),
};
debug!(
log,
"bump {:?} + {:?} -> {:?}",
&bump,
&commit_bump,
tree_bump
);
tree_bump
}
};
// Since we haven't found a tag, insert the entry into the new
// list.
debug!(log, "parent_count {:?}", parent_count);
for parent in commit.parent_ids() {
debug!(log, "parent {:?}", parent);
check_list.push((parent, new_bump.clone(), depth + 1));
}
}
}
// Report the final version.
info!(log, "final version {}", version);
println!("{}", version);
Ok(())
}
fn get_version_bump(&self, commit: &ConventionalCommit) -> VersionBump {
if commit.breaking_change().is_some() {
return VersionBump::Major;
}
return match commit.type_() {
"feat" => VersionBump::Minor,
"fix" => VersionBump::Patch,
_ => VersionBump::None,
};
}
}