Skip to content

Commit

Permalink
fix: handle General/Complex Versioning in --bump (#2889)
Browse files Browse the repository at this point in the history
* fix: handle General/Complex Versioning in --bump

The previous fix to handle non-semver versions in `--bump` used a crude
heuristic - a check whether there's a "." in the version string - to
decide whether to attempt to `chunkify` or just fall back to returning
the new version unmodified. This, however, resulted in no bump happening
(`None` returned from `check_semver_bump`) when either of the old/new
versions looked like "v1.2.3".

Additionally, versions like "1.2a1" and "1.2a2" were considered the
same, because: a) the heuristic failed to recognise they won't be
parsed/chunkified correctly, and b) `chunkify` used `nth` that silently
throws parts of the version away.

I'm proposing to fix this by reimplementing `chunkify` using the much
more generic versions::Mess format, which simply splits the version
number into chunks and separators. We then convert it into a simpler
format of just chunks (first chunk as is, further chunks with a
separator prepended).

This design has an issue: we don't recognise a change in the versioning
scheme. A bump from "<commit sha>" to "v1.2.3" will result in just "v1",
because the commit sha is parsed as a single chunk. The most obvious
case of this, "latest" being parsed as a single chunk, is handled
explicitly in the code, as it would just be a mess otherwise.

A potential workaround for this issue would be to add a flag (e.g.
`--pin`) that would make `--bump` skip the `check_semver_bump` logic and
always use the full new version (as suggested in
#2704 (comment)).
This would also help in the following case: a project using variable
length version numbers instead of the full 3-chunk semver. Trying to
follow this sequence of bumps: "20.0", "20.0.1", "20.1" isn't possible
with the current logic.

Related: 0b2c2aa ("fix: upgrade --bump with non-semver versions (#2809)")

* fix: Allow --bump from 20.0.1 to 20.1

It's weird so we still warn, but returning `None` from
`check_semver_bump` only makes sense if the versions are deemed to be
the same. Otherwise it's just confusion for the user — the UI presents
this as an upgrade, proceeds to uninstall the old version, but fails to
do the actual bump and no new version is installed.

* fix: handle prefix: versions

---------

Co-authored-by: jdx <[email protected]>
  • Loading branch information
liskin and jdx authored Nov 29, 2024
1 parent 0461143 commit e5efc7f
Showing 1 changed file with 65 additions and 37 deletions.
102 changes: 65 additions & 37 deletions src/toolset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub use tool_request_set::{ToolRequestSet, ToolRequestSetBuilder};
pub use tool_source::ToolSource;
pub use tool_version::{ResolveOptions, ToolVersion};
pub use tool_version_list::ToolVersionList;
use versions::{Version, Versioning};
use versions::{Mess, Version, Versioning};
use xx::regex;

mod builder;
Expand Down Expand Up @@ -764,53 +764,61 @@ pub fn is_outdated_version(current: &str, latest: &str) -> bool {
/// used with `mise outdated --bump` to determine what new semver range to use
/// given old: "20" and new: "21.2.3", return Some("21")
fn check_semver_bump(old: &str, new: &str) -> Option<String> {
if !old.contains('.') && !new.contains('.') {
return Some(new.to_string());
}
let old_v = Versioning::new(old);
let new_v = Versioning::new(new);
let chunkify = |v: &Versioning| {
let mut chunks = vec![];
while let Some(chunk) = v.nth(chunks.len()) {
chunks.push(chunk);
}
chunks
};
if let (Some(old), Some(new)) = (old_v, new_v) {
let old = chunkify(&old);
let new = chunkify(&new);
if old.len() > new.len() {
if let Some(("prefix", old_)) = old.split_once(':') {
return check_semver_bump(old_, new);
}
let old_chunks = chunkify_version(old);
let new_chunks = chunkify_version(new);
if !old_chunks.is_empty() && !new_chunks.is_empty() {
if old_chunks.len() > new_chunks.len() {
warn!(
"something weird happened with versioning, old: {old}, new: {new}, skipping",
old = old
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
new = new
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
"something weird happened with versioning, old: {old:?}, new: {new:?}",
old = old_chunks,
new = new_chunks,
);
return None;
}
let bump = new.into_iter().take(old.len()).collect::<Vec<_>>();
if bump == old {
let bump = new_chunks
.into_iter()
.take(old_chunks.len())
.collect::<Vec<_>>();
if bump == old_chunks {
None
} else {
Some(
bump.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
)
Some(bump.join(""))
}
} else {
Some(new.to_string())
}
}

/// split a version number into chunks
/// given v: "1.2-3a4" return ["1", ".2", "-3", "a4"]
fn chunkify_version(v: &str) -> Vec<String> {
fn chunkify(m: &Mess, sep0: &str, chunks: &mut Vec<String>) {
for (i, chunk) in m.chunks.iter().enumerate() {
let sep = if i == 0 { sep0 } else { "." };
chunks.push(format!("{}{}", sep, chunk));
}
if let Some((next_sep, next_mess)) = &m.next {
chunkify(next_mess, next_sep.to_string().as_ref(), chunks)
}
}

let mut chunks = vec![];
// don't parse "latest", otherwise bump from latest to any version would have one chunk only
if v != "latest" {
if let Some(v) = Versioning::new(v) {
let m = match v {
Versioning::Ideal(sem_ver) => sem_ver.to_mess(),
Versioning::General(version) => version.to_mess(),
Versioning::Complex(mess) => mess,
};
chunkify(&m, "", &mut chunks);
}
}
chunks
}

#[derive(Debug, Serialize, Clone, Tabled)]
pub struct OutdatedInfo {
pub name: String,
Expand Down Expand Up @@ -921,10 +929,30 @@ mod tests {
check_semver_bump("20.0.0", "20.0.1"),
Some("20.0.1".to_string())
);
std::assert_eq!(
check_semver_bump("20.0.1", "20.1"),
Some("20.1".to_string())
);
std::assert_eq!(
check_semver_bump("2024-09-16", "2024-10-21"),
Some("2024-10-21".to_string())
);
std::assert_eq!(
check_semver_bump("20.0a1", "20.0a2"),
Some("20.0a2".to_string())
);
std::assert_eq!(check_semver_bump("v20", "v20.0.0"), None);
std::assert_eq!(check_semver_bump("v20.0", "v20.0.0"), None);
std::assert_eq!(check_semver_bump("v20.0.0", "v20.0.0"), None);
std::assert_eq!(check_semver_bump("v20", "v21.0.0"), Some("v21".to_string()));
std::assert_eq!(
check_semver_bump("v20.0.0", "v20.0.1"),
Some("v20.0.1".to_string())
);
std::assert_eq!(
check_semver_bump("latest", "20.0.0"),
Some("20.0.0".to_string())
);
}

#[test]
Expand Down

0 comments on commit e5efc7f

Please sign in to comment.