use crate::ext::Merge;
use crate::ext::OidExt;
use crate::repo::Repo;
use crate::Result;
use dag::namedag::MemNameDag;
use dag::ops::DagAddHeads;
use dag::Vertex;
use gitdag::dag;
use gitdag::git2;
use std::collections::HashMap;
use std::collections::HashSet;
pub(crate) fn infer_mutation_from_reflog(repo: &Repo) -> Result<MemNameDag> {
let refs = repo.dag().git_references();
let mut replaces: HashMap<Vertex, Vertex> = Default::default();
for name in refs.keys() {
if !name.starts_with("refs/remotes/") && name.starts_with("refs/heads/") {
replaces.merge(analyse_reflog_name(repo, name).unwrap_or_default());
}
}
let parent_func = |v: Vertex| -> dag::Result<Vec<Vertex>> {
match replaces.get(&v) {
None => Ok(Vec::new()),
Some(old) => Ok(vec![old.clone()]),
}
};
let parent_func = dag::utils::break_parent_func_cycle(parent_func);
let mut heads: Vec<Vertex> = replaces
.keys()
.collect::<HashSet<_>>()
.difference(&replaces.values().collect::<HashSet<_>>())
.cloned()
.cloned()
.collect();
heads.sort_unstable();
let mut dag = MemNameDag::new();
dag.add_heads(parent_func, &heads)?;
Ok(dag)
}
fn analyse_reflog_name(repo: &Repo, name: &str) -> Result<HashMap<Vertex, Vertex>> {
let reflog = repo.git_repo().reflog(name)?;
let mut replaces: HashMap<Vertex, Vertex> = Default::default();
for entry in reflog.iter() {
let message: &str = match entry.message() {
Some(m) => m,
None => continue,
};
if message.starts_with("commit (amend):") || message.starts_with("rebase -i (finish):") {
replaces.merge(
analyse_head_rewrite(repo.git_repo(), entry.id_old(), entry.id_new())
.unwrap_or_default(),
);
}
}
Ok(replaces)
}
fn analyse_head_rewrite(
git_repo: &git2::Repository,
mut old: git2::Oid,
mut new: git2::Oid,
) -> Result<HashMap<Vertex, Vertex>> {
const MAX_DEPTH: usize = 50;
let mut old_stack = Vec::new();
let mut new_stack = Vec::new();
let mut seen = HashSet::new();
for _ in 0..MAX_DEPTH {
if old == new {
break;
}
if seen.insert(old) {
old_stack.push(old);
if let Some(next_old) = git_repo.find_commit(old)?.parent_ids().next() {
old = next_old;
}
}
if seen.insert(new) {
new_stack.push(new);
if let Some(next_new) = git_repo.find_commit(old)?.parent_ids().next() {
new = next_new;
}
}
}
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
enum Side {
Old,
New,
}
let mut tree_map = HashMap::<git2::Oid, Vec<(Side, git2::Oid)>>::new();
let mut msg_map = HashMap::<(i64, Vec<u8>), Vec<(Side, git2::Oid)>>::new();
for (side, stack) in vec![(Side::Old, old_stack), (Side::New, new_stack)] {
for oid in stack.into_iter().take(MAX_DEPTH) {
let commit = match git_repo.find_commit(oid) {
Err(_) => continue,
Ok(commit) => commit,
};
let tree_id = commit.tree_id();
tree_map.entry(tree_id).or_default().push((side, oid));
let msg = commit.message_bytes().to_vec();
let time = commit.author().when().seconds();
msg_map.entry((time, msg)).or_default().push((side, oid));
}
}
let result = tree_map
.values()
.chain(msg_map.values())
.filter_map(|pair| {
if let [(Side::Old, old), (Side::New, new)] = &pair[..] {
if old == new {
None
} else {
Some((new.to_vertex(), old.to_vertex()))
}
} else {
None
}
})
.collect();
Ok(result)
}