use crate::{
changelog_parser::{self, ChangelogRelease},
copy_dir::copy_dir,
diff::Diff,
lock_compare,
package_compare::are_packages_equal,
package_path::{manifest_dir, PackagePath},
registry_packages::{self, PackagesCollection},
repo_url::RepoUrl,
semver_check::{self, SemverCheck},
strip_prefix::strip_prefix,
tmp_repo::TempRepo,
toml_compare::are_toml_dependencies_updated,
version::NextVersionFromDiff,
ChangelogBuilder, PackagesToUpdate, PackagesUpdate, CARGO_TOML, CHANGELOG_FILENAME,
};
use anyhow::Context;
use cargo_metadata::{semver::Version, Metadata, Package};
use cargo_utils::{upgrade_requirement, LocalManifest};
use chrono::NaiveDate;
use git_cliff_core::{commit::Commit, config::Config as GitCliffConfig};
use git_cmd::{self, Repo};
use next_version::NextVersion;
use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator};
use regex::Regex;
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs, io,
path::{Path, PathBuf},
};
use tempfile::{tempdir, TempDir};
use tracing::{debug, info, instrument, warn};
pub(crate) const NO_COMMIT_ID: &str = "N/A";
pub trait RequestReleaseValidator {
fn is_release_enabled(&self, package_name: &str) -> bool;
}
#[derive(Debug, Clone)]
pub struct UpdateRequest {
local_manifest: PathBuf,
metadata: Metadata,
registry_manifest: Option<PathBuf>,
single_package: Option<String>,
changelog_req: ChangelogRequest,
registry: Option<String>,
dependencies_update: bool,
allow_dirty: bool,
repo_url: Option<RepoUrl>,
packages_config: PackagesConfig,
}
#[derive(Debug, Clone, Default)]
struct PackagesConfig {
default: UpdateConfig,
overrides: BTreeMap<String, PackageUpdateConfig>,
}
impl From<UpdateConfig> for PackageUpdateConfig {
fn from(config: UpdateConfig) -> Self {
Self {
generic: config,
changelog_path: None,
changelog_include: vec![],
}
}
}
impl PackagesConfig {
fn get(&self, package_name: &str) -> PackageUpdateConfig {
self.overrides
.get(package_name)
.cloned()
.unwrap_or(self.default.clone().into())
}
fn set_default(&mut self, config: UpdateConfig) {
self.default = config;
}
fn set(&mut self, package_name: String, config: PackageUpdateConfig) {
self.overrides.insert(package_name, config);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateConfig {
pub semver_check: bool,
pub changelog_update: bool,
pub release: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PackageUpdateConfig {
pub generic: UpdateConfig,
pub changelog_path: Option<PathBuf>,
pub changelog_include: Vec<String>,
}
impl PackageUpdateConfig {
pub fn semver_check(&self) -> bool {
self.generic.semver_check
}
pub fn should_update_changelog(&self) -> bool {
self.generic.changelog_update
}
}
impl Default for UpdateConfig {
fn default() -> Self {
Self {
semver_check: true,
changelog_update: true,
release: true,
}
}
}
impl UpdateConfig {
pub fn with_semver_check(self, semver_check: bool) -> Self {
Self {
semver_check,
..self
}
}
pub fn with_changelog_update(self, changelog_update: bool) -> Self {
Self {
changelog_update,
..self
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ChangelogRequest {
pub release_date: Option<NaiveDate>,
pub changelog_config: Option<GitCliffConfig>,
}
fn canonical_local_manifest(local_manifest: &Path) -> io::Result<PathBuf> {
let mut local_manifest = dunce::canonicalize(local_manifest)?;
if !local_manifest.ends_with(CARGO_TOML) {
local_manifest.push(CARGO_TOML)
}
Ok(local_manifest)
}
impl UpdateRequest {
pub fn new(metadata: Metadata) -> anyhow::Result<Self> {
let local_manifest = cargo_utils::workspace_manifest(&metadata);
let local_manifest = canonical_local_manifest(local_manifest.as_ref())?;
Ok(Self {
local_manifest,
metadata,
registry_manifest: None,
single_package: None,
changelog_req: ChangelogRequest::default(),
registry: None,
dependencies_update: false,
allow_dirty: false,
repo_url: None,
packages_config: PackagesConfig::default(),
})
}
pub fn changelog_path(&self, package: &Package) -> PathBuf {
let config = self.get_package_config(&package.name);
config
.changelog_path
.map(|p| self.local_manifest.parent().unwrap().join(p))
.unwrap_or_else(|| {
package
.package_path()
.expect("can't determine package path")
.join(CHANGELOG_FILENAME)
})
}
pub fn cargo_metadata(&self) -> &Metadata {
&self.metadata
}
pub fn set_local_manifest(self, local_manifest: impl AsRef<Path>) -> io::Result<Self> {
Ok(Self {
local_manifest: canonical_local_manifest(local_manifest.as_ref())?,
..self
})
}
pub fn with_registry_project_manifest(self, registry_manifest: PathBuf) -> io::Result<Self> {
let registry_manifest = fs::canonicalize(registry_manifest)?;
Ok(Self {
registry_manifest: Some(registry_manifest),
..self
})
}
pub fn with_changelog_req(self, changelog_req: ChangelogRequest) -> Self {
Self {
changelog_req,
..self
}
}
pub fn with_default_package_config(mut self, config: UpdateConfig) -> Self {
self.packages_config.set_default(config);
self
}
pub fn with_package_config(
mut self,
package: impl Into<String>,
config: PackageUpdateConfig,
) -> Self {
self.packages_config.set(package.into(), config);
self
}
pub fn get_package_config(&self, package: &str) -> PackageUpdateConfig {
self.packages_config.get(package)
}
pub fn with_registry(self, registry: String) -> Self {
Self {
registry: Some(registry),
..self
}
}
pub fn with_single_package(self, package: String) -> Self {
Self {
single_package: Some(package),
..self
}
}
pub fn with_repo_url(self, repo_url: RepoUrl) -> Self {
Self {
repo_url: Some(repo_url),
..self
}
}
pub fn local_manifest_dir(&self) -> anyhow::Result<&Path> {
self.local_manifest
.parent()
.context("wrong local manifest path")
}
pub fn local_manifest(&self) -> &Path {
&self.local_manifest
}
pub fn registry_manifest(&self) -> Option<&Path> {
self.registry_manifest.as_deref()
}
pub fn with_dependencies_update(self, dependencies_update: bool) -> Self {
Self {
dependencies_update,
..self
}
}
pub fn should_update_dependencies(&self) -> bool {
self.dependencies_update
}
pub fn with_allow_dirty(self, allow_dirty: bool) -> Self {
Self {
allow_dirty,
..self
}
}
pub fn repo_url(&self) -> Option<&RepoUrl> {
self.repo_url.as_ref()
}
}
impl RequestReleaseValidator for UpdateRequest {
fn is_release_enabled(&self, package_name: &str) -> bool {
let config = self.get_package_config(package_name);
config.generic.release
}
}
#[instrument(skip_all)]
pub fn next_versions(input: &UpdateRequest) -> anyhow::Result<(PackagesUpdate, TempRepo)> {
let overrides = input.packages_config.overrides.keys().cloned().collect();
let local_project = Project::new(
&input.local_manifest,
input.single_package.as_deref(),
overrides,
&input.metadata,
input,
)?;
let updater = Updater {
project: &local_project,
req: input,
};
let registry_packages = registry_packages::get_registry_packages(
input.registry_manifest.as_ref(),
&local_project.publishable_packages(),
input.registry.as_deref(),
)?;
let repository = local_project
.get_repo()
.context("failed to determine local project repository")?;
if !input.allow_dirty {
repository.repo.is_clean()?;
}
let packages_to_update =
updater.packages_to_update(®istry_packages, &repository.repo, input.local_manifest())?;
Ok((packages_to_update, repository))
}
fn check_for_typos(packages: &HashSet<String>, overrides: &HashSet<String>) -> anyhow::Result<()> {
let diff: Vec<_> = overrides.difference(packages).collect();
if diff.is_empty() {
Ok(())
} else {
let mut missing: Vec<_> = diff.into_iter().collect();
missing.sort();
let missing = missing
.iter()
.map(|s| format!("`{}`", s))
.collect::<Vec<_>>()
.join(", ");
Err(anyhow::anyhow!(
"The following overrides are not present in the workspace: {missing}. Check for typos"
))
}
}
#[derive(Debug)]
pub struct Project {
packages: Vec<Package>,
root: PathBuf,
manifest_dir: PathBuf,
contains_multiple_pub_packages: bool,
}
impl Project {
pub fn new(
local_manifest: &Path,
single_package: Option<&str>,
overrides: HashSet<String>,
metadata: &Metadata,
request_release_validator: &dyn RequestReleaseValidator,
) -> anyhow::Result<Self> {
let manifest = local_manifest;
let manifest_dir = manifest_dir(manifest)?.to_path_buf();
debug!("manifest_dir: {manifest_dir:?}");
let root = {
let project_root =
git_cmd::git_in_dir(&manifest_dir, &["rev-parse", "--show-toplevel"])?;
PathBuf::from(project_root)
};
debug!("project_root: {root:?}");
let mut packages = workspace_packages(metadata)?;
override_packages_path(&mut packages, metadata, &manifest_dir)
.context("failed to override packages path")?;
let packages_names: Vec<String> = packages.iter().map(|p| p.name.clone()).collect();
packages.retain(|p| request_release_validator.is_release_enabled(&p.name));
anyhow::ensure!(!packages.is_empty(), "no public packages found. Are there any public packages in your project? Analyzed packages: {packages_names:?}");
check_overrides_typos(&packages, &overrides)?;
let contains_multiple_pub_packages = packages.len() > 1;
if let Some(pac) = single_package {
packages.retain(|p| p.name == pac);
anyhow::ensure!(
!packages.is_empty(),
"package `{}` not found. If it exists, is it public?",
pac
);
}
Ok(Self {
packages,
root,
manifest_dir,
contains_multiple_pub_packages,
})
}
pub fn publishable_packages(&self) -> Vec<&Package> {
self.packages
.iter()
.filter(|p| p.is_publishable())
.collect()
}
pub fn workspace_packages(&self) -> Vec<&Package> {
self.packages.iter().collect()
}
fn get_repo(&self) -> anyhow::Result<TempRepo> {
let tmp_project_root = copy_to_temp_dir(&self.root)?;
let tmp_manifest_dir = {
let parent_root = self.root.parent().context("cannot determine parent root")?;
let relative_manifest_dir = strip_prefix(&self.manifest_dir, parent_root)
.context("cannot strip prefix for manifest dir")?;
debug!("relative_manifest_dir: {relative_manifest_dir:?}");
tmp_project_root.as_ref().join(relative_manifest_dir)
};
debug!("tmp_manifest_dir: {tmp_manifest_dir:?}");
let repository = TempRepo::new(tmp_project_root, &tmp_manifest_dir)?;
Ok(repository)
}
pub fn git_tag(&self, package_name: &str, version: &str) -> String {
if self.contains_multiple_pub_packages {
format!("{package_name}-v{version}")
} else {
format!("v{version}")
}
}
pub fn cargo_lock_path(&self) -> PathBuf {
self.root.join("Cargo.lock")
}
}
fn override_packages_path(
packages: &mut Vec<Package>,
metadata: &Metadata,
manifest_dir: &Path,
) -> Result<(), anyhow::Error> {
for p in packages {
let old_path = p.package_path()?;
let relative_package_path = strip_prefix(old_path, &metadata.workspace_root)?.to_path_buf();
p.manifest_path = cargo_metadata::camino::Utf8PathBuf::from_path_buf(
manifest_dir.join(relative_package_path).join(CARGO_TOML),
)
.expect("can't create relative path");
}
Ok(())
}
fn check_overrides_typos(
packages: &[Package],
overrides: &HashSet<String>,
) -> Result<(), anyhow::Error> {
let package_names: HashSet<_> = packages.iter().map(|p| p.name.clone()).collect();
check_for_typos(&package_names, overrides)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct UpdateResult {
pub version: Version,
pub changelog: Option<String>,
pub semver_check: SemverCheck,
}
impl UpdateResult {
pub fn last_changes(&self) -> anyhow::Result<Option<ChangelogRelease>> {
match &self.changelog {
Some(c) => changelog_parser::last_release_from_str(c),
None => Ok(None),
}
}
}
pub struct Updater<'a> {
pub project: &'a Project,
pub req: &'a UpdateRequest,
}
impl Updater<'_> {
#[instrument(skip_all)]
fn packages_to_update(
&self,
registry_packages: &PackagesCollection,
repository: &Repo,
local_manifest_path: &Path,
) -> anyhow::Result<PackagesUpdate> {
debug!("calculating local packages");
let packages_diffs = self.get_packages_diffs(registry_packages, repository)?;
let mut packages_to_check_for_deps: Vec<&Package> = vec![];
let mut packages_to_update = PackagesUpdate::default();
let workspace_version_pkgs: HashSet<String> = packages_diffs
.iter()
.filter(|(p, _)| {
let local_manifest_path = p.package_path().unwrap().join(CARGO_TOML);
let local_manifest = LocalManifest::try_new(&local_manifest_path).unwrap();
local_manifest.version_is_inherited()
})
.map(|(p, _)| p.name.clone())
.collect();
let new_workspace_version = new_workspace_version(
local_manifest_path,
&packages_diffs,
&workspace_version_pkgs,
)?;
if let Some(new_workspace_version) = &new_workspace_version {
packages_to_update.with_workspace_version(new_workspace_version.clone());
}
for (p, diff) in packages_diffs {
let next_version = if let Some(max_workspace_version) = &new_workspace_version {
if workspace_version_pkgs.contains(p.name.as_str()) {
max_workspace_version.clone()
} else {
p.version.next_from_diff(&diff)
}
} else {
p.version.next_from_diff(&diff)
};
debug!("diff: {:?}, next_version: {}", &diff, next_version);
let current_version = p.version.clone();
if next_version != current_version || !diff.registry_package_exists {
info!(
"{}: next version is {next_version}{}",
p.name,
diff.semver_check.outcome_str()
);
let update_result =
self.update_result(diff.commits, next_version, p, diff.semver_check)?;
packages_to_update
.updates_mut()
.push((p.clone(), update_result));
} else if diff.is_version_published {
packages_to_check_for_deps.push(p);
}
}
let changed_packages: Vec<(&Package, &Version)> = packages_to_update
.updates()
.iter()
.map(|(p, u)| (p, &u.version))
.collect();
let dependent_packages =
self.dependent_packages(&packages_to_check_for_deps, &changed_packages)?;
packages_to_update.updates_mut().extend(dependent_packages);
Ok(packages_to_update)
}
fn get_packages_diffs(
&self,
registry_packages: &PackagesCollection,
repository: &Repo,
) -> anyhow::Result<Vec<(&Package, Diff)>> {
let packages_diffs_res: anyhow::Result<Vec<(&Package, Diff)>> = self
.project
.publishable_packages()
.iter()
.map(|&p| {
let diff = self
.get_diff(p, registry_packages, repository)
.context("failed to retrieve difference between packages")?;
Ok((p, diff))
})
.collect();
let mut packages_diffs = packages_diffs_res?;
let packages_commits: HashMap<String, Vec<Commit>> = packages_diffs
.iter()
.map(|(p, d)| (p.name.clone(), d.commits.clone()))
.collect();
let semver_check_result: anyhow::Result<()> =
packages_diffs.par_iter_mut().try_for_each(|(p, diff)| {
let registry_package = registry_packages.get_package(&p.name);
if let Some(registry_package) = registry_package {
let package_path = get_package_path(p, repository, &self.project.root)
.context("can't retrieve package path")?;
let package_config = self.req.get_package_config(&p.name);
for pkg_to_include in &package_config.changelog_include {
if let Some(commits) = packages_commits.get(pkg_to_include) {
diff.add_commits(commits);
}
}
if should_check_semver(p, package_config.semver_check())
&& diff.should_update_version()
{
let registry_package_path = registry_package
.package_path()
.context("can't retrieve registry package path")?;
let semver_check =
semver_check::run_semver_check(&package_path, registry_package_path)
.context("error while running cargo-semver-checks")?;
diff.set_semver_check(semver_check);
}
}
Ok(())
});
semver_check_result?;
Ok(packages_diffs)
}
fn dependent_packages(
&self,
packages_to_check_for_deps: &[&Package],
changed_packages: &[(&Package, &Version)],
) -> anyhow::Result<PackagesToUpdate> {
let packages_to_update = packages_to_check_for_deps
.iter()
.filter_map(|p| match p.dependencies_to_update(changed_packages) {
Ok(deps) => {
if deps.is_empty() {
None
} else {
Some((p, deps))
}
}
Err(_e) => None,
})
.map(|(&p, deps)| {
let deps: Vec<&str> = deps.iter().map(|d| d.name.as_str()).collect();
let change = format!(
"chore: updated the following local packages: {}",
deps.join(", ")
);
let next_version = { p.version.increment_patch() };
info!(
"{}: dependencies changed. Next version is {next_version}",
p.name
);
Ok((
p.clone(),
self.update_result(
vec![Commit::new(NO_COMMIT_ID.to_string(), change)],
next_version,
p,
SemverCheck::Skipped,
)?,
))
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(packages_to_update)
}
fn update_result(
&self,
commits: Vec<Commit>,
version: Version,
package: &Package,
semver_check: SemverCheck,
) -> anyhow::Result<UpdateResult> {
let release_link = {
let prev_tag = self
.project
.git_tag(&package.name, &package.version.to_string());
let next_tag = self.project.git_tag(&package.name, &version.to_string());
self.req
.repo_url
.as_ref()
.map(|r| r.git_release_link(&prev_tag, &next_tag))
};
let pr_link = self.req.repo_url.as_ref().map(|r| r.git_pr_link());
lazy_static::lazy_static! {
static ref PR_RE: Regex = Regex::new("#(\\d+)").unwrap();
}
let changelog = {
let cfg = self.req.get_package_config(package.name.as_str());
let changelog_req = cfg
.should_update_changelog()
.then_some(self.req.changelog_req.clone());
let old_changelog = fs::read_to_string(self.req.changelog_path(package)).ok();
let commits: Vec<Commit> = commits
.into_iter()
.filter_map(|c| {
if c.clone().into_conventional().is_ok() {
Some(c)
} else {
c.message
.lines()
.next()
.map(|line| Commit::new(c.id.clone(), line.to_string()))
}
})
.map(|c| {
if let Some(pr_link) = &pr_link {
let result = PR_RE.replace_all(&c.message, format!("[#$1]({pr_link}/$1)"));
Commit::new(c.id, result.to_string())
} else {
c
}
})
.collect();
changelog_req
.map(|r| get_changelog(commits, &version, Some(r), old_changelog, release_link))
.transpose()
}?;
Ok(UpdateResult {
version,
changelog,
semver_check,
})
}
#[instrument(
skip_all,
fields(package = %package.name)
)]
fn get_diff(
&self,
package: &Package,
registry_packages: &PackagesCollection,
repository: &Repo,
) -> anyhow::Result<Diff> {
let package_path = get_package_path(package, repository, &self.project.root)
.context("failed to determine package path")?;
repository
.checkout_head()
.context("can't checkout head to calculate diff")?;
let registry_package = registry_packages.get_package(&package.name);
let mut diff = Diff::new(registry_package.is_some());
if let Err(err) = repository.checkout_last_commit_at_path(&package_path) {
if err
.to_string()
.contains("Your local changes to the following files would be overwritten")
{
return Err(err.context("The allow-dirty option can't be used in this case"));
} else {
info!("{}: there are no commits", package.name);
return Ok(diff);
}
}
let git_tag = self
.project
.git_tag(&package.name, &package.version.to_string());
let tag_commit = repository.get_tag_commit(&git_tag);
if tag_commit.is_some() {
let registry_package = registry_package.with_context(|| format!("package `{}` not found in the registry, but the git tag {git_tag} exists. Consider running `cargo publish` manually to publish this package.", package.name))?;
anyhow::ensure!(
registry_package.version == package.version,
"package `{}` has a different version ({}) with respect to the registry package ({}), but the git tag {git_tag} exists. Consider running `cargo publish` manually to publish the new version of this package.",
package.name, package.version, registry_package.version
)
}
loop {
let current_commit_message = repository.current_commit_message()?;
let current_commit_hash = repository.current_commit_hash()?;
if let Some(registry_package) = registry_package {
debug!("package {} found in cargo registry", registry_package.name);
let registry_package_path = registry_package.package_path()?;
let cargo_lock_path = self
.get_cargo_lock_path(repository)
.context("failed to determine Cargo.lock path")?;
let are_packages_equal = are_packages_equal(&package_path, registry_package_path)
.context("cannot compare packages")?;
if let Some(cargo_lock_path) = cargo_lock_path.as_deref() {
repository
.checkout(cargo_lock_path)
.context("cannot revert changes introduced when comparing packages")?;
}
if are_packages_equal
|| is_commit_too_old(repository, tag_commit.as_deref(), ¤t_commit_hash)
{
debug!(
"next version calculated starting from commits after `{current_commit_hash}`"
);
if diff.commits.is_empty() {
let are_toml_dependencies_updated = || {
are_toml_dependencies_updated(
®istry_package.dependencies,
&package.dependencies,
)
};
let are_lock_dependencies_updated = || {
lock_compare::are_lock_dependencies_updated(
&self.project.cargo_lock_path(),
registry_package_path,
)
.context("Can't check if Cargo.lock dependencies are up to date")
};
if are_toml_dependencies_updated() {
diff.commits.push(Commit::new(
NO_COMMIT_ID.to_string(),
"chore: update Cargo.toml dependencies".to_string(),
));
} else if are_lock_dependencies_updated()? {
diff.commits.push(Commit::new(
NO_COMMIT_ID.to_string(),
"chore: update Cargo.lock dependencies".to_string(),
));
} else {
info!("{}: already up to date", package.name);
}
}
break;
} else if registry_package.version != package.version {
info!("{}: the local package has already a different version with respect to the registry package, so release-plz will not update it", package.name);
diff.set_version_unpublished();
break;
} else {
debug!("packages are different");
diff.commits.push(Commit::new(
current_commit_hash,
current_commit_message.clone(),
));
}
} else {
diff.commits.push(Commit::new(
current_commit_hash,
current_commit_message.clone(),
));
}
if let Err(_err) = repository.checkout_previous_commit_at_path(&package_path) {
debug!("there are no other commits");
break;
}
}
repository
.checkout_head()
.context("can't checkout to head after calculating diff")?;
Ok(diff)
}
fn get_cargo_lock_path(&self, repository: &Repo) -> anyhow::Result<Option<String>> {
let project_cargo_lock = self.project.cargo_lock_path();
let relative_lock_path = strip_prefix(&project_cargo_lock, &self.project.root)?;
let repository_cargo_lock = repository.directory().join(relative_lock_path);
if repository_cargo_lock.exists() {
let cargo_lock_path = repository_cargo_lock
.to_str()
.context("can't convert Cargo.lock path to string")?;
Ok(Some(cargo_lock_path.to_string()))
} else {
Ok(None)
}
}
}
fn new_workspace_version(
local_manifest_path: &Path,
packages_diffs: &[(&Package, Diff)],
workspace_version_pkgs: &HashSet<String>,
) -> anyhow::Result<Option<Version>> {
let workspace_version = {
let local_manifest = LocalManifest::try_new(local_manifest_path)?;
local_manifest.get_workspace_version()
};
let new_workspace_version = workspace_version_pkgs
.iter()
.filter_map(|workspace_package| {
for (p, diff) in packages_diffs {
if workspace_package == &p.name {
let next = p.version.next_from_diff(diff);
if let Some(workspace_version) = &workspace_version {
if &next >= workspace_version {
return Some(next);
}
}
}
}
None
})
.max();
Ok(new_workspace_version)
}
fn get_changelog(
commits: Vec<Commit>,
next_version: &Version,
changelog_req: Option<ChangelogRequest>,
old_changelog: Option<String>,
release_link: Option<String>,
) -> anyhow::Result<String> {
let mut changelog_builder = ChangelogBuilder::new(commits, next_version.to_string());
if let Some(changelog_req) = changelog_req {
if let Some(release_date) = changelog_req.release_date {
changelog_builder = changelog_builder.with_release_date(release_date)
}
if let Some(config) = changelog_req.changelog_config {
changelog_builder = changelog_builder.with_config(config)
}
if let Some(link) = release_link {
changelog_builder = changelog_builder.with_release_link(link)
}
if let Some(old_changelog) = &old_changelog {
if let Ok(Some(last_version)) = changelog_parser::last_version_from_str(old_changelog) {
changelog_builder = changelog_builder.with_previous_version(last_version)
}
}
}
let new_changelog = changelog_builder.build();
let changelog = match &old_changelog {
Some(old_changelog) => new_changelog.prepend(old_changelog)?,
None => new_changelog.generate(), };
Ok(changelog)
}
fn get_package_path(
package: &Package,
repository: &Repo,
project_root: &Path,
) -> anyhow::Result<PathBuf> {
let package_path = package.package_path()?;
get_repo_path(package_path, repository, project_root)
}
fn get_repo_path(
old_path: &Path,
repository: &Repo,
project_root: &Path,
) -> anyhow::Result<PathBuf> {
let relative_path =
strip_prefix(old_path, project_root).context("error while retrieving package_path")?;
let result_path = repository.directory().join(relative_path);
Ok(result_path)
}
fn is_commit_too_old(
repository: &Repo,
tag_commit: Option<&str>,
current_commit_hash: &str,
) -> bool {
if let Some(tag_commit) = tag_commit.as_ref() {
if repository.is_ancestor(current_commit_hash, tag_commit) {
debug!("stopping looking at git history because the current commit ({}) is an ancestor of the commit ({}) tagged with the previous version.", current_commit_hash, tag_commit);
return true;
}
}
false
}
fn should_check_semver(package: &Package, run_semver_check: bool) -> bool {
let is_cargo_semver_checks_installed = semver_check::is_cargo_semver_checks_installed;
run_semver_check && is_library(package) && is_cargo_semver_checks_installed()
}
pub fn workspace_packages(metadata: &Metadata) -> anyhow::Result<Vec<Package>> {
cargo_utils::workspace_members(metadata).map(|members| members.collect())
}
pub fn publishable_packages_from_manifest(
manifest: impl AsRef<Path>,
) -> anyhow::Result<Vec<Package>> {
let metadata = cargo_utils::get_manifest_metadata(manifest.as_ref())?;
cargo_utils::workspace_members(&metadata)
.map(|members| members.filter(|p| p.is_publishable()).collect())
}
pub trait Publishable {
fn is_publishable(&self) -> bool;
}
impl Publishable for Package {
fn is_publishable(&self) -> bool {
if let Some(publish) = &self.publish {
!publish.is_empty()
} else {
!is_example_package(self)
}
}
}
fn is_example_package(package: &Package) -> bool {
package.targets.iter().all(|t| t.kind == ["example"])
}
fn is_library(package: &Package) -> bool {
package
.targets
.iter()
.any(|t| t.kind.contains(&"lib".to_string()))
}
pub fn copy_to_temp_dir(target: &Path) -> anyhow::Result<TempDir> {
let tmp_dir = tempdir().context("cannot create temporary directory")?;
copy_dir(target, tmp_dir.as_ref())
.with_context(|| format!("cannot copy directory {target:?} to {tmp_dir:?}"))?;
Ok(tmp_dir)
}
trait PackageDependencies {
fn dependencies_to_update<'a>(
&self,
updated_packages: &'a [(&Package, &Version)],
) -> anyhow::Result<Vec<&'a Package>>;
}
impl PackageDependencies for Package {
fn dependencies_to_update<'a>(
&self,
updated_packages: &'a [(&Package, &Version)],
) -> anyhow::Result<Vec<&'a Package>> {
let mut package_manifest = LocalManifest::try_new(self.manifest_path.as_std_path())?;
let package_dir = manifest_dir(&package_manifest.path)?.to_owned();
let mut deps_to_update: Vec<&Package> = vec![];
for (p, next_ver) in updated_packages {
let canonical_path = p.canonical_path()?;
let matching_deps = package_manifest
.get_dependency_tables_mut()
.flat_map(|t| t.iter_mut().filter_map(|(_, d)| d.as_table_like_mut()))
.filter(|d| d.contains_key("version"))
.filter(|d| {
let dependency_path = d
.get("path")
.and_then(|i| i.as_str())
.and_then(|relpath| fs::canonicalize(package_dir.join(relpath)).ok());
match dependency_path {
Some(dep_path) => dep_path == canonical_path,
None => false,
}
});
for dep in matching_deps {
let old_req = dep
.get("version")
.expect("filter ensures this")
.as_str()
.unwrap_or("*");
if upgrade_requirement(old_req, next_ver)?.is_some() {
deps_to_update.push(p);
}
}
}
Ok(deps_to_update)
}
}
#[cfg(test)]
mod tests {
use cargo_utils::get_manifest_metadata;
use super::*;
use super::{check_for_typos, Project};
use crate::RequestReleaseValidator;
use std::{collections::HashSet, path::Path};
fn get_project(
local_manifest: &Path,
single_package: Option<&str>,
overrides: HashSet<String>,
is_release_enabled: bool,
) -> anyhow::Result<Project> {
let metadata = get_manifest_metadata(local_manifest).unwrap();
let request_release_validator = RequestReleaseValidatorStub::new(is_release_enabled);
Project::new(
local_manifest,
single_package,
overrides,
&metadata,
&request_release_validator,
)
}
struct RequestReleaseValidatorStub {
release: bool,
}
impl RequestReleaseValidatorStub {
pub fn new(release: bool) -> Self {
Self { release }
}
}
impl RequestReleaseValidator for RequestReleaseValidatorStub {
fn is_release_enabled(&self, _: &str) -> bool {
self.release
}
}
#[test]
fn test_for_typos() {
let packages: HashSet<String> = vec!["foo".to_string()].into_iter().collect();
let overrides: HashSet<String> = vec!["bar".to_string()].into_iter().collect();
let result = check_for_typos(&packages, &overrides);
assert_eq!(
result.unwrap_err().to_string(),
"The following overrides are not present in the workspace: `bar`. Check for typos"
);
}
#[test]
fn test_empty_override() {
let local_manifest = Path::new("../../fixtures/typo-in-overrides/Cargo.toml");
let result = get_project(local_manifest, None, HashSet::default(), true);
assert!(result.is_ok());
}
#[test]
fn test_successful_override() {
let local_manifest = Path::new("../../fixtures/typo-in-overrides/Cargo.toml");
let overrides = (["typo_test".to_string()]).into();
let result = get_project(local_manifest, None, overrides, true);
assert!(result.is_ok());
}
#[test]
fn test_typo_in_crate_names() {
let local_manifest = Path::new("../../fixtures/typo-in-overrides/Cargo.toml");
let single_package = None;
let overrides = vec!["typo_tesst".to_string()].into_iter().collect();
let result = get_project(local_manifest, single_package, overrides, true);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"The following overrides are not present in the workspace: `typo_tesst`. Check for typos"
);
}
#[test]
fn same_version_is_not_added_to_changelog() {
let commits = vec![
Commit::new(crate::NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
Commit::new(crate::NO_COMMIT_ID.to_string(), "simple update".to_string()),
];
let next_version = Version::new(1, 1, 0);
let changelog_req = ChangelogRequest::default();
let old = r#"## [1.1.0] - 1970-01-01
### fix bugs
- my awesomefix
### other
- complex update
"#;
let new = get_changelog(
commits,
&next_version,
Some(changelog_req),
Some(old.to_string()),
None,
)
.unwrap();
assert_eq!(old, new)
}
#[test]
fn project_new_no_release_will_error() {
let local_manifest = Path::new("../fake_package/Cargo.toml");
let result = get_project(local_manifest, None, HashSet::default(), false);
assert!(result.is_err());
expect_test::expect![[r#"no public packages found. Are there any public packages in your project? Analyzed packages: ["cargo_utils", "fake_package", "git_cmd", "test_logs", "next_version", "release-plz", "release_plz_core"]"#]]
.assert_eq(&result.unwrap_err().to_string());
}
}