use super::common::{
ChangeOutputFormat, SplitOutputFormat, SurfaceOutputFormat, TextJsonOutputFormat, UnifyOutputFormat,
};
use crate::output::OutputProtocol;
use crate::sync::ConflictStrategy;
use clap::{Args, CommandFactory, Parser, Subcommand, error::ErrorKind};
use clap_complete::Shell;
use std::path::PathBuf;
const MAIN_HELP: &str = "\
Cargo-Rail turns one captured Rust workspace into trustworthy plans, checks, mutations, releases, and compiler reuse.
Common inspection:
cargo rail plan # Decide required repository work
cargo rail surface --check # Check Rust visibility findings
cargo rail config explain # Show configured policy overrides
cargo rail cache status # Inspect compiler-cache health
Workspace mutation:
cargo rail init # Create sparse repository policy
cargo rail unify apply # Apply dependency coherence edits
cargo rail change add --help # Record release intent
cargo rail clean --help # Select owned artifacts to remove
Advanced and external operations:
cargo rail split --help # Extract a crate with Git history
cargo rail sync --help # Synchronize split repositories
cargo rail release --help # Prepare or publish exact-SHA releases
cargo rail doctor --help # Inspect compiler integration
Docs: https://github.com/loadingalias/cargo-rail";
#[derive(Debug, Parser)]
#[command(name = "cargo-rail")]
#[command(bin_name = "cargo-rail")]
#[command(version)]
#[command(
about = "Turn one captured Rust workspace into trustworthy plans, checks, mutations, releases, and compiler reuse"
)]
#[command(long_about = MAIN_HELP)]
#[command(propagate_version = true)]
#[command(styles = get_styles())]
pub struct RailCli {
#[arg(
long,
short,
global = true,
conflicts_with = "verbose",
help_heading = "Global Options"
)]
pub quiet: bool,
#[arg(
long,
short = 'v',
global = true,
conflicts_with = "quiet",
help_heading = "Global Options"
)]
pub verbose: bool,
#[arg(long, global = true, help_heading = "Global Options")]
pub json: bool,
#[arg(long, global = true, value_name = "PATH", help_heading = "Global Options")]
pub config: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH", help_heading = "Global Options")]
pub workspace_root: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH", hide = true)]
pub diagnostics_file: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
const PLAN_HELP: &str = "\
Examples:
cargo rail plan # Changes since the default-branch merge base
cargo rail plan --json # Full machine-readable work contract
cargo rail plan --since HEAD~5 # Changes in last 5 commits
cargo rail plan --from abc --to def # Changes between two SHAs
cargo rail plan --explain # Explain required decisions
cargo rail plan --explain-work cargo.test # Explain one decision, even when skipped
cargo rail plan --all # Safely require every registered work item
cargo rail plan --evidence inputs.json # Use compatible observed-input evidence
cargo rail plan --verify plan.json # Revalidate a saved plan without executing it
cargo rail plan --schema # Print the versioned JSON Schema
cargo rail plan --json > plan.json # Redirect the exact plan to a file";
const SURFACE_HELP: &str = "\
Set `[surface] enabled = true` to include this gate in planner-selected CI.
Set `[surface] consumer_scope = \"workspace\"` only when each closed compiler
crate has no consumers outside the captured workspace.
Examples:
cargo rail surface # Inspect and report without modifying source
cargo rail surface --prepare # Prove exact-toolchain producer readiness
cargo rail surface --check --explain # Inspect complete Rust reachability
cargo rail surface --check --json # Emit the versioned machine contract
cargo rail surface --resume MANIFEST --json # Resume a partial compiler acquisition
cargo rail surface --fix --dry-run --explain # Preview exact visibility edits
cargo rail surface --fix --backup # Apply verified edits with recovery evidence
cargo rail surface --schema # Print the versioned JSON Schema";
const UNIFY_HELP: &str = "\
Examples:
cargo rail unify # Preview dependency changes (exit 0)
cargo rail unify --check # Check for pending changes (exit 1)
cargo rail unify --explain # Show why each decision was made
cargo rail unify --show-diff # Show manifest changes
cargo rail unify apply # Apply the current decision
cargo rail unify apply --backup # Apply with backup
cargo rail unify undo # Restore from backup
cargo rail unify undo --list # List available backups";
const SPLIT_HELP: &str = "\
This is an advanced feature for extracting crates to standalone repositories
while preserving git history. Most teams should start with 'plan', 'cache',
and 'unify' before using split/sync.
Examples:
cargo rail split init my-crate # Configure split for my-crate
cargo rail split init my-crate --dry-run # Preview generated config
cargo rail split run my-crate --check # Check for a pending split (exit 1)
cargo rail split run my-crate # Execute the split
cargo rail split run my-crate --yes # Non-interactive apply confirmation
cargo rail split run --all # Split all configured crates";
const SYNC_HELP: &str = "\
This is an advanced feature for bidirectional sync between monorepo and split
repositories. Requires 'split' to be configured first.
Examples:
cargo rail sync my-crate # Bidirectional sync
cargo rail sync my-crate --to-remote # Push monorepo -> split repo
cargo rail sync my-crate --from-remote # Pull split repo -> monorepo (PR branch)
cargo rail sync my-crate --to-remote --yes # Non-interactive apply confirmation
cargo rail sync --all # Sync all configured crates";
const RELEASE_HELP: &str = "\
Examples:
cargo rail release init my-crate # Configure release for my-crate
cargo rail release init my-crate --dry-run # Preview generated config
cargo rail release check my-crate # Validate the local release plan
cargo rail release check my-crate --publication # Validate registry publication readiness
cargo rail release check my-crate --publication -e # Run publish, MSRV, and semver checks
cargo rail release run my-crate # Prepare a local release without registry publication
cargo rail release run my-crate --publish # Match configured crates.io authority at invocation
cargo rail release run my-crate --include-dependents # Release selected crate plus dependent closure
cargo rail release run my-crate --yes # Non-interactive apply confirmation
cargo rail release run my-crate --bump auto # Infer each bump from the configured release source
cargo rail release run --all --bump auto --pr # Open a release PR with bumps/changelogs only
cargo rail release finalize --all --publish # Match configured crates.io authority after PR merge
cargo rail release run my-crate --bump minor
cargo rail release run my-crate --bump prerelease # 1.0.0 -> 1.0.0-rc.1
cargo rail release run my-crate --bump release # 1.0.0-rc.2 -> 1.0.0
cargo rail release run --all --bump patch # Release all crates";
const CHANGE_HELP: &str = "\
Examples:
cargo rail change add rail-core --bump minor --message \"Added auto bump planning\"
cargo rail change add rail-core rail-cli --bump patch --message \"Fixed release notes\"
cargo rail change add rail-core --bump patch --name fix-parser
cargo rail change status
cargo rail change status --format json
cargo rail change check --merge-base --required
cargo rail change check --since origin/main --format json
Omit --message in an interactive terminal to author in $VISUAL or $EDITOR.
Change files are consumed (deleted in the release commit) when released.
Consumption is all-or-nothing: a release plan that covers only some of a
file's crates is rejected so no pending intent is ever lost.";
const INIT_HELP: &str = "\
Examples:
cargo rail init # Generate .config/rail.toml
cargo rail init --dry-run # Preview generated config
cargo rail init --target wasm32-wasip1 # Declare one supported target
cargo rail init --detect-targets # Opt in to repository target detection
cargo rail init --force # Overwrite existing config";
const CLEAN_HELP: &str = "\
Examples:
cargo rail clean --all # Clean every eligible current-workspace artifact
cargo rail clean --cache # Clean current-workspace cache state
cargo rail clean --prune-backups # Prune backups beyond configured retention
cargo rail clean --all-backups # Delete every backup
cargo rail clean --reports # Clean generated reports
cargo rail clean --release-journal ID # Delete one terminal release journal
cargo rail clean --cache --check # Check selected cleanup (exit 1 when pending)";
const CACHE_HELP: &str = "\
Remote URLs, credentials, provider environments, and distributed execution are machine-owned authority. Use setup
flags only after qualification has established the required trust domain, root portability, and worker identity.
Examples:
cargo rail cache setup --check # Preview transparent compiler reuse setup
cargo rail cache setup # Install or repair the Cargo wrapper
cargo rail cache setup --remote URL --root-portability remap # Qualify cross-root L2 reuse
cargo rail cache status # Inspect workspace and shared local cache state
cargo rail cache status --scope local --json # Inspect the shared local CAS only
cargo rail cache recover --check # Preview byte-preserving markerless CAS recovery
cargo rail cache recover # Quarantine the old tree and create a fresh CAS
cargo rail cache clean --scope workspace --check # Preview workspace cache reclamation
cargo rail cache clean --scope local # Remove the validated cross-workspace CAS
cargo rail cache remove --check # Preview exact setup-state removal
cargo rail cache remove # Remove setup state but preserve the CAS";
const CONFIG_HELP: &str = "\
Examples:
cargo rail config locate # Show which config file is active
cargo rail config print # Show effective config with defaults
cargo rail config validate # Validate rail.toml
cargo rail config validate --json # JSON output for CI
cargo rail config explain # Explain effective values and sources
cargo rail config explain targets # Explain one field in full
cargo rail config explain --all # Explain the complete field inventory
cargo rail config migrate --check # Check for pending semantic migrations
cargo rail config migrate # Apply explicit semantic migrations";
const COMPLETIONS_HELP: &str = "\
Examples:
cargo rail completions bash # Output bash completions
cargo rail completions zsh # Output zsh completions
cargo rail completions fish # Output fish completions
cargo rail completions powershell # Output PowerShell completions
Installation:
# Bash (~/.bashrc)
eval \"$(cargo rail completions bash)\"
# Zsh (~/.zshrc)
eval \"$(cargo rail completions zsh)\"
# Fish (~/.config/fish/config.fish)
cargo rail completions fish | source
# PowerShell
cargo rail completions powershell | Out-String | Invoke-Expression";
#[derive(Debug, Subcommand)]
pub enum Commands {
Doctor {
#[command(subcommand)]
command: DoctorCommand,
},
#[command(after_long_help = CACHE_HELP)]
Cache {
#[command(subcommand)]
command: CacheCommand,
},
#[command(after_long_help = PLAN_HELP)]
Plan {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"], hide = true)]
merge_base: bool,
#[arg(skip)]
json: bool,
#[arg(long)]
explain: bool,
#[arg(long, value_name = "WORK_ID")]
explain_work: Option<String>,
#[arg(long)]
all: bool,
#[arg(long, value_name = "PATH")]
evidence: Option<PathBuf>,
#[arg(
long,
value_name = "PATH",
conflicts_with_all = ["since", "from", "to", "merge_base", "explain", "explain_work", "all", "evidence", "schema"]
)]
verify: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["since", "from", "to", "merge_base", "explain", "explain_work", "all", "evidence", "verify"])]
schema: bool,
},
#[command(after_long_help = SURFACE_HELP)]
Surface {
#[arg(
long,
conflicts_with_all = ["check", "fix", "dry_run", "backup", "explain", "only", "schema"]
)]
prepare: bool,
#[arg(long, conflicts_with = "fix")]
check: bool,
#[arg(long, conflicts_with = "check")]
fix: bool,
#[arg(
long,
value_name = "MANIFEST",
conflicts_with_all = ["prepare", "fix", "dry_run", "backup", "schema"]
)]
resume: Option<PathBuf>,
#[arg(long, requires = "fix", conflicts_with = "backup")]
dry_run: bool,
#[arg(long, requires = "fix")]
backup: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: SurfaceOutputFormat,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
#[arg(long)]
explain: bool,
#[arg(
long,
value_name = "LINT",
value_delimiter = ',',
value_parser = [
"dead-public",
"unnecessary-public",
"unnecessary-restricted-visibility",
"unnecessary-crate-visibility"
]
)]
only: Vec<String>,
#[arg(
long,
conflicts_with_all = ["prepare", "check", "fix", "resume", "dry_run", "backup", "output", "explain", "only"]
)]
schema: bool,
},
#[command(after_long_help = UNIFY_HELP)]
Unify {
#[command(subcommand)]
command: Option<UnifyCommand>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: UnifyOutputFormat,
#[arg(long)]
report: bool,
#[arg(long, value_name = "PATH", requires = "report")]
report_path: Option<PathBuf>,
#[arg(long, short = 'o', value_name = "PATH", requires = "check")]
output: Option<PathBuf>,
#[arg(long)]
show_diff: bool,
#[arg(long)]
explain: bool,
},
#[command(after_long_help = INIT_HELP)]
Init {
#[arg(long, short, default_value = ".config/rail.toml")]
output: String,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(long = "target", value_name = "TRIPLE", conflicts_with = "detect_targets")]
targets: Vec<String>,
#[arg(long, conflicts_with = "targets")]
detect_targets: bool,
},
#[command(after_long_help = SPLIT_HELP)]
Split {
#[command(subcommand)]
command: SplitCommand,
},
#[command(after_long_help = SYNC_HELP)]
Sync {
#[arg(conflicts_with = "all")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long)]
from_remote: bool,
#[arg(long)]
to_remote: bool,
#[arg(long, default_value_t, value_enum)]
strategy: ConflictStrategy,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(
long,
value_name = "RECEIPT",
conflicts_with_all = ["crate_name", "all", "remote", "from_remote", "to_remote", "check", "plan"]
)]
resume: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(after_long_help = RELEASE_HELP)]
Release {
#[command(subcommand)]
command: ReleaseCommand,
},
#[command(after_long_help = CHANGE_HELP)]
Change {
#[command(subcommand)]
command: ChangeCommand,
},
#[command(after_long_help = CLEAN_HELP)]
Clean {
#[arg(long, conflicts_with_all = ["cache", "prune_backups", "all_backups", "reports", "release_journal"])]
all: bool,
#[arg(long)]
cache: bool,
#[arg(long, conflicts_with = "all_backups")]
prune_backups: bool,
#[arg(long, conflicts_with = "prune_backups")]
all_backups: bool,
#[arg(long)]
reports: bool,
#[arg(long, value_name = "ID_OR_PATH")]
release_journal: Option<String>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(name = "config")]
#[command(after_long_help = CONFIG_HELP)]
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(after_long_help = COMPLETIONS_HELP)]
Completions {
#[arg(value_enum, value_name = "SHELL")]
shell: Shell,
},
}
impl Commands {
#[doc(hidden)]
pub fn requires_workspace_snapshot(&self) -> bool {
match self {
Self::Doctor { .. }
| Self::Unify { .. }
| Self::Split { .. }
| Self::Sync { .. }
| Self::Surface { schema: false, .. }
| Self::Release { .. }
| Self::Change { .. } => true,
Self::Plan { .. } => false,
_ => false,
}
}
#[doc(hidden)]
pub fn requires_planning_source_capture(&self) -> bool {
match self {
Self::Plan { from, to, schema, .. } => !(*schema || from.is_some() && to.is_some()),
_ => false,
}
}
#[doc(hidden)]
pub fn requires_worktree_source_capture(&self) -> bool {
match self {
Self::Doctor { .. } | Self::Surface { .. } => false,
_ => false,
}
}
}
#[derive(Debug, Subcommand)]
pub enum DoctorCommand {
NativeCache {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum CacheScope {
Workspace,
Local,
All,
}
impl CacheScope {
pub(crate) const fn includes_workspace(self) -> bool {
matches!(self, Self::Workspace | Self::All)
}
pub(crate) const fn includes_local(self) -> bool {
matches!(self, Self::Local | Self::All)
}
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Local => "local",
Self::All => "all",
}
}
}
#[derive(Debug, Args)]
pub struct CacheSetupArgs {
#[arg(long, value_name = "PATH")]
pub local_dir: Option<PathBuf>,
#[arg(long, value_name = "SIZE", value_parser = parse_cache_size)]
pub max_size: Option<u64>,
#[arg(long, value_name = "URL", conflicts_with = "local_only")]
pub remote: Option<String>,
#[arg(long, value_name = "MODE", value_parser = ["read", "read-write"], requires = "remote")]
pub remote_mode: Option<String>,
#[arg(long = "remote-environment", value_name = "NAME", requires = "remote")]
pub remote_environment: Vec<String>,
#[arg(long, value_name = "MODE", value_parser = ["physical", "remap"])]
pub root_portability: Option<String>,
#[arg(long, conflicts_with_all = ["remote", "remote_mode", "remote_environment", "root_portability"])]
pub local_only: bool,
#[arg(long, conflicts_with = "distributed_endpoint")]
pub distributed_local: bool,
#[arg(
long,
value_name = "IP:PORT",
conflicts_with = "distributed_local",
requires_all = [
"distributed_server_name",
"distributed_capability",
"distributed_authority",
"distributed_client_certificate",
"distributed_client_private_key"
]
)]
pub distributed_endpoint: Option<String>,
#[arg(long, value_name = "NAME", requires = "distributed_endpoint")]
pub distributed_server_name: Option<String>,
#[arg(long, value_name = "IDENTITY", requires = "distributed_endpoint")]
pub distributed_capability: Option<String>,
#[arg(long, value_name = "PATH", requires = "distributed_endpoint")]
pub distributed_authority: Option<PathBuf>,
#[arg(long, value_name = "PATH", requires = "distributed_endpoint")]
pub distributed_client_certificate: Option<PathBuf>,
#[arg(long, value_name = "PATH", requires = "distributed_endpoint")]
pub distributed_client_private_key: Option<PathBuf>,
#[arg(
long,
value_name = "MODE",
value_parser = ["automatic", "qualification"],
conflicts_with = "distributed_local"
)]
pub distributed_policy: Option<String>,
#[arg(long, short = 'c')]
pub check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
pub format: TextJsonOutputFormat,
}
#[derive(Debug, Subcommand)]
pub enum CacheCommand {
Setup(Box<CacheSetupArgs>),
Normalize {
#[arg(value_name = "URL")]
url: String,
#[arg(long, value_name = "MODE", value_parser = ["read", "read-write"])]
mode: Option<String>,
#[arg(long = "environment", value_name = "NAME")]
environment: Vec<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Status {
#[arg(long, value_enum, default_value = "all")]
scope: CacheScope,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Recover {
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Clean {
#[arg(long, value_enum)]
scope: CacheScope,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Remove {
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
}
fn parse_cache_size(value: &str) -> Result<u64, String> {
let split = value
.find(|character: char| !character.is_ascii_digit())
.ok_or_else(|| "cache size requires an explicit unit: B, KiB, MiB, GiB, or TiB".to_string())?;
let (number, unit) = value.split_at(split);
if number.is_empty() || number.starts_with('0') && number != "0" {
return Err("cache size must be a canonical positive integer".to_string());
}
let number = number
.parse::<u64>()
.map_err(|_| "cache size integer is invalid or overflowing".to_string())?;
let multiplier = match unit {
"B" => 1_u64,
"KiB" => 1024,
"MiB" => 1024 * 1024,
"GiB" => 1024 * 1024 * 1024,
"TiB" => 1024 * 1024 * 1024 * 1024,
_ => return Err("cache size unit must be B, KiB, MiB, GiB, or TiB".to_string()),
};
let bytes = number
.checked_mul(multiplier)
.filter(|bytes| *bytes > 0)
.ok_or_else(|| "cache size must be positive and fit in u64".to_string())?;
if bytes > usize::MAX as u64 {
return Err("cache size exceeds this platform's supported limit".to_string());
}
Ok(bytes)
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Locate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Print {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Validate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
#[arg(long, conflicts_with = "no_strict")]
strict: bool,
#[arg(long, conflicts_with = "strict")]
no_strict: bool,
},
Explain {
#[arg(value_name = "FIELD", conflicts_with = "all")]
fields: Vec<String>,
#[arg(long, conflicts_with = "fields")]
all: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Migrate {
#[arg(long)]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
}
#[derive(Debug, Subcommand)]
pub enum UnifyCommand {
Apply {
#[arg(long, value_name = "PATH")]
plan: Option<PathBuf>,
#[arg(long)]
backup: bool,
#[arg(long)]
report: bool,
#[arg(long, value_name = "PATH", requires = "report")]
report_path: Option<PathBuf>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: UnifyOutputFormat,
},
Doctor {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: UnifyOutputFormat,
},
Undo {
#[arg(long)]
list: bool,
#[arg(long = "backup-id")]
backup_id: Option<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
}
#[derive(Debug, Subcommand)]
pub enum SplitCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long)]
dry_run: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: SplitOutputFormat,
},
}
#[derive(Debug, Subcommand)]
pub enum ReleaseCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long)]
dry_run: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, default_value = "auto")]
bump: String,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["skip_publish", "pr"])]
publish: bool,
#[arg(long)]
#[arg(hide = true, conflicts_with = "publish")]
skip_publish: bool,
#[arg(long)]
skip_tag: bool,
#[arg(long)]
pr: bool,
#[arg(long)]
include_dependents: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
allow_non_default_branch: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Check {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, default_value = "auto")]
bump: String,
#[arg(long)]
publication: bool,
#[arg(long, short = 'e', requires = "publication")]
extended: bool,
#[arg(long, conflicts_with = "publication")]
skip_tag: bool,
#[arg(long)]
include_dependents: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Finalize {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, conflicts_with = "skip_publish")]
publish: bool,
#[arg(long)]
#[arg(hide = true, conflicts_with = "publish")]
skip_publish: bool,
#[arg(long)]
skip_tag: bool,
#[arg(long)]
include_dependents: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
allow_non_default_branch: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Resume {
#[arg(value_name = "STATE")]
state: PathBuf,
},
Status {
#[arg(value_name = "STATE")]
state: Option<PathBuf>,
#[arg(long)]
history: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Abort {
#[arg(value_name = "STATE")]
state: PathBuf,
#[arg(short = 'y', long)]
yes: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum ChangeCommand {
Add {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long)]
bump: String,
#[arg(long, short = 'm')]
message: Option<String>,
#[arg(long, value_name = "SLUG")]
name: Option<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
Status {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
Check {
#[arg(long, conflicts_with_all = ["merge_base", "all"], value_name = "REF")]
since: Option<String>,
#[arg(long, conflicts_with_all = ["since", "all"])]
merge_base: bool,
#[arg(long, conflicts_with_all = ["since", "merge_base"])]
all: bool,
#[arg(long)]
required: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
}
fn get_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
}
fn text_json_protocol(json: bool) -> OutputProtocol {
if json {
OutputProtocol::Json
} else {
OutputProtocol::Text
}
}
impl Commands {
#[doc(hidden)]
pub fn output_protocol(&self) -> OutputProtocol {
match self {
Commands::Doctor {
command: DoctorCommand::NativeCache { format },
} => text_json_protocol(format.is_json()),
Commands::Cache { command } => match command {
CacheCommand::Setup(setup) => text_json_protocol(setup.format.is_json()),
CacheCommand::Normalize { format, .. }
| CacheCommand::Status { format, .. }
| CacheCommand::Recover { format, .. }
| CacheCommand::Clean { format, .. }
| CacheCommand::Remove { format, .. } => text_json_protocol(format.is_json()),
},
Commands::Sync { format, .. } | Commands::Clean { format, .. } => text_json_protocol(format.is_json()),
Commands::Plan { schema: true, .. } => OutputProtocol::Raw,
Commands::Plan { json, .. } => text_json_protocol(*json),
Commands::Surface { schema: true, .. } => OutputProtocol::Raw,
Commands::Surface { format, .. } => match format {
SurfaceOutputFormat::Text => OutputProtocol::Text,
SurfaceOutputFormat::Json => OutputProtocol::Json,
SurfaceOutputFormat::GitHub => OutputProtocol::Raw,
},
Commands::Unify {
command: Some(UnifyCommand::Doctor { format } | UnifyCommand::Apply { format, .. }),
..
} => text_json_protocol(format.is_json()),
Commands::Unify {
command: Some(UnifyCommand::Undo { format, .. }),
..
} => text_json_protocol(format.is_json()),
Commands::Unify { format, .. } => text_json_protocol(format.is_json()),
Commands::Split { command } => match command {
SplitCommand::Init { .. } => OutputProtocol::Text,
SplitCommand::Run { format, .. } => match format {
SplitOutputFormat::Text => OutputProtocol::Text,
SplitOutputFormat::Json => OutputProtocol::Json,
SplitOutputFormat::NamesOnly | SplitOutputFormat::JsonLines => OutputProtocol::Raw,
},
},
Commands::Release { command } => match command {
ReleaseCommand::Init { .. } | ReleaseCommand::Resume { .. } | ReleaseCommand::Abort { .. } => {
OutputProtocol::Text
}
ReleaseCommand::Status { format, .. } => text_json_protocol(format.is_json()),
ReleaseCommand::Run { format, .. }
| ReleaseCommand::Check { format, .. }
| ReleaseCommand::Finalize { format, .. } => text_json_protocol(format.is_json()),
},
Commands::Change { command } => match command {
ChangeCommand::Add { format, .. }
| ChangeCommand::Status { format }
| ChangeCommand::Check { format, .. } => match format {
ChangeOutputFormat::Text => OutputProtocol::Text,
ChangeOutputFormat::Json => OutputProtocol::Json,
ChangeOutputFormat::NamesOnly => OutputProtocol::Raw,
},
},
Commands::Config { command } => match command {
ConfigCommand::Locate { format }
| ConfigCommand::Print { format }
| ConfigCommand::Explain { format, .. }
| ConfigCommand::Validate { format, .. }
| ConfigCommand::Migrate { format, .. } => text_json_protocol(format.is_json()),
},
Commands::Completions { .. } => OutputProtocol::Raw,
_ => OutputProtocol::Text,
}
}
pub fn is_json_format(&self) -> bool {
self.output_protocol() == OutputProtocol::Json
}
pub fn apply_json_override(&mut self) -> Result<(), clap::Error> {
let unsupported = match self {
Commands::Split {
command: SplitCommand::Init { .. },
} => Some("split init"),
Commands::Release {
command: ReleaseCommand::Init { .. },
} => Some("release init"),
Commands::Release {
command: ReleaseCommand::Resume { .. },
} => Some("release resume"),
Commands::Release {
command: ReleaseCommand::Abort { .. },
} => Some("release abort"),
Commands::Completions { .. } => Some("completions"),
_ => None,
};
if let Some(command) = unsupported {
return Err(RailCli::command().error(
ErrorKind::ArgumentConflict,
format!("--json is not supported by 'cargo rail {command}'"),
));
}
let incompatible_stream = match self {
Commands::Split {
command: SplitCommand::Run { format, .. },
} => match format {
SplitOutputFormat::NamesOnly => Some("names-only"),
SplitOutputFormat::JsonLines => Some("jsonl"),
SplitOutputFormat::Text | SplitOutputFormat::Json => None,
},
Commands::Surface { format, .. } => match format {
SurfaceOutputFormat::GitHub => Some("github"),
SurfaceOutputFormat::Text | SurfaceOutputFormat::Json => None,
},
Commands::Change { command } => match command {
ChangeCommand::Add { format, .. }
| ChangeCommand::Status { format }
| ChangeCommand::Check { format, .. } => match format {
ChangeOutputFormat::NamesOnly => Some("names-only"),
ChangeOutputFormat::Text | ChangeOutputFormat::Json => None,
},
},
_ => None,
};
if let Some(format) = incompatible_stream {
return Err(RailCli::command().error(
ErrorKind::ArgumentConflict,
format!("--json conflicts with the distinct '--format {format}' stream protocol"),
));
}
match self {
Commands::Doctor {
command: DoctorCommand::NativeCache { format },
} => *format = TextJsonOutputFormat::Json,
Commands::Cache { command } => match command {
CacheCommand::Setup(setup) => setup.format = TextJsonOutputFormat::Json,
CacheCommand::Normalize { format, .. }
| CacheCommand::Status { format, .. }
| CacheCommand::Recover { format, .. }
| CacheCommand::Clean { format, .. }
| CacheCommand::Remove { format, .. } => {
*format = TextJsonOutputFormat::Json;
}
},
Commands::Sync { format, .. } | Commands::Clean { format, .. } => *format = TextJsonOutputFormat::Json,
Commands::Plan { json, .. } => *json = true,
Commands::Surface { format, .. } => *format = SurfaceOutputFormat::Json,
Commands::Unify {
command: Some(UnifyCommand::Doctor { format } | UnifyCommand::Apply { format, .. }),
..
} => *format = UnifyOutputFormat::Json,
Commands::Unify {
command: Some(UnifyCommand::Undo { format, .. }),
..
} => *format = TextJsonOutputFormat::Json,
Commands::Unify { format, .. } => *format = UnifyOutputFormat::Json,
Commands::Split {
command: SplitCommand::Run { format, .. },
} => *format = SplitOutputFormat::Json,
Commands::Split { .. } => {}
Commands::Release {
command:
ReleaseCommand::Run { format, .. }
| ReleaseCommand::Check { format, .. }
| ReleaseCommand::Finalize { format, .. }
| ReleaseCommand::Status { format, .. },
} => *format = TextJsonOutputFormat::Json,
Commands::Release { .. } => {}
Commands::Change {
command:
ChangeCommand::Add { format, .. }
| ChangeCommand::Status { format }
| ChangeCommand::Check { format, .. },
} => *format = ChangeOutputFormat::Json,
Commands::Config { command } => match command {
ConfigCommand::Locate { format }
| ConfigCommand::Print { format }
| ConfigCommand::Explain { format, .. }
| ConfigCommand::Validate { format, .. }
| ConfigCommand::Migrate { format, .. } => *format = TextJsonOutputFormat::Json,
},
_ => {}
}
Ok(())
}
}
pub fn generate_completions(shell: Shell) {
let mut cmd = RailCli::command();
clap_complete::generate(shell, &mut cmd, "cargo-rail", &mut std::io::stdout());
}
#[cfg(test)]
mod tests {
use super::{RailCli, parse_cache_size};
use crate::output::OutputProtocol;
use clap::Parser as _;
#[test]
fn surface_formats_select_distinct_output_protocols() {
for (format, expected) in [
("text", OutputProtocol::Text),
("json", OutputProtocol::Json),
("github", OutputProtocol::Raw),
] {
let cli = RailCli::try_parse_from(["cargo-rail", "surface", "--format", format])
.expect("surface format must parse");
assert_eq!(
cli.command.output_protocol(),
expected,
"surface --format {format} selected the wrong transport"
);
assert_eq!(
cli.command.is_json_format(),
expected == OutputProtocol::Json,
"surface --format {format} selected the wrong JSON compatibility mode"
);
}
}
#[test]
fn command_owned_streams_select_the_raw_output_protocol() {
let cases: &[&[&str]] = &[
&["cargo-rail", "plan", "--schema"],
&["cargo-rail", "surface", "--schema"],
&["cargo-rail", "surface", "--format", "github"],
&["cargo-rail", "split", "run", "crate-a", "--format", "names-only"],
&["cargo-rail", "split", "run", "crate-a", "--format", "jsonl"],
&["cargo-rail", "change", "status", "--format", "names-only"],
&["cargo-rail", "completions", "bash"],
];
for arguments in cases {
let cli = RailCli::try_parse_from(arguments.iter().copied()).expect("raw stream command must parse");
assert_eq!(
cli.command.output_protocol(),
OutputProtocol::Raw,
"{} must retain its command-owned raw stream",
arguments[1..].join(" ")
);
assert!(
!cli.command.is_json_format(),
"{} must not select JSON error rendering",
arguments[1..].join(" ")
);
}
}
#[test]
fn transparent_cache_size_grammar_is_exact_and_bounded() {
assert_eq!(parse_cache_size("1B"), Ok(1));
assert_eq!(parse_cache_size("10GiB"), Ok(10 * 1024 * 1024 * 1024));
for invalid in [
"0B",
"01GiB",
"10",
"1GB",
"1.5GiB",
"-1GiB",
"1gib",
"18446744073709551615TiB",
] {
assert!(parse_cache_size(invalid).is_err(), "accepted invalid size {invalid}");
}
}
}