///|
pub(all) struct SyncSummary {
module_count : Int
changed : Bool
}
///|
priv struct ManifestSnapshot {
path : String
original : Bytes
updated : String
}
///|
fn workspace_members(
path : StringView,
source : String,
) -> Array[String] raise SyncError {
let (ast, parse_reports) = @moon_config.parse_moon_work(
source,
name=path.to_owned(),
)
let validation_reports = @moon_config.validate_moon_work(ast)
if !parse_reports.is_empty() || !validation_reports.is_empty() {
let message = StringBuilder()
message <+ "invalid `\{path}`"
for report in parse_reports {
message <+ "\n\{report}"
}
for report in validation_reports {
message <+ "\n\{report}"
}
raise SyncError(message.to_string())
}
guard config_field(ast, "members") is Some(Arr(entries, ..)) else {
raise SyncError("invalid `\{path}`: missing workspace members")
}
let members = []
for entry in entries {
guard entry is Str(member_entry, ..) else {
raise SyncError("invalid `\{path}`: workspace member must be a string")
}
members.push(member_entry)
}
if members.is_empty() {
raise SyncError(
"invalid `\{path}`: workspace must contain at least one member",
)
}
members
}
///|
fn join_path(base : String, child : String) -> String {
let child_path : @path.Path = child
if child_path.is_absolute() {
child_path.to_string()
} else {
let base_path : @path.Path = base
base_path.join(child_path).to_string()
}
}
///|
async fn read_manifest(path : String) -> (Bytes, String) {
try {
let data = @fs.read_file(path)
(data.binary(), data.text())
} catch {
error => raise SyncError("failed to read `\{path}`: \{error}")
}
}
///|
async fn collect_snapshots(
root : String,
members : ArrayView[String],
target : Version,
) -> Array[ManifestSnapshot] {
let snapshots = []
let seen : Set[String] = Set([])
for member_entry in members {
let member_path = @fs.realpath(join_path(root, member_entry)) catch {
error =>
raise SyncError(
"failed to resolve workspace member `\{member_entry}`: \{error}",
)
}
if seen.contains(member_path) {
raise SyncError("duplicate workspace member `\{member_entry}`")
}
seen.add(member_path)
let manifest_path = join_path(member_path, "moon.mod")
if !@fs.exists(manifest_path) {
let legacy_path = join_path(member_path, "moon.mod.json")
if @fs.exists(legacy_path) {
raise SyncError(
"workspace member `\{member_entry}` uses unsupported `moon.mod.json`",
)
}
raise SyncError(
"workspace member `\{member_entry}` does not contain `moon.mod`",
)
}
let (original, source) = read_manifest(manifest_path)
let plan = plan_moon_mod(manifest_path, source, target)
if target < plan.current {
raise SyncError(
"target version `\{target.to_string()}` is lower than `\{plan.current.to_string()}` in `\{manifest_path}`",
)
}
snapshots.push({ path: manifest_path, original, updated: plan.updated, })
}
snapshots
}
///|
async fn restore_snapshots(
snapshots : ArrayView[ManifestSnapshot],
) -> String? noraise {
let failures = []
for snapshot in snapshots {
try @fs.write_file(snapshot.path, snapshot.original) catch {
error => failures.push("`\{snapshot.path}`: \{error}")
} noraise {
_ => ()
}
}
if failures.is_empty() {
None
} else {
Some(failures.join("; "))
}
}
///|
async fn apply_and_sync(
root : String,
snapshots : ArrayView[ManifestSnapshot],
) -> Bool {
try {
for snapshot in snapshots {
if snapshot.updated != @fs.read_file(snapshot.path).text() {
@fs.write_file(snapshot.path, snapshot.updated)
}
}
let (exit_code, output) = @process.collect_output_merged(
"moon",
["-q", "work", "sync"],
cwd=root,
extra_env={ "MOON_WORK": join_path(root, "moon.work") },
)
if exit_code != 0 {
let detail = output.text().trim()
if detail.is_empty() {
raise SyncError("`moon work sync` failed with exit code \{exit_code}")
}
raise SyncError(
"`moon work sync` failed with exit code \{exit_code}:\n\{detail}",
)
}
let mut changed = false
for snapshot in snapshots {
if @fs.read_file(snapshot.path).binary() != snapshot.original {
changed = true
}
}
changed
} catch {
error =>
match restore_snapshots(snapshots) {
Some(rollback_error) =>
raise SyncError(
"synchronization failed: \{error}; rollback failed: \{rollback_error}",
)
None => raise error
}
}
}
///|
/// Synchronize every module in the workspace rooted at `root` to `target`.
pub async fn synchronize(root : StringView, target : StringView) -> SyncSummary {
let target_version = parse_target_version(target)
let canonical_root = @fs.realpath(root) catch {
error =>
raise SyncError("failed to resolve workspace root `\{root}`: \{error}")
}
let work_path = join_path(canonical_root, "moon.work")
if !@fs.exists(work_path) {
raise SyncError(
"workspace root does not contain `moon.work`: `\{canonical_root}`",
)
}
let work_source = @fs.read_file(work_path).text() catch {
error => raise SyncError("failed to read `\{work_path}`: \{error}")
}
let members = workspace_members(work_path, work_source)
let snapshots = collect_snapshots(canonical_root, members, target_version)
let changed = apply_and_sync(canonical_root, snapshots)
{ module_count: snapshots.length(), changed, }
}