1use crate::cargo::run_cargo;
2use crate::command::git::{GitRepo, GitWorkTree};
3use crate::registry_packages::{PackagesCollection, RegistryPackage};
4use crate::release_regex;
5use crate::tera::default_tag_name_template;
6use crate::tmp_repo::TempRepo;
7use crate::update_request::UpdateRequest;
8use crate::updater::Updater;
9use crate::{
10 PackagesUpdate, Project,
11 changelog_parser::{self, ChangelogRelease},
12 copy_dir::copy_dir,
13 fs_utils::{Utf8TempDir, strip_prefix, to_utf8_path},
14 package_path::manifest_dir,
15 registry_packages::{self},
16 semver_check::SemverCheck,
17};
18use anyhow::Context;
19use cargo_metadata::TargetKind;
20use cargo_metadata::{
21 Metadata, MetadataCommand, Package,
22 camino::{Utf8Path, Utf8PathBuf},
23 semver::Version,
24};
25use cargo_utils::get_manifest_metadata;
26use chrono::NaiveDate;
27use std::collections::BTreeMap;
28use std::path::PathBuf;
29use toml_edit::TableLike;
30use tracing::{debug, info, instrument, trace};
31
32pub(crate) const NO_COMMIT_ID: &str = "0000000";
36
37#[derive(Debug, Clone)]
38pub struct ReleaseMetadata {
39 pub tag_name_template: Option<String>,
41 pub release_name_template: Option<String>,
43}
44
45pub trait ReleaseMetadataBuilder {
46 fn get_release_metadata(&self, package_name: &str) -> Option<ReleaseMetadata>;
47}
48
49#[derive(Debug, Clone, Default)]
50pub struct ChangelogRequest {
51 pub release_date: Option<NaiveDate>,
53 pub changelog_config: Option<git_cliff_core::config::Config>,
54}
55
56impl ReleaseMetadataBuilder for UpdateRequest {
57 fn get_release_metadata(&self, package_name: &str) -> Option<ReleaseMetadata> {
58 let config = self.get_package_config(package_name);
59 config.generic.release.then(|| ReleaseMetadata {
60 tag_name_template: config.generic.tag_name_template.clone(),
61 release_name_template: None,
62 })
63 }
64}
65
66fn get_temp_worktree_and_repo(
74 original_repo: &mut GitRepo,
75 package_name: &str,
76) -> anyhow::Result<(GitRepo, GitWorkTree)> {
77 original_repo
79 .cleanup_worktree_if_exists(package_name)
80 .context("cleanup existing worktree")?;
81
82 let worktree = original_repo
84 .temp_worktree(Some(package_name), package_name)
85 .context("build worktree for package")?;
86
87 let repo = GitRepo::open(worktree.path()).context("open repo for package")?;
91
92 Ok((repo, worktree))
93}
94
95#[instrument(skip_all, fields(package_name = %package.name))]
100fn process_git_only_package(
101 package: &Package,
102 unreleased_project_repo: &mut GitRepo,
103 input: &UpdateRequest,
104 is_multi_package: bool,
105) -> anyhow::Result<Option<(RegistryPackage, GitWorkTree)>> {
106 let template = input
108 .get_package_tag_name(&package.name)
109 .unwrap_or_else(|| default_tag_name_template(is_multi_package));
110
111 let release_regex =
112 release_regex::get_release_regex(&template, &package.name).context("get release regex")?;
113 debug!(
114 "looking for tags matching pattern: {}",
115 release_regex.to_string()
116 );
117
118 let (mut repo, worktree) = get_temp_worktree_and_repo(unreleased_project_repo, &package.name)
120 .context("get worktree and repo for package")?;
121
122 let Some((release_tag, version)) = repo
123 .get_release_tag(&release_regex, &package.name)
124 .context("get release tag")?
125 else {
126 info!(
127 "No release tag found matching pattern `{release_regex}`. \
128 Package {} will be treated as initial release.",
129 package.name
130 );
131 return Ok(None);
132 };
133
134 info!(
135 "Latest release of package {}: tag `{release_tag}` (version {version})",
136 package.name
137 );
138
139 let release_commit = repo
141 .get_tag_commit(&release_tag)
142 .context("get release tag commit")?;
143
144 repo.checkout_commit(&release_commit)
146 .context("checkout release commit for package")?;
147
148 run_cargo_package(&worktree).context("run cargo package")?;
152
153 let single_package = get_cargo_package(&worktree, &package.name).with_context(|| {
155 format!(
156 "get cargo package {} from worktree at {:?}",
157 package.name,
158 worktree.path()
159 )
160 })?;
161
162 let registry_package = RegistryPackage::new(single_package, Some(release_commit));
163 Ok(Some((registry_package, worktree)))
164}
165
166fn run_cargo_package(worktree: &GitWorkTree) -> anyhow::Result<()> {
168 let worktree_path = to_utf8_path(worktree.path())?;
169 let output = run_cargo(worktree_path, &["package", "--allow-dirty", "--workspace"])
170 .context("run cargo package in worktree")?;
171
172 if !output.status.success() {
173 anyhow::bail!("cargo package failed: {:?}", output.stderr);
174 }
175
176 Ok(())
177}
178
179fn get_cargo_package(worktree: &GitWorkTree, package_name: &str) -> anyhow::Result<Package> {
180 let worktree_path = to_utf8_path(worktree.path())?;
181 let manifest_path = worktree_path.join("Cargo.toml");
182
183 let rust_package = MetadataCommand::new()
185 .current_dir(worktree_path.as_std_path())
186 .no_deps()
187 .manifest_path(&manifest_path)
188 .exec()
189 .context("get cargo metadata for worktree")?;
190
191 let package_details = rust_package
192 .packages
193 .iter()
194 .find(|x| x.name == package_name)
195 .with_context(|| format!("Failed to find package {package_name:?}"))?;
196
197 let package_path = rust_package.target_directory.join(format!(
198 "package/{}-{}",
199 package_details.name, package_details.version
200 ));
201 debug!("package for {package_name} is at {package_path}");
202
203 let single_package_manifest = package_path.join("Cargo.toml");
204 let single_package_meta = get_manifest_metadata(&single_package_manifest)
205 .context("get cargo metadata for package")?;
206
207 let single_package = single_package_meta
208 .workspace_packages()
209 .into_iter()
210 .find(|p| p.name == package_name)
211 .context("Couldn't find the package")?
212 .clone();
213
214 Ok(single_package)
215}
216
217#[instrument(skip_all)]
223pub async fn next_versions(input: &UpdateRequest) -> anyhow::Result<(PackagesUpdate, TempRepo)> {
224 let overrides = input.packages_config().overridden_packages();
225 let local_project = Project::new(
226 input.local_manifest(),
227 input.single_package(),
228 &overrides,
229 input.cargo_metadata(),
230 input,
231 )?;
232 let updater = Updater {
233 project: &local_project,
234 req: input,
235 };
236
237 let workspace_packages = input.cargo_metadata().workspace_packages();
239 let (git_only_packages, registry_packages_list): (Vec<_>, Vec<_>) = workspace_packages
240 .iter()
241 .partition(|p| input.should_use_git_only(&p.name));
242
243 let is_multi_package = local_project.publishable_packages().len() > 1;
244
245 let (mut all_packages, _worktrees) =
248 collect_git_only_packages(git_only_packages, input, is_multi_package)?;
249
250 let (registry_pkgs, registry_collection) = collect_registry_packages(
252 registry_packages_list,
253 &local_project.publishable_packages(),
254 input,
255 )?;
256 all_packages.extend(registry_pkgs);
257
258 let release_packages = registry_collection.with_packages(all_packages);
261
262 let repository = local_project
265 .get_repo()
266 .context("failed to determine local project repository")?;
267
268 let repo_is_clean_result = repository.repo.is_clean();
269 if !input.allow_dirty() {
270 repo_is_clean_result?;
271 } else if repo_is_clean_result.is_err() {
272 repository.repo.git(&[
276 "stash",
277 "push",
278 "--include-untracked",
279 "-m",
280 "uncommitted changes stashed by release-plz",
281 ])?;
282 }
283
284 let packages_to_update = updater
285 .packages_to_update(&release_packages, &repository.repo, input.local_manifest())
286 .await?;
287 Ok((packages_to_update, repository))
288}
289
290fn collect_git_only_packages(
296 git_only_packages: Vec<&Package>,
297 input: &UpdateRequest,
298 is_multi_package: bool,
299) -> anyhow::Result<(BTreeMap<String, RegistryPackage>, Vec<GitWorkTree>)> {
300 if git_only_packages.is_empty() {
301 return Ok((BTreeMap::new(), Vec::new()));
302 }
303
304 debug!(
305 "Processing {} packages in git_only mode",
306 git_only_packages.len()
307 );
308
309 let mut all_packages = BTreeMap::new();
310 let mut worktrees = Vec::new();
314
315 let mut unreleased_project_repo = GitRepo::open(
316 input
317 .local_manifest_dir()
318 .context("get local manifest dir")?,
319 )
320 .context("create unreleased repo for spinning worktrees")?;
321
322 for package in git_only_packages {
323 if let Some((registry_package, worktree)) = process_git_only_package(
324 package,
325 &mut unreleased_project_repo,
326 input,
327 is_multi_package,
328 )? {
329 all_packages.insert(registry_package.package.name.to_string(), registry_package);
330 worktrees.push(worktree);
331 }
332 }
333
334 Ok((all_packages, worktrees))
335}
336
337fn collect_registry_packages(
343 registry_packages_list: Vec<&Package>,
344 publishable_packages: &[&Package],
345 input: &UpdateRequest,
346) -> anyhow::Result<(BTreeMap<String, RegistryPackage>, PackagesCollection)> {
347 if registry_packages_list.is_empty() {
348 return Ok((BTreeMap::new(), PackagesCollection::default()));
349 }
350
351 debug!(
352 "Processing {} packages from registry",
353 registry_packages_list.len()
354 );
355
356 let publishable_registry_packages: Vec<&Package> = registry_packages_list
358 .into_iter()
359 .filter(|p| {
360 publishable_packages
361 .iter()
362 .any(|pub_pkg| pub_pkg.name == p.name)
363 })
364 .collect();
365
366 if publishable_registry_packages.is_empty() {
367 return Ok((BTreeMap::new(), PackagesCollection::default()));
368 }
369
370 let registry_packages = registry_packages::get_registry_packages(
374 input.registry_manifest(),
375 &publishable_registry_packages,
376 input.registry(),
377 )?;
378
379 let mut all_packages = BTreeMap::new();
380 for package_name in publishable_registry_packages.iter().map(|p| &p.name) {
381 if let Some(reg_pkg) = registry_packages.get_registry_package(package_name) {
382 all_packages.insert(
383 package_name.to_string(),
384 RegistryPackage::new(
385 reg_pkg.package.clone(),
386 reg_pkg.published_at_sha1().map(|s| s.to_string()),
387 ),
388 );
389 }
390 }
391
392 Ok((all_packages, registry_packages))
393}
394
395pub fn root_repo_path(local_manifest: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
396 let manifest_dir = manifest_dir(local_manifest)?;
397 root_repo_path_from_manifest_dir(manifest_dir)
398}
399
400pub fn root_repo_path_from_manifest_dir(manifest_dir: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
401 let root = git_cmd::git_in_dir(manifest_dir, &["rev-parse", "--show-toplevel"])?;
402 Ok(Utf8PathBuf::from(root))
403}
404
405pub fn new_manifest_dir_path(
406 old_project_root: &Utf8Path,
407 old_manifest_dir: &Utf8Path,
408 new_project_root: &Utf8Path,
409) -> anyhow::Result<Utf8PathBuf> {
410 let parent_root = old_project_root.parent().unwrap_or(old_project_root);
411 let relative_manifest_dir = strip_prefix(old_manifest_dir, parent_root)
412 .context("cannot strip prefix for manifest dir")?;
413 Ok(new_project_root.join(relative_manifest_dir))
414}
415
416#[derive(Debug, Clone)]
417pub struct UpdateResult {
418 pub version: Version,
420 pub changelog: Option<String>,
422 pub semver_check: SemverCheck,
423 pub new_changelog_entry: Option<String>,
424 pub registry_version: Option<Version>,
429}
430
431impl UpdateResult {
432 pub fn last_changes(&self) -> anyhow::Result<Option<ChangelogRelease>> {
433 match &self.changelog {
434 Some(c) => changelog_parser::last_release_from_str(c),
435 None => Ok(None),
436 }
437 }
438}
439
440pub fn workspace_packages(metadata: &Metadata) -> anyhow::Result<Vec<Package>> {
441 cargo_utils::workspace_members(metadata).map(|members| members.collect())
442}
443
444pub fn publishable_packages_from_manifest(
445 manifest: impl AsRef<Utf8Path>,
446) -> anyhow::Result<Vec<Package>> {
447 let metadata = cargo_utils::get_manifest_metadata(manifest.as_ref())?;
448 cargo_utils::workspace_members(&metadata)
449 .map(|members| members.filter(|p| p.is_publishable()).collect())
450}
451
452pub trait Publishable {
453 fn is_publishable(&self) -> bool;
454}
455
456impl Publishable for Package {
457 fn is_publishable(&self) -> bool {
459 let res = if let Some(publish) = &self.publish {
460 !publish.is_empty()
465 } else {
466 !is_example_package(self)
468 };
469 trace!("package {} is publishable: {res}", self.name);
470 res
471 }
472}
473
474fn is_example_package(package: &Package) -> bool {
475 package
476 .targets
477 .iter()
478 .all(|t| t.kind == [TargetKind::Example])
479}
480
481pub fn copy_to_temp_dir(target: &Utf8Path) -> anyhow::Result<Utf8TempDir> {
482 let tmp_dir = Utf8TempDir::new().context("cannot create temporary directory")?;
483 copy_dir(target, tmp_dir.path())
484 .with_context(|| format!("cannot copy directory {target:?} to {tmp_dir:?}"))?;
485 Ok(tmp_dir)
486}
487
488pub(crate) fn is_dependency_referred_to_package(
492 dependency: &dyn TableLike,
493 package_dir: &Utf8Path,
494 dependency_package_dir: &Utf8Path,
495) -> bool {
496 canonicalized_path(dependency, package_dir)
497 .is_some_and(|dep_path| dep_path == dependency_package_dir)
498}
499
500fn canonicalized_path(dependency: &dyn TableLike, package_dir: &Utf8Path) -> Option<PathBuf> {
508 dependency
509 .get("path")
510 .and_then(|i| i.as_str())
511 .and_then(|relpath| dunce::canonicalize(package_dir.join(relpath)).ok())
512}