Skip to content

Commit 3ae29af

Browse files
committed
Add graph subcommand to invoke graph visualization in the real world.
Also make some tweaks for better performance logging and better abort condition handling. Now integrated workspace tips will be limited as well, if a limit was set.
1 parent 9f21f99 commit 3ae29af

File tree

10 files changed

+428
-168
lines changed

10 files changed

+428
-168
lines changed

crates/but-graph/src/api.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,15 @@ impl Graph {
177177
self.inner.edge_count()
178178
}
179179

180+
/// Return the number of commits in all segments.
181+
pub fn num_commits(&self) -> usize {
182+
self.inner
183+
.raw_nodes()
184+
.iter()
185+
.map(|n| n.weight.commits.len())
186+
.sum::<usize>()
187+
}
188+
180189
/// Return an iterator over all indices of segments in the graph.
181190
pub fn segments(&self) -> impl Iterator<Item = SegmentIndex> {
182191
self.inner.node_indices()
@@ -257,7 +266,7 @@ impl Graph {
257266

258267
/// Validate the graph for consistency and fail loudly when an issue was found, after printing the dot graph.
259268
/// Mostly useful for debugging to stop early when a connection wasn't created correctly.
260-
#[cfg(target_os = "macos")]
269+
#[cfg(unix)]
261270
pub fn validated_or_open_as_svg(self) -> anyhow::Result<Self> {
262271
for edge in self.inner.edge_references() {
263272
let res = check_edge(&self.inner, edge);
@@ -283,7 +292,8 @@ impl Graph {
283292
}
284293

285294
/// Open an SVG dot visualization in the browser or panic if the `dot` or `open` tool can't be found.
286-
#[cfg(target_os = "macos")]
295+
#[cfg(unix)]
296+
#[tracing::instrument(skip(self))]
287297
pub fn open_as_svg(&self) {
288298
use std::io::Write;
289299
use std::process::Stdio;

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

Lines changed: 48 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use crate::{CommitFlags, Edge};
22
use crate::{CommitIndex, Graph, Segment, SegmentIndex, SegmentMetadata};
3-
use anyhow::{Context, bail};
3+
use anyhow::bail;
44
use but_core::RefMetadata;
5-
use gix::ObjectId;
65
use gix::hashtable::hash_map::Entry;
76
use gix::prelude::{ObjectIdExt, ReferenceExt};
87
use gix::refs::Category;
98
use petgraph::graph::EdgeReference;
109
use petgraph::prelude::EdgeRef;
1110
use std::collections::VecDeque;
11+
use tracing::instrument;
1212

1313
mod utils;
1414
use utils::*;
@@ -27,39 +27,41 @@ pub struct Options {
2727
///
2828
/// If `false`, tags are not collected.
2929
pub collect_tags: bool,
30-
/// The maximum number of commits we should traverse outside any workspace *with a target branch*.
30+
/// The (soft) maximum number of commits we should traverse.
3131
/// Workspaces with a target branch automatically have unlimited traversals as they rely on the target
3232
/// branch to eventually stop the traversal.
3333
///
3434
/// If `None`, there is no limit, which typically means that when lacking a workspace, the traversal
3535
/// will end only when no commit is left to traverse.
36-
/// `Some(0)` means nothing is going to be returned.
36+
/// `Some(0)` means nothing but the first commit is going to be returned, but it should be avoided.
3737
///
3838
/// Note that this doesn't affect the traversal of integrated commits, which is always stopped once there
3939
/// is nothing interesting left to traverse.
4040
///
41-
/// Also note: This is not a perfectly exact measure, and it's always possible to receive a few more commits
42-
/// than the maximum as for simplicity, we assign each 'split' the same limit, effectively doubling it.
41+
/// Also note: This is a hint and not an exact measure, and it's always possible to receive a more commits
42+
/// for various reasons, for instance the need to let remote branches find their local brnach independently
43+
/// of the limit.
4344
///
4445
/// ### Tip Configuration
4546
///
4647
/// * HEAD - uses the limit
4748
/// * workspaces with target branch - no limit, but auto-stop if workspace is exhausted as everything is integrated.
4849
/// - The target branch: no limit
50+
/// - Integrated workspace branches: use the limit
4951
/// * workspace without target branch - uses the limit
50-
/// * remotes tracking branches - use the limit
51-
pub max_commits_outside_of_workspace: Option<usize>,
52+
/// * remotes tracking branches - use the limit, but only once they have reached a local branch.
53+
pub commits_limit_hint: Option<usize>,
5254
/// A list of the last commits of partial segments previously returned that reset the amount of available
53-
/// commits to traverse back to `max_commits_outside_of_workspace`.
55+
/// commits to traverse back to `commit_limit_hint`.
5456
/// Imagine it like a gas station that can be chosen to direct where the commit-budge should be spent.
55-
pub max_commits_recharge_location: Vec<gix::ObjectId>,
57+
pub commits_limit_recharge_location: Vec<gix::ObjectId>,
5658
}
5759

5860
/// Builder
5961
impl Options {
6062
/// Set the maximum amount of commits that each lane in a tip may traverse.
6163
pub fn with_limit(mut self, limit: usize) -> Self {
62-
self.max_commits_outside_of_workspace = Some(limit);
64+
self.commits_limit_hint = Some(limit);
6365
self
6466
}
6567

@@ -68,7 +70,7 @@ impl Options {
6870
mut self,
6971
commits: impl IntoIterator<Item = gix::ObjectId>,
7072
) -> Self {
71-
self.max_commits_recharge_location.extend(commits);
73+
self.commits_limit_recharge_location.extend(commits);
7274
self
7375
}
7476
}
@@ -146,16 +148,18 @@ impl Graph {
146148
/// * The traversal is cut short when there is only tips which are integrated, even though named segments that are
147149
/// supposed to be in the workspace will be fully traversed (implying they will stop at the first anon segment
148150
/// as will happen at merge commits).
151+
#[instrument(skip(meta, ref_name), err(Debug))]
149152
pub fn from_commit_traversal(
150153
tip: gix::Id<'_>,
151154
ref_name: impl Into<Option<gix::refs::FullName>>,
152155
meta: &impl RefMetadata,
153156
Options {
154157
collect_tags,
155-
max_commits_outside_of_workspace: limit,
156-
mut max_commits_recharge_location,
158+
commits_limit_hint: limit,
159+
commits_limit_recharge_location: mut max_commits_recharge_location,
157160
}: Options,
158161
) -> anyhow::Result<Self> {
162+
let limit = Limit(limit);
159163
// TODO: also traverse (outside)-branches that ought to be in the workspace. That way we have the desired ones
160164
// automatically and just have to find a way to prune the undesired ones.
161165
let repo = tip.repo;
@@ -249,7 +253,7 @@ impl Graph {
249253
let mut ws_segment = branch_segment_from_name_and_meta(Some(ws_ref), meta, None)?;
250254
// Drop the limit if we have a target ref
251255
let limit = if workspace_info.target_ref.is_some() {
252-
None
256+
Limit::unspecified()
253257
} else {
254258
limit
255259
};
@@ -279,22 +283,23 @@ impl Graph {
279283
Instruction::CollectCommit {
280284
into: target_segment,
281285
},
282-
/* unlimited traversal for 'negative' commits */
283-
None,
286+
/* unlimited traversal for integrated commits */
287+
Limit::unspecified(),
284288
));
285289
}
286290
}
287291

288292
max_commits_recharge_location.sort();
289293
// Set max-limit so that we compensate for the way this is counted.
290-
let max_limit = limit.map(|l| l + 1);
294+
// let max_limit = limit.incremented();
295+
let max_limit = limit;
291296
while let Some((id, mut propagated_flags, instruction, mut limit)) = next.pop_front() {
292297
if max_commits_recharge_location.binary_search(&id).is_ok() {
293298
limit = max_limit;
294299
}
295-
if limit.is_some_and(|l| l == 0) {
296-
continue;
297-
}
300+
// if limit.is_exhausted() {
301+
// continue;
302+
// }
298303
let info = find(commit_graph.as_ref(), repo, id, &mut buf)?;
299304
let src_flags = graph[instruction.segment_idx()]
300305
.commits
@@ -307,67 +312,15 @@ impl Graph {
307312
propagated_flags |= src_flags;
308313
let segment_idx_for_id = match instruction {
309314
Instruction::CollectCommit { into: src_sidx } => match seen.entry(id) {
310-
Entry::Occupied(mut existing_sidx) => {
311-
let dst_sidx = *existing_sidx.get();
312-
let (top_sidx, mut bottom_sidx) =
313-
// If a normal branch walks into a workspace branch, put the workspace branch on top.
314-
if graph[dst_sidx].workspace_metadata().is_some() &&
315-
graph[src_sidx].ref_name.as_ref()
316-
.is_some_and(|rn| rn.category().is_some_and(|c| matches!(c, Category::LocalBranch))) {
317-
// `dst` is basically swapping with `src`, so must swap commits and connections.
318-
swap_commits_and_connections(&mut graph.inner, dst_sidx, src_sidx);
319-
swap_queued_segments(&mut next, dst_sidx, src_sidx);
320-
321-
// Assure the first commit doesn't name the new owner segment.
322-
{
323-
let s = &mut graph[src_sidx];
324-
if let Some(c) = s.commits.first_mut() {
325-
c.refs.retain(|rn| Some(rn) != s.ref_name.as_ref())
326-
}
327-
// Update the commit-ownership of the connecting commit, but also
328-
// of all other commits in the segment.
329-
existing_sidx.insert(src_sidx);
330-
for commit_id in s.commits.iter().skip(1).map(|c| c.id) {
331-
seen.entry(commit_id).insert(src_sidx);
332-
}
333-
}
334-
(dst_sidx, src_sidx)
335-
} else {
336-
// `src` naturally runs into destination, so nothing needs to be done
337-
// except for connecting both. Commit ownership doesn't change.
338-
(src_sidx, dst_sidx)
339-
};
340-
let top_cidx = graph[top_sidx].last_commit_index();
341-
let mut bottom_cidx =
342-
graph[bottom_sidx].commit_index_of(id).with_context(|| {
343-
format!(
344-
"BUG: Didn't find commit {id} in segment {bottom_sidx}",
345-
bottom_sidx = dst_sidx.index(),
346-
)
347-
})?;
348-
349-
if bottom_cidx != 0 {
350-
let new_bottom_sidx = split_commit_into_segment(
351-
&mut graph,
352-
&mut next,
353-
&mut seen,
354-
bottom_sidx,
355-
bottom_cidx,
356-
)?;
357-
bottom_sidx = new_bottom_sidx;
358-
bottom_cidx = 0;
359-
}
360-
graph.connect_segments(top_sidx, top_cidx, bottom_sidx, bottom_cidx);
361-
let top_flags = top_cidx
362-
.map(|cidx| graph[top_sidx].commits[cidx].flags)
363-
.unwrap_or_default();
364-
let bottom_flags = graph[bottom_sidx].commits[bottom_cidx].flags;
365-
propagate_flags_downward(
366-
&mut graph.inner,
367-
propagated_flags | top_flags | bottom_flags,
368-
bottom_sidx,
369-
Some(bottom_cidx),
370-
);
315+
Entry::Occupied(_) => {
316+
possibly_split_occupied_segment(
317+
&mut graph,
318+
&mut seen,
319+
&mut next,
320+
id,
321+
propagated_flags,
322+
src_sidx,
323+
)?;
371324
continue;
372325
}
373326
Entry::Vacant(e) => {
@@ -387,23 +340,15 @@ impl Graph {
387340
parent_above,
388341
at_commit,
389342
} => match seen.entry(id) {
390-
Entry::Occupied(existing_sidx) => {
391-
let bottom_sidx = *existing_sidx.get();
392-
let bottom = &graph[bottom_sidx];
393-
let bottom_cidx = bottom.commit_index_of(id).context(
394-
"BUG: bottom segment must contain ID, `seen` seems out of date",
343+
Entry::Occupied(_) => {
344+
possibly_split_occupied_segment(
345+
&mut graph,
346+
&mut seen,
347+
&mut next,
348+
id,
349+
propagated_flags,
350+
parent_above,
395351
)?;
396-
if bottom_cidx != 0 {
397-
todo!("split bottom segment at `at_commit`");
398-
}
399-
let bottom_flags = bottom.commits[bottom_cidx].flags;
400-
graph.connect_segments(parent_above, at_commit, bottom_sidx, bottom_cidx);
401-
propagate_flags_downward(
402-
&mut graph.inner,
403-
propagated_flags | bottom_flags,
404-
bottom_sidx,
405-
Some(bottom_cidx),
406-
);
407352
continue;
408353
}
409354
Entry::Vacant(e) => {
@@ -463,7 +408,7 @@ impl Graph {
463408
limit,
464409
)?;
465410

466-
prune_integrated_tips(&mut graph.inner, &mut next, &desired_refs);
411+
prune_integrated_tips(&mut graph.inner, &mut next, &desired_refs, max_limit);
467412
}
468413

469414
graph.post_processed(
@@ -476,6 +421,9 @@ impl Graph {
476421
}
477422
}
478423

424+
#[derive(Debug, Copy, Clone)]
425+
struct Limit(Option<usize>);
426+
479427
#[derive(Debug, Copy, Clone)]
480428
enum Instruction {
481429
/// Contains the segment into which to place this commit.
@@ -511,7 +459,7 @@ impl Instruction {
511459
}
512460
}
513461

514-
type QueueItem = (ObjectId, CommitFlags, Instruction, Option<usize>);
462+
type QueueItem = (gix::ObjectId, CommitFlags, Instruction, Limit);
515463

516464
#[derive(Debug)]
517465
pub(crate) struct EdgeOwned {

0 commit comments

Comments
 (0)