mod add;
mod additional_packages;
mod init;
mod manage;
mod release;
mod status;
mod verify;
use std::path::Path;
use clap::{Args, Subcommand, ValueEnum};
use changeset_core::{BumpType, ChangeCategory};
use changeset_manifest::{
ChangelogLocation, ComparisonLinks, NoneBumpBehavior, TagFormat, ZeroVersionBehavior,
};
use changeset_operations::OperationError;
use crate::error::Result;
use crate::output::StdoutCliWriter;
#[derive(Args)]
pub(crate) struct AddArgs {
#[arg(long = "package", short = 'p', value_name = "NAME")]
pub packages: Vec<String>,
#[arg(long, short = 'b', value_enum)]
pub bump: Option<BumpType>,
#[arg(long = "package-bump", value_name = "NAME:TYPE")]
pub package_bumps: Vec<String>,
#[arg(long, short = 'c', value_enum, default_value = "changed")]
pub category: ChangeCategory,
#[arg(long, short = 'm')]
pub message: Option<String>,
#[arg(long)]
pub editor: bool,
#[arg(long)]
pub exclude_dependents: bool,
}
#[derive(Args)]
pub(crate) struct VerifyArgs {
#[arg(long)]
pub base: Option<String>,
#[arg(long)]
pub head: Option<String>,
#[arg(long, short = 'q')]
pub quiet: bool,
#[arg(long, short = 'd')]
pub allow_deleted_changesets: bool,
#[arg(long)]
pub exclude_dependents: bool,
#[arg(long)]
pub ignore_dirty: bool,
}
#[derive(Args)]
pub(crate) struct ReleaseArgs {
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub convert: bool,
#[arg(long)]
pub no_commit: bool,
#[arg(long)]
pub no_tags: bool,
#[arg(long)]
pub keep_changesets: bool,
#[arg(long, value_name = "CRATE:TAG", num_args = 0..=1, default_missing_value = "")]
pub prerelease: Vec<String>,
#[arg(long, short = 'f')]
pub force: bool,
#[arg(long, value_name = "CRATE", num_args = 0..=1, default_missing_value = "")]
pub graduate: Vec<String>,
}
#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum TagFormatArg {
VersionOnly,
CratePrefixed,
}
impl From<TagFormatArg> for TagFormat {
fn from(arg: TagFormatArg) -> Self {
match arg {
TagFormatArg::VersionOnly => Self::VersionOnly,
TagFormatArg::CratePrefixed => Self::CratePrefixed,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum ChangelogLocationArg {
Root,
PerPackage,
}
impl From<ChangelogLocationArg> for ChangelogLocation {
fn from(arg: ChangelogLocationArg) -> Self {
match arg {
ChangelogLocationArg::Root => Self::Root,
ChangelogLocationArg::PerPackage => Self::PerPackage,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum ComparisonLinksArg {
Auto,
Enabled,
Disabled,
}
impl From<ComparisonLinksArg> for ComparisonLinks {
fn from(arg: ComparisonLinksArg) -> Self {
match arg {
ComparisonLinksArg::Auto => Self::Auto,
ComparisonLinksArg::Enabled => Self::Enabled,
ComparisonLinksArg::Disabled => Self::Disabled,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum ZeroVersionBehaviorArg {
EffectiveMinor,
AutoPromoteOnMajor,
}
impl From<ZeroVersionBehaviorArg> for ZeroVersionBehavior {
fn from(arg: ZeroVersionBehaviorArg) -> Self {
match arg {
ZeroVersionBehaviorArg::EffectiveMinor => Self::EffectiveMinor,
ZeroVersionBehaviorArg::AutoPromoteOnMajor => Self::AutoPromoteOnMajor,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
pub(crate) enum NoneBumpBehaviorArg {
PromoteToPatch,
Allow,
Disallow,
}
impl From<NoneBumpBehaviorArg> for NoneBumpBehavior {
fn from(value: NoneBumpBehaviorArg) -> Self {
match value {
NoneBumpBehaviorArg::PromoteToPatch => Self::PromoteToPatch,
NoneBumpBehaviorArg::Allow => Self::Allow,
NoneBumpBehaviorArg::Disallow => Self::Disallow,
}
}
}
#[derive(Args)]
pub(crate) struct InitArgs {
#[arg(long)]
pub defaults: bool,
#[arg(long)]
pub no_interactive: bool,
#[arg(long)]
pub commit: Option<bool>,
#[arg(long)]
pub tags: Option<bool>,
#[arg(long)]
pub keep_changesets: Option<bool>,
#[arg(long, value_name = "FORMAT")]
pub tag_format: Option<TagFormatArg>,
#[arg(long, value_name = "LOCATION")]
pub changelog: Option<ChangelogLocationArg>,
#[arg(long, value_name = "MODE")]
pub comparison_links: Option<ComparisonLinksArg>,
#[arg(long, value_name = "BEHAVIOR")]
pub zero_version_behavior: Option<ZeroVersionBehaviorArg>,
#[arg(long, value_name = "BRANCH")]
pub base_branch: Option<String>,
#[arg(long, value_name = "BEHAVIOR")]
pub none_bump_behavior: Option<NoneBumpBehaviorArg>,
#[arg(long, value_name = "MESSAGE")]
pub none_bump_promote_message_template: Option<String>,
#[arg(long, value_name = "TEMPLATE")]
pub commit_title_template: Option<String>,
#[arg(long)]
pub changes_in_body: Option<bool>,
#[arg(long, value_name = "TEMPLATE")]
pub comparison_links_template: Option<String>,
#[arg(long, value_name = "TEMPLATE")]
pub dependency_bump_changelog_template: Option<String>,
#[arg(long = "ignored-file", value_name = "PATTERN")]
pub ignored_files: Vec<String>,
}
#[derive(Args)]
pub(crate) struct ManagePrereleaseArgs {
#[arg(long, value_name = "CRATE:TAG")]
pub add: Vec<String>,
#[arg(long, value_name = "CRATE")]
pub remove: Vec<String>,
#[arg(long, value_name = "CRATE")]
pub graduate: Vec<String>,
#[arg(long, short)]
pub list: bool,
}
#[derive(Args)]
pub(crate) struct ManageGraduationArgs {
#[arg(long, value_name = "CRATE")]
pub add: Vec<String>,
#[arg(long, value_name = "CRATE")]
pub remove: Vec<String>,
#[arg(long, short)]
pub list: bool,
}
#[derive(Subcommand)]
pub(crate) enum ManageCommand {
#[command(name = "pre-release")]
Prerelease(ManagePrereleaseArgs),
Graduation(ManageGraduationArgs),
}
#[derive(Args)]
pub(crate) struct ManageArgs {
#[command(subcommand)]
pub command: ManageCommand,
}
pub(crate) struct ExecuteResult {
pub(crate) quiet: bool,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
Add(AddArgs),
Verify(VerifyArgs),
Status,
#[command(
verbatim_doc_comment,
after_long_help = "\
Pre-release workflow:
1. cargo changeset release --prerelease alpha → All packages get alpha tag
2. cargo changeset release --prerelease foo:alpha → Only foo gets alpha tag
3. cargo changeset release → Graduates prereleases to stable
Graduation (0.x to 1.0.0):
- cargo changeset release --graduate foo --graduate bar
- Or configure in .changeset/graduation.toml
Per-package configuration can also be set via:
- .changeset/pre-release.toml
- .changeset/graduation.toml
Use 'cargo changeset manage' to configure these files."
)]
Release(ReleaseArgs),
Init(InitArgs),
Manage(ManageArgs),
#[command(name = "additional-packages")]
AdditionalPackages(additional_packages::AdditionalPackagesArgs),
}
impl Commands {
pub(crate) fn execute(self, start_path: &Path) -> (Result<()>, ExecuteResult) {
let writer = StdoutCliWriter;
match self {
Self::Add(args) => (
add::run(args, start_path, &writer),
ExecuteResult { quiet: false },
),
Self::Verify(args) => {
let quiet = args.quiet;
(
verify::run(args, start_path, &writer),
ExecuteResult { quiet },
)
}
Self::Status => (
status::run(start_path, &writer),
ExecuteResult { quiet: false },
),
Self::Release(args) => (
release::run(args, start_path, &writer),
ExecuteResult { quiet: false },
),
Self::Init(args) => (
init::run(args, start_path, &writer),
ExecuteResult { quiet: false },
),
Self::Manage(args) => (
manage::run(args, start_path, &writer),
ExecuteResult { quiet: false },
),
Self::AdditionalPackages(args) => (
additional_packages::run(args, start_path, &writer),
ExecuteResult { quiet: false },
),
}
}
}
pub(super) fn dialoguer_to_operation_error(e: dialoguer::Error) -> OperationError {
match e {
dialoguer::Error::IO(io_err) => OperationError::Io(io_err),
}
}
pub(super) fn cli_error_to_operation_error(e: crate::error::CliError) -> OperationError {
use crate::error::CliError;
match e {
CliError::Io(io) => OperationError::Io(io),
CliError::NotATty => OperationError::InteractionRequired,
CliError::EditorFailed { source } => OperationError::Io(source),
CliError::Core(e) => OperationError::Core(e),
CliError::Git(e) => OperationError::Git(e),
CliError::Project(e) => OperationError::Project(e),
CliError::Operation(e) => e,
CliError::CurrentDir(io) => OperationError::Io(io),
CliError::ManifestFormatRequired | CliError::IncompleteArgs => {
OperationError::InteractionRequired
}
CliError::InvalidPackageBumpFormat { .. }
| CliError::InvalidBumpType { .. }
| CliError::VerificationFailed { .. }
| CliError::ChangesetDeleted { .. } => OperationError::Cancelled,
}
}
#[cfg(test)]
mod dialoguer_conversion_tests {
use std::io;
use changeset_operations::OperationError;
use super::dialoguer_to_operation_error;
#[test]
fn converts_io_error_to_operation_io_variant() {
let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed");
let dialoguer_err = dialoguer::Error::IO(io_err);
let result = dialoguer_to_operation_error(dialoguer_err);
assert!(matches!(result, OperationError::Io(_)));
}
#[test]
fn preserves_io_error_kind() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
let dialoguer_err = dialoguer::Error::IO(io_err);
let result = dialoguer_to_operation_error(dialoguer_err);
match result {
OperationError::Io(inner) => {
assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
}
other => panic!("expected OperationError::Io, got {other:?}"),
}
}
#[test]
fn preserves_error_chain() {
let io_err = io::Error::other("chain test");
let dialoguer_err = dialoguer::Error::IO(io_err);
let result = dialoguer_to_operation_error(dialoguer_err);
let source = std::error::Error::source(&result);
assert!(
source.is_some(),
"error chain should be preserved through conversion"
);
}
#[test]
fn preserves_io_error_message() {
let io_err = io::Error::other("terminal unavailable");
let dialoguer_err = dialoguer::Error::IO(io_err);
let result = dialoguer_to_operation_error(dialoguer_err);
assert!(
result.to_string().contains("IO error"),
"expected display to contain 'IO error', got: {}",
result
);
match result {
OperationError::Io(inner) => {
assert_eq!(inner.to_string(), "terminal unavailable");
}
other => panic!("expected OperationError::Io, got {other:?}"),
}
}
}
#[cfg(test)]
mod cli_error_conversion_tests {
use std::io;
use changeset_operations::OperationError;
use crate::error::CliError;
use super::cli_error_to_operation_error;
#[test]
fn io_error_maps_to_operation_io() {
let err = CliError::Io(io::Error::new(io::ErrorKind::NotFound, "not found"));
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::Io(_)));
}
#[test]
fn not_a_tty_maps_to_interaction_required() {
let err = CliError::NotATty;
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::InteractionRequired));
}
#[test]
fn incomplete_args_maps_to_interaction_required() {
let err = CliError::IncompleteArgs;
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::InteractionRequired));
}
#[test]
fn operation_error_passes_through() {
let inner = OperationError::Cancelled;
let err = CliError::Operation(inner);
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::Cancelled));
}
#[test]
fn project_error_maps_to_operation_project() {
use std::path::PathBuf;
let err = CliError::Project(changeset_project::ProjectError::NotFound {
start_dir: PathBuf::from("/test"),
});
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::Project(_)));
}
#[test]
fn editor_failed_maps_to_operation_io() {
let err = CliError::EditorFailed {
source: io::Error::new(io::ErrorKind::NotFound, "editor not found"),
};
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::Io(_)));
}
#[test]
fn invalid_package_bump_format_maps_to_cancelled() {
let err = CliError::InvalidPackageBumpFormat {
input: "bad".to_string(),
};
let result = cli_error_to_operation_error(err);
assert!(matches!(result, OperationError::Cancelled));
}
}