Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

visualize commit-graphs as SVG #893

Merged
merged 10 commits into from
Jun 12, 2023
45 changes: 44 additions & 1 deletion Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ resolver = "2"

[[bin]]
name = "ein"
doc = false
path = "src/ein.rs"
test = false
doctest = false

[[bin]]
name = "gix"
path = "src/gix.rs"
doc = false
test = false
doctest = false

Expand Down Expand Up @@ -182,7 +184,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
10 changes: 4 additions & 6 deletions gitoxide-core/src/repository/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,20 @@ pub(crate) mod function {
}

match fetch_outcome.status {
Status::NoPackReceived { .. } => {
Status::NoPackReceived { dry_run, .. } => {
assert!(!dry_run, "dry-run unsupported");
writeln!(err, "The cloned repository appears to be empty")?;
}
Status::DryRun { .. } => unreachable!("dry-run unsupported"),
Status::Change {
update_refs,
negotiation_rounds,
..
update_refs, negotiate, ..
} => {
let remote = repo
.find_default_remote(gix::remote::Direction::Fetch)
.expect("one origin remote")?;
let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
print_updates(
&repo,
negotiation_rounds,
&negotiate,
update_refs,
ref_specs,
fetch_outcome.ref_map,
Expand Down
137 changes: 115 additions & 22 deletions gitoxide-core/src/repository/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ pub struct Options {
pub ref_specs: Vec<BString>,
pub shallow: gix::remote::fetch::Shallow,
pub handshake_info: bool,
pub negotiation_info: bool,
pub open_negotiation_graph: Option<std::path::PathBuf>,
}

pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;

pub(crate) mod function {
use anyhow::bail;
use gix::{prelude::ObjectIdExt, refspec::match_group::validate::Fix, remote::fetch::Status};
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 super::Options;
use crate::OutputFormat;
Expand All @@ -31,6 +38,8 @@ pub(crate) mod function {
dry_run,
remote,
handshake_info,
negotiation_info,
open_negotiation_graph,
shallow,
ref_specs,
}: Options,
Expand Down Expand Up @@ -62,41 +71,49 @@ pub(crate) mod function {

let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
match res.status {
Status::NoPackReceived { update_refs } => {
print_updates(&repo, 1, update_refs, ref_specs, res.ref_map, &mut out, err)
}
Status::DryRun {
update_refs,
negotiation_rounds,
} => print_updates(
&repo,
negotiation_rounds,
update_refs,
ref_specs,
res.ref_map,
&mut out,
err,
),
Status::Change {
Status::NoPackReceived {
update_refs,
write_pack_bundle,
negotiation_rounds,
negotiate,
dry_run: _,
} => {
let negotiate_default = Default::default();
print_updates(
&repo,
negotiation_rounds,
negotiate.as_ref().unwrap_or(&negotiate_default),
update_refs,
ref_specs,
res.ref_map,
&mut out,
err,
)?;
if negotiation_info {
print_negotiate_info(&mut out, negotiate.as_ref())?;
}
if let Some((negotiate, path)) =
open_negotiation_graph.and_then(|path| negotiate.as_ref().map(|n| (n, path)))
{
render_graph(&repo, &negotiate.graph, &path, progress)?;
}
Ok::<_, anyhow::Error>(())
}
Status::Change {
update_refs,
write_pack_bundle,
negotiate,
} => {
print_updates(&repo, &negotiate, update_refs, ref_specs, res.ref_map, &mut out, err)?;
if let Some(data_path) = write_pack_bundle.data_path {
writeln!(out, "pack file: \"{}\"", data_path.display()).ok();
}
if let Some(index_path) = write_pack_bundle.index_path {
writeln!(out, "index file: \"{}\"", index_path.display()).ok();
}
if negotiation_info {
print_negotiate_info(&mut out, Some(&negotiate))?;
}
if let Some(path) = open_negotiation_graph {
render_graph(&repo, &negotiate.graph, &path, progress)?;
}
Ok(())
}
}?;
Expand All @@ -106,9 +123,83 @@ pub(crate) mod function {
Ok(())
}

fn render_graph(
repo: &gix::Repository,
graph: &gix::negotiate::IdMap,
path: &std::path::Path,
mut progress: impl gix::Progress,
) -> anyhow::Result<()> {
progress.init(Some(graph.len()), gix::progress::count("commits"));
progress.set_name("building graph");

let mut map = gix::hashtable::HashMap::default();
let mut vg = layout::topo::layout::VisualGraph::new(Orientation::TopToBottom);

for (id, commit) in graph.iter().inspect(|_| progress.inc()) {
let source = match map.get(id) {
Some(handle) => *handle,
None => {
let handle = vg.add_node(new_node(id.attach(repo), commit.data.flags));
map.insert(*id, handle);
handle
}
};

for parent_id in &commit.parents {
let dest = match map.get(parent_id) {
Some(handle) => *handle,
None => {
let flags = match graph.get(parent_id) {
Some(c) => c.data.flags,
None => continue,
};
let dest = vg.add_node(new_node(parent_id.attach(repo), flags));
map.insert(*parent_id, dest);
dest
}
};
let arrow = Arrow::simple("");
vg.add_edge(arrow, source, dest);
}
}

let start = std::time::Instant::now();
progress.set_name("layout 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);

return Ok(());

fn new_node(id: gix::Id<'_>, flags: gix::negotiate::Flags) -> Element {
let pt = Point::new(250., 50.);
let name = format!("{}\n\n{flags:?}", id.shorten_or_id());
let shape = ShapeKind::new_box(name.as_str());
let style = StyleAttr::simple();
Element::create(shape, style, Orientation::LeftToRight, pt)
}
}

fn print_negotiate_info(
mut out: impl std::io::Write,
negotiate: Option<&gix::remote::fetch::outcome::Negotiate>,
) -> std::io::Result<()> {
writeln!(out, "Negotiation Phase Information")?;
match negotiate {
Some(negotiate) => {
writeln!(out, "\t{:?}", negotiate.rounds)?;
writeln!(out, "\tnum commits traversed in graph: {}", negotiate.graph.len())
}
None => writeln!(out, "\tno negotiation performed"),
}
}

pub(crate) fn print_updates(
repo: &gix::Repository,
negotiation_rounds: usize,
negotiate: &gix::remote::fetch::outcome::Negotiate,
update_refs: gix::remote::fetch::refs::update::Outcome,
refspecs: &[gix::refspec::RefSpec],
mut map: gix::remote::fetch::RefMap,
Expand Down Expand Up @@ -212,8 +303,10 @@ pub(crate) mod function {
refspecs.len()
)?;
}
if negotiation_rounds != 1 {
writeln!(err, "needed {negotiation_rounds} rounds of pack-negotiation")?;
match negotiate.rounds.len() {
0 => writeln!(err, "no negotiation was necessary")?,
1 => {}
rounds => writeln!(err, "needed {rounds} rounds of pack-negotiation")?,
}
Ok(())
}
Expand Down
Loading