M8: branch version history and finer list merges

- A branch keeps its own version history. On a branch tab, `file.saveVersion`, `version.autosave` and the `version.*` commands (list, preview, restore, rename, delete, duplicate) act on the branch's versions; the main file's history is untouched. The Version history panel on a branch tab lists the branch's versions (its banner gives way to the version banner while one is viewed).
- Branch versions are saved in the `.figcraft` file with the branch (`branches/<id>/versions/index.json` plus one snapshot each, the same layout as the file's own history; images shared; damaged entries skipped and reported; the snapshot byte budget is shared across branches).
- Merging adds a version to the main file's history (`Merged "<name>"`, after an autosave of main when it had changed); the merged or archived branch keeps its own history and opens with it.
- Fills, strokes and effects merge item by item in `figcraft_collab::merge`: different items changed on each side, or items added at the end on one side (or the same ones on both), merge without a conflict; the same item changed both ways, an item removed on one side, or different items added on both sides still conflict as the whole list. Both sides' writes become the merged list, so merging and Update from main write the same result.
This commit is contained in:
bflatastic
2026-10-12 02:30:25 +07:00
committed by BFlat
parent 970eb001f1
commit c50bbd08bd
18 changed files with 470 additions and 80 deletions
+8 -5
View File
@@ -121,7 +121,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"file.saveVersion",
"Save to version history",
"{name?, description?} snapshot the document into the file's version history (kept when the file is saved); returns the version",
"{name?, description?} snapshot the document into the file's version history (on a branch tab: the branch's own history; kept when the file is saved); returns the version",
false,
save_version
),
@@ -252,8 +252,7 @@ pub const MAX_AUTOSAVES: usize = 20;
/// `version.autosave`: a checkpoint of a document with unsaved changes since the last version.
fn autosave(s: &mut Session, _: &Value) -> Result<Value> {
let ds = s.ds_mut()?;
// A branch's versions would have nowhere to be saved.
if ds.preview.is_some() || ds.branch_of.is_some() || !ds.is_dirty() || !ds.changed_since_checkpoint() {
if ds.preview.is_some() || !ds.is_dirty() || !ds.changed_since_checkpoint() {
return Ok(json!({"saved": false}));
}
Ok(match add_version(ds, AUTOSAVE, "", true) {
@@ -348,10 +347,14 @@ pub(crate) fn unique_id(taken: impl Fn(&str) -> bool) -> String {
}
}
/// `file.saveVersion`; on a branch tab the version goes into the branch's own history.
fn save_version(s: &mut Session, p: &Value) -> Result<Value> {
let ds = s.ds_mut()?;
if ds.branch_of.is_some() {
return Err(EngineError::Disabled("file.saveVersion".into(), "versions are saved on the main file, not on a branch".into()));
if ds.versions.len() >= figcraft_format::MAX_VERSIONS {
return Err(EngineError::Disabled(
"file.saveVersion".into(),
format!("the version history is full ({} versions): delete old versions first", figcraft_format::MAX_VERSIONS),
));
}
let name =
string(p, "name").map(|n| n.trim().to_string()).filter(|n| !n.is_empty()).unwrap_or_else(|| format!("Version {}", ds.versions.len() + 1));