Skip to content

Commit 795d8ee

Browse files
authored
feat: draw a subset of the full graph by specifying refspecs (#143)
## Summary - Add positional argument to scope the graph to specific branches - When multiple refspecs are given, the graph is bounded by their merge-base at the bottom and the branch tips at the top - A single refspec with an upstream tracking branch auto-detects the fork point - No changes to default behavior when no refspecs are specified ## Example ```console $ git-graph master A B ○<──┐ cf43da7 (HEAD -> master) Merge branch A into master ○<┐ │ ddcfb2d Merge branch B into master │ ● │ bcfb676 (B) ... │ │ ● 019ae78 (A) ... ├─┴─┘ ○ 9cca380 Merge branch X into master ``` ## Lazygit integration ```yaml git: branchLogCmd: "git-graph --color always {{branchName}}" ``` Closes #55
1 parent 87b4473 commit 795d8ee

2 files changed

Lines changed: 100 additions & 12 deletions

File tree

src/graph.rs

Lines changed: 82 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,86 @@ 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).map_err(|err| err.message().to_string())?;
332+
resolved_oids.push(oid);
333+
}
334+
335+
if resolved_oids.len() == 1 {
336+
// Single refspec: auto-detect base branch
337+
if let Some(base_oid) = find_base_oid(repository, &refspecs[0], resolved_oids[0]) {
338+
walk.push(base_oid)
339+
.map_err(|err| err.message().to_string())?;
340+
if let Ok(mb) = repository.merge_base(resolved_oids[0], base_oid) {
341+
hide_ancestors_of(repository, walk, mb);
342+
}
343+
}
344+
} else {
345+
// Multiple refspecs: compute merge-base of all
346+
let mut base = resolved_oids[0];
347+
let mut base_found = true;
348+
for oid in &resolved_oids[1..] {
349+
match repository.merge_base(base, *oid) {
350+
Ok(mb) => base = mb,
351+
Err(_) => {
352+
base_found = false;
353+
break;
354+
}
355+
}
356+
}
357+
if base_found {
358+
hide_ancestors_of(repository, walk, base);
359+
}
360+
}
361+
} else if let Some(start) = start_point {
362+
let object = repository
363+
.revparse_single(&start)
364+
.map_err(|err| format!("Failed to resolve start point '{}': {}", start, err))?;
365+
walk.push(object.id())
366+
.map_err(|err| err.message().to_string())?;
367+
} else {
368+
walk.push_glob("*")
369+
.map_err(|err| err.message().to_string())?;
370+
}
371+
Ok(())
372+
}
373+
303374
/// Walks through the commits and adds each commit's Oid to the children of its parents.
304375
fn assign_children(commits: &mut [CommitInfo], indices: &HashMap<Oid, usize>) {
305376
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)