Skip to content

Commit bd32e39

Browse files
committed
feat: bit revision list --svg to create a visual graph of commits.
It's mainly a test of how well `layout-rs` performs.
1 parent 71efcbb commit bd32e39

File tree

7 files changed

+207
-44
lines changed

7 files changed

+207
-44
lines changed

Diff for: Cargo.lock

+43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ sha1_smol = { opt-level = 3 }
182182

183183
[profile.release]
184184
overflow-checks = false
185-
lto = "fat"
185+
#lto = "fat"
186186
# this bloats files but assures destructors are called, important for tempfiles. One day I hope we
187187
# can wire up the 'abrt' signal handler so tempfiles will be removed in case of panics.
188188
panic = 'unwind'

Diff for: gitoxide-core/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ smallvec = { version = "1.10.0", optional = true }
6969
# for 'query'
7070
rusqlite = { version = "0.29.0", optional = true, features = ["bundled"] }
7171

72+
# for svg graph output
73+
layout-rs = "0.1.1"
74+
open = "4.1.0"
75+
7276
document-features = { version = "0.2.0", optional = true }
7377

7478
[package.metadata.docs.rs]

Diff for: gitoxide-core/src/repository/revision/list.rs

+134-36
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,140 @@
1+
use crate::OutputFormat;
12
use std::ffi::OsString;
3+
use std::path::PathBuf;
24

3-
use anyhow::{bail, Context};
4-
use gix::traverse::commit::Sorting;
5+
pub struct Context {
6+
pub limit: Option<usize>,
7+
pub spec: OsString,
8+
pub format: OutputFormat,
9+
pub text: Format,
10+
}
511

6-
use crate::OutputFormat;
12+
pub enum Format {
13+
Text,
14+
Svg { path: PathBuf },
15+
}
16+
pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 0..=2;
717

8-
pub fn list(
9-
mut repo: gix::Repository,
10-
spec: OsString,
11-
mut out: impl std::io::Write,
12-
format: OutputFormat,
13-
) -> anyhow::Result<()> {
14-
if format != OutputFormat::Human {
15-
bail!("Only human output is currently supported");
16-
}
17-
repo.object_cache_size_if_unset(4 * 1024 * 1024);
18-
19-
let spec = gix::path::os_str_into_bstr(&spec)?;
20-
let id = repo
21-
.rev_parse_single(spec)
22-
.context("Only single revisions are currently supported")?;
23-
let commits = id
24-
.object()?
25-
.peel_to_kind(gix::object::Kind::Commit)
26-
.context("Need commitish as starting point")?
27-
.id()
28-
.ancestors()
29-
.sorting(Sorting::ByCommitTimeNewestFirst)
30-
.all()?;
31-
for commit in commits {
32-
let commit = commit?;
33-
writeln!(
34-
out,
35-
"{} {} {}",
36-
commit.id().shorten_or_id(),
37-
commit.commit_time.expect("traversal with date"),
38-
commit.parent_ids.len()
39-
)?;
18+
pub(crate) mod function {
19+
use anyhow::{bail, Context};
20+
use gix::traverse::commit::Sorting;
21+
use std::collections::HashMap;
22+
23+
use gix::Progress;
24+
use layout::backends::svg::SVGWriter;
25+
use layout::core::base::Orientation;
26+
use layout::core::geometry::Point;
27+
use layout::core::style::StyleAttr;
28+
use layout::std_shapes::shapes::{Arrow, Element, ShapeKind};
29+
30+
use crate::repository::revision::list::Format;
31+
use crate::OutputFormat;
32+
33+
pub fn list(
34+
mut repo: gix::Repository,
35+
mut progress: impl Progress,
36+
mut out: impl std::io::Write,
37+
super::Context {
38+
spec,
39+
format,
40+
text,
41+
limit,
42+
}: super::Context,
43+
) -> anyhow::Result<()> {
44+
if format != OutputFormat::Human {
45+
bail!("Only human output is currently supported");
46+
}
47+
repo.object_cache_size_if_unset(4 * 1024 * 1024);
48+
49+
let spec = gix::path::os_str_into_bstr(&spec)?;
50+
let id = repo
51+
.rev_parse_single(spec)
52+
.context("Only single revisions are currently supported")?;
53+
let commits = id
54+
.object()?
55+
.peel_to_kind(gix::object::Kind::Commit)
56+
.context("Need commitish as starting point")?
57+
.id()
58+
.ancestors()
59+
.sorting(Sorting::ByCommitTimeNewestFirst)
60+
.all()?;
61+
62+
let mut vg = match text {
63+
Format::Svg { path } => (
64+
layout::topo::layout::VisualGraph::new(Orientation::TopToBottom),
65+
path,
66+
HashMap::new(),
67+
)
68+
.into(),
69+
Format::Text => None,
70+
};
71+
progress.init(None, gix::progress::count("commits"));
72+
progress.set_name("traverse");
73+
74+
let start = std::time::Instant::now();
75+
for commit in commits {
76+
if gix::interrupt::is_triggered() {
77+
bail!("interrupted by user");
78+
}
79+
let commit = commit?;
80+
match vg.as_mut() {
81+
Some((vg, _path, map)) => {
82+
let pt = Point::new(100., 30.);
83+
let source = match map.get(&commit.id) {
84+
Some(handle) => *handle,
85+
None => {
86+
let name = commit.id().shorten_or_id().to_string();
87+
let shape = ShapeKind::new_box(name.as_str());
88+
let style = StyleAttr::simple();
89+
let handle = vg.add_node(Element::create(shape, style, Orientation::LeftToRight, pt));
90+
map.insert(commit.id, handle);
91+
handle
92+
}
93+
};
94+
95+
for parent_id in commit.parent_ids() {
96+
let dest = match map.get(parent_id.as_ref()) {
97+
Some(handle) => *handle,
98+
None => {
99+
let name = parent_id.shorten_or_id().to_string();
100+
let shape = ShapeKind::new_box(name.as_str());
101+
let style = StyleAttr::simple();
102+
let dest = vg.add_node(Element::create(shape, style, Orientation::LeftToRight, pt));
103+
map.insert(parent_id.detach(), dest);
104+
dest
105+
}
106+
};
107+
let arrow = Arrow::simple("");
108+
vg.add_edge(arrow, source, dest);
109+
}
110+
}
111+
None => {
112+
writeln!(
113+
out,
114+
"{} {} {}",
115+
commit.id().shorten_or_id(),
116+
commit.commit_time.expect("traversal with date"),
117+
commit.parent_ids.len()
118+
)?;
119+
}
120+
}
121+
progress.inc();
122+
if limit.map_or(false, |limit| limit == progress.step()) {
123+
break;
124+
}
125+
}
126+
127+
progress.show_throughput(start);
128+
if let Some((mut vg, path, _)) = vg {
129+
let start = std::time::Instant::now();
130+
progress.set_name("computing graph");
131+
progress.info(format!("writing {path:?}…"));
132+
let mut svg = SVGWriter::new();
133+
vg.do_it(false, false, false, &mut svg);
134+
std::fs::write(&path, svg.finalize().as_bytes())?;
135+
open::that(path)?;
136+
progress.show_throughput(start);
137+
}
138+
Ok(())
40139
}
41-
Ok(())
42140
}

Diff for: gitoxide-core/src/repository/revision/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
mod list;
2-
pub use list::list;
1+
pub mod list;
2+
pub use list::function::list;
33
mod explain;
44
pub use explain::explain;
55

Diff for: src/plumbing/main.rs

+17-5
Original file line numberDiff line numberDiff line change
@@ -693,14 +693,26 @@ pub fn main() -> Result<()> {
693693
},
694694
),
695695
Subcommands::Revision(cmd) => match cmd {
696-
revision::Subcommands::List { spec } => prepare_and_run(
696+
revision::Subcommands::List { spec, svg, limit } => prepare_and_run(
697697
"revision-list",
698-
verbose,
698+
auto_verbose,
699699
progress,
700700
progress_keep_open,
701-
None,
702-
move |_progress, out, _err| {
703-
core::repository::revision::list(repository(Mode::Lenient)?, spec, out, format)
701+
core::repository::revision::list::PROGRESS_RANGE,
702+
move |progress, out, _err| {
703+
core::repository::revision::list(
704+
repository(Mode::Lenient)?,
705+
progress,
706+
out,
707+
core::repository::revision::list::Context {
708+
limit,
709+
spec,
710+
format,
711+
text: svg.map_or(core::repository::revision::list::Format::Text, |path| {
712+
core::repository::revision::list::Format::Svg { path }
713+
}),
714+
},
715+
)
704716
},
705717
),
706718
revision::Subcommands::PreviousBranches => prepare_and_run(

Diff for: src/plumbing/options/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,12 @@ pub mod revision {
441441
/// List all commits reachable from the given rev-spec.
442442
#[clap(visible_alias = "l")]
443443
List {
444+
/// How many commits to list at most.
445+
#[clap(long, short = 'l')]
446+
limit: Option<usize>,
447+
/// Write the graph as SVG file to the given path.
448+
#[clap(long, short = 's')]
449+
svg: Option<std::path::PathBuf>,
444450
/// The rev-spec to list reachable commits from.
445451
#[clap(default_value = "@")]
446452
spec: std::ffi::OsString,

0 commit comments

Comments
 (0)