Skip to content

Commit 4d35504

Browse files
feat(smartlog): Support for rendering a sparse smartlog
This should only happen when the user has requested to render a specific revset, as opposed to calling `smartlog` without any arguments, or when any of the other commands trigger a call to `smartlog`. Note that HEAD and the head of the main branch are always displayed, regardless of the revset provided by the user.
1 parent 11a0b0b commit 4d35504

File tree

3 files changed

+277
-53
lines changed

3 files changed

+277
-53
lines changed

git-branchless/src/commands/smartlog.rs

Lines changed: 163 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,20 @@ mod graph {
5252

5353
/// The OID of the parent node in the smartlog commit graph.
5454
///
55-
/// This is different from inspecting `commit.parents()`,& since the smartlog
55+
/// This is different from inspecting `commit.parents()`, since the smartlog
5656
/// will hide most nodes from the commit graph, including parent nodes.
5757
pub parent: Option<NonZeroOid>,
5858

5959
/// The OIDs of the children nodes in the smartlog commit graph.
6060
pub children: Vec<NonZeroOid>,
6161

62+
/// Does this commit have any non-immediate, non-main branch ancestor
63+
/// nodes in the smartlog commit graph?
64+
pub has_ancestors: bool,
65+
66+
/// The OIDs of any non-immediate descendant nodes in the smartlog commit graph.
67+
pub descendants: Vec<NonZeroOid>,
68+
6269
/// Indicates that this is a commit to the main branch.
6370
///
6471
/// These commits are considered to be immutable and should never leave the
@@ -80,6 +87,13 @@ mod graph {
8087
/// where you commit directly to the main branch and then later rewrite the
8188
/// commit.
8289
pub is_obsolete: bool,
90+
91+
/// Indicates that this commit has descendants, but that none of them
92+
/// are included in the graph.
93+
///
94+
/// This allows us to indicate this "false head" to the user. Otherwise,
95+
/// this commit would look like a normal, descendant-less head.
96+
pub is_false_head: bool,
8397
}
8498

8599
/// Graph of commits that the user is working on.
@@ -111,31 +125,27 @@ mod graph {
111125
}
112126
}
113127

114-
/// Find additional commits that should be displayed.
128+
/// Build the smartlog graph by finding additional commits that should be displayed.
115129
///
116130
/// For example, if you check out a commit that has intermediate parent commits
117131
/// between it and the main branch, those intermediate commits should be shown
118132
/// (or else you won't get a good idea of the line of development that happened
119133
/// for this commit since the main branch).
120134
#[instrument]
121-
fn walk_from_commits<'repo>(
135+
fn build_graph<'repo>(
122136
effects: &Effects,
123137
repo: &'repo Repo,
124138
dag: &Dag,
125-
active_heads: &CommitSet,
139+
commits: &CommitSet,
126140
) -> eyre::Result<SmartlogGraph<'repo>> {
127141
let mut graph: HashMap<NonZeroOid, Node> = {
128142
let mut result = HashMap::new();
129-
for vertex in commit_set_to_vec(active_heads)? {
143+
for vertex in commit_set_to_vec(commits)? {
130144
let vertex = CommitSet::from(vertex);
131145
let merge_bases = dag.query().gca_all(dag.main_branch_commit.union(&vertex))?;
132-
let intermediate_commits = if merge_bases.is_empty()? {
133-
vertex
134-
} else {
135-
dag.query().range(merge_bases, vertex)?
136-
};
146+
let vertices = vertex.union(&merge_bases);
137147

138-
for oid in commit_set_to_vec(&intermediate_commits)? {
148+
for oid in commit_set_to_vec(&vertices)? {
139149
let object = match repo.find_commit(oid)? {
140150
Some(commit) => NodeObject::Commit { commit },
141151
None => {
@@ -150,40 +160,103 @@ mod graph {
150160
object,
151161
parent: None, // populated below
152162
children: Vec::new(), // populated below
163+
has_ancestors: false,
164+
descendants: Vec::new(), // populated below
153165
is_main: dag.is_public_commit(oid)?,
154166
is_obsolete: dag.query_obsolete_commits().contains(&oid.into())?,
167+
is_false_head: false,
155168
},
156169
);
157170
}
158171
}
159172
result
160173
};
161174

162-
// Find immediate parent-child links.
163-
let links: Vec<(NonZeroOid, NonZeroOid)> = {
164-
let non_main_node_oids =
165-
graph.iter().filter_map(
166-
|(child_oid, node)| if !node.is_main { Some(child_oid) } else { None },
167-
);
168-
169-
let mut links = Vec::new();
170-
for child_oid in non_main_node_oids {
171-
let parent_vertexes = dag.query().parents(CommitSet::from(*child_oid))?;
172-
let parent_oids = commit_set_to_vec(&parent_vertexes)?;
173-
for parent_oid in parent_oids {
174-
if graph.contains_key(&parent_oid) {
175-
links.push((*child_oid, parent_oid))
175+
let mut immediate_links: Vec<(NonZeroOid, NonZeroOid)> = Vec::new();
176+
let mut non_immediate_links: Vec<(NonZeroOid, NonZeroOid)> = Vec::new();
177+
178+
let non_main_node_oids =
179+
graph
180+
.iter()
181+
.filter_map(|(child_oid, node)| if !node.is_main { Some(child_oid) } else { None });
182+
183+
let graph_vertices: CommitSet = graph.keys().cloned().collect();
184+
for child_oid in non_main_node_oids {
185+
let parent_vertices = dag.query().parents(CommitSet::from(*child_oid))?;
186+
187+
// Find immediate parent-child links.
188+
let parents_in_graph = parent_vertices.intersection(&graph_vertices);
189+
let parent_oids = commit_set_to_vec(&parents_in_graph)?;
190+
for parent_oid in parent_oids {
191+
immediate_links.push((*child_oid, parent_oid))
192+
}
193+
194+
if parent_vertices.count()? != parents_in_graph.count()? {
195+
// Find non-immediate ancestor links.
196+
let excluded_parents = parent_vertices.difference(&graph_vertices);
197+
let excluded_parent_oids = commit_set_to_vec(&excluded_parents)?;
198+
for parent_oid in excluded_parent_oids {
199+
// Find the nearest ancestor that is included in the graph and
200+
// also on the same branch.
201+
202+
let parent_set = CommitSet::from(parent_oid);
203+
let merge_base = dag
204+
.query()
205+
.gca_one(dag.main_branch_commit.union(&parent_set))?;
206+
207+
let path_to_main_branch = match merge_base {
208+
Some(merge_base) => {
209+
dag.query().range(CommitSet::from(merge_base), parent_set)?
210+
}
211+
None => CommitSet::empty(),
212+
};
213+
let nearest_branch_ancestor = dag
214+
.query()
215+
.heads_ancestors(path_to_main_branch.intersection(&graph_vertices))?;
216+
217+
let ancestor_oids = commit_set_to_vec(&nearest_branch_ancestor)?;
218+
for ancestor_oid in ancestor_oids.iter() {
219+
non_immediate_links.push((*ancestor_oid, *child_oid));
176220
}
177221
}
178222
}
179-
links
180-
};
223+
}
181224

182-
for (child_oid, parent_oid) in links.iter() {
225+
for (child_oid, parent_oid) in immediate_links.iter() {
183226
graph.get_mut(child_oid).unwrap().parent = Some(*parent_oid);
184227
graph.get_mut(parent_oid).unwrap().children.push(*child_oid);
185228
}
186229

230+
for (ancestor_oid, descendent_oid) in non_immediate_links.iter() {
231+
graph.get_mut(descendent_oid).unwrap().has_ancestors = true;
232+
graph
233+
.get_mut(ancestor_oid)
234+
.unwrap()
235+
.descendants
236+
.push(*descendent_oid);
237+
}
238+
239+
for (oid, node) in graph.iter_mut() {
240+
let oid_set = CommitSet::from(*oid);
241+
let is_main_head = !dag.main_branch_commit.intersection(&oid_set).is_empty()?;
242+
let ancestor_of_main = node.is_main && !is_main_head;
243+
let has_descendants_in_graph =
244+
!node.children.is_empty() || !node.descendants.is_empty();
245+
246+
if ancestor_of_main || has_descendants_in_graph {
247+
continue;
248+
}
249+
250+
// This node has no descendants in the graph, so it's a
251+
// false head if it has *any* (non-obsolete) children.
252+
let children_not_in_graph = dag
253+
.query()
254+
.children(oid_set)?
255+
.difference(&dag.query_obsolete_commits());
256+
257+
node.is_false_head = !children_not_in_graph.is_empty()?;
258+
}
259+
187260
Ok(SmartlogGraph { nodes: graph })
188261
}
189262

@@ -224,11 +297,16 @@ mod graph {
224297
let mut graph = {
225298
let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
226299

227-
for oid in commit_set_to_vec(commits)? {
300+
// HEAD and main head must be included
301+
let commits = commits
302+
.union(&dag.head_commit)
303+
.union(&dag.main_branch_commit);
304+
305+
for oid in commit_set_to_vec(&commits)? {
228306
mark_commit_reachable(repo, oid)?;
229307
}
230308

231-
walk_from_commits(&effects, repo, dag, commits)?
309+
build_graph(&effects, repo, dag, &commits)?
232310
};
233311
sort_children(&mut graph);
234312
Ok(graph)
@@ -237,6 +315,7 @@ mod graph {
237315

238316
mod render {
239317
use std::cmp::Ordering;
318+
use std::collections::HashSet;
240319
use std::convert::TryFrom;
241320

242321
use cursive::theme::Effect;
@@ -270,7 +349,16 @@ mod render {
270349
let mut root_commit_oids: Vec<NonZeroOid> = graph
271350
.nodes
272351
.iter()
273-
.filter(|(_oid, node)| node.parent.is_none())
352+
.filter(|(_oid, node)| {
353+
// Common case: on main w/ no parents in graph, eg a merge base
354+
node.parent.is_none() && node.is_main ||
355+
// Pathological cases: orphaned, garbage collected, etc
356+
node.parent.is_none()
357+
&& !node.is_main
358+
&& node.children.is_empty()
359+
&& node.descendants.is_empty()
360+
&& !node.has_ancestors
361+
})
274362
.map(|(oid, _node)| oid)
275363
.copied()
276364
.collect();
@@ -337,7 +425,13 @@ mod render {
337425
(true, true, true) => glyphs.commit_main_obsolete_head,
338426
};
339427

340-
let first_line = {
428+
let mut lines = vec![];
429+
430+
if current_node.has_ancestors {
431+
lines.push(StyledString::plain(glyphs.vertical_ellipsis.to_string()));
432+
};
433+
434+
lines.push({
341435
let mut first_line = StyledString::new();
342436
first_line.append_plain(cursor);
343437
first_line.append_plain(" ");
@@ -347,36 +441,50 @@ mod render {
347441
} else {
348442
first_line
349443
}
444+
});
445+
446+
if current_node.is_false_head {
447+
lines.push(StyledString::plain(glyphs.vertical_ellipsis.to_string()));
350448
};
351449

352-
let mut lines = vec![first_line];
353450
let children: Vec<_> = current_node
354451
.children
355452
.iter()
356453
.filter(|child_oid| graph.nodes.contains_key(child_oid))
357454
.copied()
358455
.collect();
359-
for (child_idx, child_oid) in children.iter().enumerate() {
456+
let descendants: HashSet<_> = current_node
457+
.descendants
458+
.iter()
459+
.filter(|descendent_oid| graph.nodes.contains_key(descendent_oid))
460+
.copied()
461+
.collect();
462+
for (child_idx, child_oid) in children.iter().chain(descendants.iter()).enumerate() {
360463
if root_oids.contains(child_oid) {
361464
// Will be rendered by the parent.
362465
continue;
363466
}
364467

365-
if child_idx == children.len() - 1 {
468+
let is_last_child = child_idx == (children.len() + descendants.len()) - 1;
469+
if is_last_child {
366470
let line = match last_child_line_char {
367-
Some(_) => StyledString::plain(format!(
471+
Some(_) => Some(StyledString::plain(format!(
368472
"{}{}",
369473
glyphs.line_with_offshoot, glyphs.slash
370-
)),
371-
372-
None => StyledString::plain(glyphs.line.to_string()),
474+
))),
475+
None if current_node.descendants.is_empty() => {
476+
Some(StyledString::plain(glyphs.line.to_string()))
477+
}
478+
None => None,
373479
};
374-
lines.push(line)
480+
if let Some(line) = line {
481+
lines.push(line);
482+
}
375483
} else {
376484
lines.push(StyledString::plain(format!(
377485
"{}{}",
378486
glyphs.line_with_offshoot, glyphs.slash
379-
)))
487+
)));
380488
}
381489

382490
let child_output = get_child_output(
@@ -389,7 +497,7 @@ mod render {
389497
None,
390498
)?;
391499
for child_line in child_output {
392-
let line = if child_idx == children.len() - 1 {
500+
let line = if is_last_child {
393501
match last_child_line_char {
394502
Some(last_child_line_char) => StyledStringBuilder::new()
395503
.append_plain(format!("{} ", last_child_line_char))
@@ -399,7 +507,14 @@ mod render {
399507
}
400508
} else {
401509
StyledStringBuilder::new()
402-
.append_plain(format!("{} ", glyphs.line))
510+
.append_plain(format!(
511+
"{} ",
512+
if !current_node.descendants.is_empty() {
513+
glyphs.vertical_ellipsis
514+
} else {
515+
glyphs.line
516+
}
517+
))
403518
.append(child_line)
404519
.build()
405520
};
@@ -453,13 +568,10 @@ mod render {
453568
let last_child_line_char = {
454569
if root_idx == root_oids.len() - 1 {
455570
None
571+
} else if has_real_parent(root_oids[root_idx + 1], *root_oid)? {
572+
Some(glyphs.line)
456573
} else {
457-
let next_root_oid = root_oids[root_idx + 1];
458-
if has_real_parent(next_root_oid, *root_oid)? {
459-
Some(glyphs.line)
460-
} else {
461-
Some(glyphs.vertical_ellipsis)
462-
}
574+
Some(glyphs.vertical_ellipsis)
463575
}
464576
};
465577

@@ -508,8 +620,8 @@ mod render {
508620
/// as an offset from the current event.
509621
pub event_id: Option<isize>,
510622

511-
/// The commits to render. These commits and their ancestors up to the
512-
/// main branch will be rendered.
623+
/// The commits to render. These commits, plus any related commits, will
624+
/// be rendered.
513625
pub revset: Revset,
514626

515627
pub resolve_revset_options: ResolveRevsetOptions,

git-branchless/src/opts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,8 @@ pub enum Command {
460460
#[clap(value_parser, long = "event-id")]
461461
event_id: Option<isize>,
462462

463-
/// The commits to render. These commits and their ancestors up to the
464-
/// main branch will be rendered.
463+
/// The commits to render. These commits, plus any related commits, will
464+
/// be rendered.
465465
#[clap(value_parser)]
466466
revset: Option<Revset>,
467467

0 commit comments

Comments
 (0)