Allow explicit values with `uv version --bump` (#16555)

Resolves https://github.com/astral-sh/uv/issues/16427

This PR updates `uv version --bump` so you can pin the exact number
you’re targeting, i.e. `--bump patch=10` or `--bump dev=42`. The
command-line interface now parses those `component=value` flags, and the
bump logic actually sets the version to the number you asked for.
This commit is contained in:
liam 2025-11-11 12:27:32 -05:00 committed by GitHub
parent 3ccad58166
commit 63ab247765
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 786 additions and 90 deletions

View File

@ -1,11 +1,14 @@
use std::ffi::OsString;
use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{Result, anyhow};
use clap::builder::Styles;
use clap::ValueEnum;
use clap::builder::styling::{AnsiColor, Effects, Style};
use clap::builder::{PossibleValue, Styles, TypedValueParser, ValueParserFactory};
use clap::error::ErrorKind;
use clap::{Args, Parser, Subcommand};
use uv_auth::Service;
@ -587,8 +590,8 @@ pub struct VersionArgs {
/// Update the project version using the given semantics
///
/// This flag can be passed multiple times.
#[arg(group = "operation", long)]
pub bump: Vec<VersionBump>,
#[arg(group = "operation", long, value_name = "BUMP[=VALUE]")]
pub bump: Vec<VersionBumpSpec>,
/// Don't write a new version to the `pyproject.toml`
///
@ -698,8 +701,8 @@ pub enum VersionBump {
Dev,
}
impl std::fmt::Display for VersionBump {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for VersionBump {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let string = match self {
Self::Major => "major",
Self::Minor => "minor",
@ -715,6 +718,110 @@ impl std::fmt::Display for VersionBump {
}
}
impl FromStr for VersionBump {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"major" => Ok(Self::Major),
"minor" => Ok(Self::Minor),
"patch" => Ok(Self::Patch),
"stable" => Ok(Self::Stable),
"alpha" => Ok(Self::Alpha),
"beta" => Ok(Self::Beta),
"rc" => Ok(Self::Rc),
"post" => Ok(Self::Post),
"dev" => Ok(Self::Dev),
_ => Err(format!("invalid bump component `{value}`")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct VersionBumpSpec {
pub bump: VersionBump,
pub value: Option<u64>,
}
impl Display for VersionBumpSpec {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.value {
Some(value) => write!(f, "{}={value}", self.bump),
None => self.bump.fmt(f),
}
}
}
impl FromStr for VersionBumpSpec {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let (name, value) = match input.split_once('=') {
Some((name, value)) => (name, Some(value)),
None => (input, None),
};
let bump = name.parse::<VersionBump>()?;
if bump == VersionBump::Stable && value.is_some() {
return Err("`--bump stable` does not accept a value".to_string());
}
let value = match value {
Some("") => {
return Err("`--bump` values cannot be empty".to_string());
}
Some(raw) => Some(
raw.parse::<u64>()
.map_err(|_| format!("invalid numeric value `{raw}` for `--bump {name}`"))?,
),
None => None,
};
Ok(Self { bump, value })
}
}
impl ValueParserFactory for VersionBumpSpec {
type Parser = VersionBumpSpecValueParser;
fn value_parser() -> Self::Parser {
VersionBumpSpecValueParser
}
}
#[derive(Clone, Debug)]
pub struct VersionBumpSpecValueParser;
impl TypedValueParser for VersionBumpSpecValueParser {
type Value = VersionBumpSpec;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let raw = value.to_str().ok_or_else(|| {
clap::Error::raw(
ErrorKind::InvalidUtf8,
"`--bump` values must be valid UTF-8",
)
})?;
VersionBumpSpec::from_str(raw)
.map_err(|message| clap::Error::raw(ErrorKind::InvalidValue, message))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
VersionBump::value_variants()
.iter()
.filter_map(ValueEnum::to_possible_value),
))
}
}
#[derive(Args)]
pub struct SelfNamespace {
#[command(subcommand)]

View File

@ -676,7 +676,7 @@ impl Version {
let full = self.make_full();
match bump {
BumpCommand::BumpRelease { index } => {
BumpCommand::BumpRelease { index, value } => {
// Clear all sub-release items
full.pre = None;
full.post = None;
@ -690,7 +690,9 @@ impl Version {
// Everything before the bumped value is preserved (or is an implicit 0)
Ordering::Less => old_parts.get(i).copied().unwrap_or(0),
// This is the value to bump (could be implicit 0)
Ordering::Equal => old_parts.get(i).copied().unwrap_or(0) + 1,
Ordering::Equal => {
value.unwrap_or_else(|| old_parts.get(i).copied().unwrap_or(0) + 1)
}
// Everything after the bumped value becomes 0
Ordering::Greater => 0,
})
@ -703,37 +705,50 @@ impl Version {
full.post = None;
full.dev = None;
}
BumpCommand::BumpPrerelease { kind } => {
BumpCommand::BumpPrerelease { kind, value } => {
// Clear all sub-prerelease items
full.post = None;
full.dev = None;
// Either bump the matching kind or set to 1
if let Some(prerelease) = &mut full.pre {
if prerelease.kind == kind {
prerelease.number += 1;
return;
if let Some(value) = value {
full.pre = Some(Prerelease {
kind,
number: value,
});
} else {
// Either bump the matching kind or set to 1
if let Some(prerelease) = &mut full.pre {
if prerelease.kind == kind {
prerelease.number += 1;
return;
}
}
full.pre = Some(Prerelease { kind, number: 1 });
}
full.pre = Some(Prerelease { kind, number: 1 });
}
BumpCommand::BumpPost => {
BumpCommand::BumpPost { value } => {
// Clear sub-post items
full.dev = None;
// Either bump or set to 1
if let Some(post) = &mut full.post {
*post += 1;
if let Some(value) = value {
full.post = Some(value);
} else {
full.post = Some(1);
// Either bump or set to 1
if let Some(post) = &mut full.post {
*post += 1;
} else {
full.post = Some(1);
}
}
}
BumpCommand::BumpDev => {
// Either bump or set to 1
if let Some(dev) = &mut full.dev {
*dev += 1;
BumpCommand::BumpDev { value } => {
if let Some(value) = value {
full.dev = Some(value);
} else {
full.dev = Some(1);
// Either bump or set to 1
if let Some(dev) = &mut full.dev {
*dev += 1;
} else {
full.dev = Some(1);
}
}
}
}
@ -1018,22 +1033,32 @@ impl FromStr for Version {
/// Various ways to "bump" a version
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BumpCommand {
/// Bump the release component
/// Bump or set the release component
BumpRelease {
/// The release component to bump (0 is major, 1 is minor, 2 is patch)
index: usize,
/// Explicit value to set; when absent the component is incremented
value: Option<u64>,
},
/// Bump the prerelease component
/// Bump or set the prerelease component
BumpPrerelease {
/// prerelease component to bump
kind: PrereleaseKind,
/// Explicit value to set; when absent the component is incremented
value: Option<u64>,
},
/// Bump to the associated stable release
MakeStable,
/// Bump the post component
BumpPost,
/// Bump the dev component
BumpDev,
/// Bump or set the post component
BumpPost {
/// Explicit value to set; when absent the component is incremented
value: Option<u64>,
},
/// Bump or set the dev component
BumpDev {
/// Explicit value to set; when absent the component is incremented
value: Option<u64>,
},
}
/// A small representation of a version.
@ -4239,36 +4264,57 @@ mod tests {
fn bump_major() {
// one digit
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "1");
// two digit
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "2.0");
// three digit (zero major)
let mut version = "0.1.2".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.0.0");
// three digit (non-zero major)
let mut version = "1.2.3".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "2.0.0");
// four digit
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "2.0.0.0");
// All the version junk
let mut version = "5!1.7.3.5b2.post345.dev456+local"
.parse::<Version>()
.unwrap();
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!2.0.0.0+local");
version.bump(BumpCommand::BumpRelease { index: 0 });
version.bump(BumpCommand::BumpRelease {
index: 0,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!3.0.0.0+local");
}
@ -4278,31 +4324,49 @@ mod tests {
fn bump_minor() {
// one digit
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "0.1");
// two digit
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.6");
// three digit (non-zero major)
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "5.4.0");
// four digit
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.3.0.0");
// All the version junk
let mut version = "5!1.7.3.5b2.post345.dev456+local"
.parse::<Version>()
.unwrap();
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.8.0.0+local");
version.bump(BumpCommand::BumpRelease { index: 1 });
version.bump(BumpCommand::BumpRelease {
index: 1,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.9.0.0+local");
}
@ -4312,31 +4376,49 @@ mod tests {
fn bump_patch() {
// one digit
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "0.0.1");
// two digit
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.5.1");
// three digit
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "5.3.7");
// four digit
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.2.4.0");
// All the version junk
let mut version = "5!1.7.3.5b2.post345.dev456+local"
.parse::<Version>()
.unwrap();
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.4.0+local");
version.bump(BumpCommand::BumpRelease { index: 2 });
version.bump(BumpCommand::BumpRelease {
index: 2,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.5.0+local");
}
@ -4348,6 +4430,7 @@ mod tests {
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "0a1");
@ -4355,6 +4438,7 @@ mod tests {
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.5a1");
@ -4362,6 +4446,7 @@ mod tests {
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "5.3.6a1");
@ -4369,6 +4454,7 @@ mod tests {
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.2.3.4a1");
@ -4378,10 +4464,12 @@ mod tests {
.unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5a1+local");
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5a2+local");
}
@ -4394,6 +4482,7 @@ mod tests {
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "0b1");
@ -4401,6 +4490,7 @@ mod tests {
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.5b1");
@ -4408,6 +4498,7 @@ mod tests {
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "5.3.6b1");
@ -4415,6 +4506,7 @@ mod tests {
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.2.3.4b1");
@ -4424,10 +4516,12 @@ mod tests {
.unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5b1+local");
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2+local");
}
@ -4440,6 +4534,7 @@ mod tests {
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "0rc1");
@ -4447,6 +4542,7 @@ mod tests {
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.5rc1");
@ -4454,6 +4550,7 @@ mod tests {
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "5.3.6rc1");
@ -4461,6 +4558,7 @@ mod tests {
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "1.2.3.4rc1");
@ -4470,10 +4568,12 @@ mod tests {
.unwrap();
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5rc1+local");
version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: None,
});
assert_eq!(version.to_string().as_str(), "5!1.7.3.5rc2+local");
}
@ -4484,29 +4584,29 @@ mod tests {
fn bump_post() {
// one digit
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "0.post1");
// two digit
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "1.5.post1");
// three digit
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "5.3.6.post1");
// four digit
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "1.2.3.4.post1");
// All the version junk
let mut version = "5!1.7.3.5b2.dev123+local".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2.post1+local");
version.bump(BumpCommand::BumpPost);
version.bump(BumpCommand::BumpPost { value: None });
assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2.post2+local");
}
@ -4516,32 +4616,32 @@ mod tests {
fn bump_dev() {
// one digit
let mut version = "0".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(version.to_string().as_str(), "0.dev1");
// two digit
let mut version = "1.5".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(version.to_string().as_str(), "1.5.dev1");
// three digit
let mut version = "5.3.6".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(version.to_string().as_str(), "5.3.6.dev1");
// four digit
let mut version = "1.2.3.4".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(version.to_string().as_str(), "1.2.3.4.dev1");
// All the version junk
let mut version = "5!1.7.3.5b2.post345+local".parse::<Version>().unwrap();
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(
version.to_string().as_str(),
"5!1.7.3.5b2.post345.dev1+local"
);
version.bump(BumpCommand::BumpDev);
version.bump(BumpCommand::BumpDev { value: None });
assert_eq!(
version.to_string().as_str(),
"5!1.7.3.5b2.post345.dev2+local"

View File

@ -8,7 +8,7 @@ use owo_colors::OwoColorize;
use tracing::debug;
use uv_cache::Cache;
use uv_cli::version::VersionInfo;
use uv_cli::{VersionBump, VersionFormat};
use uv_cli::{VersionBump, VersionBumpSpec, VersionFormat};
use uv_client::BaseClientBuilder;
use uv_configuration::{
Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification,
@ -56,7 +56,7 @@ pub(crate) fn self_version(
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn project_version(
value: Option<String>,
mut bump: Vec<VersionBump>,
mut bump: Vec<VersionBumpSpec>,
short: bool,
output_format: VersionFormat,
project_dir: &Path,
@ -164,29 +164,29 @@ pub(crate) async fn project_version(
// because that makes perfect sense and is reasonable to do.
let release_components: Vec<_> = bump
.iter()
.filter(|bump| {
.filter(|spec| {
matches!(
bump,
spec.bump,
VersionBump::Major | VersionBump::Minor | VersionBump::Patch
)
})
.collect();
let prerelease_components: Vec<_> = bump
.iter()
.filter(|bump| {
.filter(|spec| {
matches!(
bump,
spec.bump,
VersionBump::Alpha | VersionBump::Beta | VersionBump::Rc | VersionBump::Dev
)
})
.collect();
let post_count = bump
.iter()
.filter(|bump| *bump == &VersionBump::Post)
.filter(|spec| spec.bump == VersionBump::Post)
.count();
let stable_count = bump
.iter()
.filter(|bump| *bump == &VersionBump::Stable)
.filter(|spec| spec.bump == VersionBump::Stable)
.count();
// Very little reason to do "bump to stable" and then do other things,
@ -252,25 +252,37 @@ pub(crate) async fn project_version(
// Apply all the bumps
let mut new_version = old_version.clone();
for bump in &bump {
let command = match *bump {
VersionBump::Major => BumpCommand::BumpRelease { index: 0 },
VersionBump::Minor => BumpCommand::BumpRelease { index: 1 },
VersionBump::Patch => BumpCommand::BumpRelease { index: 2 },
VersionBump::Alpha => BumpCommand::BumpPrerelease {
for spec in &bump {
match spec.bump {
VersionBump::Major => new_version.bump(BumpCommand::BumpRelease {
index: 0,
value: spec.value,
}),
VersionBump::Minor => new_version.bump(BumpCommand::BumpRelease {
index: 1,
value: spec.value,
}),
VersionBump::Patch => new_version.bump(BumpCommand::BumpRelease {
index: 2,
value: spec.value,
}),
VersionBump::Stable => new_version.bump(BumpCommand::MakeStable),
VersionBump::Alpha => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
},
VersionBump::Beta => BumpCommand::BumpPrerelease {
value: spec.value,
}),
VersionBump::Beta => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
},
VersionBump::Rc => BumpCommand::BumpPrerelease {
value: spec.value,
}),
VersionBump::Rc => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
},
VersionBump::Post => BumpCommand::BumpPost,
VersionBump::Dev => BumpCommand::BumpDev,
VersionBump::Stable => BumpCommand::MakeStable,
};
new_version.bump(command);
value: spec.value,
}),
VersionBump::Post => new_version.bump(BumpCommand::BumpPost { value: spec.value }),
VersionBump::Dev => new_version.bump(BumpCommand::BumpDev { value: spec.value }),
}
}
if new_version <= old_version {

View File

@ -15,7 +15,7 @@ use uv_cli::{
PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, PythonPinArgs,
PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, SyncArgs, SyncFormat, ToolDirArgs,
ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs,
VersionBump, VersionFormat,
VersionBumpSpec, VersionFormat,
};
use uv_cli::{
AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs,
@ -1792,7 +1792,7 @@ impl RemoveSettings {
#[derive(Debug, Clone)]
pub(crate) struct VersionSettings {
pub(crate) value: Option<String>,
pub(crate) bump: Vec<VersionBump>,
pub(crate) bump: Vec<VersionBumpSpec>,
pub(crate) short: bool,
pub(crate) output_format: VersionFormat,
pub(crate) dry_run: bool,

View File

@ -248,6 +248,123 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn version_bump_patch_value() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.10.31"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("patch=40"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 1.10.31 => 1.10.40
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "1.10.40"
requires-python = ">=3.12"
"#
);
Ok(())
}
#[test]
fn version_bump_minor_value() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("minor=10"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 1.2.3 => 1.10.0
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "1.10.0"
requires-python = ">=3.12"
"#
);
Ok(())
}
#[test]
fn version_bump_major_value() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "2.3.4"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("major=7"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 2.3.4 => 7.0.0
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "7.0.0"
requires-python = ">=3.12"
"#
);
Ok(())
}
// Bump patch version (--short)
#[test]
fn version_bump_patch_short() -> Result<()> {
@ -289,6 +406,43 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn version_bump_patch_value_must_increase() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "0.0.12"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("patch=11"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: 0.0.12 => 0.0.11 didn't increase the version; provide the exact version to force an update
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "0.0.12"
requires-python = ">=3.12"
"#
);
Ok(())
}
/// Preserve comments immediately preceding the version when bumping
#[test]
fn version_bump_preserves_preceding_comments() -> Result<()> {
@ -781,6 +935,85 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn bump_beta_with_value_existing() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3b4"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("beta=42"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 1.2.3b4 => 1.2.3b42
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "1.2.3b42"
requires-python = ">=3.12"
"#
);
Ok(())
}
#[test]
fn bump_beta_with_value_new() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("beta=5")
.arg("--bump").arg("patch"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 1.2.3 => 1.2.4b5
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "1.2.4b5"
requires-python = ">=3.12"
"#
);
Ok(())
}
// --bump rc
#[test]
fn bump_rc() -> Result<()> {
@ -861,6 +1094,45 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn bump_post_with_value_clears_dev() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3.post4.dev9"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("post=10"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 1.2.3.post4.dev9 => 1.2.3.post10
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "1.2.3.post10"
requires-python = ">=3.12"
"#
);
Ok(())
}
// --bump dev
#[test]
fn bump_dev() -> Result<()> {
@ -901,6 +1173,125 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn bump_dev_with_value() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "0.1.0.dev4"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("dev=42"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 0.1.0.dev4 => 0.1.0.dev42
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "0.1.0.dev42"
requires-python = ">=3.12"
"#
);
Ok(())
}
#[test]
fn bump_patch_and_dev_value() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "0.0.1"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("patch")
.arg("--bump").arg("dev=66463664"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 0.0.1 => 0.0.2.dev66463664
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "0.0.2.dev66463664"
requires-python = ">=3.12"
"#
);
Ok(())
}
#[test]
fn bump_patch_and_dev_explicit_values_sorted() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "0.1.2.dev3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("dev=0")
.arg("--bump").arg("patch=10"), @r"
success: true
exit_code: 0
----- stdout -----
myproject 0.1.2.dev3 => 0.1.10.dev0
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
");
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
assert_snapshot!(
pyproject,
@r#"
[project]
name = "myproject"
version = "0.1.10.dev0"
requires-python = ">=3.12"
"#
);
Ok(())
}
// Bump major but the input version is .post
#[test]
fn version_major_post() -> Result<()> {
@ -941,6 +1332,84 @@ requires-python = ">=3.12"
Ok(())
}
#[test]
fn bump_stable_with_value_fails() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("stable=1"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `--bump stable` does not accept a value
");
Ok(())
}
#[test]
fn bump_empty_value_fails() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("patch="), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `--bump` values cannot be empty
");
Ok(())
}
#[test]
fn bump_invalid_numeric_value_fails() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "myproject"
version = "1.2.3"
requires-python = ">=3.12"
"#,
)?;
uv_snapshot!(context.filters(), context.version()
.arg("--bump").arg("dev=foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid numeric value `foo` for `--bump dev`
");
Ok(())
}
// --bump stable but it decreases the version
#[test]
fn bump_decrease_stable() -> Result<()> {

View File

@ -88,6 +88,14 @@ The `--bump` option supports the following common version components: `major`, `
`stable`, `alpha`, `beta`, `rc`, `post`, and `dev`. When provided more than once, the components
will be applied in order, from largest (`major`) to smallest (`dev`).
You can optionally provide a numeric value with `--bump <component>=<value>` to set the resulting
component explicitly:
```console
$ uv version --bump patch --bump dev=66463664
hello-world 0.0.1 => 0.0.2.dev66463664
```
To move from a stable to pre-release version, bump one of the major, minor, or patch components in
addition to the pre-release component:

View File

@ -1168,7 +1168,7 @@ uv version [OPTIONS] [VALUE]
<p>Can be provided multiple times.</p>
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
<p>WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-version--bump"><a href="#uv-version--bump"><code>--bump</code></a> <i>bump</i></dt><dd><p>Update the project version using the given semantics</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-version--bump"><a href="#uv-version--bump"><code>--bump</code></a> <i>bump[=value]</i></dt><dd><p>Update the project version using the given semantics</p>
<p>This flag can be passed multiple times.</p>
<p>Possible values:</p>
<ul>