use std::fs;
use anyhow::{Result, anyhow};
use refs::Head;
use repo::Repository;
use super::{snapshot::ensure_current_state, worktree_safety::ensure_worktree_clean};
use crate::{
cli::{Cli, should_output_json},
config::UserConfig,
};
mod rebase_ops;
mod rebase_state;
use rebase_ops::{replay_commits, replay_commits_silent};
pub(crate) use rebase_state::load_rebase_state as load_persisted_rebase_state;
use rebase_state::{
RebaseState, collect_commits_to_rebase, is_ancestor_of, load_rebase_state, save_rebase_state,
};
const REBASE_STATE_FILE: &str = "REBASE_STATE";
pub(crate) enum OperatorContinueStatus {
Continued,
Completed,
Blocked,
}
pub fn cmd_rebase(
cli: &Cli,
thread: Option<&str>,
abort: bool,
cont: bool,
force: bool,
) -> Result<()> {
let cwd_repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
let target_path = cwd_repo.active_worktree_path()?;
let repo = if target_path == *cwd_repo.root() {
cwd_repo
} else {
Repository::open(&target_path)?
};
if !force && !abort && !cont {
ensure_worktree_clean(&repo, "rebase")?;
}
run_rebase(&repo, thread, abort, cont, Some(cli))
}
pub(crate) fn cmd_rebase_silent(
repo: &Repository,
thread: Option<&str>,
abort: bool,
cont: bool,
) -> Result<()> {
run_rebase(repo, thread, abort, cont, None)
}
pub(crate) fn continue_rebase_for_operator(repo: &Repository) -> Result<OperatorContinueStatus> {
let rebase_state_path = repo.heddle_dir().join(REBASE_STATE_FILE);
if !rebase_state_path.exists() {
return Err(anyhow!("No rebase in progress"));
}
let before = load_rebase_state(&rebase_state_path)?;
if let Some(pre_conflict_head) = before.pre_conflict_head {
let current_state = repo
.current_state()?
.ok_or_else(|| anyhow!("No current state"))?;
if current_state.change_id != pre_conflict_head
&& Some(current_state.change_id) != before.pending_manual_resolution
{
let current_tree = repo
.store()
.get_tree(¤t_state.tree)?
.ok_or_else(|| anyhow!("Current state tree not found"))?;
let worktree_status = repo.compare_worktree_cached(¤t_tree)?;
let worktree_is_clean = worktree_status.modified.is_empty()
&& worktree_status.added.is_empty()
&& worktree_status.deleted.is_empty();
if !worktree_is_clean {
return Ok(OperatorContinueStatus::Blocked);
}
}
}
let before_index = before.current_index;
let before_pending_manual_resolution = before.pending_manual_resolution;
cmd_rebase_silent(repo, None, false, true)?;
if !rebase_state_path.exists() {
return Ok(OperatorContinueStatus::Completed);
}
let after = load_rebase_state(&rebase_state_path)?;
if after.pending_manual_resolution.is_some()
&& after.current_index == before_index
&& after.pending_manual_resolution == before_pending_manual_resolution
{
return Ok(OperatorContinueStatus::Blocked);
}
Ok(OperatorContinueStatus::Continued)
}
pub(crate) fn has_persisted_rebase_state(repo: &Repository) -> bool {
repo.heddle_dir().join(REBASE_STATE_FILE).exists()
}
fn run_rebase(
repo: &Repository,
thread: Option<&str>,
abort: bool,
cont: bool,
cli: Option<&Cli>,
) -> Result<()> {
let rebase_state_path = repo.heddle_dir().join(REBASE_STATE_FILE);
if abort {
return handle_abort(repo, &rebase_state_path, cli);
}
if cont {
return handle_continue(repo, &rebase_state_path, cli);
}
let target_thread = thread.ok_or_else(|| anyhow!("Usage: heddle rebase <thread>"))?;
let current_change = ensure_current_state(
repo,
&UserConfig::load_default().unwrap_or_default(),
Some(format!(
"Bootstrap git-overlay before rebasing onto {}",
target_thread
)),
)?;
let current_state = repo
.store()
.get_state(¤t_change)?
.ok_or_else(|| anyhow!("Current state not found"))?;
let target_change_id = repo
.refs()
.get_thread(target_thread)?
.ok_or_else(|| anyhow!("Thread '{}' not found", target_thread))?;
if current_state.change_id == target_change_id {
if let Some(cli) = cli
&& should_output_json(cli, Some(repo.config()))
{
println!("{{\"status\": \"up_to_date\"}}");
} else if cli.is_some() {
println!("Already up to date");
}
return Ok(());
}
let is_ancestor = is_ancestor_of(repo, ¤t_state.change_id, &target_change_id)?;
if is_ancestor {
repo.fast_forward_attached(&target_change_id)?;
if let Some(cli) = cli
&& should_output_json(cli, Some(repo.config()))
{
println!(
"{{\"status\": \"fast_forwarded\", \"to\": \"{}\"}}",
target_change_id
);
} else if cli.is_some() {
match repo.head_ref()? {
Head::Attached { thread } => {
println!("Fast-forwarded {} to {}", thread, target_change_id.short())
}
Head::Detached { .. } => {
println!("Fast-forwarded to {}", target_change_id.short())
}
}
}
return Ok(());
}
let commits_to_replay =
collect_commits_to_rebase(repo, ¤t_state.change_id, &target_change_id)?;
if commits_to_replay.is_empty() {
repo.fast_forward_attached(&target_change_id)?;
if let Some(cli) = cli
&& should_output_json(cli, Some(repo.config()))
{
println!("{{\"status\": \"up_to_date\"}}");
} else if cli.is_some() {
println!("Already up to date");
}
return Ok(());
}
let rebase_state = RebaseState {
onto: target_change_id,
commits_to_replay: commits_to_replay.clone(),
current_index: 0,
original_head: current_state.change_id,
pending_manual_resolution: None,
pre_conflict_head: None,
};
save_rebase_state(&rebase_state_path, &rebase_state)?;
if let Some(cli) = cli
&& should_output_json(cli, Some(repo.config()))
{
println!(
"{{\"status\": \"started\", \"commits\": {}}}",
commits_to_replay.len()
);
} else if cli.is_some() {
println!(
"Rebasing {} commits onto {}",
commits_to_replay.len(),
target_change_id.short()
);
}
if let Some(cli) = cli {
replay_commits(repo, &rebase_state_path, cli)
} else {
replay_commits_silent(repo, &rebase_state_path)
}
}
fn handle_abort(
repo: &Repository,
rebase_state_path: &std::path::Path,
cli: Option<&Cli>,
) -> Result<()> {
if !rebase_state_path.exists() {
return Err(anyhow!("No rebase in progress"));
}
let state = load_rebase_state(rebase_state_path)?;
repo.goto_without_record(&state.original_head)?;
fs::remove_file(rebase_state_path)?;
if let Some(cli) = cli
&& should_output_json(cli, Some(repo.config()))
{
println!("{{\"status\": \"aborted\"}}");
} else if cli.is_some() {
println!("Rebase aborted");
}
Ok(())
}
fn handle_continue(
repo: &Repository,
rebase_state_path: &std::path::Path,
cli: Option<&Cli>,
) -> Result<()> {
if !rebase_state_path.exists() {
return Err(anyhow!("No rebase in progress"));
}
if let Some(cli) = cli {
replay_commits(repo, rebase_state_path, cli)
} else {
replay_commits_silent(repo, rebase_state_path)
}
}