use std::collections::HashSet;
use std::io::Write;
use std::time::SystemTime;
use fn_error_context::context;
use git2::ErrorCode;
use crate::core::eventlog::{CommitVisibility, Event};
use crate::core::eventlog::{EventLogDb, EventReplayer};
use crate::core::graph::{make_graph, BranchOids, CommitGraph, HeadOid, MainBranchOid, Node};
use crate::core::mergebase::MergeBaseDb;
use crate::core::metadata::{render_commit_metadata, CommitMessageProvider, CommitOidProvider};
use crate::util::{
get_branch_oid_to_names, get_db_conn, get_head_oid, get_main_branch_oid, get_repo,
};
enum ProcessHashesResult<'repo> {
Ok { commits: Vec<git2::Commit<'repo>> },
CommitNotFound { hash: String },
}
#[context("Processing hashes")]
fn process_hashes(
repo: &git2::Repository,
hashes: Vec<String>,
) -> anyhow::Result<ProcessHashesResult> {
let mut commits = Vec::new();
for hash in hashes {
let commit = match repo.revparse_single(&hash) {
Ok(commit) => match commit.into_commit() {
Ok(commit) => commit,
Err(_) => return Ok(ProcessHashesResult::CommitNotFound { hash }),
},
Err(err) if err.code() == ErrorCode::NotFound => {
return Ok(ProcessHashesResult::CommitNotFound { hash })
}
Err(err) => return Err(err.into()),
};
commits.push(commit)
}
Ok(ProcessHashesResult::Ok { commits })
}
fn recurse_on_commits_helper<
'repo,
'graph,
Condition: Fn(&'graph Node<'repo>) -> bool,
Callback: FnMut(&'graph Node<'repo>),
>(
graph: &'graph CommitGraph<'repo>,
condition: &Condition,
commit: &git2::Commit<'repo>,
callback: &mut Callback,
) {
let node = &graph[&commit.id()];
if condition(node) {
callback(node);
};
for child_oid in node.children.iter() {
let child_commit = &graph[&child_oid].commit;
recurse_on_commits_helper(graph, condition, child_commit, callback)
}
}
fn recurse_on_commits<'repo, F: Fn(&Node) -> bool>(
repo: &'repo git2::Repository,
merge_base_db: &MergeBaseDb,
event_replayer: &EventReplayer,
commits: Vec<git2::Commit<'repo>>,
condition: F,
) -> anyhow::Result<Vec<git2::Commit<'repo>>> {
let head_oid = get_head_oid(repo)?;
let main_branch_oid = get_main_branch_oid(repo)?;
let branch_oid_to_names = get_branch_oid_to_names(repo)?;
let graph = make_graph(
repo,
merge_base_db,
event_replayer,
event_replayer.make_default_cursor(),
&HeadOid(head_oid),
&MainBranchOid(main_branch_oid),
&BranchOids(branch_oid_to_names.keys().copied().collect()),
false,
)?;
let mut result: Vec<git2::Commit<'repo>> = Vec::new();
let mut seen_oids = HashSet::new();
for commit in commits {
recurse_on_commits_helper(&graph, &condition, &commit, &mut |child_node| {
let child_commit = &child_node.commit;
if !seen_oids.contains(&child_commit.id()) {
seen_oids.insert(child_commit.id());
result.push(child_commit.clone());
}
});
}
Ok(result)
}
pub fn hide(out: &mut impl Write, hashes: Vec<String>, recursive: bool) -> anyhow::Result<isize> {
let now = SystemTime::now();
let repo = get_repo()?;
let conn = get_db_conn(&repo)?;
let mut event_log_db = EventLogDb::new(&conn)?;
let event_replayer = EventReplayer::from_event_log_db(&event_log_db)?;
let merge_base_db = MergeBaseDb::new(&conn)?;
let commits = process_hashes(&repo, hashes)?;
let commits = match commits {
ProcessHashesResult::Ok { commits } => commits,
ProcessHashesResult::CommitNotFound { hash } => {
writeln!(out, "Commit not found: {}", hash)?;
return Ok(1);
}
};
let commits = if recursive {
recurse_on_commits(&repo, &merge_base_db, &event_replayer, commits, |node| {
node.is_visible
})?
} else {
commits
};
let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64();
let event_tx_id = event_log_db.make_transaction_id(now, "hide")?;
let events = commits
.iter()
.map(|commit| Event::HideEvent {
timestamp,
event_tx_id,
commit_oid: commit.id(),
})
.collect();
event_log_db.add_events(events)?;
let cursor = event_replayer.make_default_cursor();
for commit in commits {
let hidden_commit_text = {
render_commit_metadata(
&commit,
&[
&CommitOidProvider::new(true)?,
&CommitMessageProvider::new()?,
],
)?
};
writeln!(out, "Hid commit: {}", hidden_commit_text)?;
if let Some(CommitVisibility::Hidden) =
event_replayer.get_cursor_commit_visibility(cursor, commit.id())
{
writeln!(
out,
"(It was already hidden, so this operation had no effect.)"
)?;
}
let commit_target_oid =
render_commit_metadata(&commit, &[&CommitOidProvider::new(false)?])?;
writeln!(
out,
"To unhide this commit, run: git unhide {}",
commit_target_oid
)?;
}
Ok(0)
}
pub fn unhide(out: &mut impl Write, hashes: Vec<String>, recursive: bool) -> anyhow::Result<isize> {
let now = SystemTime::now();
let repo = get_repo()?;
let conn = get_db_conn(&repo)?;
let mut event_log_db = EventLogDb::new(&conn)?;
let event_replayer = EventReplayer::from_event_log_db(&event_log_db)?;
let merge_base_db = MergeBaseDb::new(&conn)?;
let commits = process_hashes(&repo, hashes)?;
let commits = match commits {
ProcessHashesResult::Ok { commits } => commits,
ProcessHashesResult::CommitNotFound { hash } => {
writeln!(out, "Commit not found: {}", hash)?;
return Ok(1);
}
};
let commits = if recursive {
recurse_on_commits(&repo, &merge_base_db, &event_replayer, commits, |node| {
!node.is_visible
})?
} else {
commits
};
let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64();
let event_tx_id = event_log_db.make_transaction_id(now, "unhide")?;
let events = commits
.iter()
.map(|commit| Event::UnhideEvent {
timestamp,
event_tx_id,
commit_oid: commit.id(),
})
.collect();
event_log_db.add_events(events)?;
let cursor = event_replayer.make_default_cursor();
for commit in commits {
let unhidden_commit_text = {
render_commit_metadata(
&commit,
&[
&CommitOidProvider::new(true)?,
&CommitMessageProvider::new()?,
],
)?
};
writeln!(out, "Unhid commit: {}", unhidden_commit_text)?;
if let Some(CommitVisibility::Visible) =
event_replayer.get_cursor_commit_visibility(cursor, commit.id())
{
writeln!(out, "(It was not hidden, so this operation had no effect.)")?;
}
let commit_target_oid =
render_commit_metadata(&commit, &[&CommitOidProvider::new(false)?])?;
writeln!(
out,
"To hide this commit, run: git hide {}",
commit_target_oid
)?;
}
Ok(0)
}