use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fmt::Write;
use std::time::SystemTime;
use eyre::Context;
use os_str_bytes::OsStrBytes;
use tracing::warn;
use crate::core::eventlog::EventTransactionId;
use crate::core::formatting::printable_styled_string;
use crate::git::{GitRunInfo, MaybeZeroOid, NonZeroOid, Repo};
use crate::tui::Effects;
use super::plan::RebasePlan;
pub fn move_branches<'a>(
git_run_info: &GitRunInfo,
repo: &'a Repo,
event_tx_id: EventTransactionId,
rewritten_oids_map: &'a HashMap<NonZeroOid, MaybeZeroOid>,
) -> eyre::Result<()> {
let branch_oid_to_names = repo.get_branch_oid_to_names()?;
let mut branch_moves: Vec<(NonZeroOid, MaybeZeroOid, &OsStr)> = Vec::new();
let mut branch_move_err: Option<eyre::Error> = None;
'outer: for (old_oid, names) in branch_oid_to_names.iter() {
let new_oid = match rewritten_oids_map.get(old_oid) {
Some(new_oid) => new_oid,
None => continue,
};
let mut names: Vec<_> = names.iter().collect();
names.sort_unstable();
match new_oid {
MaybeZeroOid::NonZero(new_oid) => {
let new_commit = match repo.find_commit(*new_oid) {
Ok(Some(commit)) => commit,
Ok(None) => {
branch_move_err = Some(eyre::eyre!(
"Could not find newly-rewritten commit with old OID: {:?}, new OID: {:?}",
old_oid,
new_oid
));
break 'outer;
}
Err(err) => {
branch_move_err = Some(err);
break 'outer;
}
};
for name in names {
if let Err(err) =
repo.create_reference(name, new_commit.get_oid(), true, "move branches")
{
branch_move_err = Some(err);
break 'outer;
}
branch_moves.push((*old_oid, MaybeZeroOid::NonZero(*new_oid), name));
}
}
MaybeZeroOid::Zero => {
for name in names {
match repo.find_reference(name) {
Ok(Some(mut reference)) => {
if let Err(err) = reference.delete() {
branch_move_err = Some(err);
break 'outer;
}
}
Ok(None) => {
warn!(?name, "Reference not found, not deleting")
}
Err(err) => {
branch_move_err = Some(err);
break 'outer;
}
};
branch_moves.push((*old_oid, MaybeZeroOid::Zero, name));
}
}
}
}
let branch_moves_stdin: Vec<u8> = branch_moves
.into_iter()
.flat_map(|(old_oid, new_oid, name)| {
let mut line = Vec::new();
line.extend(old_oid.to_string().as_bytes());
line.push(b' ');
line.extend(new_oid.to_string().as_bytes());
line.push(b' ');
line.extend(name.to_raw_bytes().iter());
line.push(b'\n');
line
})
.collect();
let branch_moves_stdin = OsStrBytes::from_raw_bytes(branch_moves_stdin)
.wrap_err_with(|| "Encoding branch moves stdin")?;
let branch_moves_stdin = OsString::from(branch_moves_stdin);
git_run_info.run_hook(
repo,
"reference-transaction",
event_tx_id,
&["committed"],
Some(branch_moves_stdin),
)?;
match branch_move_err {
Some(err) => Err(err),
None => Ok(()),
}
}
mod in_memory {
use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt::Write;
use eyre::Context;
use indicatif::{ProgressBar, ProgressStyle};
use tracing::{instrument, warn};
use crate::commands::gc::mark_commit_reachable;
use crate::core::formatting::printable_styled_string;
use crate::core::rewrite::move_branches;
use crate::core::rewrite::plan::{RebaseCommand, RebasePlan};
use crate::git::{GitRunInfo, MaybeZeroOid, NonZeroOid, Repo};
use crate::tui::Effects;
use super::ExecuteRebasePlanOptions;
pub enum RebaseInMemoryResult {
Succeeded {
rewritten_oids: Vec<(NonZeroOid, MaybeZeroOid)>,
new_head_oid: Option<NonZeroOid>,
},
CannotRebaseMergeCommit {
commit_oid: NonZeroOid,
},
MergeConflict {
commit_oid: NonZeroOid,
},
}
#[instrument]
pub fn rebase_in_memory(
effects: &Effects,
repo: &Repo,
rebase_plan: &RebasePlan,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<RebaseInMemoryResult> {
let ExecuteRebasePlanOptions {
now,
event_tx_id: _,
preserve_timestamps,
force_in_memory: _,
force_on_disk: _,
} = options;
let mut current_oid = rebase_plan.first_dest_oid;
let mut labels: HashMap<String, NonZeroOid> = HashMap::new();
let mut rewritten_oids: Vec<(NonZeroOid, MaybeZeroOid)> = Vec::new();
let head_oid = repo.get_head_info()?.oid;
let mut skipped_head_new_oid = None;
let mut maybe_set_skipped_head_new_oid = |skipped_head_oid, current_oid| {
if Some(skipped_head_oid) == head_oid {
skipped_head_new_oid.get_or_insert(current_oid);
}
};
let mut i = 0;
let num_picks = rebase_plan
.commands
.iter()
.filter(|command| match command {
RebaseCommand::CreateLabel { .. }
| RebaseCommand::ResetToLabel { .. }
| RebaseCommand::ResetToOid { .. }
| RebaseCommand::RegisterExtraPostRewriteHook
| RebaseCommand::DetectEmptyCommit { .. } => false,
RebaseCommand::Pick { .. } | RebaseCommand::SkipUpstreamAppliedCommit { .. } => {
true
}
})
.count();
for command in rebase_plan.commands.iter() {
match command {
RebaseCommand::CreateLabel { label_name } => {
labels.insert(label_name.clone(), current_oid);
}
RebaseCommand::ResetToLabel { label_name } => {
current_oid = match labels.get(label_name) {
Some(oid) => *oid,
None => eyre::bail!("BUG: no associated OID for label: {}", label_name),
};
}
RebaseCommand::ResetToOid { commit_oid } => {
current_oid = *commit_oid;
}
RebaseCommand::Pick { commit_oid } => {
let current_commit = repo.find_commit(current_oid).wrap_err_with(|| {
format!("Finding current commit by OID: {:?}", current_oid)
})?;
let current_commit = match current_commit {
Some(commit) => commit,
None => {
eyre::bail!("Unable to find current commit with OID: {:?}", current_oid)
}
};
let commit_to_apply = repo.find_commit(*commit_oid).wrap_err_with(|| {
format!("Finding commit to apply by OID: {:?}", commit_oid)
})?;
let commit_to_apply = match commit_to_apply {
Some(commit) => commit,
None => {
eyre::bail!(
"Unable to find commit to apply with OID: {:?}",
current_oid
)
}
};
i += 1;
let commit_description = printable_styled_string(
effects.get_glyphs(),
commit_to_apply.friendly_describe()?,
)?;
let commit_num = format!("[{}/{}]", i, num_picks);
let progress_template = format!("{} {{spinner}} {{wide_msg}}", commit_num);
let progress = ProgressBar::new_spinner();
progress.set_style(
ProgressStyle::default_spinner().template(progress_template.trim()),
);
progress.set_message("Starting");
progress.enable_steady_tick(100);
if commit_to_apply.get_parent_count() > 1 {
return Ok(RebaseInMemoryResult::CannotRebaseMergeCommit {
commit_oid: *commit_oid,
});
};
progress
.set_message(format!("Applying patch for commit: {}", commit_description));
let mut rebased_index =
repo.cherrypick_commit(&commit_to_apply, ¤t_commit, 0)?;
progress.set_message(format!(
"Checking for merge conflicts: {}",
commit_description
));
if rebased_index.has_conflicts() {
return Ok(RebaseInMemoryResult::MergeConflict {
commit_oid: *commit_oid,
});
}
progress.set_message(format!(
"Writing commit data to disk: {}",
commit_description
));
let commit_tree_oid = repo
.write_index_to_tree(&mut rebased_index)
.wrap_err_with(|| "Converting index to tree")?;
let commit_tree = match repo.find_tree(commit_tree_oid)? {
Some(tree) => tree,
None => eyre::bail!(
"Could not find freshly-written tree for OID: {:?}",
commit_tree_oid
),
};
let commit_message = commit_to_apply.get_message_raw()?;
let commit_message = match commit_message.to_str() {
Some(message) => message,
None => eyre::bail!(
"Could not decode commit message for commit: {:?}",
commit_oid
),
};
progress
.set_message(format!("Committing to repository: {}", commit_description));
let committer_signature = if *preserve_timestamps {
commit_to_apply.get_committer()
} else {
commit_to_apply.get_committer().update_timestamp(*now)?
};
let rebased_commit_oid = repo
.create_commit(
None,
&commit_to_apply.get_author(),
&committer_signature,
commit_message,
&commit_tree,
&[¤t_commit],
)
.wrap_err_with(|| "Applying rebased commit")?;
let rebased_commit = match repo
.find_commit(rebased_commit_oid)
.wrap_err_with(|| "Looking up just-rebased commit")?
{
Some(commit) => commit,
None => {
eyre::bail!(
"Could not find just-rebased commit: {:?}",
rebased_commit_oid
)
}
};
let commit_description = printable_styled_string(
effects.get_glyphs(),
repo.friendly_describe_commit_from_oid(rebased_commit_oid)?,
)?;
if rebased_commit.is_empty() {
rewritten_oids.push((*commit_oid, MaybeZeroOid::Zero));
maybe_set_skipped_head_new_oid(*commit_oid, current_oid);
progress.finish_and_clear();
writeln!(
effects.get_output_stream(),
"[{}/{}] Skipped now-empty commit: {}",
i,
num_picks,
commit_description
)?;
} else {
rewritten_oids
.push((*commit_oid, MaybeZeroOid::NonZero(rebased_commit_oid)));
current_oid = rebased_commit_oid;
progress.finish_and_clear();
writeln!(
effects.get_output_stream(),
"{} Committed as: {}",
commit_num,
commit_description
)?;
}
}
RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => {
let progress = ProgressBar::new_spinner();
i += 1;
let commit_num = format!("[{}/{}]", i, num_picks);
let progress_template = format!("{} {{spinner}} {{wide_msg}}", commit_num);
progress.set_style(
ProgressStyle::default_spinner().template(progress_template.trim()),
);
let commit = match repo.find_commit(*commit_oid)? {
Some(commit) => commit,
None => eyre::bail!("Could not find commit: {:?}", commit_oid),
};
rewritten_oids.push((*commit_oid, MaybeZeroOid::Zero));
maybe_set_skipped_head_new_oid(*commit_oid, current_oid);
progress.finish_and_clear();
let commit_description = commit.friendly_describe()?;
let commit_description =
printable_styled_string(effects.get_glyphs(), commit_description)?;
writeln!(
effects.get_output_stream(),
"{} Skipped commit (was already applied upstream): {}",
commit_num,
commit_description
)?;
}
RebaseCommand::RegisterExtraPostRewriteHook
| RebaseCommand::DetectEmptyCommit { .. } => {
}
}
}
let new_head_oid: Option<NonZeroOid> = match head_oid {
None => {
None
}
Some(head_oid) => {
let new_head_oid = rewritten_oids.iter().find_map(|(source_oid, dest_oid)| {
if *source_oid == head_oid {
Some(*dest_oid)
} else {
None
}
});
match new_head_oid {
Some(MaybeZeroOid::NonZero(new_head_oid)) => {
Some(new_head_oid)
}
Some(MaybeZeroOid::Zero) => {
let new_head_oid = match skipped_head_new_oid {
Some(new_head_oid) => new_head_oid,
None => {
warn!(
?head_oid,
"`HEAD` OID was rewritten to 0, but no skipped `HEAD` OID was set",
);
head_oid
}
};
Some(new_head_oid)
}
None => {
Some(head_oid)
}
}
}
};
Ok(RebaseInMemoryResult::Succeeded {
rewritten_oids,
new_head_oid,
})
}
pub fn post_rebase_in_memory(
effects: &Effects,
git_run_info: &GitRunInfo,
repo: &Repo,
rewritten_oids: &[(NonZeroOid, MaybeZeroOid)],
new_head_oid: Option<NonZeroOid>,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<isize> {
let ExecuteRebasePlanOptions {
now: _,
event_tx_id,
preserve_timestamps: _,
force_in_memory: _,
force_on_disk: _,
} = options;
let rewritten_oids_map: HashMap<NonZeroOid, MaybeZeroOid> =
rewritten_oids.iter().copied().collect();
for new_oid in rewritten_oids_map.values() {
if let MaybeZeroOid::NonZero(new_oid) = new_oid {
mark_commit_reachable(repo, *new_oid)?;
}
}
let head_info = repo.get_head_info()?;
if head_info.oid.is_some() {
repo.detach_head(&head_info)?;
}
move_branches(git_run_info, repo, *event_tx_id, &rewritten_oids_map)?;
let post_rewrite_stdin: String = rewritten_oids
.iter()
.map(|(old_oid, new_oid)| format!("{} {}\n", old_oid.to_string(), new_oid.to_string()))
.collect();
let post_rewrite_stdin = OsString::from(post_rewrite_stdin);
git_run_info.run_hook(
repo,
"post-rewrite",
*event_tx_id,
&["rebase"],
Some(post_rewrite_stdin),
)?;
let (previous_head_oid, new_head_oid) = match head_info.oid {
None => {
return Ok(0);
}
Some(previous_head_oid) => {
let new_head_oid = match new_head_oid {
Some(new_head_oid) => new_head_oid,
None => eyre::bail!(
"`None` provided for `new_head_oid`,
but it should have been `Some`
because the previous `HEAD` OID was not `None`: {:?}",
previous_head_oid
),
};
(previous_head_oid, new_head_oid)
}
};
let head_target = match (
head_info.get_branch_name(),
rewritten_oids_map.get(&previous_head_oid),
) {
(Some(head_branch), Some(MaybeZeroOid::NonZero(_))) => {
head_branch.to_string()
}
(Some(_), Some(MaybeZeroOid::Zero)) => {
new_head_oid.to_string()
}
(Some(head_branch), None) => {
head_branch.to_string()
}
(None, _) => {
new_head_oid.to_string()
}
};
let result = git_run_info.run(effects, Some(*event_tx_id), &["checkout", &head_target])?;
if result != 0 {
return Ok(result);
}
Ok(0)
}
}
mod on_disk {
use std::fmt::Write;
use eyre::Context;
use tracing::instrument;
use crate::core::rewrite::plan::RebasePlan;
use crate::git::{GitRunInfo, MaybeZeroOid, Repo};
use crate::tui::{Effects, OperationType};
use super::ExecuteRebasePlanOptions;
pub enum Error {
ChangedFilesInRepository,
OperationAlreadyInProgress { operation_type: String },
}
fn write_rebase_state_to_disk(
effects: &Effects,
git_run_info: &GitRunInfo,
repo: &Repo,
rebase_plan: &RebasePlan,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<Result<(), Error>> {
let ExecuteRebasePlanOptions {
now: _,
event_tx_id: _,
preserve_timestamps,
force_in_memory: _,
force_on_disk: _,
} = options;
let (effects, _progress) = effects.start_operation(OperationType::InitializeRebase);
let head_info = repo.get_head_info()?;
let current_operation_type = repo.get_current_operation_type();
if let Some(current_operation_type) = current_operation_type {
return Ok(Err(Error::OperationAlreadyInProgress {
operation_type: current_operation_type.to_string(),
}));
}
if repo.has_changed_files(&effects, git_run_info)? {
return Ok(Err(Error::ChangedFilesInRepository));
}
let rebase_state_dir = repo.get_rebase_state_dir_path();
std::fs::create_dir_all(&rebase_state_dir).wrap_err_with(|| {
format!(
"Creating rebase state directory at: {:?}",
&rebase_state_dir
)
})?;
let interactive_file_path = rebase_state_dir.join("interactive");
std::fs::write(&interactive_file_path, "")
.wrap_err_with(|| format!("Writing interactive to: {:?}", &interactive_file_path))?;
if head_info.oid.is_some() {
let repo_head_file_path = repo.get_path().join("HEAD");
let orig_head_file_path = repo.get_path().join("ORIG_HEAD");
std::fs::copy(&repo_head_file_path, &orig_head_file_path)
.wrap_err_with(|| format!("Copying `HEAD` to: {:?}", &orig_head_file_path))?;
let rebase_orig_head_oid: MaybeZeroOid = head_info.oid.into();
let rebase_orig_head_file_path = rebase_state_dir.join("orig-head");
std::fs::write(
&rebase_orig_head_file_path,
rebase_orig_head_oid.to_string(),
)
.wrap_err_with(|| {
format!("Writing `orig-head` to: {:?}", &rebase_orig_head_file_path)
})?;
let head_name_file_path = rebase_state_dir.join("head-name");
std::fs::write(
&head_name_file_path,
head_info.get_branch_name().unwrap_or("detached HEAD"),
)
.wrap_err_with(|| format!("Writing head-name to: {:?}", &head_name_file_path))?;
let rebase_merge_head_file_path = rebase_state_dir.join("head");
std::fs::write(
&rebase_merge_head_file_path,
rebase_plan.first_dest_oid.to_string(),
)
.wrap_err_with(|| format!("Writing head to: {:?}", &rebase_merge_head_file_path))?;
}
let onto_file_path = rebase_state_dir.join("onto");
std::fs::write(&onto_file_path, rebase_plan.first_dest_oid.to_string()).wrap_err_with(
|| {
format!(
"Writing onto {:?} to: {:?}",
&rebase_plan.first_dest_oid, &onto_file_path
)
},
)?;
let todo_file_path = rebase_state_dir.join("git-rebase-todo");
std::fs::write(
&todo_file_path,
rebase_plan
.commands
.iter()
.map(|command| format!("{}\n", command.to_string()))
.collect::<String>(),
)
.wrap_err_with(|| {
format!(
"Writing `git-rebase-todo` to: {:?}",
todo_file_path.as_path()
)
})?;
let end_file_path = rebase_state_dir.join("end");
std::fs::write(
end_file_path.as_path(),
format!("{}\n", rebase_plan.commands.len()),
)
.wrap_err_with(|| format!("Writing `end` to: {:?}", end_file_path.as_path()))?;
let keep_redundant_commits_file_path = rebase_state_dir.join("keep_redundant_commits");
std::fs::write(&keep_redundant_commits_file_path, "").wrap_err_with(|| {
format!(
"Writing `keep_redundant_commits` to: {:?}",
&keep_redundant_commits_file_path
)
})?;
if *preserve_timestamps {
let cdate_is_adate_file_path = rebase_state_dir.join("cdate_is_adate");
std::fs::write(&cdate_is_adate_file_path, "")
.wrap_err_with(|| "Writing `cdate_is_adate` option file")?;
}
if head_info.oid.is_some() {
repo.detach_head(&head_info)?;
}
Ok(Ok(()))
}
#[instrument]
pub fn rebase_on_disk(
effects: &Effects,
git_run_info: &GitRunInfo,
repo: &Repo,
rebase_plan: &RebasePlan,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<Result<isize, Error>> {
let ExecuteRebasePlanOptions {
now: _,
event_tx_id,
preserve_timestamps: _,
force_in_memory: _,
force_on_disk: _,
} = options;
match write_rebase_state_to_disk(effects, git_run_info, repo, rebase_plan, options)? {
Ok(()) => {}
Err(err) => return Ok(Err(err)),
};
writeln!(
effects.get_output_stream(),
"Calling Git for on-disk rebase..."
)?;
let exit_code = git_run_info.run(effects, Some(*event_tx_id), &["rebase", "--continue"])?;
Ok(Ok(exit_code))
}
}
#[derive(Clone, Debug)]
pub struct ExecuteRebasePlanOptions {
pub now: SystemTime,
pub event_tx_id: EventTransactionId,
pub preserve_timestamps: bool,
pub force_in_memory: bool,
pub force_on_disk: bool,
}
pub fn execute_rebase_plan(
effects: &Effects,
git_run_info: &GitRunInfo,
repo: &Repo,
rebase_plan: &RebasePlan,
options: &ExecuteRebasePlanOptions,
) -> eyre::Result<isize> {
let ExecuteRebasePlanOptions {
now: _,
event_tx_id: _,
preserve_timestamps: _,
force_in_memory,
force_on_disk,
} = options;
if !force_on_disk {
use in_memory::*;
writeln!(
effects.get_output_stream(),
"Attempting rebase in-memory..."
)?;
match rebase_in_memory(effects, repo, rebase_plan, options)? {
RebaseInMemoryResult::Succeeded {
rewritten_oids,
new_head_oid,
} => {
post_rebase_in_memory(
effects,
git_run_info,
repo,
&rewritten_oids,
new_head_oid,
options,
)?;
writeln!(effects.get_output_stream(), "In-memory rebase succeeded.")?;
return Ok(0);
}
RebaseInMemoryResult::CannotRebaseMergeCommit { commit_oid } => {
writeln!(effects.get_output_stream(),
"Merge commits currently can't be rebased with `git move`. The merge commit was: {}",
printable_styled_string(effects.get_glyphs(), repo.friendly_describe_commit_from_oid(commit_oid)?)?,
)?;
return Ok(1);
}
RebaseInMemoryResult::MergeConflict { commit_oid } => {
if *force_in_memory {
writeln!(
effects.get_output_stream(),
"Merge conflict. The conflicting commit was: {}",
printable_styled_string(
effects.get_glyphs(),
repo.friendly_describe_commit_from_oid(commit_oid)?,
)?,
)?;
writeln!(
effects.get_output_stream(),
"Aborting since an in-memory rebase was requested."
)?;
return Ok(1);
} else {
writeln!(effects.get_output_stream(),
"Merge conflict, falling back to rebase on-disk. The conflicting commit was: {}",
printable_styled_string(effects.get_glyphs(), repo.friendly_describe_commit_from_oid(commit_oid)?)?,
)?;
}
}
}
}
if !force_in_memory {
use on_disk::*;
match rebase_on_disk(effects, git_run_info, repo, rebase_plan, options)? {
Ok(exit_code) => return Ok(exit_code),
Err(Error::ChangedFilesInRepository) => {
write!(
effects.get_output_stream(),
"\
This operation would modify the working copy, but you have uncommitted changes
in your working copy which might be overwritten as a result.
Commit your changes and then try again.
"
)?;
return Ok(1);
}
Err(Error::OperationAlreadyInProgress { operation_type }) => {
writeln!(
effects.get_output_stream(),
"A {} operation is already in progress.",
operation_type
)?;
writeln!(
effects.get_output_stream(),
"Run git {0} --continue or git {0} --abort to resolve it and proceed.",
operation_type
)?;
return Ok(1);
}
}
}
eyre::bail!("Both force_in_memory and force_on_disk were requested, but these options conflict")
}