use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::time::SystemTime;
use lib::core::check_out::CheckOutCommitOptions;
use lib::core::repo_ext::RepoExt;
use lib::util::ExitCode;
use rayon::{ThreadPool, ThreadPoolBuilder};
use tracing::{instrument, warn};
use crate::commands::smartlog::smartlog;
use crate::opts::{MoveOptions, ResolveRevsetOptions, Revset};
use crate::revset::resolve_commits;
use lib::core::config::get_restack_preserve_timestamps;
use lib::core::dag::{commit_set_to_vec, union_all, CommitSet, Dag};
use lib::core::effects::Effects;
use lib::core::eventlog::{EventCursor, EventLogDb, EventReplayer};
use lib::core::rewrite::{
execute_rebase_plan, find_abandoned_children, find_rewrite_target, move_branches,
BuildRebasePlanOptions, ExecuteRebasePlanOptions, ExecuteRebasePlanResult,
MergeConflictRemediation, RebasePlanBuilder, RebasePlanPermissions, RepoPool, RepoResource,
};
use lib::git::{GitRunInfo, NonZeroOid, Repo};
#[instrument(skip(commits))]
fn restack_commits(
effects: &Effects,
thread_pool: &ThreadPool,
repo_pool: &RepoPool,
dag: &Dag,
event_replayer: &EventReplayer,
event_log_db: &EventLogDb,
event_cursor: EventCursor,
git_run_info: &GitRunInfo,
commits: Option<impl IntoIterator<Item = NonZeroOid>>,
build_options: &BuildRebasePlanOptions,
execute_options: &ExecuteRebasePlanOptions,
merge_conflict_remediation: MergeConflictRemediation,
) -> eyre::Result<ExitCode> {
let repo = repo_pool.try_create()?;
let commit_set: CommitSet = match commits {
Some(commits) => commits.into_iter().collect(),
None => dag.query_obsolete_commits(),
};
let commits = commit_set_to_vec(&commit_set)?;
let draft_commits = dag.query_draft_commits()?;
struct RebaseInfo {
dest_oid: NonZeroOid,
abandoned_child_oids: Vec<NonZeroOid>,
}
let rebases: Vec<RebaseInfo> = {
let mut result = Vec::new();
for original_commit_oid in commits {
let abandoned_children = find_abandoned_children(
dag,
draft_commits,
event_replayer,
event_cursor,
original_commit_oid,
)?;
if let Some((rewritten_oid, abandoned_child_oids)) = abandoned_children {
result.push(RebaseInfo {
dest_oid: rewritten_oid,
abandoned_child_oids,
});
}
}
result
};
let rebase_plan = {
let permissions = match RebasePlanPermissions::verify_rewrite_set(
dag,
build_options,
&rebases
.iter()
.flat_map(
|RebaseInfo {
dest_oid: _,
abandoned_child_oids,
}| abandoned_child_oids.iter().copied(),
)
.collect(),
)? {
Ok(permissions) => permissions,
Err(err) => {
err.describe(effects, &repo)?;
return Ok(ExitCode(1));
}
};
let mut builder = RebasePlanBuilder::new(dag, permissions);
for RebaseInfo {
dest_oid,
abandoned_child_oids,
} in rebases
{
for child_oid in abandoned_child_oids {
builder.move_subtree(child_oid, vec![dest_oid])?;
}
}
match builder.build(effects, thread_pool, repo_pool)? {
Ok(Some(rebase_plan)) => rebase_plan,
Ok(None) => {
writeln!(
effects.get_output_stream(),
"No abandoned commits to restack."
)?;
return Ok(ExitCode(0));
}
Err(err) => {
err.describe(effects, &repo)?;
return Ok(ExitCode(1));
}
}
};
let execute_rebase_plan_result = execute_rebase_plan(
effects,
git_run_info,
&repo,
event_log_db,
&rebase_plan,
execute_options,
)?;
match execute_rebase_plan_result {
ExecuteRebasePlanResult::Succeeded { rewritten_oids: _ } => {
writeln!(effects.get_output_stream(), "Finished restacking commits.")?;
Ok(ExitCode(0))
}
ExecuteRebasePlanResult::DeclinedToMerge { merge_conflict } => {
merge_conflict.describe(effects, &repo, merge_conflict_remediation)?;
Ok(ExitCode(1))
}
ExecuteRebasePlanResult::Failed { exit_code } => {
writeln!(
effects.get_output_stream(),
"Error: Could not restack commits (exit code {}).",
{
let ExitCode(exit_code) = exit_code;
exit_code
}
)?;
writeln!(
effects.get_output_stream(),
"You can resolve the error and try running `git restack` again."
)?;
Ok(exit_code)
}
}
}
#[instrument]
fn restack_branches(
effects: &Effects,
repo: &Repo,
conn: &rusqlite::Connection,
git_run_info: &GitRunInfo,
event_log_db: &EventLogDb,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<ExitCode> {
let event_replayer = EventReplayer::from_event_log_db(effects, repo, event_log_db)?;
let mut rewritten_oids = HashMap::new();
for branch in repo.get_all_local_branches()? {
let branch_target = match branch.get_oid()? {
Some(branch_target) => branch_target,
None => {
warn!(
branch_name = ?branch.get_reference_name()?,
"Branch was not a direct reference, could not resolve target"
);
continue;
}
};
if let Some(new_oid) = find_rewrite_target(
&event_replayer,
event_replayer.make_default_cursor(),
branch_target,
) {
rewritten_oids.insert(branch_target, new_oid);
};
}
if rewritten_oids.is_empty() {
writeln!(
effects.get_output_stream(),
"No abandoned branches to restack."
)?;
} else {
move_branches(
effects,
git_run_info,
repo,
options.event_tx_id,
&rewritten_oids,
)?;
writeln!(effects.get_output_stream(), "Finished restacking branches.")?;
}
Ok(ExitCode(0))
}
#[instrument]
pub fn restack(
effects: &Effects,
git_run_info: &GitRunInfo,
revsets: Vec<Revset>,
resolve_revset_options: &ResolveRevsetOptions,
move_options: &MoveOptions,
merge_conflict_remediation: MergeConflictRemediation,
) -> eyre::Result<ExitCode> {
let now = SystemTime::now();
let repo = Repo::from_current_dir()?;
let conn = repo.get_db_conn()?;
let event_log_db = EventLogDb::new(&conn)?;
let event_tx_id = event_log_db.make_transaction_id(now, "restack")?;
let references_snapshot = repo.get_references_snapshot()?;
let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
let event_cursor = event_replayer.make_default_cursor();
let mut dag = Dag::open_and_sync(
effects,
&repo,
&event_replayer,
event_cursor,
&references_snapshot,
)?;
let commit_sets =
match resolve_commits(effects, &repo, &mut dag, &revsets, resolve_revset_options) {
Ok(commit_sets) => commit_sets,
Err(err) => {
err.describe(effects)?;
return Ok(ExitCode(1));
}
};
let commits: Option<HashSet<NonZeroOid>> = if commit_sets.is_empty() {
None
} else {
Some(
commit_set_to_vec(&union_all(&commit_sets))?
.into_iter()
.collect(),
)
};
let MoveOptions {
force_rewrite_public_commits,
force_in_memory,
force_on_disk,
detect_duplicate_commits_via_patch_id,
resolve_merge_conflicts,
dump_rebase_constraints,
dump_rebase_plan,
} = *move_options;
let build_options = BuildRebasePlanOptions {
force_rewrite_public_commits,
dump_rebase_constraints,
dump_rebase_plan,
detect_duplicate_commits_via_patch_id,
};
let execute_options = ExecuteRebasePlanOptions {
now,
event_tx_id,
preserve_timestamps: get_restack_preserve_timestamps(&repo)?,
force_in_memory,
force_on_disk,
resolve_merge_conflicts,
check_out_commit_options: CheckOutCommitOptions {
additional_args: Default::default(),
render_smartlog: false,
},
};
let pool = ThreadPoolBuilder::new().build()?;
let repo_pool = RepoResource::new_pool(&repo)?;
let exit_code = restack_commits(
effects,
&pool,
&repo_pool,
&dag,
&event_replayer,
&event_log_db,
event_cursor,
git_run_info,
commits,
&build_options,
&execute_options,
merge_conflict_remediation,
)?;
if !exit_code.is_success() {
return Ok(exit_code);
}
let exit_code = restack_branches(
effects,
&repo,
&conn,
git_run_info,
&event_log_db,
&execute_options,
)?;
if !exit_code.is_success() {
return Ok(exit_code);
}
smartlog(effects, git_run_info, &Default::default())
}