Skip to content

Commit

Permalink
feat: bit revision list --svg to create a visual graph of commits.
Browse files Browse the repository at this point in the history
It's mainly a test of how well `layout-rs` performs.
  • Loading branch information
Byron committed Jun 12, 2023
1 parent 71efcbb commit bd32e39
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 44 deletions.
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ sha1_smol = { opt-level = 3 }

[profile.release]
overflow-checks = false
lto = "fat"
#lto = "fat"
# this bloats files but assures destructors are called, important for tempfiles. One day I hope we
# can wire up the 'abrt' signal handler so tempfiles will be removed in case of panics.
panic = 'unwind'
Expand Down
4 changes: 4 additions & 0 deletions gitoxide-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ smallvec = { version = "1.10.0", optional = true }
# for 'query'
rusqlite = { version = "0.29.0", optional = true, features = ["bundled"] }

# for svg graph output
layout-rs = "0.1.1"
open = "4.1.0"

document-features = { version = "0.2.0", optional = true }

[package.metadata.docs.rs]
Expand Down
170 changes: 134 additions & 36 deletions gitoxide-core/src/repository/revision/list.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,140 @@
use crate::OutputFormat;
use std::ffi::OsString;
use std::path::PathBuf;

use anyhow::{bail, Context};
use gix::traverse::commit::Sorting;
pub struct Context {
pub limit: Option<usize>,
pub spec: OsString,
pub format: OutputFormat,
pub text: Format,
}

use crate::OutputFormat;
pub enum Format {
Text,
Svg { path: PathBuf },
}
pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 0..=2;

pub fn list(
mut repo: gix::Repository,
spec: OsString,
mut out: impl std::io::Write,
format: OutputFormat,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("Only human output is currently supported");
}
repo.object_cache_size_if_unset(4 * 1024 * 1024);

let spec = gix::path::os_str_into_bstr(&spec)?;
let id = repo
.rev_parse_single(spec)
.context("Only single revisions are currently supported")?;
let commits = id
.object()?
.peel_to_kind(gix::object::Kind::Commit)
.context("Need commitish as starting point")?
.id()
.ancestors()
.sorting(Sorting::ByCommitTimeNewestFirst)
.all()?;
for commit in commits {
let commit = commit?;
writeln!(
out,
"{} {} {}",
commit.id().shorten_or_id(),
commit.commit_time.expect("traversal with date"),
commit.parent_ids.len()
)?;
pub(crate) mod function {
use anyhow::{bail, Context};
use gix::traverse::commit::Sorting;
use std::collections::HashMap;

use gix::Progress;
use layout::backends::svg::SVGWriter;
use layout::core::base::Orientation;
use layout::core::geometry::Point;
use layout::core::style::StyleAttr;
use layout::std_shapes::shapes::{Arrow, Element, ShapeKind};

use crate::repository::revision::list::Format;
use crate::OutputFormat;

pub fn list(
mut repo: gix::Repository,
mut progress: impl Progress,
mut out: impl std::io::Write,
super::Context {
spec,
format,
text,
limit,
}: super::Context,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("Only human output is currently supported");
}
repo.object_cache_size_if_unset(4 * 1024 * 1024);

let spec = gix::path::os_str_into_bstr(&spec)?;
let id = repo
.rev_parse_single(spec)
.context("Only single revisions are currently supported")?;
let commits = id
.object()?
.peel_to_kind(gix::object::Kind::Commit)
.context("Need commitish as starting point")?
.id()
.ancestors()
.sorting(Sorting::ByCommitTimeNewestFirst)
.all()?;

let mut vg = match text {
Format::Svg { path } => (
layout::topo::layout::VisualGraph::new(Orientation::TopToBottom),
path,
HashMap::new(),
)
.into(),
Format::Text => None,
};
progress.init(None, gix::progress::count("commits"));
progress.set_name("traverse");

let start = std::time::Instant::now();
for commit in commits {
if gix::interrupt::is_triggered() {
bail!("interrupted by user");
}
let commit = commit?;
match vg.as_mut() {
Some((vg, _path, map)) => {
let pt = Point::new(100., 30.);
let source = match map.get(&commit.id) {
Some(handle) => *handle,
None => {
let name = commit.id().shorten_or_id().to_string();
let shape = ShapeKind::new_box(name.as_str());
let style = StyleAttr::simple();
let handle = vg.add_node(Element::create(shape, style, Orientation::LeftToRight, pt));
map.insert(commit.id, handle);
handle
}
};

for parent_id in commit.parent_ids() {
let dest = match map.get(parent_id.as_ref()) {
Some(handle) => *handle,
None => {
let name = parent_id.shorten_or_id().to_string();
let shape = ShapeKind::new_box(name.as_str());
let style = StyleAttr::simple();
let dest = vg.add_node(Element::create(shape, style, Orientation::LeftToRight, pt));
map.insert(parent_id.detach(), dest);
dest
}
};
let arrow = Arrow::simple("");
vg.add_edge(arrow, source, dest);
}
}
None => {
writeln!(
out,
"{} {} {}",
commit.id().shorten_or_id(),
commit.commit_time.expect("traversal with date"),
commit.parent_ids.len()
)?;
}
}
progress.inc();
if limit.map_or(false, |limit| limit == progress.step()) {
break;
}
}

progress.show_throughput(start);
if let Some((mut vg, path, _)) = vg {
let start = std::time::Instant::now();
progress.set_name("computing graph");
progress.info(format!("writing {path:?}…"));
let mut svg = SVGWriter::new();
vg.do_it(false, false, false, &mut svg);
std::fs::write(&path, svg.finalize().as_bytes())?;
open::that(path)?;
progress.show_throughput(start);
}
Ok(())
}
Ok(())
}
4 changes: 2 additions & 2 deletions gitoxide-core/src/repository/revision/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod list;
pub use list::list;
pub mod list;
pub use list::function::list;
mod explain;
pub use explain::explain;

Expand Down
22 changes: 17 additions & 5 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,14 +693,26 @@ pub fn main() -> Result<()> {
},
),
Subcommands::Revision(cmd) => match cmd {
revision::Subcommands::List { spec } => prepare_and_run(
revision::Subcommands::List { spec, svg, limit } => prepare_and_run(
"revision-list",
verbose,
auto_verbose,
progress,
progress_keep_open,
None,
move |_progress, out, _err| {
core::repository::revision::list(repository(Mode::Lenient)?, spec, out, format)
core::repository::revision::list::PROGRESS_RANGE,
move |progress, out, _err| {
core::repository::revision::list(
repository(Mode::Lenient)?,
progress,
out,
core::repository::revision::list::Context {
limit,
spec,
format,
text: svg.map_or(core::repository::revision::list::Format::Text, |path| {
core::repository::revision::list::Format::Svg { path }
}),
},
)
},
),
revision::Subcommands::PreviousBranches => prepare_and_run(
Expand Down
6 changes: 6 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ pub mod revision {
/// List all commits reachable from the given rev-spec.
#[clap(visible_alias = "l")]
List {
/// How many commits to list at most.
#[clap(long, short = 'l')]
limit: Option<usize>,
/// Write the graph as SVG file to the given path.
#[clap(long, short = 's')]
svg: Option<std::path::PathBuf>,
/// The rev-spec to list reachable commits from.
#[clap(default_value = "@")]
spec: std::ffi::OsString,
Expand Down

0 comments on commit bd32e39

Please sign in to comment.