use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::change_id::logical_change_id_for_commit;
use crate::hooks::{
run_post_checkout, run_post_rewrite, HookName, PostOperationHooks, RewriteKind,
};
use crate::lockfile::{LockFile, LockWait};
use crate::merge_trees::materialized_merge_result_tree;
use crate::objects::ObjectId;
use crate::operation::conflicts_from_tree_merge;
use crate::porcelain::checkout::checkout_hard_to_tree_from_head_tree;
use crate::porcelain::merge::apply_conflict_merge_worktree;
use crate::porcelain::reset::{reset_to_commit, ResetMode, ResetOptions};
use crate::porcelain::worktree_guard::{
ensure_safe_for_rebase_stop, ensure_worktree_clean_for_rebase_exec,
};
use crate::progress::{
poll_cancel, Cancel, NeverCancel, NullProgress, ProgressSink, UncancellableProgress,
};
use crate::refs;
use crate::repo::Repository;
use crate::state::{resolve_head_for_repository, write_orig_head, HeadState};
use crate::write_tree::{write_tree_update_index, WriteTreeFlags};
use super::apply::{apply_rewrite, log_head_move};
use super::error::HistoryError;
use super::last::{write_history_last, HistoryLastRecord};
use super::plan::{
commit_parents, plan_rewrite_with_order_resumable_seeded, write_rebase_staged_resolution,
ResumablePlanOutcome,
};
use super::rebase::{
commits_in_rebase_range, plan_rebase_resumable, PreRebase, RebaseOntoOptions, RebasePlan,
RebaseRequest,
};
use super::types::RefScope;
use super::types::{
AppliedRewrite, ApplyOptions, CommitEdit, HistoryEdits, PlannedRefUpdate, RewriteOptions,
RewritePlan, RewrittenCommit,
};
use crate::operation::Conflict;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StartRebaseOptions {
pub request: RebaseRequest,
pub rebase: RebaseOntoOptions,
pub apply: ApplyOptions,
pub pre_rebase: PreRebase,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
pub struct RebaseStop {
pub commit: ObjectId,
pub conflicts: Vec<Conflict>,
pub done: Vec<ObjectId>,
pub remaining: Vec<ObjectId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
pub enum RebaseOutcome {
Done {
applied: AppliedRewrite,
plan: Box<RewritePlan>,
head_change: Option<(ObjectId, ObjectId)>,
},
ExecDone {
applied: AppliedRewrite,
plan: Box<RewritePlan>,
exec: super::ExecReport,
},
Stopped(RebaseStop),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
pub struct RebaseAbort {
pub overwritten: Vec<PlannedRefUpdate>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RebaseSessionError {
#[error("no rebase in progress")]
NoRebaseInProgress,
#[error("rebase is already in progress")]
RebaseAlreadyInProgress,
#[error("a rebase started by git is in progress")]
ForeignRebaseInProgress,
#[error("cannot continue: unmerged paths remain in the index")]
UnmergedPaths,
#[error("nothing to commit after conflict resolution")]
NothingToCommit,
#[error(
"HEAD moved away from the conflict resolution parent (expected {expected}); save commits on a branch or reset before `grit rebase --continue`"
)]
ResolutionHeadChanged {
expected: ObjectId,
},
#[error("rebase session requires HEAD on a branch")]
NotOnBranch,
#[error("exec replay HEAD changed; expected {expected}; save new commits on a branch, then abort and restart the rebase")]
ExecHeadChanged {
expected: ObjectId,
},
#[error(
"ref {name} moved from {expected} to {} while the rebase was stopped; abort the rebase to restore it",
.actual.map_or_else(|| "(deleted)".to_owned(), |oid| oid.to_hex())
)]
StaleExecPlan {
name: String,
expected: ObjectId,
actual: Option<ObjectId>,
},
#[error("ref {name} moved while aborting the rebase; retry the abort")]
AbortRefMoved {
name: String,
},
#[error(transparent)]
History(#[from] HistoryError),
#[error(transparent)]
Library(#[from] crate::error::Error),
}
impl RebaseSessionError {
#[must_use]
pub fn is_cancelled(&self) -> bool {
match self {
Self::Library(e) => e.is_cancelled(),
Self::History(e) => e.is_cancelled(),
Self::NoRebaseInProgress
| Self::RebaseAlreadyInProgress
| Self::ForeignRebaseInProgress
| Self::UnmergedPaths
| Self::NothingToCommit
| Self::ResolutionHeadChanged { .. }
| Self::NotOnBranch
| Self::ExecHeadChanged { .. }
| Self::StaleExecPlan { .. }
| Self::AbortRefMoved { .. } => false,
}
}
}
pub fn start_rebase(
repo: &Repository,
opts: &StartRebaseOptions,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
super::ensure_rewrite_apply_ready(repo)?;
if let Some(active) = active_rebase(repo.git_dir()) {
return Err(match active {
ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
});
}
let (head_ref, orig_head) = branch_head(repo)?;
let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
poll_cancel(progress, cancel).map_err(map_lib)?;
match plan {
Err(RebasePlan::UpToDate) => Ok(up_to_date_outcome(orig_head)),
Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
message: "unexpected non-resumable rebase plan".into(),
}
.into()),
Ok(ResumablePlanOutcome::Complete(plan)) => {
let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
write_orig_head(repo, orig_head).map_err(map_lib)?;
Ok(RebaseOutcome::Done {
head_change: (orig_head != applied.head).then_some((orig_head, applied.head)),
applied,
plan,
})
}
Ok(ResumablePlanOutcome::Partial(partial)) => {
let orig_tree = commit_tree(repo, orig_head)?;
let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
let stop = install_stopped_rebase(
repo,
&head_ref,
orig_head,
&opts.request,
&opts.rebase,
&opts.apply,
*partial,
progress,
cancel,
)?;
write_orig_head(repo, orig_head).map_err(map_lib)?;
if opts.apply.hooks.enabled() {
let onto = opts.request.onto.unwrap_or(opts.request.upstream);
run_post_checkout(repo, Some(orig_head), onto, true);
}
Ok(RebaseOutcome::Stopped(stop))
}
}
}
pub fn start_rebase_with_exec(
repo: &Repository,
opts: &StartRebaseOptions,
spec: &super::ExecSpec,
runner: &dyn crate::command_runner::CommandRunner,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
super::ensure_rewrite_apply_ready(repo)?;
let orig_head = opts.request.branch_tip;
let mut plan = match super::rebase::plan_rebase_with_hook(
repo,
&opts.request,
&opts.rebase,
&opts.pre_rebase,
)? {
RebasePlan::UpToDate => {
if spec.commands.is_empty() {
return Ok(up_to_date_outcome(orig_head));
}
let fork =
super::rebase::rebase_fork(repo, opts.request.upstream, opts.request.branch_tip)?;
let effective_onto = opts.request.onto.unwrap_or(opts.request.upstream);
if opts.request.branch_tip == effective_onto {
return Ok(up_to_date_outcome(orig_head));
}
let range =
super::rebase::commits_in_rebase_range(repo, fork, &[opts.request.branch_tip])?;
if range.is_empty() {
return Ok(up_to_date_outcome(orig_head));
}
opts.pre_rebase.run(repo)?;
super::rebase::plan_rebase_identity_exec(
repo,
fork,
effective_onto,
&range,
&opts.rebase,
)?
}
RebasePlan::Conflict {
commit,
subject,
conflicts,
refs,
} => {
return Err(HistoryError::Conflict {
commit,
subject,
conflicts,
refs,
}
.into());
}
RebasePlan::Plan(plan) => plan,
};
super::apply::verify_rewrite_plan(repo, &plan)?;
let exec =
match super::verify_rebase_with_exec(repo, &mut plan, spec, &opts.rebase.rewrite, runner) {
Ok(exec) => exec,
Err(stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. })) => {
write_orig_head(repo, orig_head).map_err(map_lib)?;
return Err(stop.into());
}
Err(err) => return Err(err.into()),
};
let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
write_orig_head(repo, orig_head).map_err(map_lib)?;
Ok(RebaseOutcome::ExecDone {
applied,
plan: Box::new(plan),
exec,
})
}
fn up_to_date_outcome(head: ObjectId) -> RebaseOutcome {
RebaseOutcome::Done {
head_change: None,
applied: AppliedRewrite {
ref_updates: Vec::new(),
head,
hooks_run: Vec::new(),
},
plan: Box::new(RewritePlan {
commits: Vec::new(),
ref_updates: Vec::new(),
head: super::types::HeadEffect::default(),
}),
}
}
pub(crate) fn persist_rebase_stopped_head_if_missing(
repo: &Repository,
) -> Result<(), RebaseSessionError> {
if !crate::state::grit_rebase_conflict_stop_active(repo.git_dir()) {
return Ok(());
}
let path = repo.git_dir().join("rebase-merge/grit-plan");
let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
let mut parsed: PersistedRebaseState =
serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
if parsed.stopped_head.is_some() {
return Ok(());
}
let head = resolve_head_oid(repo)?;
parsed.stopped_head = Some(oid_hex(&head));
write_persisted_state(repo.git_dir(), &parsed)
}
fn replay_base_parent(state: &LoadedState, oid: ObjectId) -> bool {
oid == state.onto || state.mapping.values().any(|mapped| *mapped == oid)
}
fn stopped_head_for_continue(
state: &LoadedState,
repo: &Repository,
) -> Result<ObjectId, RebaseSessionError> {
if let Some(oid) = state.stopped_head {
return Ok(oid);
}
let head = resolve_head_oid(repo)?;
if replay_base_parent(state, head) {
return Ok(head);
}
let head_commit = load_commit(repo, head)?;
if head_commit.parents.len() == 1 {
if let Some(&parent) = head_commit.parents.first() {
if replay_base_parent(state, parent) {
return Ok(parent);
}
}
} else {
for &parent in &head_commit.parents {
if replay_base_parent(state, parent) {
return Ok(parent);
}
}
}
Err(RebaseSessionError::NoRebaseInProgress)
}
fn is_first_parent_descendant_of(
repo: &Repository,
ancestor: ObjectId,
tip: ObjectId,
) -> Result<bool, RebaseSessionError> {
if tip == ancestor {
return Ok(false);
}
let mut current = Some(tip);
while let Some(oid) = current {
let parents = commit_parents(repo, oid).map_err(RebaseSessionError::from)?;
let Some(first) = parents.first().copied() else {
break;
};
if first == ancestor {
return Ok(true);
}
current = Some(first);
}
Ok(false)
}
fn advance_after_user_resolution_commit(
repo: &Repository,
state: &LoadedState,
opts: &StartRebaseOptions,
current: ObjectId,
head_oid: ObjectId,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
let current_data = load_commit(repo, current)?;
let mut mapping = state.mapping.clone();
mapping.insert(current, head_oid);
let mut rewritten = state.rewritten.clone();
rewritten.push(RewrittenCommit {
old: current,
new: Some(head_oid),
change_id: logical_change_id_for_commit(repo, head_oid).map_err(map_lib)?,
subject: super::subject::subject_for_rewrite_row(repo, ¤t_data, Some(head_oid))
.map_err(RebaseSessionError::from)?,
reused: false,
});
let resume_order: Vec<ObjectId> = replay_order_for_resume(state, current)
.into_iter()
.filter(|commit| *commit != current)
.collect();
let mut edits = state.edits.clone();
edits.retain(|commit, _| resume_order.contains(commit));
let rewrite = replan_rewrite_options(state);
let outcome = plan_rewrite_with_order_resumable_seeded(
repo,
&edits,
&rewrite,
Some(&resume_order),
state.flatten_leaf_merges.then_some(state.onto),
state.rebase.favor,
Some((mapping, rewritten)),
Some(state.scoped_refs.clone()),
)?;
advance_after_plan(
repo,
state,
&opts.apply,
outcome,
head_oid,
progress,
cancel,
)
}
pub fn continue_rebase(
repo: &Repository,
opts: &StartRebaseOptions,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
return super::exec::resume_exec_rebase(repo, &opts.apply, false, progress, cancel);
}
super::ensure_rewrite_apply_ready(repo)?;
let state = load_persisted_state(repo.git_dir())?;
if repo.load_index()?.has_unmerged_entries() {
return Err(RebaseSessionError::UnmergedPaths);
}
let current = state
.stopped_commit
.or_else(|| state.remaining.first().copied())
.ok_or(RebaseSessionError::NoRebaseInProgress)?;
let new_tree = {
let mut index = repo.load_index()?;
write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
.map_err(map_lib)?
};
let head_oid = resolve_head_oid(repo)?;
let head_tree = commit_tree(repo, head_oid)?;
if new_tree == head_tree {
let stopped_head = stopped_head_for_continue(&state, repo)?;
if head_oid == stopped_head {
return Err(RebaseSessionError::NothingToCommit);
}
if is_first_parent_descendant_of(repo, stopped_head, head_oid)? {
ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
return advance_after_user_resolution_commit(
repo, &state, opts, current, head_oid, progress, cancel,
);
}
return Err(RebaseSessionError::ResolutionHeadChanged {
expected: stopped_head,
});
}
let original_parents = commit_parents(repo, current).map_err(RebaseSessionError::from)?;
let parents = if original_parents.len() <= 1 {
vec![head_oid]
} else {
let mut mapped = vec![head_oid];
mapped.extend(
original_parents[1..]
.iter()
.map(|parent| state.mapping.get(parent).copied().unwrap_or(*parent)),
);
mapped
};
if state.remaining.len() > 1 {
let stopped_head = stopped_head_for_continue(&state, repo)?;
if head_oid != stopped_head {
return Err(RebaseSessionError::ResolutionHeadChanged {
expected: stopped_head,
});
}
let resolved =
write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
.map_err(RebaseSessionError::from)?;
refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
.map_err(map_lib)?;
ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
return advance_after_user_resolution_commit(
repo, &state, opts, current, resolved, progress, cancel,
);
}
let resume_order = replay_order_for_resume(&state, current);
let mut edits = state.edits.clone();
edits.insert(
current,
CommitEdit::Rewrite {
tree: Some(new_tree),
message: None,
author: None,
parents: Some(parents),
},
);
edits.retain(|commit, _| resume_order.contains(commit));
let seed = (state.mapping.clone(), state.rewritten.clone());
let rewrite = replan_rewrite_options(&state);
let outcome = plan_rewrite_with_order_resumable_seeded(
repo,
&edits,
&rewrite,
Some(&resume_order),
state.flatten_leaf_merges.then_some(state.onto),
state.rebase.favor,
Some(seed),
Some(state.scoped_refs.clone()),
)?;
advance_after_plan(
repo,
&state,
&opts.apply,
outcome,
head_oid,
progress,
cancel,
)
}
pub fn skip_rebase(
repo: &Repository,
opts: &StartRebaseOptions,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
return super::exec::resume_exec_rebase(repo, &opts.apply, true, progress, cancel);
}
let state = load_persisted_state(repo.git_dir())?;
let current = state
.remaining
.first()
.copied()
.ok_or(RebaseSessionError::NoRebaseInProgress)?;
let head_oid = resolve_head_oid(repo)?;
reset_index_worktree_to_head(repo, head_oid, progress, cancel)?;
let replayed_parent = head_oid;
let mut mapping = state.mapping.clone();
mapping.insert(current, replayed_parent);
let mut rewritten = state.rewritten.clone();
rewritten.push(RewrittenCommit {
old: current,
new: None,
change_id: None,
subject: None,
reused: false,
});
let resume_order: Vec<ObjectId> = state
.remaining
.iter()
.copied()
.filter(|c| *c != current)
.collect();
let mut edits = state.edits.clone();
edits.retain(|commit, _| resume_order.contains(commit));
let rewrite = replan_rewrite_options(&state);
let outcome = plan_rewrite_with_order_resumable_seeded(
repo,
&edits,
&rewrite,
Some(&resume_order),
state.flatten_leaf_merges.then_some(state.onto),
state.rebase.favor,
Some((mapping, rewritten)),
Some(state.scoped_refs.clone()),
)?;
advance_after_plan(
repo,
&state,
&opts.apply,
outcome,
head_oid,
progress,
cancel,
)
}
pub fn abort_rebase(
repo: &Repository,
identity: &crate::ident::Signature,
) -> Result<RebaseAbort, RebaseSessionError> {
identity
.validate()
.map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
if had_journal {
super::abort_interrupted_rewrite_apply(repo)?;
}
match active_rebase(repo.git_dir()) {
None => {
if had_journal {
Ok(RebaseAbort::default())
} else {
Err(RebaseSessionError::NoRebaseInProgress)
}
}
Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
Some(ActiveRebase::Exec) => super::exec::abort_exec_rebase(repo, identity),
Some(ActiveRebase::Grit) => {
let state = load_persisted_state(repo.git_dir())?;
crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
let abort = hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?;
clear_rebase_state(repo.git_dir())?;
Ok(abort)
}
}
}
fn abort_foreign_rebase(
repo: &Repository,
state_dir: &Path,
identity: &crate::ident::Signature,
) -> Result<RebaseAbort, RebaseSessionError> {
if state_dir.join("autostash").exists() {
return Err(RebaseSessionError::ForeignRebaseInProgress);
}
let orig_head = first_line(&state_dir.join("orig-head"))
.and_then(|hex| ObjectId::from_hex(&hex).ok())
.ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
let abort =
match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
None => {
hard_reset_head(repo, orig_head, identity)?;
RebaseAbort::default()
}
};
for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
remove_if_exists(&repo.git_dir().join(name))?;
}
fs::remove_dir_all(state_dir).map_err(map_lib)?;
Ok(abort)
}
enum ActiveRebase {
Exec,
Grit,
Foreign(std::path::PathBuf),
}
fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
let merge = git_dir.join("rebase-merge");
if merge.join("grit-exec").is_file() {
return Some(ActiveRebase::Exec);
}
if merge.join("grit-plan").is_file() {
return Some(ActiveRebase::Grit);
}
if merge.is_dir() {
return Some(ActiveRebase::Foreign(merge));
}
let apply = git_dir.join("rebase-apply");
(apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
}
fn first_line(path: &Path) -> Option<String> {
let raw = fs::read_to_string(path).ok()?;
let line = raw.lines().next()?.trim();
(!line.is_empty()).then(|| line.to_owned())
}
fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
match fs::remove_file(path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
_ => Ok(()),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedRebaseState {
mapping: HashMap<String, String>,
rewritten: Vec<PersistedRewrittenRow>,
edits: Vec<PersistedEdit>,
rewrite: RewriteOptions,
rebase: RebaseOntoOptions,
apply: ApplyOptions,
replay_order: Vec<String>,
remaining: Vec<String>,
done: Vec<String>,
fork: String,
onto: String,
upstream: String,
head_ref: String,
orig_head: String,
branch_tip: String,
flatten_leaf_merges: bool,
scoped_refs: Vec<(String, String)>,
#[serde(default)]
stopped_head: Option<String>,
#[serde(default)]
stopped_commit: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedRewrittenRow {
old: String,
new: Option<String>,
#[serde(default)]
reused: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum PersistedEdit {
Reparent {
commit: String,
parent: String,
},
Reuse {
commit: String,
existing: String,
#[serde(default)]
parent: Option<String>,
},
Drop {
commit: String,
},
Rewrite {
commit: String,
tree: Option<String>,
message: Option<String>,
author: Option<String>,
},
}
fn oid_hex(oid: &ObjectId) -> String {
oid.to_hex()
}
fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
ObjectId::from_hex(hex).map_err(|e| {
HistoryError::Planning {
message: e.to_string(),
}
.into()
})
}
fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
edits
.iter()
.map(|(commit, edit)| match edit {
CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
commit: oid_hex(commit),
parent: oid_hex(parent),
},
CommitEdit::Reuse { existing, parent } => PersistedEdit::Reuse {
commit: oid_hex(commit),
existing: oid_hex(existing),
parent: Some(oid_hex(parent)),
},
CommitEdit::Drop => PersistedEdit::Drop {
commit: oid_hex(commit),
},
CommitEdit::Rewrite {
tree,
message,
author,
parents: _,
} => PersistedEdit::Rewrite {
commit: oid_hex(commit),
tree: tree.as_ref().map(oid_hex),
message: message.clone(),
author: author.clone(),
},
CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
commit: oid_hex(commit),
},
})
.collect()
}
fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
let mut edits = HistoryEdits::new();
for row in rows {
match row {
PersistedEdit::Reparent { commit, parent } => {
edits.insert(
parse_oid(commit)?,
CommitEdit::Reparent {
parent: parse_oid(parent)?,
},
);
}
PersistedEdit::Reuse {
commit,
existing,
parent,
} => {
let existing = parse_oid(existing)?;
let parent = parent.as_deref().map(parse_oid).transpose()?;
edits.insert(
parse_oid(commit)?,
CommitEdit::Reuse {
existing,
parent: parent.unwrap_or(existing),
},
);
}
PersistedEdit::Drop { commit } => {
edits.insert(parse_oid(commit)?, CommitEdit::Drop);
}
PersistedEdit::Rewrite {
commit,
tree,
message,
author,
} => {
edits.insert(
parse_oid(commit)?,
CommitEdit::Rewrite {
tree: tree.as_deref().map(parse_oid).transpose()?,
message: message.clone(),
author: author.clone(),
parents: None,
},
);
}
}
}
Ok(edits)
}
fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
}
fn mapping_from_persist(
map: &HashMap<String, String>,
) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
map.iter()
.map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
.collect()
}
fn replay_order_for_resume(state: &LoadedState, current: ObjectId) -> Vec<ObjectId> {
if let Some(pos) = state.replay_order.iter().position(|c| *c == current) {
return state.replay_order[pos..].to_vec();
}
let mut order = if state.remaining.is_empty() {
state.replay_order.clone()
} else {
state.remaining.clone()
};
if !order.contains(¤t) {
order.insert(0, current);
} else if order.first() != Some(¤t) {
order.retain(|c| *c != current);
order.insert(0, current);
}
order
}
#[allow(clippy::too_many_arguments)]
fn state_from_partial(
partial: &super::plan::PartialRewritePlan,
head_ref: &str,
orig_head: ObjectId,
request: &RebaseRequest,
rebase: &RebaseOntoOptions,
apply: &ApplyOptions,
effective_onto: ObjectId,
fork: ObjectId,
) -> PersistedRebaseState {
let conflict = partial.conflict.commit;
let stop_idx = partial
.commits_to_replay
.iter()
.position(|c| *c == conflict)
.unwrap_or(0);
let done: Vec<ObjectId> = partial.commits_to_replay[..stop_idx]
.iter()
.copied()
.filter(|c| partial.mapping.contains_key(c))
.collect();
let remaining: Vec<ObjectId> = partial.commits_to_replay[stop_idx..].to_vec();
PersistedRebaseState {
mapping: mapping_to_persist(&partial.mapping),
rewritten: partial
.rewritten
.iter()
.map(|r| PersistedRewrittenRow {
old: oid_hex(&r.old),
new: r.new.as_ref().map(oid_hex),
reused: r.reused,
})
.collect(),
edits: edits_to_persist(&partial.edits),
rewrite: partial.opts.clone(),
rebase: rebase.clone(),
apply: apply.clone(),
replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
remaining: remaining.iter().map(oid_hex).collect(),
done: done.iter().map(oid_hex).collect(),
fork: oid_hex(&fork),
onto: oid_hex(&effective_onto),
upstream: oid_hex(&request.upstream),
head_ref: head_ref.to_owned(),
orig_head: oid_hex(&orig_head),
branch_tip: oid_hex(&request.branch_tip),
flatten_leaf_merges: partial.linearize_onto.is_some(),
scoped_refs: partial
.scoped_refs
.iter()
.map(|(n, o)| (n.clone(), oid_hex(o)))
.collect(),
stopped_head: None,
stopped_commit: Some(oid_hex(&conflict)),
}
}
struct LoadedState {
mapping: HashMap<ObjectId, ObjectId>,
rewritten: Vec<RewrittenCommit>,
edits: HistoryEdits,
rewrite: RewriteOptions,
rebase: RebaseOntoOptions,
replay_order: Vec<ObjectId>,
remaining: Vec<ObjectId>,
head_ref: String,
orig_head: ObjectId,
onto: ObjectId,
upstream: ObjectId,
branch_tip: ObjectId,
flatten_leaf_merges: bool,
scoped_refs: Vec<(String, ObjectId)>,
stopped_head: Option<ObjectId>,
stopped_commit: Option<ObjectId>,
}
fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
match active_rebase(git_dir) {
None => return Err(RebaseSessionError::NoRebaseInProgress),
Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
Some(ActiveRebase::Exec) => return Err(RebaseSessionError::NoRebaseInProgress),
Some(ActiveRebase::Grit) => {}
}
let path = git_dir.join("rebase-merge").join("grit-plan");
let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
let parsed: PersistedRebaseState =
serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
Ok(LoadedState {
mapping: mapping_from_persist(&parsed.mapping)?,
rewritten: parsed
.rewritten
.iter()
.map(|r| {
Ok(RewrittenCommit {
old: parse_oid(&r.old)?,
new: r.new.as_deref().map(parse_oid).transpose()?,
change_id: None,
subject: None,
reused: r.reused,
})
})
.collect::<Result<_, RebaseSessionError>>()?,
edits: edits_from_persist(&parsed.edits)?,
rewrite: parsed.rewrite,
rebase: parsed.rebase,
replay_order: parsed
.replay_order
.iter()
.map(|h| parse_oid(h))
.collect::<Result<_, _>>()?,
remaining: parsed
.remaining
.iter()
.map(|h| parse_oid(h))
.collect::<Result<_, _>>()?,
head_ref: parsed.head_ref,
orig_head: parse_oid(&parsed.orig_head)?,
onto: parse_oid(&parsed.onto)?,
upstream: parse_oid(&parsed.upstream)?,
branch_tip: parse_oid(&parsed.branch_tip)?,
flatten_leaf_merges: parsed.flatten_leaf_merges,
scoped_refs: parsed
.scoped_refs
.iter()
.map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
.collect::<Result<_, _>>()?,
stopped_head: parsed.stopped_head.as_deref().map(parse_oid).transpose()?,
stopped_commit: parsed
.stopped_commit
.as_deref()
.map(parse_oid)
.transpose()?,
})
}
fn write_persisted_state(
git_dir: &Path,
state: &PersistedRebaseState,
) -> Result<(), RebaseSessionError> {
let dir = git_dir.join("rebase-merge");
fs::create_dir_all(&dir).map_err(map_lib)?;
let lock_path = dir.join("grit-plan.lock");
let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
.map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
lock.write_all(&body).map_err(map_lib)?;
lock.commit_to(&dir.join("grit-plan"))
.map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn install_stopped_rebase(
repo: &Repository,
head_ref: &str,
orig_head: ObjectId,
request: &RebaseRequest,
rebase_opts: &RebaseOntoOptions,
apply: &ApplyOptions,
partial: super::plan::PartialRewritePlan,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseStop, RebaseSessionError> {
poll_cancel(progress, cancel).map_err(map_lib)?;
let effective_onto = request.onto.unwrap_or(request.upstream);
let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
let mut persisted = state_from_partial(
&partial,
head_ref,
orig_head,
request,
rebase_opts,
apply,
effective_onto,
fork,
);
let conflict = partial.conflict;
persisted.stopped_head = Some(oid_hex(&conflict.replayed_parent));
let done = persisted
.done
.iter()
.map(|h| parse_oid(h))
.collect::<Result<Vec<_>, _>>()?;
let remaining = persisted
.remaining
.iter()
.map(|h| parse_oid(h))
.collect::<Result<Vec<_>, _>>()?;
let git_progress = git_visible_rebase_progress(
repo,
fork,
request.branch_tip,
&rebase_opts.rewrite,
&done,
&remaining,
conflict.commit,
)?;
write_git_rebase_merge_files(
repo,
head_ref,
orig_head,
effective_onto,
&git_progress.done,
&git_progress.remaining,
git_progress.stop.as_ref(),
)?;
write_persisted_state(repo.git_dir(), &persisted)?;
refs::update_direct_ref_on_store(
repo.refs(),
repo.git_dir(),
"HEAD",
&conflict.replayed_parent,
)
.map_err(map_lib)?;
let mut progress = UncancellableProgress::new(progress);
reset_index_worktree_to_head(repo, conflict.replayed_parent, &mut progress, &NeverCancel)?;
let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
let materialized =
materialized_merge_result_tree(repo.odb(), &conflict.merge).map_err(map_lib)?;
apply_conflict_merge_worktree(
repo,
&ours_tree,
&conflict.merge,
&materialized,
&mut progress,
&NeverCancel,
)
.map_err(map_lib)?;
let message = load_commit(repo, conflict.commit)?.message;
fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
Ok(RebaseStop {
commit: conflict.commit,
conflicts: conflicts_from_tree_merge(&conflict.merge),
done,
remaining,
})
}
#[allow(clippy::too_many_arguments)]
fn advance_after_plan(
repo: &Repository,
prev: &LoadedState,
apply: &ApplyOptions,
outcome: ResumablePlanOutcome,
head_before: ObjectId,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<RebaseOutcome, RebaseSessionError> {
poll_cancel(progress, cancel).map_err(map_lib)?;
match outcome {
ResumablePlanOutcome::Complete(mut plan) => {
if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
if !plan.ref_updates.iter().any(|u| u.name == prev.head_ref) {
plan.ref_updates.push(PlannedRefUpdate {
name: prev.head_ref.clone(),
old: prev.orig_head,
new: new_tip,
});
}
}
let apply_without_hooks = ApplyOptions {
hooks: PostOperationHooks::Skip,
..apply.clone()
};
let mut applied = apply_rewrite(repo, &plan, &apply_without_hooks, progress, cancel)?;
if let Some(branch) = plan
.ref_updates
.iter()
.find(|u| u.name == prev.head_ref && u.old != u.new)
{
log_head_move(repo, branch, apply)?;
}
refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &prev.head_ref)
.map_err(map_lib)?;
let branch_tip = repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?;
reset_index_worktree_to_head(repo, branch_tip, &mut NullProgress, &NeverCancel)?;
applied.head = branch_tip;
if let Some(operation) = apply.record_operation.as_deref() {
let head_old_tree = commit_tree(repo, prev.orig_head)?;
let head_new_tree = commit_tree(repo, branch_tip)?;
let record = HistoryLastRecord {
operation: operation.into(),
head_ref: Some(prev.head_ref.clone()),
head_old_commit: Some(prev.orig_head),
head_new_commit: Some(branch_tip),
head_old_tree: Some(head_old_tree),
head_new_tree: Some(head_new_tree),
ref_updates: plan.ref_updates.clone(),
};
write_history_last(repo, &record, None)?;
}
clear_rebase_state(repo.git_dir())?;
if apply.hooks.enabled()
&& run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
{
applied.hooks_run.push(HookName::PostRewrite);
}
Ok(RebaseOutcome::Done {
head_change: (head_before != applied.head).then_some((head_before, applied.head)),
applied,
plan,
})
}
ResumablePlanOutcome::Partial(partial) => {
let request = RebaseRequest {
upstream: prev.upstream,
onto: Some(prev.onto),
branch_tip: prev.branch_tip,
};
let stop = install_stopped_rebase(
repo,
&prev.head_ref,
prev.orig_head,
&request,
&prev.rebase,
apply,
*partial,
progress,
cancel,
)?;
Ok(RebaseOutcome::Stopped(stop))
}
}
}
struct GitVisibleRebaseProgress {
done: Vec<ObjectId>,
remaining: Vec<ObjectId>,
stop: Option<ObjectId>,
}
fn git_visible_rebase_progress(
repo: &Repository,
fork: ObjectId,
branch_tip: ObjectId,
rewrite: &RewriteOptions,
done: &[ObjectId],
remaining: &[ObjectId],
conflict: ObjectId,
) -> Result<GitVisibleRebaseProgress, RebaseSessionError> {
if matches!(rewrite.scope, RefScope::Head) {
return Ok(GitVisibleRebaseProgress {
done: done.to_vec(),
remaining: remaining.to_vec(),
stop: Some(conflict),
});
}
let primary = commits_in_rebase_range(repo, fork, &[branch_tip])?;
let primary_set: HashSet<ObjectId> = primary.iter().copied().collect();
let git_done: Vec<ObjectId> = done
.iter()
.copied()
.filter(|oid| primary_set.contains(oid))
.collect();
let git_remaining: Vec<ObjectId> = remaining
.iter()
.copied()
.filter(|oid| primary_set.contains(oid))
.collect();
let git_stop = primary_set.contains(&conflict).then_some(conflict);
Ok(GitVisibleRebaseProgress {
done: git_done,
remaining: git_remaining,
stop: git_stop,
})
}
fn write_git_rebase_merge_files(
repo: &Repository,
head_ref: &str,
orig_head: ObjectId,
onto: ObjectId,
done: &[ObjectId],
remaining: &[ObjectId],
current: Option<&ObjectId>,
) -> Result<(), RebaseSessionError> {
let dir = repo.git_dir().join("rebase-merge");
fs::create_dir_all(&dir).map_err(map_lib)?;
fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
let todo_remaining: Vec<ObjectId> = match current {
Some(current) if remaining.first() == Some(current) => {
remaining.iter().skip(1).copied().collect()
}
_ => remaining.to_vec(),
};
fs::write(
dir.join("git-rebase-todo"),
todo_lines(repo, &todo_remaining)?,
)
.map_err(map_lib)?;
let done_with_current: Vec<ObjectId> = match current {
Some(current) => done.iter().chain([current]).copied().collect(),
None => done.to_vec(),
};
fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
let msgnum = done_with_current.len();
let end = done_with_current.len() + todo_remaining.len();
fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
if let Some(current) = current {
fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
let commit = load_commit(repo, *current)?;
match author_script(&commit.author) {
Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
None => remove_if_exists(&dir.join("author-script"))?,
}
fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
} else {
remove_if_exists(&dir.join("stopped-sha"))?;
remove_if_exists(&dir.join("author-script"))?;
remove_if_exists(&dir.join("message"))?;
}
Ok(())
}
fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
let mut out = String::new();
for &oid in commits {
let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
let abbrev = &oid.to_hex()[..7];
out.push_str(&format!("pick {abbrev} {subject}\n"));
}
Ok(out)
}
fn author_script(author: &str) -> Option<String> {
let lt = author.find('<')?;
let gt = lt + author[lt..].find('>')?;
let name = author[..lt].trim_end();
let email = &author[lt + 1..gt];
let date = format!("@{}", author[gt + 1..].trim());
let quote = crate::transport::sq_quote_shell_arg;
Some(format!(
"GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
quote(name),
quote(email),
quote(&date)
))
}
pub(super) fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
remove_if_exists(&git_dir.join("MERGE_MSG"))?;
let dir = git_dir.join("rebase-merge");
if dir.is_dir() {
fs::remove_dir_all(&dir).map_err(map_lib)?;
}
Ok(())
}
fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
match resolve_head_for_repository(repo).map_err(map_lib)? {
HeadState::Branch {
refname,
oid: Some(oid),
..
} => Ok((refname, oid)),
HeadState::Branch { oid: None, .. } | HeadState::Detached { .. } | HeadState::Invalid => {
Err(RebaseSessionError::NotOnBranch)
}
}
}
fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
resolve_head_for_repository(repo)
.map_err(map_lib)?
.oid()
.copied()
.ok_or(RebaseSessionError::NotOnBranch)
}
fn commit_tree(repo: &Repository, commit: ObjectId) -> Result<ObjectId, RebaseSessionError> {
repo.odb().commit_tree(&commit).map_err(map_lib)
}
fn load_commit(
repo: &Repository,
oid: ObjectId,
) -> Result<crate::objects::CommitData, RebaseSessionError> {
repo.odb().read_commit(&oid).map_err(map_lib)
}
fn reset_index_worktree_to_head(
repo: &Repository,
head_oid: ObjectId,
progress: &mut dyn ProgressSink,
cancel: &dyn Cancel,
) -> Result<(), RebaseSessionError> {
let head_tree = commit_tree(repo, head_oid)?;
checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
.map_err(map_lib)?;
Ok(())
}
fn hard_reset_branch(
repo: &Repository,
refname: &str,
target: ObjectId,
identity: &crate::ident::Signature,
) -> Result<RebaseAbort, RebaseSessionError> {
let before = repo
.references()
.resolve_if_exists(refname)
.map_err(map_lib)?;
refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
.map_err(map_lib)?;
hard_reset_head(repo, target, identity)?;
let overwritten = (before != Some(target)).then(|| PlannedRefUpdate {
name: refname.to_owned(),
old: before.unwrap_or_else(crate::diff::zero_oid),
new: target,
});
Ok(RebaseAbort {
overwritten: overwritten.into_iter().collect(),
})
}
fn hard_reset_head(
repo: &Repository,
target: ObjectId,
identity: &crate::ident::Signature,
) -> Result<(), RebaseSessionError> {
reset_to_commit(
repo,
&ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
&mut crate::progress::NullProgress,
&crate::progress::NeverCancel,
)
.map_err(map_lib)?;
Ok(())
}
use std::io::Write;
fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
RebaseSessionError::Library(err.into())
}
fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
state.rewrite.clone()
}
fn map_json(err: serde_json::Error) -> RebaseSessionError {
RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
}
#[cfg(test)]
mod tests {
use super::{author_script, edits_from_persist, edits_to_persist, PersistedEdit};
use crate::history::types::{CommitEdit, HistoryEdits};
use crate::objects::ObjectId;
fn oid(byte: u8) -> ObjectId {
ObjectId::from_bytes(&[byte; 20]).unwrap_or_else(|_| ObjectId::zero())
}
#[test]
fn reuse_edit_round_trips_its_continuation_parent() {
let mut edits = HistoryEdits::new();
edits.insert(
oid(1),
CommitEdit::Reuse {
existing: oid(2),
parent: oid(3),
},
);
let rows = edits_to_persist(&edits);
let json = serde_json::to_string(&rows).unwrap_or_default();
let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
assert_eq!(edits_from_persist(&parsed).ok(), Some(edits));
}
#[test]
fn reuse_edit_saved_without_parent_continues_from_the_reused_commit() {
let json = format!(
r#"[{{"Reuse":{{"commit":"{}","existing":"{}"}}}}]"#,
oid(1).to_hex(),
oid(2).to_hex()
);
let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
let edits = edits_from_persist(&parsed).ok();
assert_eq!(
edits.and_then(|edits| edits.get(&oid(1)).cloned()),
Some(CommitEdit::Reuse {
existing: oid(2),
parent: oid(2),
})
);
}
#[test]
fn author_script_quotes_values_and_prefixes_date() {
assert_eq!(
author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
Some(
"GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
)
);
assert_eq!(
author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
Some(
"GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
)
);
assert_eq!(author_script("no email here 1 +0000"), None);
}
}