|
|
… |
16 unchanged lines |
| 17 |
17 |
|
fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> { |
| 18 |
18 |
|
let heads = self.bookmark_heads()?; |
| 19 |
19 |
|
if !heads.contains(hash) { |
| 20 |
|
- |
return fail(format!( |
| 21 |
|
- |
"change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet", |
| 22 |
|
- |
hash.short(12), |
| 23 |
|
- |
self.config.bookmark |
| 24 |
|
- |
)); |
|
20 |
+ |
return fail(format!("change #{} is not a head of {}; amend works on heads only (describe, split, and abandon work anywhere)", hash.short(12), self.config.bookmark)); |
| 25 |
21 |
|
} |
| 26 |
22 |
|
let c = self.store.get_change(hash)?; |
| 27 |
23 |
|
if c.kind == ChangeKind::WorkingCopy { |
|
|
… |
356 unchanged lines |
| 384 |
380 |
|
Ok(new_hash) |
| 385 |
381 |
|
} |
| 386 |
382 |
|
|
| 387 |
|
- |
/// Split a head into two changes: one holding the head's effect on |
| 388 |
|
- |
/// `paths`, the other the rest (keeping the head's ChangeId). The |
| 389 |
|
- |
/// working copy is unaffected. |
|
383 |
+ |
/// Split a change into two: one holding its effect on `paths`, the |
|
384 |
+ |
/// other the rest (keeping the ChangeId). A head is rebuilt from its |
|
385 |
+ |
/// materialized files; any other change is split by node, which is |
|
386 |
+ |
/// exact, and is refused when a dependent refers to the split-off |
|
387 |
+ |
/// nodes. The working copy is unaffected. |
| 390 |
388 |
|
pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> { |
| 391 |
|
- |
let old = self.rewritable_head(hash)?; |
| 392 |
389 |
|
if paths.is_empty() { |
| 393 |
390 |
|
return fail("name at least one path to split out"); |
| 394 |
391 |
|
} |
|
392 |
+ |
if !self.bookmark_heads()?.contains(hash) { |
|
393 |
+ |
return self.split_by_node(hash, paths); |
|
394 |
+ |
} |
|
395 |
+ |
let old = self.rewritable_head(hash)?; |
| 395 |
396 |
|
let beneath = self.load_state(&old.deps)?; |
| 396 |
397 |
|
let beneath_snapshot = beneath.materialize(); |
| 397 |
398 |
|
let full = self.load_state(&[*hash])?; |
|
|
… |
65 unchanged lines |
| 463 |
464 |
|
c2.deps = d2.into_iter().collect(); |
| 464 |
465 |
|
c2.normalize(); |
| 465 |
466 |
|
let h2 = self.replace_head(hash, Some(&c2), &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap(); |
| 466 |
|
- |
// c1 may be a head of its own if c2 does not depend on it. |
|
467 |
+ |
self.add_to_bookmark(h1)?; |
|
468 |
+ |
Ok((h1, h2)) |
|
469 |
+ |
} |
|
470 |
+ |
|
|
471 |
+ |
/// Put `h` into the bookmark's state (a head unless something already |
|
472 |
+ |
/// depends on it) and under the working copy. |
|
473 |
+ |
fn add_to_bookmark(&self, h: ChangeHash) -> Result<()> { |
| 467 |
474 |
|
let (op_ref, op) = self.head_op()?; |
| 468 |
475 |
|
let bookmark = self.config.bookmark.clone(); |
| 469 |
476 |
|
let mut heads = self.heads_of(&op.view, &bookmark)?; |
| 470 |
|
- |
heads.push(h1); |
|
477 |
+ |
heads.push(h); |
| 471 |
478 |
|
let st = self.load_state(&heads)?; |
| 472 |
479 |
|
let heads = st.maximal(&heads); |
| 473 |
480 |
|
let mut view = op.view.clone(); |
| 474 |
481 |
|
view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(heads.clone()))?); |
| 475 |
482 |
|
let (_, wc) = self.working_copy()?; |
| 476 |
483 |
|
let mut base = wc.deps.clone(); |
| 477 |
|
- |
base.push(h1); |
|
484 |
+ |
base.push(h); |
| 478 |
485 |
|
let wc_base = self.load_state(&base)?.maximal(&base); |
| 479 |
486 |
|
let fresh = self.new_working_copy(wc_base, &wc.message); |
| 480 |
487 |
|
view.workspaces.insert(self.workspace, self.store.put_change(&fresh)?); |
| 481 |
488 |
|
self.record_op(&[op_ref], "oxy split (heads)", view)?; |
| 482 |
489 |
|
self.drop_manifest(); |
|
490 |
+ |
Ok(()) |
|
491 |
+ |
} |
|
492 |
+ |
|
|
493 |
+ |
/// Split a non-head change by node: the operations and edits on nodes |
|
494 |
+ |
/// under `paths` become a new change, the rest keeps the ChangeId with |
|
495 |
+ |
/// every remaining atom at its old index, so dependents are rewritten |
|
496 |
+ |
/// exactly. Refused when a dependent touches a split-off node. |
|
497 |
+ |
fn split_by_node(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> { |
|
498 |
+ |
let old = self.store.get_change(hash)?; |
|
499 |
+ |
if old.kind == ChangeKind::WorkingCopy { |
|
500 |
+ |
return fail("that is the working copy; commit it instead"); |
|
501 |
+ |
} |
|
502 |
+ |
let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))); |
|
503 |
+ |
let beneath = self.load_state(&old.deps)?; |
|
504 |
+ |
let full = self.load_state(&[*hash])?; |
|
505 |
+ |
let mut removed: BTreeSet<NodeId> = BTreeSet::new(); |
|
506 |
+ |
for snap in [beneath.materialize(), full.materialize()] { |
|
507 |
+ |
for (p, f) in &snap.files { |
|
508 |
+ |
if selected(p) { |
|
509 |
+ |
removed.insert(f.node); |
|
510 |
+ |
} |
|
511 |
+ |
} |
|
512 |
+ |
} |
|
513 |
+ |
// Nodes the change itself adds under a removed directory. |
|
514 |
+ |
for op in &old.tree_ops { |
|
515 |
+ |
if let TreeOp::AddNode { node, parent, .. } = op { |
|
516 |
+ |
if removed.contains(parent) { |
|
517 |
+ |
removed.insert(*node); |
|
518 |
+ |
} |
|
519 |
+ |
} |
|
520 |
+ |
} |
|
521 |
+ |
if removed.is_empty() { |
|
522 |
+ |
return fail("the named paths are not touched by that change"); |
|
523 |
+ |
} |
|
524 |
+ |
// Dependents must not touch the split-off nodes: their references |
|
525 |
+ |
// would have to move to the new change, which a rewrite cannot express. |
|
526 |
+ |
let heads = self.bookmark_heads()?; |
|
527 |
+ |
let (wc_hash, _) = self.working_copy()?; |
|
528 |
+ |
let mut all = heads.clone(); |
|
529 |
+ |
all.push(wc_hash); |
|
530 |
+ |
let state = self.load_state(&all)?; |
|
531 |
+ |
for (h, meta) in state.changes() { |
|
532 |
+ |
if !meta.deps.contains(hash) { |
|
533 |
+ |
continue; |
|
534 |
+ |
} |
|
535 |
+ |
let d = self.store.get_change(h)?; |
|
536 |
+ |
if d.tree_ops.iter().any(|op| removed.contains(&op.node())) || d.edits.iter().any(|e| removed.contains(&e.node())) { |
|
537 |
+ |
return fail(format!("change {} depends on the split-off paths of {}; split it first", d.change_id.short(8), old.change_id.short(8))); |
|
538 |
+ |
} |
|
539 |
+ |
} |
|
540 |
+ |
let mut c1 = Change { |
|
541 |
+ |
change_id: ChangeId::random(), |
|
542 |
+ |
deps: old.deps.clone(), |
|
543 |
+ |
author: old.author, |
|
544 |
+ |
hlc: Hlc::now(), |
|
545 |
+ |
message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")), |
|
546 |
+ |
tree_ops: old.tree_ops.iter().filter(|op| removed.contains(&op.node())).cloned().collect(), |
|
547 |
+ |
edits: old.edits.iter().filter(|e| removed.contains(&e.node())).cloned().collect(), |
|
548 |
+ |
kind: ChangeKind::Normal, |
|
549 |
+ |
}; |
|
550 |
+ |
let mut c2 = old.clone(); |
|
551 |
+ |
c2.hlc = Hlc::now(); |
|
552 |
+ |
let mut ops_map: HashMap<u32, u32> = HashMap::new(); |
|
553 |
+ |
let mut kept_ops = Vec::new(); |
|
554 |
+ |
for (i, op) in old.tree_ops.iter().enumerate() { |
|
555 |
+ |
if !removed.contains(&op.node()) { |
|
556 |
+ |
ops_map.insert(i as u32, kept_ops.len() as u32); |
|
557 |
+ |
kept_ops.push(op.clone()); |
|
558 |
+ |
} |
|
559 |
+ |
} |
|
560 |
+ |
c2.tree_ops = kept_ops; |
|
561 |
+ |
c2.edits.retain(|e| !removed.contains(&e.node())); |
|
562 |
+ |
if c1.tree_ops.is_empty() && c1.edits.is_empty() { |
|
563 |
+ |
return fail("the named paths are not touched by that change"); |
|
564 |
+ |
} |
|
565 |
+ |
if c2.tree_ops.is_empty() && c2.edits.is_empty() { |
|
566 |
+ |
return fail("everything in that change is under the named paths; use `oxy abandon` instead"); |
|
567 |
+ |
} |
|
568 |
+ |
let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath); |
|
569 |
+ |
d1.extend(old.deps.iter().copied()); |
|
570 |
+ |
c1.deps = d1.into_iter().collect(); |
|
571 |
+ |
c1.normalize(); |
|
572 |
+ |
let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &beneath); |
|
573 |
+ |
d2.extend(old.deps.iter().copied()); |
|
574 |
+ |
c2.deps = d2.into_iter().collect(); |
|
575 |
+ |
c2.normalize(); |
|
576 |
+ |
let corr = Correspondence { atoms: Correspondence::identity(&old).atoms, ops: ops_map }; |
|
577 |
+ |
let h2 = self.rewrite_change(hash, Some(&c2), &corr, &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap(); |
|
578 |
+ |
let h1 = self.store_signed(&c1)?; |
|
579 |
+ |
self.add_to_bookmark(h1)?; |
| 483 |
580 |
|
Ok((h1, h2)) |
| 484 |
581 |
|
} |
| 485 |
582 |
|
} |