|
| 1 | +use std::{ |
| 2 | + cmp::Reverse, |
| 3 | + collections::{BinaryHeap, HashMap, HashSet}, |
| 4 | +}; |
| 5 | + |
| 6 | +use anyhow::{Context as _, Result, bail}; |
| 7 | +use but_core::RefMetadata; |
| 8 | +use but_graph::{SegmentIndex, SegmentRelation, projection::Workspace}; |
| 9 | +use but_rebase::graph_rebase::{Editor, Selector, ToCommitSelector}; |
| 10 | + |
| 11 | +#[derive(Debug, Clone, Copy)] |
| 12 | +struct SelectedCommit { |
| 13 | + selector: Selector, |
| 14 | + id: gix::ObjectId, |
| 15 | + segment_id: SegmentIndex, |
| 16 | + input_order: usize, |
| 17 | +} |
| 18 | + |
| 19 | +fn find_commit_segment_index( |
| 20 | + workspace: &Workspace, |
| 21 | + commit_id: gix::ObjectId, |
| 22 | +) -> Option<SegmentIndex> { |
| 23 | + let (_, stack_segment, _) = workspace.find_commit_and_containers(commit_id)?; |
| 24 | + let commit_offset = stack_segment |
| 25 | + .commits |
| 26 | + .iter() |
| 27 | + .position(|c| c.id == commit_id)?; |
| 28 | + |
| 29 | + let mut owning_segment = stack_segment.id; |
| 30 | + for (segment_id, offset) in &stack_segment.commits_by_segment { |
| 31 | + if *offset > commit_offset { |
| 32 | + break; |
| 33 | + } |
| 34 | + owning_segment = *segment_id; |
| 35 | + } |
| 36 | + |
| 37 | + Some(owning_segment) |
| 38 | +} |
| 39 | + |
| 40 | +/// Order commit selectors by parentage, with parents first and children last. |
| 41 | +/// |
| 42 | +/// If two commits are unrelated by ancestry, their relative order is determined by |
| 43 | +/// workspace traversal order. Duplicate selectors are deduplicated by commit-id |
| 44 | +/// with first occurrence winning. |
| 45 | +/// |
| 46 | +/// Returns an error if any selected commit isn't present in the editor workspace |
| 47 | +/// traversal. |
| 48 | +pub fn order_commit_selectors_by_parentage<'ws, 'meta, M: RefMetadata, I, S>( |
| 49 | + editor: &Editor<'ws, 'meta, M>, |
| 50 | + selectors: I, |
| 51 | +) -> Result<Vec<Selector>> |
| 52 | +where |
| 53 | + I: IntoIterator<Item = S>, |
| 54 | + S: ToCommitSelector, |
| 55 | +{ |
| 56 | + let mut selected = Vec::<SelectedCommit>::new(); |
| 57 | + let mut seen_ids = HashSet::<gix::ObjectId>::new(); |
| 58 | + for (input_order, selector_like) in selectors.into_iter().enumerate() { |
| 59 | + let (selector, commit) = editor.find_selectable_commit(selector_like)?; |
| 60 | + if seen_ids.insert(commit.id) { |
| 61 | + let segment_id = |
| 62 | + find_commit_segment_index(editor.workspace, commit.id).with_context(|| { |
| 63 | + format!( |
| 64 | + "Selected commit {id} is not part of the workspace traversal", |
| 65 | + id = commit.id |
| 66 | + ) |
| 67 | + })?; |
| 68 | + selected.push(SelectedCommit { |
| 69 | + selector, |
| 70 | + id: commit.id, |
| 71 | + segment_id, |
| 72 | + input_order, |
| 73 | + }); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + if selected.len() <= 1 { |
| 78 | + return Ok(selected.into_iter().map(|s| s.selector).collect()); |
| 79 | + } |
| 80 | + |
| 81 | + let workspace_rank = workspace_parent_to_child_rank(editor, &selected)?; |
| 82 | + |
| 83 | + let mut adjacency = vec![Vec::<usize>::new(); selected.len()]; |
| 84 | + let mut indegree = vec![0usize; selected.len()]; |
| 85 | + |
| 86 | + for (i, left_commit) in selected.iter().enumerate() { |
| 87 | + for (offset, right_commit) in selected.iter().skip(i + 1).enumerate() { |
| 88 | + let j = i + 1 + offset; |
| 89 | + match ancestry_relation(editor, left_commit, right_commit)? { |
| 90 | + Relation::LeftIsAncestorOfRight => { |
| 91 | + adjacency |
| 92 | + .get_mut(i) |
| 93 | + .context("BUG: adjacency index should always be valid")? |
| 94 | + .push(j); |
| 95 | + *indegree |
| 96 | + .get_mut(j) |
| 97 | + .context("BUG: indegree index should always be valid")? += 1; |
| 98 | + } |
| 99 | + Relation::RightIsAncestorOfLeft => { |
| 100 | + adjacency |
| 101 | + .get_mut(j) |
| 102 | + .context("BUG: adjacency index should always be valid")? |
| 103 | + .push(i); |
| 104 | + *indegree |
| 105 | + .get_mut(i) |
| 106 | + .context("BUG: indegree index should always be valid")? += 1; |
| 107 | + } |
| 108 | + Relation::Unrelated => {} |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + let mut output = Vec::with_capacity(selected.len()); |
| 114 | + let mut ready: BinaryHeap<Reverse<(usize, usize, usize)>> = indegree |
| 115 | + .iter() |
| 116 | + .enumerate() |
| 117 | + .filter_map(|(idx, degree)| { |
| 118 | + if *degree != 0 { |
| 119 | + return None; |
| 120 | + } |
| 121 | + let commit = selected |
| 122 | + .get(idx) |
| 123 | + .expect("all indegree indexes point to selected commits"); |
| 124 | + let rank = *workspace_rank |
| 125 | + .get(&commit.id) |
| 126 | + .expect("all selected commits are ranked"); |
| 127 | + Some(Reverse((rank, commit.input_order, idx))) |
| 128 | + }) |
| 129 | + .collect(); |
| 130 | + |
| 131 | + while let Some(Reverse((_, _, next))) = ready.pop() { |
| 132 | + output.push( |
| 133 | + selected |
| 134 | + .get(next) |
| 135 | + .context("BUG: ready index should be in-bounds")? |
| 136 | + .selector, |
| 137 | + ); |
| 138 | + for &child in adjacency |
| 139 | + .get(next) |
| 140 | + .context("BUG: adjacency index should be in-bounds")? |
| 141 | + { |
| 142 | + let degree = indegree |
| 143 | + .get_mut(child) |
| 144 | + .context("BUG: child index should be in-bounds")?; |
| 145 | + *degree -= 1; |
| 146 | + if *degree == 0 { |
| 147 | + let commit = selected |
| 148 | + .get(child) |
| 149 | + .expect("all child indexes point to selected commits"); |
| 150 | + let rank = *workspace_rank |
| 151 | + .get(&commit.id) |
| 152 | + .expect("all selected commits are ranked"); |
| 153 | + ready.push(Reverse((rank, commit.input_order, child))); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + if output.len() != selected.len() { |
| 159 | + bail!("Cannot order selected commits by parentage due to cyclic ancestry constraints") |
| 160 | + } |
| 161 | + |
| 162 | + Ok(output) |
| 163 | +} |
| 164 | + |
| 165 | +#[derive(Debug, Clone, Copy, Eq, PartialEq)] |
| 166 | +enum Relation { |
| 167 | + LeftIsAncestorOfRight, |
| 168 | + RightIsAncestorOfLeft, |
| 169 | + Unrelated, |
| 170 | +} |
| 171 | + |
| 172 | +fn ancestry_relation( |
| 173 | + editor: &Editor<'_, '_, impl RefMetadata>, |
| 174 | + left: &SelectedCommit, |
| 175 | + right: &SelectedCommit, |
| 176 | +) -> Result<Relation> { |
| 177 | + match editor |
| 178 | + .workspace |
| 179 | + .graph |
| 180 | + .relation_between(left.segment_id, right.segment_id) |
| 181 | + { |
| 182 | + SegmentRelation::Ancestor => return Ok(Relation::LeftIsAncestorOfRight), |
| 183 | + SegmentRelation::Descendant => return Ok(Relation::RightIsAncestorOfLeft), |
| 184 | + SegmentRelation::Disjoint | SegmentRelation::Diverged => return Ok(Relation::Unrelated), |
| 185 | + SegmentRelation::Identity => { |
| 186 | + // Commits can still be in parent/child relation inside one segment. |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + let merge_base = match editor.repo().merge_base(left.id, right.id) { |
| 191 | + Ok(base) => base.detach(), |
| 192 | + Err(error) => match error { |
| 193 | + gix::repository::merge_base::Error::FindMergeBase(_) |
| 194 | + | gix::repository::merge_base::Error::NotFound { .. } => { |
| 195 | + return Ok(Relation::Unrelated); |
| 196 | + } |
| 197 | + _ => return Err(error.into()), |
| 198 | + }, |
| 199 | + }; |
| 200 | + |
| 201 | + if merge_base == left.id { |
| 202 | + return Ok(Relation::LeftIsAncestorOfRight); |
| 203 | + } |
| 204 | + if merge_base == right.id { |
| 205 | + return Ok(Relation::RightIsAncestorOfLeft); |
| 206 | + } |
| 207 | + Ok(Relation::Unrelated) |
| 208 | +} |
| 209 | + |
| 210 | +fn workspace_parent_to_child_rank<M: RefMetadata>( |
| 211 | + editor: &Editor<'_, '_, M>, |
| 212 | + selected: &[SelectedCommit], |
| 213 | +) -> Result<HashMap<gix::ObjectId, usize>> { |
| 214 | + let mut rank_by_id = HashMap::<gix::ObjectId, usize>::new(); |
| 215 | + let mut rank = 0usize; |
| 216 | + for stack in &editor.workspace.stacks { |
| 217 | + for segment in &stack.segments { |
| 218 | + for commit in segment.commits.iter().rev() { |
| 219 | + rank_by_id.entry(commit.id).or_insert_with(|| { |
| 220 | + let current = rank; |
| 221 | + rank += 1; |
| 222 | + current |
| 223 | + }); |
| 224 | + } |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + for selected_commit in selected { |
| 229 | + rank_by_id.get(&selected_commit.id).with_context(|| { |
| 230 | + format!( |
| 231 | + "Selected commit {id} is not part of the workspace traversal", |
| 232 | + id = selected_commit.id |
| 233 | + ) |
| 234 | + })?; |
| 235 | + } |
| 236 | + |
| 237 | + Ok(rank_by_id) |
| 238 | +} |
0 commit comments