use git2::{Oid, Repository, Sort};
use std::path::Path;
use crate::config::BranchFilter;
mod refs;
mod segments;
#[cfg(test)]
mod tests;
use refs::{merge_stash_labels, resolve_refs};
pub(crate) use segments::{BranchSegment, compute_branch_segments};
const MAX_COMMITS: usize = 200;
const PALETTE_SIZE: usize = 6;
#[derive(Clone, Debug)]
pub(crate) struct BranchLabel {
pub name: String,
pub is_head: bool,
pub is_remote: bool,
pub is_worktree: bool,
pub is_tag: bool,
pub is_stash: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct GraphOptions {
pub branch_filter: BranchFilter,
pub label_max_len: usize,
pub first_parent: bool,
pub show_stats: bool,
}
impl Default for GraphOptions {
fn default() -> Self {
Self {
branch_filter: BranchFilter::All,
label_max_len: 24,
first_parent: false,
show_stats: true,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct DiffStat {
pub additions: usize,
pub deletions: usize,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct GraphRow {
pub commit_col: usize,
pub lanes: Vec<LaneSegment>,
pub oid: Oid,
pub short_id: String,
pub message: String,
pub author: String,
pub time: i64,
pub labels: Vec<BranchLabel>,
pub is_merge: bool,
pub horizontal_spans: Vec<(usize, usize, usize)>,
pub parent_oids: Vec<Oid>,
pub diff_stat: Option<DiffStat>,
pub collapsed: Option<(String, usize)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum LaneSegment {
Empty,
Straight,
Commit,
MergeLeft,
MergeRight,
ForkLeft,
ForkRight,
Horizontal,
CrossHorizontal,
RightTee,
LeftTee,
}
#[derive(Clone, Debug)]
pub(crate) struct GraphBuilder {
active_lanes: Vec<Option<Oid>>,
}
impl GraphBuilder {
pub fn new() -> Self {
Self {
active_lanes: Vec::new(),
}
}
pub fn build(
mut self,
path: &Path,
options: &GraphOptions,
) -> color_eyre::Result<Vec<GraphRow>> {
let mut repo = Repository::open(path)?;
let mut ref_map = resolve_refs(&repo, &options.branch_filter);
merge_stash_labels(&mut repo, &mut ref_map);
let mut revwalk = repo.revwalk()?;
revwalk.push_head().ok(); for &oid in ref_map.keys() {
revwalk.push(oid).ok(); }
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
if options.first_parent {
revwalk.simplify_first_parent()?;
}
let mut rows = Vec::new();
for oid_result in revwalk.take(MAX_COMMITS) {
let oid = oid_result?;
let commit = repo.find_commit(oid)?;
let parent_oids: Vec<Oid> = commit.parent_ids().collect();
let is_merge = commit.parent_count() > 1;
let labels = ref_map.remove(&oid).unwrap_or_default();
let (commit_col, lanes, horizontal_spans) = self.process_commit(oid, &parent_oids);
let short_id = oid.to_string()[..7].to_string();
let message = commit.summary().ok().flatten().unwrap_or("").to_string();
let author = commit.author().name().unwrap_or("").to_string();
let time = commit.time().seconds();
rows.push(GraphRow {
commit_col,
lanes,
oid,
short_id,
message,
author,
time,
labels,
is_merge,
horizontal_spans,
parent_oids,
diff_stat: None,
collapsed: None,
});
}
Ok(rows)
}
fn process_commit(
&mut self,
oid: Oid,
parent_oids: &[Oid],
) -> (usize, Vec<LaneSegment>, Vec<(usize, usize, usize)>) {
let commit_col = self
.active_lanes
.iter()
.position(|lane| *lane == Some(oid))
.unwrap_or_else(|| {
let col = self.find_free_lane();
if col < self.active_lanes.len() {
self.active_lanes[col] = Some(oid);
} else {
self.active_lanes.push(Some(oid));
}
col
});
let lane_count = self.active_lanes.len().max(commit_col + 1);
let mut lanes = vec![LaneSegment::Empty; lane_count];
for (i, lane) in self.active_lanes.iter().enumerate() {
if i < lanes.len() && lane.is_some() && i != commit_col {
lanes[i] = LaneSegment::Straight;
}
}
lanes[commit_col] = LaneSegment::Commit;
self.active_lanes[commit_col] = None;
let mut spans: Vec<(usize, usize, usize)> = Vec::new();
if !parent_oids.is_empty() {
let first_parent = parent_oids[0];
let existing_lane = self
.active_lanes
.iter()
.position(|lane| *lane == Some(first_parent));
if let Some(existing) = existing_lane {
if existing < commit_col {
lanes[commit_col] = LaneSegment::MergeLeft;
spans.push((existing, commit_col, lane_color(commit_col)));
} else if existing > commit_col {
lanes[commit_col] = LaneSegment::MergeRight;
spans.push((commit_col, existing, lane_color(commit_col)));
}
} else {
self.active_lanes[commit_col] = Some(first_parent);
}
for &parent_oid in &parent_oids[1..] {
let existing = self
.active_lanes
.iter()
.position(|lane| *lane == Some(parent_oid));
if existing.is_none() {
let new_col = self.find_free_lane();
if new_col < self.active_lanes.len() {
self.active_lanes[new_col] = Some(parent_oid);
} else {
self.active_lanes.push(Some(parent_oid));
}
while lanes.len() <= new_col {
lanes.push(LaneSegment::Empty);
}
if new_col > commit_col {
lanes[new_col] = LaneSegment::ForkRight;
spans.push((commit_col, new_col, lane_color(new_col)));
} else {
lanes[new_col] = LaneSegment::ForkLeft;
spans.push((new_col, commit_col, lane_color(new_col)));
}
}
}
}
for &(left, right, _) in &spans {
if lanes[left] == LaneSegment::Straight {
lanes[left] = LaneSegment::RightTee;
}
if right < lanes.len() && lanes[right] == LaneSegment::Straight {
lanes[right] = LaneSegment::LeftTee;
}
for col in (left + 1)..right {
if col < lanes.len() {
if lanes[col] == LaneSegment::Straight {
lanes[col] = LaneSegment::CrossHorizontal;
} else if lanes[col] == LaneSegment::Empty {
lanes[col] = LaneSegment::Horizontal;
}
}
}
}
while self.active_lanes.last() == Some(&None) {
self.active_lanes.pop();
}
(commit_col, lanes, spans)
}
fn find_free_lane(&self) -> usize {
self.active_lanes
.iter()
.position(|lane| lane.is_none())
.unwrap_or(self.active_lanes.len())
}
}
pub(crate) fn lane_color(col: usize) -> usize {
col % PALETTE_SIZE
}