Skip to content

Commit e916590

Browse files
committed
commit ordering
Create but-workspace commit ordering function that sorts the commits by their parentage, according to the workspace appearance.
1 parent 20551ca commit e916590

6 files changed

Lines changed: 406 additions & 0 deletions

File tree

crates/but-workspace/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub mod legacy;
3535
/// Types specifically for the user-interface.
3636
pub mod ui;
3737

38+
/// Utilities for deterministic ordering operations.
39+
pub mod ordering;
40+
3841
pub mod commit_engine;
3942
/// Tools for manipulating trees
4043
pub mod tree_manipulation;
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Utilities for deterministic commit ordering.
2+
3+
mod commit_parentage;
4+
pub use commit_parentage::order_commit_selectors_by_parentage;

crates/but-workspace/tests/workspace/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod branch_details;
55
mod commit;
66
mod commit_engine;
77
mod flatten_diff_specs;
8+
mod ordering;
89
mod ref_info;
910
mod tree_manipulation;
1011
mod ui;

0 commit comments

Comments
 (0)