///|
/// Diffing compares two folio inventories without re-parsing Markdown. This
/// is useful for release notes: changed content should be visible before a
/// site or documentation set is published.
///|
pub(all) enum FolioChangeKind {
Added
Removed
Changed
Renamed
Unchanged
} derive(Eq, Debug)
///|
pub(all) struct FolioSnapshotItem {
path : String
slug : String
fingerprint : String
title : String
permalink : String
format : MatterFormat
} derive(Eq, Debug)
///|
pub(all) struct FolioSnapshot {
items : Array[FolioSnapshotItem]
} derive(Eq, Debug)
///|
pub(all) struct FolioChange {
kind : FolioChangeKind
before_path : String
after_path : String
slug : String
before_fingerprint : String
after_fingerprint : String
title : String
message : String
} derive(Eq, Debug)
///|
pub(all) struct DiffReport {
has_changes : Bool
added : Int
removed : Int
changed : Int
renamed : Int
unchanged : Int
changes : Array[FolioChange]
} derive(Eq, Debug)
///|
pub fn snapshot_from_registry(registry : FolioRegistry) -> FolioSnapshot {
let items : Array[FolioSnapshotItem] = []
for record in registry.records {
items.push(snapshot_item_from_record(record))
}
{ items, }
}
///|
pub fn snapshot_item_from_record(record : FolioRecord) -> FolioSnapshotItem {
{
path: record.path,
slug: record.folio.slug,
fingerprint: record.folio.fingerprint,
title: record.folio.title,
permalink: record.folio.permalink,
format: record.folio.format,
}
}
///|
pub fn snapshot_from_manifest(manifest : String) -> FolioSnapshot {
let items : Array[FolioSnapshotItem] = []
let lines = manifest.split("\n").map(view => view.to_owned()).to_array()
for line in lines {
let trimmed = line.trim().to_owned()
if !trimmed.is_empty() {
match snapshot_item_from_manifest_line(trimmed) {
Some(item) => items.push(item)
None => ()
}
}
}
{ items, }
}
///|
pub fn snapshot_item_from_manifest_line(line : String) -> FolioSnapshotItem? {
let parts = line.split("|").map(view => view.to_owned()).to_array()
if parts.length() < 4 {
return None
}
let slug = parts[0]
let fingerprint = parts[1]
let format = matter_format_from_name(parts[2])
let title = join_manifest_title(parts, 3)
Some({
path: slug + ".md",
slug,
fingerprint,
title,
permalink: "/" + slug,
format,
})
}
///|
pub fn diff_registries(
before : FolioRegistry,
after : FolioRegistry,
) -> DiffReport {
diff_snapshots(snapshot_from_registry(before), snapshot_from_registry(after))
}
///|
pub fn diff_manifests(before : String, after : String) -> DiffReport {
diff_snapshots(snapshot_from_manifest(before), snapshot_from_manifest(after))
}
///|
pub fn diff_snapshots(
before : FolioSnapshot,
after : FolioSnapshot,
) -> DiffReport {
let changes : Array[FolioChange] = []
for before_item in before.items {
match snapshot_find_by_path(after, before_item.path) {
Some(after_item) =>
if before_item.fingerprint == after_item.fingerprint {
changes.push(
make_change(
Unchanged,
before_item,
after_item,
"unchanged folio `" + before_item.slug + "`",
),
)
} else {
changes.push(
make_change(
Changed,
before_item,
after_item,
"changed folio `" + before_item.slug + "`",
),
)
}
None =>
match snapshot_find_by_fingerprint(after, before_item.fingerprint) {
Some(after_item) =>
changes.push(
make_change(
Renamed,
before_item,
after_item,
"renamed folio `" +
before_item.path +
"` -> `" +
after_item.path +
"`",
),
)
None => changes.push(make_removed_change(before_item))
}
}
}
for after_item in after.items {
if snapshot_find_by_path(before, after_item.path) is Some(_) {
continue
}
if snapshot_find_by_fingerprint(before, after_item.fingerprint) is Some(_) {
continue
}
changes.push(make_added_change(after_item))
}
summarize_changes(changes)
}
///|
pub fn diff_added(report : DiffReport) -> Array[FolioChange] {
changes_by_kind(report, Added)
}
///|
pub fn diff_removed(report : DiffReport) -> Array[FolioChange] {
changes_by_kind(report, Removed)
}
///|
pub fn diff_changed(report : DiffReport) -> Array[FolioChange] {
changes_by_kind(report, Changed)
}
///|
pub fn diff_renamed(report : DiffReport) -> Array[FolioChange] {
changes_by_kind(report, Renamed)
}
///|
pub fn diff_unchanged(report : DiffReport) -> Array[FolioChange] {
changes_by_kind(report, Unchanged)
}
///|
pub fn diff_is_release_safe(report : DiffReport) -> Bool {
report.removed == 0 && report.changed == 0
}
///|
pub fn diff_summary_line(report : DiffReport) -> String {
"added=" +
report.added.to_string() +
", removed=" +
report.removed.to_string() +
", changed=" +
report.changed.to_string() +
", renamed=" +
report.renamed.to_string() +
", unchanged=" +
report.unchanged.to_string()
}
///|
pub fn diff_change_kind_name(kind : FolioChangeKind) -> String {
match kind {
Added => "added"
Removed => "removed"
Changed => "changed"
Renamed => "renamed"
Unchanged => "unchanged"
}
}
///|
pub fn diff_change_lines(report : DiffReport) -> Array[String] {
let lines : Array[String] = []
for change in report.changes {
lines.push(diff_change_kind_name(change.kind) + ": " + change.message)
}
lines
}
///|
pub fn diff_paths(report : DiffReport) -> Array[String] {
let paths : Array[String] = []
for change in report.changes {
let path = if change.after_path.is_empty() {
change.before_path
} else {
change.after_path
}
if !contains_string(paths, path) {
paths.push(path)
}
}
sorted_strings(paths)
}
///|
pub fn snapshot_paths(snapshot : FolioSnapshot) -> Array[String] {
let paths : Array[String] = []
for item in snapshot.items {
paths.push(item.path)
}
sorted_strings(paths)
}
///|
pub fn snapshot_slugs(snapshot : FolioSnapshot) -> Array[String] {
let slugs : Array[String] = []
for item in snapshot.items {
if !contains_string(slugs, item.slug) {
slugs.push(item.slug)
}
}
sorted_strings(slugs)
}
///|
pub fn snapshot_fingerprints(snapshot : FolioSnapshot) -> Array[String] {
let values : Array[String] = []
for item in snapshot.items {
if !contains_string(values, item.fingerprint) {
values.push(item.fingerprint)
}
}
sorted_strings(values)
}
///|
pub fn snapshot_find_by_path(
snapshot : FolioSnapshot,
path : String,
) -> FolioSnapshotItem? {
for item in snapshot.items {
if item.path == path {
return Some(item)
}
}
None
}
///|
pub fn snapshot_find_by_slug(
snapshot : FolioSnapshot,
slug : String,
) -> FolioSnapshotItem? {
for item in snapshot.items {
if item.slug == slug {
return Some(item)
}
}
None
}
///|
pub fn snapshot_find_by_fingerprint(
snapshot : FolioSnapshot,
fingerprint : String,
) -> FolioSnapshotItem? {
for item in snapshot.items {
if item.fingerprint == fingerprint {
return Some(item)
}
}
None
}
///|
pub fn snapshot_manifest(snapshot : FolioSnapshot) -> String {
let lines : Array[String] = []
for item in snapshot.items {
lines.push(
item.slug +
"|" +
item.fingerprint +
"|" +
format_name(item.format) +
"|" +
item.title,
)
}
lines.join("\n")
}
///|
pub fn snapshot_sorted(snapshot : FolioSnapshot) -> FolioSnapshot {
let items : Array[FolioSnapshotItem] = []
for item in snapshot.items {
insert_snapshot_item_by_path(items, item)
}
{ items, }
}
///|
pub fn snapshot_without_unchanged(report : DiffReport) -> DiffReport {
let changes : Array[FolioChange] = []
for change in report.changes {
if change.kind != Unchanged {
changes.push(change)
}
}
summarize_changes(changes)
}
///|
fn changes_by_kind(
report : DiffReport,
kind : FolioChangeKind,
) -> Array[FolioChange] {
let values : Array[FolioChange] = []
for change in report.changes {
if change.kind == kind {
values.push(change)
}
}
values
}
///|
fn summarize_changes(changes : Array[FolioChange]) -> DiffReport {
let mut added = 0
let mut removed = 0
let mut changed = 0
let mut renamed = 0
let mut unchanged = 0
for change in changes {
match change.kind {
Added => added = added + 1
Removed => removed = removed + 1
Changed => changed = changed + 1
Renamed => renamed = renamed + 1
Unchanged => unchanged = unchanged + 1
}
}
{
has_changes: added + removed + changed + renamed > 0,
added,
removed,
changed,
renamed,
unchanged,
changes,
}
}
///|
fn make_added_change(item : FolioSnapshotItem) -> FolioChange {
{
kind: Added,
before_path: "",
after_path: item.path,
slug: item.slug,
before_fingerprint: "",
after_fingerprint: item.fingerprint,
title: item.title,
message: "added folio `" + item.path + "`",
}
}
///|
fn make_removed_change(item : FolioSnapshotItem) -> FolioChange {
{
kind: Removed,
before_path: item.path,
after_path: "",
slug: item.slug,
before_fingerprint: item.fingerprint,
after_fingerprint: "",
title: item.title,
message: "removed folio `" + item.path + "`",
}
}
///|
fn make_change(
kind : FolioChangeKind,
before : FolioSnapshotItem,
after : FolioSnapshotItem,
message : String,
) -> FolioChange {
{
kind,
before_path: before.path,
after_path: after.path,
slug: after.slug,
before_fingerprint: before.fingerprint,
after_fingerprint: after.fingerprint,
title: after.title,
message,
}
}
///|
fn matter_format_from_name(name : String) -> MatterFormat {
match name {
"yaml" => Yaml
"toml" => Toml
"json" => Json
_ => None
}
}
///|
fn join_manifest_title(parts : Array[String], start : Int) -> String {
let rest : Array[String] = []
for i in start.. Unit {
for i in 0..