// `mctl upgrade` — move the moonbitstack pins in a `moon.mod` to a release, without
// touching anything else in the file.
//
// A `moon.mod` pins its dependencies inside an `import { "org/name@1.2.3" }` block,
// one quoted string per line. The rewrite is line-oriented on purpose: comments,
// ordering and everything mctl does not own survive it untouched.
///|
/// The version `name` is pinned at in `mod`, or `None` when the file does not pin
/// it.
pub fn pinned_version(mod : String, name : String) -> String? {
let head = "\"" + name + "@"
for line in @text.split_char(mod, '\n') {
let at = @text.index_of_str(line, head)
if at < 0 {
continue
}
let start = at + head.length()
let rest = line[start:].to_owned()
let close = @text.index_of_str(rest, "\"")
if close > 0 {
return Some(rest[0:close].to_owned())
}
}
None
}
///|
/// The version strings of `mod`, as `(name, version)` pairs: every pin the file
/// carries, in order.
pub fn mod_pins(mod : String) -> Array[(String, String)] {
let out : Array[(String, String)] = []
for line in @text.split_char(mod, '\n') {
let open_at = @text.index_of_str(line, "\"")
if open_at < 0 {
continue
}
let rest = line[open_at + 1:].to_owned()
let close = @text.index_of_str(rest, "\"")
if close < 0 {
continue
}
let pinned = rest[0:close].to_owned()
let at = @text.index_of_str(pinned, "@")
if at <= 0 {
continue
}
out.push((pinned[0:at].to_owned(), pinned[at + 1:].to_owned()))
}
out
}
///|
/// `mod` with `name` pinned at `version`, or `mod` unchanged when it does not pin
/// that name. Returns the new text and the change it made, if any.
pub fn pin_module(
mod : String,
name : String,
version : String,
) -> (String, String?) {
let old = pinned_version(mod, name)
match old {
None => (mod, None)
Some(was) =>
if was == version {
(mod, None)
} else {
let head = "\"" + name + "@"
let lines : Array[String] = []
for line in @text.split_char(mod, '\n') {
let at = @text.index_of_str(line, head)
if at < 0 {
lines.push(line)
continue
}
let start = at + head.length()
let rest = line[start:].to_owned()
let close = @text.index_of_str(rest, "\"")
if close > 0 {
lines.push(
line[0:start].to_owned() + version + rest[close:].to_owned(),
)
} else {
lines.push(line)
}
}
(@text.join_lines(lines), Some(name + " " + was + " → " + version))
}
}
}
///|
/// `mctl upgrade `: every name in `modules` moved to `version`, plus one
/// line per name that changed. A name the file does not pin is left alone and
/// reported as skipped — pinning it would be a change the user did not ask for.
pub fn upgrade_pins(
mod : String,
modules : Array[String],
version : String,
) -> (String, Array[String]) {
let changes : Array[String] = []
let mut text = mod
for m in modules {
match pin_module(text, m, version) {
(next, Some(change)) => {
text = next
changes.push(change)
}
(_, None) =>
if pinned_version(text, m) == None {
changes.push(m + " is not pinned, left alone")
}
}
}
(text, changes)
}