| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
use super::*; |
| 11 |
use crate::rewrite::{propagate, Correspondence}; |
| 12 |
|
| 13 |
impl Repo { |
| 14 |
|
| 15 |
fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> { |
| 16 |
let heads = self.bookmark_heads()?; |
| 17 |
if !heads.contains(hash) { |
| 18 |
return fail(format!( |
| 19 |
"change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet", |
| 20 |
hash.short(12), |
| 21 |
self.config.bookmark |
| 22 |
)); |
| 23 |
} |
| 24 |
let c = self.store.get_change(hash)?; |
| 25 |
if c.kind == ChangeKind::WorkingCopy { |
| 26 |
return fail("that is the working copy; commit it instead"); |
| 27 |
} |
| 28 |
Ok(c) |
| 29 |
} |
| 30 |
|
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> { |
| 35 |
let new_hash = match new { |
| 36 |
Some(c) => Some(self.store_signed(c)?), |
| 37 |
None => None, |
| 38 |
}; |
| 39 |
let old_change = self.store.get_change(old)?; |
| 40 |
let (op_ref, op) = self.head_op()?; |
| 41 |
let bookmark = self.config.bookmark.clone(); |
| 42 |
let heads = self.heads_of(&op.view, &bookmark)?; |
| 43 |
let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> { |
| 44 |
let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect(); |
| 45 |
match new_hash { |
| 46 |
Some(n) => out.push(n), |
| 47 |
None => out.extend(old_change.deps.iter().copied()), |
| 48 |
} |
| 49 |
out |
| 50 |
}; |
| 51 |
let candidates = replace(&heads); |
| 52 |
let st = self.load_state(&candidates)?; |
| 53 |
let new_heads = st.maximal(&candidates); |
| 54 |
|
| 55 |
let (_, wc) = self.working_copy()?; |
| 56 |
let base = self.load_state(&wc.deps)?; |
| 57 |
let wc_base = base.maximal(&wc.deps); |
| 58 |
let wc_base = if wc_base.contains(old) { |
| 59 |
let c = replace(&wc_base); |
| 60 |
let st = self.load_state(&c)?; |
| 61 |
st.maximal(&c) |
| 62 |
} else { |
| 63 |
wc_base |
| 64 |
}; |
| 65 |
let fresh = self.new_working_copy(wc_base, &wc.message); |
| 66 |
let wc_hash = self.store.put_change(&fresh)?; |
| 67 |
|
| 68 |
let mut view = op.view.clone(); |
| 69 |
view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?); |
| 70 |
view.workspaces.insert(self.workspace, wc_hash); |
| 71 |
self.record_op(&[op_ref], command, view)?; |
| 72 |
self.drop_manifest(); |
| 73 |
Ok(new_hash) |
| 74 |
} |
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
|
| 79 |
fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> { |
| 80 |
let (op_ref, op) = self.head_op()?; |
| 81 |
let bookmark = self.config.bookmark.clone(); |
| 82 |
let heads = self.heads_of(&op.view, &bookmark)?; |
| 83 |
let (wc_hash, _) = self.working_copy()?; |
| 84 |
let mut all = heads.clone(); |
| 85 |
all.push(wc_hash); |
| 86 |
let state = self.load_state(&all)?; |
| 87 |
if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) { |
| 88 |
return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark)); |
| 89 |
} |
| 90 |
let old_change = self.store.get_change(old)?; |
| 91 |
if old_change.kind == ChangeKind::WorkingCopy { |
| 92 |
return fail("that is the working copy; commit it instead"); |
| 93 |
} |
| 94 |
let load = |h: &ChangeHash| self.store.get_change(h).ok(); |
| 95 |
let rewritten = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(format!("{e}; rewriting through such references is not implemented yet")))?; |
| 96 |
|
| 97 |
let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new(); |
| 98 |
let new_hash = match new { |
| 99 |
Some(c) => { |
| 100 |
let h = self.store_signed(c)?; |
| 101 |
mapping.insert(*old, h); |
| 102 |
Some(h) |
| 103 |
} |
| 104 |
None => None, |
| 105 |
}; |
| 106 |
for (h, c) in &rewritten { |
| 107 |
let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? }; |
| 108 |
mapping.insert(*h, nh); |
| 109 |
} |
| 110 |
let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> { |
| 111 |
let mut out = Vec::new(); |
| 112 |
for h in set { |
| 113 |
if h == old && new_hash.is_none() { |
| 114 |
out.extend(old_change.deps.iter().copied()); |
| 115 |
} else { |
| 116 |
out.push(*mapping.get(h).unwrap_or(h)); |
| 117 |
} |
| 118 |
} |
| 119 |
out |
| 120 |
}; |
| 121 |
let candidates = remap(&heads); |
| 122 |
let st = self.load_state(&candidates)?; |
| 123 |
let new_heads = st.maximal(&candidates); |
| 124 |
let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash); |
| 125 |
|
| 126 |
let mut view = op.view.clone(); |
| 127 |
view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?); |
| 128 |
view.workspaces.insert(self.workspace, new_wc); |
| 129 |
self.record_op(&[op_ref], command, view)?; |
| 130 |
self.drop_manifest(); |
| 131 |
Ok(new_hash) |
| 132 |
} |
| 133 |
|
| 134 |
|
| 135 |
pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> { |
| 136 |
if message.trim().is_empty() { |
| 137 |
return fail("a message is required"); |
| 138 |
} |
| 139 |
let mut c = self.store.get_change(hash)?; |
| 140 |
if c.kind == ChangeKind::WorkingCopy { |
| 141 |
return fail("that is the working copy; commit it instead"); |
| 142 |
} |
| 143 |
if c.message == message.trim_end() { |
| 144 |
return Ok(*hash); |
| 145 |
} |
| 146 |
let corr = Correspondence::identity(&c); |
| 147 |
c.message = message.trim_end().to_string(); |
| 148 |
c.hlc = Hlc::now(); |
| 149 |
let id = c.change_id; |
| 150 |
Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap()) |
| 151 |
} |
| 152 |
|
| 153 |
|
| 154 |
|
| 155 |
pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> { |
| 156 |
let snap = self.snapshot("oxy amend")?; |
| 157 |
if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) { |
| 158 |
return fail("a file still contains conflict markers"); |
| 159 |
} |
| 160 |
let base_heads = snap.base.maximal(&snap.wc.deps); |
| 161 |
let [head] = base_heads.as_slice() else { |
| 162 |
return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len())); |
| 163 |
}; |
| 164 |
let head = *head; |
| 165 |
let old = self.rewritable_head(&head)?; |
| 166 |
if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() { |
| 167 |
return fail("nothing to amend: the working copy is clean and no new message was given"); |
| 168 |
} |
| 169 |
|
| 170 |
let beneath = self.load_state(&old.deps)?; |
| 171 |
let beneath_snapshot = beneath.materialize(); |
| 172 |
let reuse = added_node_paths(&old, &beneath_snapshot); |
| 173 |
let built = self.build_against(&beneath, &beneath_snapshot, reuse)?; |
| 174 |
if built.tree_ops.is_empty() && built.edits.is_empty() { |
| 175 |
return fail("amending would leave the change empty; use `oxy abandon` to drop it"); |
| 176 |
} |
| 177 |
let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect(); |
| 178 |
deps.extend(built.contributors.iter().copied()); |
| 179 |
let mut new = Change { |
| 180 |
change_id: old.change_id, |
| 181 |
deps: vec![], |
| 182 |
author: self.author(), |
| 183 |
hlc: Hlc::now(), |
| 184 |
message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()), |
| 185 |
tree_ops: built.tree_ops, |
| 186 |
edits: built.edits, |
| 187 |
kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal }, |
| 188 |
}; |
| 189 |
deps.extend(referenced_changes(&new, &beneath)); |
| 190 |
new.deps = deps.into_iter().collect(); |
| 191 |
new.normalize(); |
| 192 |
Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap()) |
| 193 |
} |
| 194 |
|
| 195 |
|
| 196 |
|
| 197 |
|
| 198 |
|
| 199 |
pub fn abandon(&self, hash: &ChangeHash) -> Result<()> { |
| 200 |
let c = self.store.get_change(hash)?; |
| 201 |
if c.kind == ChangeKind::WorkingCopy { |
| 202 |
return fail("that is the working copy; commit it instead"); |
| 203 |
} |
| 204 |
let heads = self.bookmark_heads()?; |
| 205 |
if heads.contains(hash) { |
| 206 |
self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?; |
| 207 |
} else { |
| 208 |
self.rewrite_change(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?; |
| 209 |
} |
| 210 |
Ok(()) |
| 211 |
} |
| 212 |
} |