| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
use super::*; |
| 10 |
|
| 11 |
impl Repo { |
| 12 |
|
| 13 |
fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> { |
| 14 |
let heads = self.bookmark_heads()?; |
| 15 |
if !heads.contains(hash) { |
| 16 |
return fail(format!( |
| 17 |
"change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet", |
| 18 |
hash.short(12), |
| 19 |
self.config.bookmark |
| 20 |
)); |
| 21 |
} |
| 22 |
let c = self.store.get_change(hash)?; |
| 23 |
if c.kind == ChangeKind::WorkingCopy { |
| 24 |
return fail("that is the working copy; commit it instead"); |
| 25 |
} |
| 26 |
Ok(c) |
| 27 |
} |
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> { |
| 33 |
let new_hash = match new { |
| 34 |
Some(c) => Some(self.store_signed(c)?), |
| 35 |
None => None, |
| 36 |
}; |
| 37 |
let old_change = self.store.get_change(old)?; |
| 38 |
let (op_ref, op) = self.head_op()?; |
| 39 |
let bookmark = self.config.bookmark.clone(); |
| 40 |
let heads = self.heads_of(&op.view, &bookmark)?; |
| 41 |
let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> { |
| 42 |
let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect(); |
| 43 |
match new_hash { |
| 44 |
Some(n) => out.push(n), |
| 45 |
None => out.extend(old_change.deps.iter().copied()), |
| 46 |
} |
| 47 |
out |
| 48 |
}; |
| 49 |
let candidates = replace(&heads); |
| 50 |
let st = self.load_state(&candidates)?; |
| 51 |
let new_heads = st.maximal(&candidates); |
| 52 |
|
| 53 |
let (_, wc) = self.working_copy()?; |
| 54 |
let base = self.load_state(&wc.deps)?; |
| 55 |
let wc_base = base.maximal(&wc.deps); |
| 56 |
let wc_base = if wc_base.contains(old) { |
| 57 |
let c = replace(&wc_base); |
| 58 |
let st = self.load_state(&c)?; |
| 59 |
st.maximal(&c) |
| 60 |
} else { |
| 61 |
wc_base |
| 62 |
}; |
| 63 |
let fresh = self.new_working_copy(wc_base, &wc.message); |
| 64 |
let wc_hash = self.store.put_change(&fresh)?; |
| 65 |
|
| 66 |
let mut view = op.view.clone(); |
| 67 |
view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?); |
| 68 |
view.workspaces.insert(self.workspace, wc_hash); |
| 69 |
self.record_op(&[op_ref], command, view)?; |
| 70 |
Ok(new_hash) |
| 71 |
} |
| 72 |
|
| 73 |
|
| 74 |
pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> { |
| 75 |
if message.trim().is_empty() { |
| 76 |
return fail("a message is required"); |
| 77 |
} |
| 78 |
let mut c = self.rewritable_head(hash)?; |
| 79 |
if c.message == message.trim_end() { |
| 80 |
return Ok(*hash); |
| 81 |
} |
| 82 |
c.message = message.trim_end().to_string(); |
| 83 |
c.hlc = Hlc::now(); |
| 84 |
Ok(self.replace_head(hash, Some(&c), &format!("oxy describe {} -m {message:?}", c.change_id.short(8)))?.unwrap()) |
| 85 |
} |
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> { |
| 90 |
let snap = self.snapshot("oxy amend")?; |
| 91 |
if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) { |
| 92 |
return fail("a file still contains conflict markers"); |
| 93 |
} |
| 94 |
let base_heads = snap.base.maximal(&snap.wc.deps); |
| 95 |
let [head] = base_heads.as_slice() else { |
| 96 |
return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len())); |
| 97 |
}; |
| 98 |
let head = *head; |
| 99 |
let old = self.rewritable_head(&head)?; |
| 100 |
if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() { |
| 101 |
return fail("nothing to amend: the working copy is clean and no new message was given"); |
| 102 |
} |
| 103 |
|
| 104 |
let beneath = self.load_state(&old.deps)?; |
| 105 |
let beneath_snapshot = beneath.materialize(); |
| 106 |
let reuse = added_node_paths(&old, &beneath_snapshot); |
| 107 |
let built = self.build_against(&beneath, &beneath_snapshot, reuse)?; |
| 108 |
if built.tree_ops.is_empty() && built.edits.is_empty() { |
| 109 |
return fail("amending would leave the change empty; use `oxy abandon` to drop it"); |
| 110 |
} |
| 111 |
let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect(); |
| 112 |
deps.extend(built.contributors.iter().copied()); |
| 113 |
let mut new = Change { |
| 114 |
change_id: old.change_id, |
| 115 |
deps: vec![], |
| 116 |
author: self.author(), |
| 117 |
hlc: Hlc::now(), |
| 118 |
message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()), |
| 119 |
tree_ops: built.tree_ops, |
| 120 |
edits: built.edits, |
| 121 |
kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal }, |
| 122 |
}; |
| 123 |
deps.extend(referenced_changes(&new, &beneath)); |
| 124 |
new.deps = deps.into_iter().collect(); |
| 125 |
new.normalize(); |
| 126 |
Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap()) |
| 127 |
} |
| 128 |
|
| 129 |
|
| 130 |
|
| 131 |
pub fn abandon(&self, hash: &ChangeHash) -> Result<()> { |
| 132 |
let c = self.rewritable_head(hash)?; |
| 133 |
self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?; |
| 134 |
Ok(()) |
| 135 |
} |
| 136 |
} |