@@ -531,12 +531,15 @@ impl JjBackend {
531531 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
532532 let op_id = unpublished.operation().id().clone();
533533 let new_repo = unpublished.leave_unpublished();
534− // the working-copy state tracks the snapshot tree, against the
535− // current (published) head; persist will finish against its own op
536− locked_ws
537− .finish(self.current_repo().operation().id().clone())
538− .await
539− .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
534+ // Release the lock *without* finishing: `finish` saves the scanned
535+ // tree state to disk, which would record the working directory as
536+ // already snapshotted while the published head still has the old wc
537+ // tree. The next run's incremental snapshot would then find nothing
538+ // to do and build the Repo from the stale tree — and a persisting run
539+ // would check that stale tree back out, destroying the edits. The
540+ // state is advanced only by `persist`'s checkout, which finishes
541+ // against the operation that actually recorded the tree (§7.4/§7.7).
542+ drop(locked_ws);
540543 let _ = op_id;
541544 let pending = PendingSnapshot {
542545 pre_repo: self.current_repo(),
@@ -1049,7 +1052,12 @@ impl JjBackend {
10491052 break;
10501053 }
10511054 (Some(m), false) => {
1052− // a redo: move past the undo it reversed
1055+ // a redo: "move past the operation it reapplied" (§7.7).
1056+ // The redo marker names the *undo* it reversed; the
1057+ // operation that undo discarded is the one this redo put
1058+ // back, so that is where the walk continues. Stopping at
1059+ // the undo instead would make the loop treat it as an undo
1060+ // to follow and step back one operation too far.
10531061 if !m.authored_by_j {
10541062 let Some(parent) = cur.parent_ids().first().cloned() else {
10551063 return Err((1, "nothing to undo".to_string()));
@@ -1057,18 +1065,31 @@ impl JjBackend {
10571065 target_op_id = parent;
10581066 break;
10591067 }
1060− cur = block_on(loader.load_operation(&m.id))
1068+ let undo_op = block_on(loader.load_operation(&m.id))
10611069 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1070+ cur = match op_marker(undo_op.metadata()) {
1071+ Some(u) if u.kind == "undo" && u.authored_by_j => {
1072+ block_on(loader.load_operation(&u.id))
1073+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?
1074+ }
1075+ // cannot follow the marker: degrade to the undo itself
1076+ _ => undo_op,
1077+ };
10621078 }
10631079 (Some(m), true) => {
1064− // a redo: reverse the undo it reversed, following further
1065− // redos to the operation ultimately reapplied
1066− target_op_id = if m.authored_by_j {
1067− block_on(chain_through_redos(&loader, &m.id))?
1068− } else {
1069− m.id
1080+ // a redo: the undo it reversed is already accounted for,
1081+ // so keep looking outward from that undo's parent for an
1082+ // undo that has not been reversed yet (§7.7: the head must
1083+ // be an undo, "possibly reached through redos").
1084+ if !m.authored_by_j {
1085+ target_op_id = m.id;
1086+ break;
1087+ }
1088+ let Some(prev) = op_parent_id(&loader, &m.id)? else {
1089+ return Err((1, "nothing to redo".to_string()));
10701090 };
1071− break;
1091+ cur = block_on(loader.load_operation(&prev))
1092+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
10721093 }
10731094 }
10741095 }
@@ -1136,9 +1157,18 @@ impl JjBackend {
11361157 let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
11371158 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
11381159 let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1139− // discard: finish against the current op so no state changes
1140− block_on(locked_ws.finish(base.operation().id().clone()))
1141− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1160+ if same {
1161+ // the scanned tree is the one already recorded: saving the state
1162+ // only refreshes the mtime cache
1163+ block_on(locked_ws.finish(base.operation().id().clone()))
1164+ .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1165+ } else {
1166+ // the working copy differs from @ and this command records
1167+ // nothing: drop the lock without saving, or the state would claim
1168+ // the directory was snapshotted and hide the changes from the next
1169+ // run (§7.7)
1170+ drop(locked_ws);
1171+ }
11421172 drop(ws_guard);
11431173 if !same {
11441174 return Err(dirty);
@@ -1460,7 +1490,7 @@ enum OpKind {
14601490 /// the operation this undo discarded the view of
14611491 Undo { #[allow(dead_code)] undid: OperationId },
14621492 /// the undo this redo reversed
1463− Redo { redid: OperationId },
1493+ Redo { #[allow(dead_code)] redid: OperationId },
14641494}
14651495
14661496/// a marker parsed from an operation description (§7.7 kinds)
@@ -1526,24 +1556,6 @@ fn op_parent_id(
15261556 Ok(op.parent_ids().first().cloned())
15271557}
15281558
1529−/// follow a chain of redos to the operation that was ultimately reapplied
1530−async fn chain_through_redos(
1531− loader: &jj_lib::repo::RepoLoader,
1532− first: &OperationId,
1533−) -> Result<OperationId, OpenError> {
1534− let mut id = first.clone();
1535− loop {
1536− let op = loader
1537− .load_operation(&id)
1538− .await
1539− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1540− match op_kind(op.metadata()) {
1541− OpKind::Redo { redid } => id = redid,
1542− _ => return Ok(id),
1543− }
1544− }
1545−}
1546−
15471559/// parse the push-record list (§7.6): records of shape {id, name} set a
15481560/// bookmark, records of shape {delete} remove one; duplicates merge, the same
15491561/// name with different effects crashes (exit 1)