Skip to content

Commit 4217fda

Browse files
committed
feat: draw a subset of the full graph by specifying refspecs
Allow passing branch names or refspecs as positional arguments to scope the graph visualization to only the relevant subgraph. When multiple refspecs are given, the graph is bounded by their merge-base at the bottom. A single refspec with an upstream tracking branch auto-detects the fork point. Closes #55
1 parent 87b4473 commit 4217fda

2 files changed

Lines changed: 101 additions & 12 deletions

File tree

‎src/graph.rs‎

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ impl GitGraph {
4545
settings: &Settings,
4646
start_point: Option<String>,
4747
max_count: Option<usize>,
48+
refspecs: Vec<String>,
4849
) -> Result<Self, String> {
4950
#![doc = include_str!("../docs/branch_assignment.md")]
5051
let mut stashes = HashSet::new();
@@ -62,17 +63,7 @@ impl GitGraph {
6263
walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
6364
.map_err(|err| err.message().to_string())?;
6465

65-
// Use starting point if specified
66-
if let Some(start) = start_point {
67-
let object = repository
68-
.revparse_single(&start)
69-
.map_err(|err| format!("Failed to resolve start point '{}': {}", start, err))?;
70-
walk.push(object.id())
71-
.map_err(|err| err.message().to_string())?;
72-
} else {
73-
walk.push_glob("*")
74-
.map_err(|err| err.message().to_string())?;
75-
}
66+
configure_revwalk(&repository, &mut walk, start_point, &refspecs)?;
7667

7768
if repository.is_shallow() {
7869
return Err("ERROR: git-graph does not support shallow clones due to a missing feature in the underlying libgit2 library.".to_string());
@@ -300,6 +291,87 @@ impl BranchVis {
300291
}
301292
}
302293

294+
/// For a single refspec, find a base branch to compare against
295+
/// using the branch's upstream tracking ref.
296+
fn find_base_oid(repository: &Repository, refspec: &str, tip_oid: Oid) -> Option<Oid> {
297+
if let Ok(branch) = repository.find_branch(refspec, BranchType::Local) {
298+
if let Ok(upstream) = branch.upstream() {
299+
if let Some(oid) = upstream.get().target() {
300+
if oid != tip_oid {
301+
return Some(oid);
302+
}
303+
}
304+
}
305+
}
306+
307+
None
308+
}
309+
310+
fn hide_ancestors_of(repository: &Repository, walk: &mut git2::Revwalk, merge_base: Oid) {
311+
if let Ok(commit) = repository.find_commit(merge_base) {
312+
for parent in commit.parents() {
313+
let _ = walk.hide(parent.id());
314+
}
315+
}
316+
}
317+
318+
fn configure_revwalk(
319+
repository: &Repository,
320+
walk: &mut git2::Revwalk,
321+
start_point: Option<String>,
322+
refspecs: &[String],
323+
) -> Result<(), String> {
324+
if !refspecs.is_empty() {
325+
let mut resolved_oids = Vec::with_capacity(refspecs.len());
326+
for refspec in refspecs {
327+
let object = repository
328+
.revparse_single(refspec)
329+
.map_err(|err| format!("Failed to resolve refspec '{}': {}", refspec, err))?;
330+
let oid = object.id();
331+
walk.push(oid)
332+
.map_err(|err| err.message().to_string())?;
333+
resolved_oids.push(oid);
334+
}
335+
336+
if resolved_oids.len() == 1 {
337+
// Single refspec: auto-detect base branch
338+
if let Some(base_oid) = find_base_oid(repository, &refspecs[0], resolved_oids[0]) {
339+
walk.push(base_oid)
340+
.map_err(|err| err.message().to_string())?;
341+
if let Ok(mb) = repository.merge_base(resolved_oids[0], base_oid) {
342+
hide_ancestors_of(repository, walk, mb);
343+
}
344+
}
345+
} else {
346+
// Multiple refspecs: compute merge-base of all
347+
let mut base = resolved_oids[0];
348+
let mut base_found = true;
349+
for oid in &resolved_oids[1..] {
350+
match repository.merge_base(base, *oid) {
351+
Ok(mb) => base = mb,
352+
Err(_) => {
353+
base_found = false;
354+
break;
355+
}
356+
}
357+
}
358+
if base_found {
359+
hide_ancestors_of(repository, walk, base);
360+
}
361+
}
362+
} else if let Some(start) = start_point {
363+
let object = repository
364+
.revparse_single(&start)
365+
.map_err(|err| format!("Failed to resolve start point '{}': {}", start, err))?;
366+
walk.push(object.id())
367+
.map_err(|err| err.message().to_string())?;
368+
} else {
369+
walk.push_glob("*")
370+
.map_err(|err| err.message().to_string())?;
371+
}
372+
Ok(())
373+
}
374+
303375
/// Walks through the commits and adds each commit's Oid to the children of its parents.
304376
fn assign_children(commits: &mut [CommitInfo], indices: &HashMap<Oid, usize>) {
305377
for idx in 0..commits.len() {

‎src/main.rs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ fn from_args() -> Result<(), String> {
4949
ses.settings.as_ref().unwrap(),
5050
ses.svg,
5151
ses.commit_limit,
52+
ses.refspecs,
5253
)
5354
}
5455

@@ -126,6 +127,14 @@ fn match_args() -> ArgMatches {
126127
)
127128
.required(false)
128129
.num_args(1),
130+
)
131+
.arg(
132+
Arg::new("refspecs")
133+
.help(
134+
"Branch names or refspecs to show.\n \
135+
Only the subgraph between the merge-base and the tips is displayed.",
136+
)
137+
.num_args(1..),
129138
);
130139

131140
// Return match of declared arguments with what is present on command line
@@ -190,6 +199,11 @@ fn configure_session(ses: &mut Session, matches: &ArgMatches) -> Result<bool, St
190199
};
191200
ses.settings = Some(settings);
192201

202+
ses.refspecs = matches
203+
.get_many::<String>("refspecs")
204+
.map(|vals| vals.cloned().collect())
205+
.unwrap_or_default();
206+
193207
Ok(run_application)
194208
}
195209

@@ -204,6 +218,7 @@ struct Session {
204218
pub repository: Option<Repository>,
205219
pub svg: bool,
206220
pub commit_limit: Option<usize>,
221+
pub refspecs: Vec<String>,
207222
}
208223

209224
impl Session {
@@ -219,6 +234,7 @@ impl Session {
219234
repository: None,
220235
svg: false,
221236
commit_limit: None,
237+
refspecs: Vec::new(),
222238
}
223239
}
224240
}
@@ -597,9 +613,10 @@ fn run(
597613
settings: &Settings,
598614
svg: bool,
599615
max_commits: Option<usize>,
616+
refspecs: Vec<String>,
600617
) -> Result<(), String> {
601618
let now = Instant::now();
602-
let graph = GitGraph::new(repository, settings, None, max_commits)?;
619+
let graph = GitGraph::new(repository, settings, None, max_commits, refspecs)?;
603620

604621
let duration_graph = now.elapsed().as_micros();
605622

0 commit comments

Comments
 (0)