@@ -531,12 +531,15 @@ impl JjBackend {
531 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
531 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
532 let op_id = unpublished.operation().id().clone();
532 let op_id = unpublished.operation().id().clone();
533 let new_repo = unpublished.leave_unpublished();
533 let new_repo = unpublished.leave_unpublished();
534− // the working-copy state tracks the snapshot tree, against the
534+ // Release the lock *without* finishing: `finish` saves the scanned
535− // current (published) head; persist will finish against its own op
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).
536− locked_ws
542+ drop(locked_ws);
537− .finish(self.current_repo().operation().id().clone())
539− .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
540 let _ = op_id;
543 let _ = op_id;
541 let pending = PendingSnapshot {
544 let pending = PendingSnapshot {
542 pre_repo: self.current_repo(),
545 pre_repo: self.current_repo(),
@@ -1049,7 +1052,12 @@ impl JjBackend {
1051 (Some(m), false) => {
1054 (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.
1053 if !m.authored_by_j {
1061 if !m.authored_by_j {
1054 let Some(parent) = cur.parent_ids().first().cloned() else {
1062 let Some(parent) = cur.parent_ids().first().cloned() else {
1055 return Err((1, "nothing to undo".to_string()));
1063 return Err((1, "nothing to undo".to_string()));
@@ -1057,18 +1065,31 @@ impl JjBackend {
1057 target_op_id = parent;
1065 target_op_id = parent;
1060− cur = block_on(loader.load_operation(&m.id))
1068+ let undo_op = block_on(loader.load_operation(&m.id))
1061 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1069 .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)))?
1075+ // cannot follow the marker: degrade to the undo itself
1063 (Some(m), true) => {
1079 (Some(m), true) => {
1064− // a redo: reverse the undo it reversed, following further
1080+ // a redo: the undo it reversed is already accounted for,
1065− // redos to the operation ultimately reapplied
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").
1066− target_op_id = if m.authored_by_j {
1084+ if !m.authored_by_j {
1067− block_on(chain_through_redos(&loader, &m.id))?
1085+ target_op_id = m.id;
1068− } else {
1086+ break;
1088+ let Some(prev) = op_parent_id(&loader, &m.id)? else {
1089+ return Err((1, "nothing to redo".to_string()));
1071− break;
1091+ cur = block_on(loader.load_operation(&prev))
1092+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
@@ -1136,9 +1157,18 @@ impl JjBackend {
1136 let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
1157 let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
1137 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1158 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1138 let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1159 let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1139− // discard: finish against the current op so no state changes
1160+ if same {
1161+ // the scanned tree is the one already recorded: saving the state
1162+ // only refreshes the mtime cache
1140− block_on(locked_ws.finish(base.operation().id().clone()))
1163+ block_on(locked_ws.finish(base.operation().id().clone()))
1141− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1164+ .map_err(|e| (2, format!("cannot finish: {}", e)))?;
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
1142 drop(ws_guard);
1172 drop(ws_guard);
1143 if !same {
1173 if !same {
1144 return Err(dirty);
1174 return Err(dirty);
@@ -1460,7 +1490,7 @@ enum OpKind {
1460 /// the operation this undo discarded the view of
1490 /// the operation this undo discarded the view of
1461 Undo { #[allow(dead_code)] undid: OperationId },
1491 Undo { #[allow(dead_code)] undid: OperationId },
1462 /// the undo this redo reversed
1492 /// the undo this redo reversed
1463− Redo { redid: OperationId },
1493+ Redo { #[allow(dead_code)] redid: OperationId },
1466/// a marker parsed from an operation description (§7.7 kinds)
1496/// a marker parsed from an operation description (§7.7 kinds)
@@ -1526,24 +1556,6 @@ fn op_parent_id(
1526 Ok(op.parent_ids().first().cloned())
1556 Ok(op.parent_ids().first().cloned())
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();
1537− .load_operation(&id)
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),
1547/// parse the push-record list (§7.6): records of shape {id, name} set a
1559/// parse the push-record list (§7.6): records of shape {id, name} set a
1548/// bookmark, records of shape {delete} remove one; duplicates merge, the same
1560/// bookmark, records of shape {delete} remove one; duplicates merge, the same
1549/// name with different effects crashes (exit 1)
1561/// name with different effects crashes (exit 1)