Skip to content

Commit

Permalink
fix: handle General/Complex Versioning in --bump
Browse files Browse the repository at this point in the history
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)")
  • Loading branch information
liskin committed Nov 1, 2024
1 parent 40966a9 commit 6b183df
Showing 1 changed file with 58 additions and 36 deletions.
94 changes: 58 additions & 36 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::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 @@ -663,53 +663,59 @@ 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() {
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:?}, skipping",
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 @@ -822,6 +828,22 @@ mod tests {
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 6b183df

Please sign in to comment.