Skip to content

Commit 4cc5aa7

Browse files
committed
Improve workspace handling so GitLab results are more usable.
1 parent 4961311 commit 4cc5aa7

File tree

8 files changed

+259
-50
lines changed

8 files changed

+259
-50
lines changed

crates/but-graph/src/api.rs

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::init::PetGraph;
2-
use crate::{CommitFlags, CommitIndex, Edge, EntryPoint, Graph, Segment, SegmentIndex};
2+
use crate::{
3+
CommitFlags, CommitIndex, Edge, EntryPoint, Graph, Segment, SegmentIndex, SegmentMetadata,
4+
Statistics,
5+
};
36
use anyhow::{Context, bail};
47
use bstr::ByteSlice;
58
use gix::refs::Category;
@@ -177,7 +180,7 @@ impl Graph {
177180
}
178181

179182
/// Return the number of edges that are connecting segments.
180-
pub fn num_edges(&self) -> usize {
183+
pub fn num_connections(&self) -> usize {
181184
self.inner.edge_count()
182185
}
183186

@@ -190,19 +193,124 @@ impl Graph {
190193
.sum::<usize>()
191194
}
192195

193-
/// Return the number segments whose commits are all exclusively in a remote.
194-
pub fn num_remote_segments(&self) -> usize {
195-
self.inner
196-
.raw_nodes()
197-
.iter()
198-
.map(|n| usize::from(n.weight.commits.iter().all(|c| c.flags.is_empty())))
199-
.sum::<usize>()
200-
}
201-
202196
/// Return an iterator over all indices of segments in the graph.
203197
pub fn segments(&self) -> impl Iterator<Item = SegmentIndex> {
204198
self.inner.node_indices()
205199
}
200+
201+
/// Return the number segments whose commits are all exclusively in a remote.
202+
pub fn statistics(&self) -> Statistics {
203+
let mut out = Statistics::default();
204+
let Statistics {
205+
segments,
206+
segments_integrated,
207+
segments_remote,
208+
segments_with_remote_tracking_branch,
209+
segments_empty,
210+
segments_unnamed,
211+
segments_in_workspace,
212+
segments_in_workspace_and_integrated,
213+
segments_with_workspace_metadata,
214+
segments_with_branch_metadata,
215+
entrypoint_in_workspace,
216+
segments_behind_of_entrypoint,
217+
segments_ahead_of_entrypoint,
218+
connections,
219+
commits,
220+
commit_references,
221+
commits_at_cutoff,
222+
} = &mut out;
223+
224+
*segments = self.inner.node_count();
225+
*connections = self.inner.edge_count();
226+
227+
if let Ok(ep) = self.lookup_entrypoint() {
228+
*entrypoint_in_workspace = ep
229+
.segment
230+
.commits
231+
.first()
232+
.map(|c| c.flags.contains(CommitFlags::InWorkspace));
233+
for (storage, direction, start_cidx) in [
234+
(
235+
segments_behind_of_entrypoint,
236+
Direction::Outgoing,
237+
ep.segment.commits.first().map(|_| 0),
238+
),
239+
(
240+
segments_ahead_of_entrypoint,
241+
Direction::Incoming,
242+
ep.segment.commits.last().map(|_| ep.segment.commits.len()),
243+
),
244+
] {
245+
let mut walk = crate::init::walk::TopoWalk::start_from(
246+
ep.segment_index,
247+
start_cidx,
248+
direction,
249+
)
250+
.skip_tip_segment();
251+
while walk.next(&self.inner).is_some() {
252+
*storage += 1;
253+
}
254+
}
255+
}
256+
257+
for node in self.inner.raw_nodes() {
258+
let n = &node.weight;
259+
*commits += n.commits.len();
260+
261+
if n.ref_name.is_none() {
262+
*segments_unnamed += 1;
263+
}
264+
if n.remote_tracking_ref_name.is_some() {
265+
*segments_with_remote_tracking_branch += 1;
266+
}
267+
match n.metadata {
268+
None => {}
269+
Some(SegmentMetadata::Workspace(_)) => {
270+
*segments_with_workspace_metadata += 1;
271+
}
272+
Some(SegmentMetadata::Branch(_)) => {
273+
*segments_with_branch_metadata += 1;
274+
}
275+
}
276+
// We assume proper segmentation, so the first commit is all we need
277+
match n.commits.first() {
278+
Some(c) => {
279+
if c.flags.contains(CommitFlags::InWorkspace) {
280+
*segments_in_workspace += 1
281+
}
282+
if c.flags.contains(CommitFlags::Integrated) {
283+
*segments_integrated += 1
284+
}
285+
if c.flags
286+
.contains(CommitFlags::InWorkspace | CommitFlags::Integrated)
287+
{
288+
*segments_in_workspace_and_integrated += 1
289+
}
290+
if c.flags.is_empty() {
291+
*segments_remote += 1;
292+
}
293+
}
294+
None => {
295+
*segments_empty += 1;
296+
}
297+
}
298+
299+
*commit_references += n.commits.iter().map(|c| c.refs.len()).sum::<usize>();
300+
}
301+
302+
for sidx in self.inner.node_indices() {
303+
*commits_at_cutoff += usize::from(self[sidx].commits.last().is_some_and(|c| {
304+
!c.parent_ids.is_empty()
305+
&& self
306+
.inner
307+
.edges_directed(sidx, Direction::Outgoing)
308+
.next()
309+
.is_none()
310+
}));
311+
}
312+
out
313+
}
206314
}
207315

208316
/// Debugging

crates/but-graph/src/init/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use utils::*;
1616
mod remotes;
1717

1818
mod post;
19-
mod walk;
19+
pub(crate) mod walk;
2020

2121
pub(super) type PetGraph = petgraph::Graph<Segment, Edge>;
2222

crates/but-graph/src/init/post.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ impl Graph {
172172
) -> anyhow::Result<()> {
173173
// Map (segment-to-be-named, [candidate-remote]), so we don't set a name if there is more
174174
// than one remote.
175-
let mut remotes_by_segment_map = BTreeMap::<SegmentIndex, Vec<gix::refs::FullName>>::new();
175+
let mut remotes_by_segment_map =
176+
BTreeMap::<SegmentIndex, Vec<(gix::refs::FullName, gix::refs::FullName)>>::new();
176177

177178
for (remote_sidx, remote_ref_name) in self.inner.node_indices().filter_map(|sidx| {
178179
self[sidx]
@@ -182,8 +183,8 @@ impl Graph {
182183
.map(|rn| (sidx, rn))
183184
}) {
184185
let start_idx = self[remote_sidx].commits.first().map(|_| 0);
185-
let mut walk =
186-
TopoWalk::start_from(remote_sidx, start_idx, Direction::Outgoing).skip_tip();
186+
let mut walk = TopoWalk::start_from(remote_sidx, start_idx, Direction::Outgoing)
187+
.skip_tip_segment();
187188

188189
while let Some((sidx, commit_range)) = walk.next(&self.inner) {
189190
let segment = &self[sidx];
@@ -204,7 +205,7 @@ impl Graph {
204205
.iter()
205206
.all(|c| c.flags.contains(CommitFlags::NotInRemote))
206207
{
207-
// a candidate for naming, and we'd either expect all or none of the comits
208+
// a candidate for naming, and we'd either expect all or none of the commits
208209
// to be in or outside a remote.
209210
let first_commit = segment.commits.first().expect("we know there is commits");
210211
if let Some(local_tracking_branch) = first_commit.refs.iter().find_map(|rn| {
@@ -223,7 +224,7 @@ impl Graph {
223224
remotes_by_segment_map
224225
.entry(sidx)
225226
.or_default()
226-
.push(local_tracking_branch);
227+
.push((local_tracking_branch, remote_ref_name.clone()));
227228
}
228229
break;
229230
}
@@ -237,10 +238,29 @@ impl Graph {
237238
.filter(|(_, candidates)| candidates.len() == 1)
238239
{
239240
let s = &mut self[anon_sidx];
240-
s.ref_name = disambiguated_name.pop();
241+
let (local, remote) = disambiguated_name.pop().expect("one item as checked above");
242+
s.ref_name = Some(local);
243+
s.remote_tracking_ref_name = Some(remote);
241244
let rn = s.ref_name.as_ref().unwrap();
242245
s.commits.first_mut().unwrap().refs.retain(|crn| crn != rn);
243246
}
247+
248+
// TODO: we should probably try to set this right when we traverse the segment
249+
// to save remote-ref lookup.
250+
for segment in self.inner.node_weights_mut() {
251+
if segment.remote_tracking_ref_name.is_some() {
252+
continue;
253+
};
254+
let Some(ref_name) = segment.ref_name.as_ref() else {
255+
continue;
256+
};
257+
segment.remote_tracking_ref_name = remotes::lookup_remote_tracking_branch_or_deduce_it(
258+
repo,
259+
ref_name.as_ref(),
260+
symbolic_remote_names,
261+
configured_remote_tracking_branches,
262+
)?;
263+
}
244264
Ok(())
245265
}
246266
}

crates/but-graph/src/init/walk.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl TopoWalk {
5353
/// Builder
5454
impl TopoWalk {
5555
/// Call to not return the tip as part of the iteration.
56-
pub fn skip_tip(mut self) -> Self {
56+
pub fn skip_tip_segment(mut self) -> Self {
5757
self.skip_tip = Some(());
5858
self
5959
}
@@ -107,7 +107,8 @@ impl TopoWalk {
107107
{
108108
continue;
109109
}
110-
self.next.push_back((edge.source(), edge.weight().src));
110+
self.next
111+
.push_back((edge.source(), edge.weight().src.map(|cidx| cidx + 1)));
111112
}
112113
}
113114
}

crates/but-graph/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,50 @@ pub struct Graph {
2222
hard_limit_hit: bool,
2323
}
2424

25+
/// All kinds of numbers generated from a graph, returned by [Graph::statistics()].
26+
///
27+
/// Note that the segment counts aren't mutually exclusive, so the sum of these fields can be more
28+
/// than the total of segments.
29+
#[derive(Default, Debug, Copy, Clone)]
30+
pub struct Statistics {
31+
/// The number of segments in the graph.
32+
pub segments: usize,
33+
/// Segments where all commits are integrated.
34+
pub segments_integrated: usize,
35+
/// Segments where all commits are on a remote tracking branch.
36+
pub segments_remote: usize,
37+
/// Segments where the remote tracking branch is set
38+
pub segments_with_remote_tracking_branch: usize,
39+
/// Segments that are empty.
40+
pub segments_empty: usize,
41+
/// Segments that are anonymous.
42+
pub segments_unnamed: usize,
43+
/// Segments that are reachable by the workspace commit.
44+
pub segments_in_workspace: usize,
45+
/// Segments that are reachable by the workspace commit and are integrated.
46+
pub segments_in_workspace_and_integrated: usize,
47+
/// Segments that have metadata for workspaces.
48+
pub segments_with_workspace_metadata: usize,
49+
/// Segments that have metadata for branches.
50+
pub segments_with_branch_metadata: usize,
51+
/// `true` if the start of the traversal is in a workspace.
52+
/// `None` if the information could not be determined, maybe because the entrypoint
53+
/// is invalid (bug) or it's empty (unusual)
54+
pub entrypoint_in_workspace: Option<bool>,
55+
/// Segments, excluding the entrypoint, that can be reached downwards through the entrypoint.
56+
pub segments_behind_of_entrypoint: usize,
57+
/// Segments, excluding the entrypoint, that can be reached upwards through the entrypoint.
58+
pub segments_ahead_of_entrypoint: usize,
59+
/// Connections between segments.
60+
pub connections: usize,
61+
/// All commits within segments.
62+
pub commits: usize,
63+
/// All references stored with commits, i.e. not the ref-names absorbed by segments.
64+
pub commit_references: usize,
65+
/// The traversal was stopped at this many commits.
66+
pub commits_at_cutoff: usize,
67+
}
68+
2569
/// A resolved entry point into the graph for easy access to the segment, commit,
2670
/// and the respective indices for later traversal.
2771
#[derive(Debug, Copy, Clone)]

crates/but-graph/tests/graph/init/mod.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ fn four_diamond() -> anyhow::Result<()> {
206206
);
207207
assert_eq!(graph.num_commits(), 8, "one commit per node");
208208
assert_eq!(
209-
graph.num_edges(),
209+
graph.num_connections(),
210210
10,
211211
"however, we see only a portion of the edges as the tree can only show simple stacks"
212212
);
@@ -229,9 +229,9 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
229229
let graph =
230230
Graph::from_head(&repo, &*meta, standard_options().with_limit_hint(1))?.validated()?;
231231
insta::assert_snapshot!(graph_tree(&graph), @r#"
232-
├── 👉►:0:B
232+
├── 👉►:0:B <> origin/B
233233
│ └── ·312f819 (⌂)❱"B"
234-
│ └── ►:2:A
234+
│ └── ►:2:A <> origin/A
235235
│ └── ·e255adc (⌂)❱"A"
236236
│ └── ►:4:main
237237
│ └── ·fafd9d0 (⌂)❱"init"
@@ -259,9 +259,9 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
259259
// Everything we encounter is checked for remotes.
260260
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
261261
insta::assert_snapshot!(graph_tree(&graph), @r#"
262-
├── 👉►:0:B
262+
├── 👉►:0:B <> origin/B
263263
│ └── ·312f819 (⌂)❱"B"
264-
│ └── ►:2:A
264+
│ └── ►:2:A <> origin/A
265265
│ └── ·e255adc (⌂)❱"A"
266266
│ └── ►:4:main
267267
│ └── ·fafd9d0 (⌂)❱"init"
@@ -276,7 +276,7 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
276276
let (id, name) = id_at(&repo, "A");
277277
let graph = Graph::from_commit_traversal(id, name, &*meta, standard_options())?.validated()?;
278278
insta::assert_snapshot!(graph_tree(&graph), @r#"
279-
├── 👉►:0:A
279+
├── 👉►:0:A <> origin/A
280280
│ └── ·e255adc (⌂)❱"A"
281281
│ └── ►:2:main
282282
│ └── ·fafd9d0 (⌂)❱"init"
@@ -432,6 +432,30 @@ fn with_limits() -> anyhow::Result<()> {
432432
├── ·4f1f248 (⌂)❱"C2"
433433
└── ✂️·487ffce (⌂)❱"C1"
434434
"#);
435+
436+
insta::assert_debug_snapshot!(graph.statistics(), @r"
437+
Statistics {
438+
segments: 5,
439+
segments_integrated: 0,
440+
segments_remote: 0,
441+
segments_with_remote_tracking_branch: 0,
442+
segments_empty: 0,
443+
segments_unnamed: 1,
444+
segments_in_workspace: 0,
445+
segments_in_workspace_and_integrated: 0,
446+
segments_with_workspace_metadata: 0,
447+
segments_with_branch_metadata: 0,
448+
entrypoint_in_workspace: Some(
449+
false,
450+
),
451+
segments_behind_of_entrypoint: 4,
452+
segments_ahead_of_entrypoint: 0,
453+
connections: 4,
454+
commits: 12,
455+
commit_references: 0,
456+
commits_at_cutoff: 3,
457+
}
458+
");
435459
Ok(())
436460
}
437461

0 commit comments

Comments
 (0)