use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::Debug;
use std::ops::Deref;
use tracing::{instrument, warn};
use crate::core::eventlog::{CommitVisibility, Event, EventCursor, EventReplayer};
use crate::core::mergebase::MergeBaseDb;
use crate::git::{Commit, NonZeroOid, Repo};
use crate::tui::{Effects, OperationType};
#[derive(Debug)]
pub struct HeadOid(pub Option<NonZeroOid>);
#[derive(Debug)]
pub struct MainBranchOid(pub NonZeroOid);
#[derive(Debug)]
pub struct BranchOids(pub HashSet<NonZeroOid>);
#[derive(Debug)]
pub struct CommitOids(pub HashSet<NonZeroOid>);
#[derive(Debug)]
pub struct Node<'repo> {
pub commit: Commit<'repo>,
pub parent: Option<NonZeroOid>,
pub children: Vec<NonZeroOid>,
pub is_main: bool,
pub is_visible: bool,
pub event: Option<Event>,
}
#[derive(Default)]
pub struct CommitGraph<'repo> {
nodes: HashMap<NonZeroOid, Node<'repo>>,
}
impl std::fmt::Debug for CommitGraph<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<CommitGraph len={}>", self.nodes.len())
}
}
impl<'repo> Deref for CommitGraph<'repo> {
type Target = HashMap<NonZeroOid, Node<'repo>>;
fn deref(&self) -> &Self::Target {
&self.nodes
}
}
fn find_path_to_merge_base_internal<'repo>(
effects: &Effects,
repo: &'repo Repo,
merge_base_db: &MergeBaseDb,
commit_oid: NonZeroOid,
target_oid: NonZeroOid,
mut visited_commit_callback: impl FnMut(NonZeroOid),
) -> eyre::Result<Option<Vec<Commit<'repo>>>> {
let (effects, _progress) = effects.start_operation(OperationType::FindPathToMergeBase);
let mut queue = VecDeque::new();
visited_commit_callback(commit_oid);
let first_commit = match repo.find_commit(commit_oid)? {
Some(commit) => commit,
None => eyre::bail!("Unable to find commit with OID: {:?}", commit_oid),
};
queue.push_back(vec![first_commit]);
let merge_base_oid =
merge_base_db.get_merge_base_oid(&effects, repo, commit_oid, target_oid)?;
while let Some(path) = queue.pop_front() {
let last_commit = path
.last()
.expect("find_path_to_merge_base: empty path in queue");
if last_commit.get_oid() == target_oid {
return Ok(Some(path));
}
if Some(last_commit.get_oid()) == merge_base_oid {
continue;
}
for parent in last_commit.get_parents() {
visited_commit_callback(parent.get_oid());
let mut new_path = path.clone();
new_path.push(parent);
queue.push_back(new_path);
}
}
Ok(None)
}
#[instrument]
pub fn find_path_to_merge_base<'repo>(
effects: &Effects,
repo: &'repo Repo,
merge_base_db: &MergeBaseDb,
commit_oid: NonZeroOid,
target_oid: NonZeroOid,
) -> eyre::Result<Option<Vec<Commit<'repo>>>> {
find_path_to_merge_base_internal(
effects,
repo,
merge_base_db,
commit_oid,
target_oid,
|_commit| {},
)
}
#[instrument(skip(commit_oids))]
fn walk_from_commits<'repo>(
effects: &Effects,
repo: &'repo Repo,
merge_base_db: &MergeBaseDb,
event_replayer: &EventReplayer,
event_cursor: EventCursor,
main_branch_oid: &MainBranchOid,
commit_oids: &CommitOids,
) -> eyre::Result<CommitGraph<'repo>> {
let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
let mut graph: HashMap<NonZeroOid, Node> = Default::default();
for commit_oid in &commit_oids.0 {
let commit = repo.find_commit(*commit_oid)?;
let current_commit = match commit {
Some(commit) => commit,
None => continue,
};
let merge_base_oid = merge_base_db.get_merge_base_oid(
&effects,
repo,
current_commit.get_oid(),
main_branch_oid.0,
)?;
let path_to_merge_base = match merge_base_oid {
None => vec![current_commit],
Some(merge_base_oid) => {
let path_to_merge_base = find_path_to_merge_base(
&effects,
repo,
merge_base_db,
current_commit.get_oid(),
merge_base_oid,
)?;
match path_to_merge_base {
None => {
warn!(
current_commit_oid = ?current_commit.get_oid(),
"No path to merge-base for commit",
);
continue;
}
Some(path_to_merge_base) => path_to_merge_base,
}
}
};
for current_commit in path_to_merge_base.iter() {
if graph.contains_key(¤t_commit.get_oid()) {
break;
}
let visibility =
event_replayer.get_cursor_commit_visibility(event_cursor, current_commit.get_oid());
let is_visible = match visibility {
Some(CommitVisibility::Visible) | None => true,
Some(CommitVisibility::Hidden) => false,
};
let is_main = match merge_base_oid {
Some(merge_base_oid) => (current_commit.get_oid() == merge_base_oid),
None => false,
};
let event = event_replayer
.get_cursor_commit_latest_event(event_cursor, current_commit.get_oid())
.cloned();
graph.insert(
current_commit.get_oid(),
Node {
commit: current_commit.clone(),
parent: None,
children: Vec::new(),
is_main,
is_visible,
event,
},
);
}
if let Some(merge_base_oid) = merge_base_oid {
if !graph.contains_key(&merge_base_oid) {
warn!(?merge_base_oid, "Could not find merge base OID");
}
}
}
let links: Vec<(NonZeroOid, NonZeroOid)> = graph
.iter()
.filter(|(_child_oid, node)| !node.is_main)
.flat_map(|(child_oid, node)| {
node.commit
.get_parent_oids()
.into_iter()
.filter(|parent_oid| graph.contains_key(parent_oid))
.map(move |parent_oid| (*child_oid, parent_oid))
})
.collect();
for (child_oid, parent_oid) in links.iter() {
graph.get_mut(child_oid).unwrap().parent = Some(*parent_oid);
graph.get_mut(parent_oid).unwrap().children.push(*child_oid);
}
Ok(CommitGraph { nodes: graph })
}
fn sort_children(graph: &mut CommitGraph) {
let commit_times: HashMap<NonZeroOid, git2::Time> = graph
.iter()
.map(|(oid, node)| (*oid, node.commit.get_time()))
.collect();
for node in graph.nodes.values_mut() {
node.children
.sort_by_key(|child_oid| (commit_times[child_oid], child_oid.to_string()));
}
}
fn should_hide(
cache: &mut HashMap<NonZeroOid, bool>,
graph: &CommitGraph,
unhideable_oids: &HashSet<NonZeroOid>,
oid: &NonZeroOid,
) -> bool {
let result = {
match cache.get(oid) {
Some(result) => *result,
None => {
if unhideable_oids.contains(oid) {
false
} else {
let node = &graph[oid];
if node.is_main {
node.is_visible
&& node
.children
.iter()
.filter(|child_oid| !graph[child_oid].is_main)
.all(|child_oid| {
should_hide(cache, graph, unhideable_oids, child_oid)
})
} else {
!node.is_visible
&& node.children.iter().all(|child_oid| {
should_hide(cache, graph, unhideable_oids, child_oid)
})
}
}
}
}
};
cache.insert(*oid, result);
result
}
fn do_remove_commits(graph: &mut CommitGraph, head_oid: &HeadOid, branch_oids: &BranchOids) {
let mut unhideable_oids = branch_oids.0.clone();
if let Some(head_oid) = head_oid.0 {
unhideable_oids.insert(head_oid);
}
let mut cache = HashMap::new();
let all_oids_to_hide: HashSet<NonZeroOid> = graph
.keys()
.filter(|oid| should_hide(&mut cache, graph, &unhideable_oids, oid))
.cloned()
.collect();
for oid in all_oids_to_hide {
let parent_oid = graph[&oid].parent;
graph.nodes.remove(&oid);
match parent_oid {
Some(parent_oid) if graph.contains_key(&parent_oid) => {
let children = &mut graph.nodes.get_mut(&parent_oid).unwrap().children;
*children = children
.iter()
.filter_map(|child_oid| {
if *child_oid != oid {
Some(*child_oid)
} else {
None
}
})
.collect();
}
_ => {}
}
}
}
#[instrument]
pub fn make_graph<'repo>(
effects: &Effects,
repo: &'repo Repo,
merge_base_db: &MergeBaseDb,
event_replayer: &EventReplayer,
event_cursor: EventCursor,
head_oid: &HeadOid,
main_branch_oid: &MainBranchOid,
branch_oids: &BranchOids,
remove_commits: bool,
) -> eyre::Result<CommitGraph<'repo>> {
let (effects, _progress) = effects.start_operation(OperationType::MakeGraph);
let mut commit_oids: HashSet<NonZeroOid> = event_replayer
.get_cursor_active_oids(event_cursor)
.into_iter()
.collect();
commit_oids.extend(branch_oids.0.iter().cloned());
if let HeadOid(Some(head_oid)) = head_oid {
commit_oids.insert(*head_oid);
}
let commit_oids = &CommitOids(commit_oids);
let mut graph = walk_from_commits(
&effects,
repo,
merge_base_db,
event_replayer,
event_cursor,
main_branch_oid,
commit_oids,
)?;
sort_children(&mut graph);
if remove_commits {
do_remove_commits(&mut graph, head_oid, branch_oids);
}
Ok(graph)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use crate::core::formatting::Glyphs;
use crate::core::mergebase::MergeBaseDb;
use crate::testing::make_git;
#[test]
fn test_find_path_to_merge_base_stop_early() -> eyre::Result<()> {
let git = make_git()?;
git.init_repo()?;
let test1_oid = git.commit_file("test1", 1)?;
let test2_oid = git.commit_file("test2", 2)?;
git.detach_head()?;
let test3_oid = git.commit_file("test3", 3)?;
let repo = git.get_repo()?;
let conn = repo.get_db_conn()?;
let merge_base_db = MergeBaseDb::new(&conn)?;
let mut effects = Effects::new_suppress_for_test(Glyphs::detect());
let mut seen_oids = HashSet::new();
let path = find_path_to_merge_base_internal(
&mut effects,
&repo,
&merge_base_db,
test2_oid,
test3_oid,
|oid| {
seen_oids.insert(oid);
},
)?;
assert!(path.is_none());
assert!(seen_oids.contains(&test2_oid));
assert!(!seen_oids.contains(&test3_oid));
assert!(!seen_oids.contains(&test1_oid));
Ok(())
}
}
pub enum ResolveCommitsResult<'repo> {
Ok {
commits: Vec<Commit<'repo>>,
},
CommitNotFound {
commit: String,
},
}
#[instrument]
pub fn resolve_commits(repo: &Repo, hashes: Vec<String>) -> eyre::Result<ResolveCommitsResult> {
let mut commits = Vec::new();
for hash in hashes {
let commit = match repo.revparse_single_commit(&hash)? {
Some(commit) => commit,
None => return Ok(ResolveCommitsResult::CommitNotFound { commit: hash }),
};
commits.push(commit)
}
Ok(ResolveCommitsResult::Ok { commits })
}