1use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7use serde::{Deserialize, Serialize};
8
9use crate::hooks::{
10 run_post_checkout, run_post_rewrite, HookName, PostOperationHooks, RewriteKind,
11};
12use crate::lockfile::{LockFile, LockWait};
13use crate::objects::ObjectId;
14use crate::operation::conflicts_from_tree_merge;
15use crate::porcelain::checkout::checkout_hard_to_tree_from_head_tree;
16use crate::porcelain::merge::apply_conflict_merge_worktree;
17use crate::porcelain::reset::{reset_to_commit, ResetMode, ResetOptions};
18use crate::porcelain::worktree_guard::ensure_safe_for_rebase_stop;
19use crate::progress::{
20 poll_cancel, Cancel, NeverCancel, NullProgress, ProgressSink, UncancellableProgress,
21};
22use crate::refs;
23use crate::repo::Repository;
24use crate::state::{resolve_head_for_repository, HeadState};
25use crate::write_tree::{write_tree_update_index, WriteTreeFlags};
26
27use super::apply::{apply_rewrite, log_head_move};
28use super::error::HistoryError;
29use super::plan::{plan_rewrite_with_order_resumable_seeded, ResumablePlanOutcome};
30use super::rebase::{plan_rebase_resumable, RebaseOntoOptions, RebasePlan, RebaseRequest};
31use super::types::{
32 AppliedRewrite, ApplyOptions, CommitEdit, HistoryEdits, PlannedRefUpdate, RewriteOptions,
33 RewritePlan, RewrittenCommit,
34};
35use crate::operation::Conflict;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct StartRebaseOptions {
40 pub request: RebaseRequest,
42 pub rebase: RebaseOntoOptions,
44 pub apply: ApplyOptions,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
51pub struct RebaseStop {
52 pub commit: ObjectId,
54 pub conflicts: Vec<Conflict>,
56 pub done: Vec<ObjectId>,
58 pub remaining: Vec<ObjectId>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
65#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
66pub enum RebaseOutcome {
67 Done {
69 applied: AppliedRewrite,
71 plan: Box<RewritePlan>,
73 head_change: Option<(ObjectId, ObjectId)>,
80 },
81 ExecDone {
83 applied: AppliedRewrite,
85 plan: Box<RewritePlan>,
87 exec: super::ExecReport,
89 },
90 Stopped(RebaseStop),
92}
93
94#[derive(Debug, thiserror::Error)]
96#[non_exhaustive]
97pub enum RebaseSessionError {
98 #[error("no rebase in progress")]
100 NoRebaseInProgress,
101
102 #[error("rebase is already in progress")]
104 RebaseAlreadyInProgress,
105
106 #[error("a rebase started by git is in progress")]
112 ForeignRebaseInProgress,
113
114 #[error("cannot continue: unmerged paths remain in the index")]
116 UnmergedPaths,
117
118 #[error("nothing to commit after conflict resolution")]
120 NothingToCommit,
121
122 #[error("rebase session requires HEAD on a branch")]
124 NotOnBranch,
125
126 #[error("exec replay HEAD changed; expected {expected}; save new commits on a branch, then abort and restart the rebase")]
129 ExecHeadChanged {
130 expected: ObjectId,
132 },
133
134 #[error(transparent)]
136 History(#[from] HistoryError),
137
138 #[error(transparent)]
140 Library(#[from] crate::error::Error),
141}
142
143impl RebaseSessionError {
144 #[must_use]
146 pub fn is_cancelled(&self) -> bool {
147 match self {
148 Self::Library(e) => e.is_cancelled(),
149 Self::History(e) => e.is_cancelled(),
150 Self::NoRebaseInProgress
151 | Self::RebaseAlreadyInProgress
152 | Self::ForeignRebaseInProgress
153 | Self::UnmergedPaths
154 | Self::NothingToCommit
155 | Self::NotOnBranch
156 | Self::ExecHeadChanged { .. } => false,
157 }
158 }
159}
160
161pub fn start_rebase(
185 repo: &Repository,
186 opts: &StartRebaseOptions,
187 progress: &mut dyn ProgressSink,
188 cancel: &dyn Cancel,
189) -> Result<RebaseOutcome, RebaseSessionError> {
190 super::ensure_rewrite_apply_ready(repo)?;
191 if active_rebase(repo.git_dir()).is_some() {
192 return Err(RebaseSessionError::RebaseAlreadyInProgress);
193 }
194 let (head_ref, orig_head) = branch_head(repo)?;
195 let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase)?;
196 poll_cancel(progress, cancel).map_err(map_lib)?;
197 match plan {
198 Err(RebasePlan::UpToDate) => Ok(RebaseOutcome::Done {
199 head_change: None,
200 applied: AppliedRewrite {
201 ref_updates: Vec::new(),
202 head: orig_head,
203 hooks_run: Vec::new(),
204 },
205 plan: Box::new(RewritePlan {
206 commits: Vec::new(),
207 ref_updates: Vec::new(),
208 head: super::types::HeadEffect::default(),
209 }),
210 }),
211 Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
212 message: "unexpected non-resumable rebase plan".into(),
213 }
214 .into()),
215 Ok(ResumablePlanOutcome::Complete(plan)) => {
216 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
217 Ok(RebaseOutcome::Done {
218 head_change: (orig_head != applied.head).then_some((orig_head, applied.head)),
219 applied,
220 plan,
221 })
222 }
223 Ok(ResumablePlanOutcome::Partial(partial)) => {
224 let orig_tree = commit_tree(repo, orig_head)?;
225 let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
226 ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
227 let stop = install_stopped_rebase(
228 repo,
229 &head_ref,
230 orig_head,
231 &opts.request,
232 &opts.rebase,
233 &opts.apply,
234 *partial,
235 progress,
236 cancel,
237 )?;
238 if opts.apply.hooks.enabled() {
239 let onto = opts.request.onto.unwrap_or(opts.request.upstream);
240 run_post_checkout(repo, Some(orig_head), onto, true);
241 }
242 Ok(RebaseOutcome::Stopped(stop))
243 }
244 }
245}
246
247pub fn continue_rebase(
262 repo: &Repository,
263 opts: &StartRebaseOptions,
264 progress: &mut dyn ProgressSink,
265 cancel: &dyn Cancel,
266) -> Result<RebaseOutcome, RebaseSessionError> {
267 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
268 return super::exec::resume_exec_rebase(repo, &opts.apply, false);
269 }
270 super::ensure_rewrite_apply_ready(repo)?;
271 let state = load_persisted_state(repo.git_dir())?;
272 if repo.load_index()?.has_unmerged_entries() {
273 return Err(RebaseSessionError::UnmergedPaths);
274 }
275 let current = state
276 .remaining
277 .first()
278 .copied()
279 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
280 let new_tree = {
281 let mut index = repo.load_index()?;
282 write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
283 .map_err(map_lib)?
284 };
285 let head_oid = resolve_head_oid(repo)?;
286 let head_tree = commit_tree(repo, head_oid)?;
287 if new_tree == head_tree {
288 return Err(RebaseSessionError::NothingToCommit);
289 }
290
291 let mut edits = state.edits.clone();
292 edits.insert(
293 current,
294 CommitEdit::Rewrite {
295 tree: Some(new_tree),
296 message: None,
297 author: None,
298 parents: Some(vec![head_oid]),
299 },
300 );
301
302 let seed = (state.mapping.clone(), state.rewritten.clone());
303 let rewrite = replan_rewrite_options(&state);
304 let outcome = plan_rewrite_with_order_resumable_seeded(
305 repo,
306 &edits,
307 &rewrite,
308 Some(&state.replay_order),
309 state.flatten_leaf_merges.then_some(state.onto),
310 Some(seed),
311 Some(state.scoped_refs.clone()),
312 )?;
313 advance_after_plan(
314 repo,
315 &state,
316 &opts.apply,
317 outcome,
318 head_oid,
319 progress,
320 cancel,
321 )
322}
323
324pub fn skip_rebase(
335 repo: &Repository,
336 opts: &StartRebaseOptions,
337 progress: &mut dyn ProgressSink,
338 cancel: &dyn Cancel,
339) -> Result<RebaseOutcome, RebaseSessionError> {
340 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
341 return super::exec::resume_exec_rebase(repo, &opts.apply, true);
342 }
343 let state = load_persisted_state(repo.git_dir())?;
344 let current = state
345 .remaining
346 .first()
347 .copied()
348 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
349 let head_oid = resolve_head_oid(repo)?;
350 reset_index_worktree_to_head(repo, head_oid, progress, cancel)?;
351
352 let replayed_parent = head_oid;
353 let mut mapping = state.mapping.clone();
354 mapping.insert(current, replayed_parent);
355 let mut rewritten = state.rewritten.clone();
356 rewritten.push(RewrittenCommit {
357 old: current,
358 new: None,
359 change_id: None,
360 subject: None,
361 reused: false,
362 });
363
364 let rewrite = replan_rewrite_options(&state);
365 let outcome = plan_rewrite_with_order_resumable_seeded(
366 repo,
367 &state.edits,
368 &rewrite,
369 Some(&state.replay_order),
370 state.flatten_leaf_merges.then_some(state.onto),
371 Some((mapping, rewritten)),
372 Some(state.scoped_refs.clone()),
373 )?;
374 advance_after_plan(
375 repo,
376 &state,
377 &opts.apply,
378 outcome,
379 head_oid,
380 progress,
381 cancel,
382 )
383}
384
385pub fn abort_rebase(
399 repo: &Repository,
400 identity: &crate::ident::Signature,
401) -> Result<(), RebaseSessionError> {
402 identity
403 .validate()
404 .map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
405 let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
406 if had_journal {
407 super::abort_interrupted_rewrite_apply(repo)?;
408 }
409 match active_rebase(repo.git_dir()) {
410 None => {
411 if had_journal {
412 Ok(())
413 } else {
414 Err(RebaseSessionError::NoRebaseInProgress)
415 }
416 }
417 Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
418 Some(ActiveRebase::Exec) => super::exec::abort_exec_rebase(repo),
419 Some(ActiveRebase::Grit) => {
420 let state = load_persisted_state(repo.git_dir())?;
421 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
422 hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?;
423 clear_rebase_state(repo.git_dir())?;
424 Ok(())
425 }
426 }
427}
428
429fn abort_foreign_rebase(
430 repo: &Repository,
431 state_dir: &Path,
432 identity: &crate::ident::Signature,
433) -> Result<(), RebaseSessionError> {
434 if state_dir.join("autostash").exists() {
435 return Err(RebaseSessionError::ForeignRebaseInProgress);
436 }
437 let orig_head = first_line(&state_dir.join("orig-head"))
438 .and_then(|hex| ObjectId::from_hex(&hex).ok())
439 .ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
440 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
441 match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
442 Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
443 None => hard_reset_head(repo, orig_head, identity)?,
444 }
445 for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
446 remove_if_exists(&repo.git_dir().join(name))?;
447 }
448 fs::remove_dir_all(state_dir).map_err(map_lib)?;
449 Ok(())
450}
451
452enum ActiveRebase {
454 Exec,
456 Grit,
458 Foreign(std::path::PathBuf),
460}
461
462fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
463 let merge = git_dir.join("rebase-merge");
464 if merge.join("grit-exec").is_file() {
465 return Some(ActiveRebase::Exec);
466 }
467 if merge.join("grit-plan").is_file() {
468 return Some(ActiveRebase::Grit);
469 }
470 if merge.is_dir() {
471 return Some(ActiveRebase::Foreign(merge));
472 }
473 let apply = git_dir.join("rebase-apply");
474 (apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
475}
476
477fn first_line(path: &Path) -> Option<String> {
478 let raw = fs::read_to_string(path).ok()?;
479 let line = raw.lines().next()?.trim();
480 (!line.is_empty()).then(|| line.to_owned())
481}
482
483fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
484 match fs::remove_file(path) {
485 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
486 _ => Ok(()),
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
493struct PersistedRebaseState {
494 mapping: HashMap<String, String>,
495 rewritten: Vec<PersistedRewrittenRow>,
496 edits: Vec<PersistedEdit>,
497 rewrite: RewriteOptions,
498 rebase: RebaseOntoOptions,
499 apply: ApplyOptions,
500 replay_order: Vec<String>,
501 remaining: Vec<String>,
502 done: Vec<String>,
503 fork: String,
504 onto: String,
505 upstream: String,
506 head_ref: String,
507 orig_head: String,
508 branch_tip: String,
509 flatten_leaf_merges: bool,
510 scoped_refs: Vec<(String, String)>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
514struct PersistedRewrittenRow {
515 old: String,
516 new: Option<String>,
517 #[serde(default)]
518 reused: bool,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
522enum PersistedEdit {
523 Reparent {
524 commit: String,
525 parent: String,
526 },
527 Reuse {
528 commit: String,
529 existing: String,
530 #[serde(default)]
532 parent: Option<String>,
533 },
534 Drop {
535 commit: String,
536 },
537 Rewrite {
538 commit: String,
539 tree: Option<String>,
540 message: Option<String>,
541 author: Option<String>,
542 },
543}
544
545fn oid_hex(oid: &ObjectId) -> String {
546 oid.to_hex()
547}
548
549fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
550 ObjectId::from_hex(hex).map_err(|e| {
551 HistoryError::Planning {
552 message: e.to_string(),
553 }
554 .into()
555 })
556}
557
558fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
559 edits
560 .iter()
561 .map(|(commit, edit)| match edit {
562 CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
563 commit: oid_hex(commit),
564 parent: oid_hex(parent),
565 },
566 CommitEdit::Reuse { existing, parent } => PersistedEdit::Reuse {
567 commit: oid_hex(commit),
568 existing: oid_hex(existing),
569 parent: Some(oid_hex(parent)),
570 },
571 CommitEdit::Drop => PersistedEdit::Drop {
572 commit: oid_hex(commit),
573 },
574 CommitEdit::Rewrite {
575 tree,
576 message,
577 author,
578 parents: _,
579 } => PersistedEdit::Rewrite {
580 commit: oid_hex(commit),
581 tree: tree.as_ref().map(oid_hex),
582 message: message.clone(),
583 author: author.clone(),
584 },
585 CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
586 commit: oid_hex(commit),
587 },
588 })
589 .collect()
590}
591
592fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
593 let mut edits = HistoryEdits::new();
594 for row in rows {
595 match row {
596 PersistedEdit::Reparent { commit, parent } => {
597 edits.insert(
598 parse_oid(commit)?,
599 CommitEdit::Reparent {
600 parent: parse_oid(parent)?,
601 },
602 );
603 }
604 PersistedEdit::Reuse {
605 commit,
606 existing,
607 parent,
608 } => {
609 let existing = parse_oid(existing)?;
610 let parent = parent.as_deref().map(parse_oid).transpose()?;
611 edits.insert(
612 parse_oid(commit)?,
613 CommitEdit::Reuse {
614 existing,
615 parent: parent.unwrap_or(existing),
616 },
617 );
618 }
619 PersistedEdit::Drop { commit } => {
620 edits.insert(parse_oid(commit)?, CommitEdit::Drop);
621 }
622 PersistedEdit::Rewrite {
623 commit,
624 tree,
625 message,
626 author,
627 } => {
628 edits.insert(
629 parse_oid(commit)?,
630 CommitEdit::Rewrite {
631 tree: tree.as_deref().map(parse_oid).transpose()?,
632 message: message.clone(),
633 author: author.clone(),
634 parents: None,
635 },
636 );
637 }
638 }
639 }
640 Ok(edits)
641}
642
643fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
644 map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
645}
646
647fn mapping_from_persist(
648 map: &HashMap<String, String>,
649) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
650 map.iter()
651 .map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
652 .collect()
653}
654
655#[allow(clippy::too_many_arguments)]
656fn state_from_partial(
657 partial: &super::plan::PartialRewritePlan,
658 head_ref: &str,
659 orig_head: ObjectId,
660 request: &RebaseRequest,
661 rebase: &RebaseOntoOptions,
662 apply: &ApplyOptions,
663 effective_onto: ObjectId,
664 fork: ObjectId,
665) -> PersistedRebaseState {
666 let done: Vec<ObjectId> = partial
667 .commits_to_replay
668 .iter()
669 .copied()
670 .filter(|c| partial.mapping.contains_key(c))
671 .collect();
672 let remaining: Vec<ObjectId> = partial
673 .commits_to_replay
674 .iter()
675 .copied()
676 .filter(|c| !partial.mapping.contains_key(c))
677 .collect();
678 PersistedRebaseState {
679 mapping: mapping_to_persist(&partial.mapping),
680 rewritten: partial
681 .rewritten
682 .iter()
683 .map(|r| PersistedRewrittenRow {
684 old: oid_hex(&r.old),
685 new: r.new.as_ref().map(oid_hex),
686 reused: r.reused,
687 })
688 .collect(),
689 edits: edits_to_persist(&partial.edits),
690 rewrite: partial.opts.clone(),
691 rebase: rebase.clone(),
692 apply: apply.clone(),
693 replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
694 remaining: remaining.iter().map(oid_hex).collect(),
695 done: done.iter().map(oid_hex).collect(),
696 fork: oid_hex(&fork),
697 onto: oid_hex(&effective_onto),
698 upstream: oid_hex(&request.upstream),
699 head_ref: head_ref.to_owned(),
700 orig_head: oid_hex(&orig_head),
701 branch_tip: oid_hex(&request.branch_tip),
702 flatten_leaf_merges: partial.linearize_onto.is_some(),
703 scoped_refs: partial
704 .scoped_refs
705 .iter()
706 .map(|(n, o)| (n.clone(), oid_hex(o)))
707 .collect(),
708 }
709}
710
711struct LoadedState {
712 mapping: HashMap<ObjectId, ObjectId>,
713 rewritten: Vec<RewrittenCommit>,
714 edits: HistoryEdits,
715 rewrite: RewriteOptions,
716 rebase: RebaseOntoOptions,
717 replay_order: Vec<ObjectId>,
718 remaining: Vec<ObjectId>,
719 head_ref: String,
720 orig_head: ObjectId,
721 onto: ObjectId,
722 upstream: ObjectId,
723 branch_tip: ObjectId,
724 flatten_leaf_merges: bool,
725 scoped_refs: Vec<(String, ObjectId)>,
726}
727
728fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
729 match active_rebase(git_dir) {
730 None => return Err(RebaseSessionError::NoRebaseInProgress),
731 Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
732 Some(ActiveRebase::Exec) => return Err(RebaseSessionError::NoRebaseInProgress),
733 Some(ActiveRebase::Grit) => {}
734 }
735 let path = git_dir.join("rebase-merge").join("grit-plan");
736 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
737 let parsed: PersistedRebaseState =
738 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
739 Ok(LoadedState {
740 mapping: mapping_from_persist(&parsed.mapping)?,
741 rewritten: parsed
742 .rewritten
743 .iter()
744 .map(|r| {
745 Ok(RewrittenCommit {
746 old: parse_oid(&r.old)?,
747 new: r.new.as_deref().map(parse_oid).transpose()?,
748 change_id: None,
749 subject: None,
750 reused: r.reused,
751 })
752 })
753 .collect::<Result<_, RebaseSessionError>>()?,
754 edits: edits_from_persist(&parsed.edits)?,
755 rewrite: parsed.rewrite,
756 rebase: parsed.rebase,
757 replay_order: parsed
758 .replay_order
759 .iter()
760 .map(|h| parse_oid(h))
761 .collect::<Result<_, _>>()?,
762 remaining: parsed
763 .remaining
764 .iter()
765 .map(|h| parse_oid(h))
766 .collect::<Result<_, _>>()?,
767 head_ref: parsed.head_ref,
768 orig_head: parse_oid(&parsed.orig_head)?,
769 onto: parse_oid(&parsed.onto)?,
770 upstream: parse_oid(&parsed.upstream)?,
771 branch_tip: parse_oid(&parsed.branch_tip)?,
772 flatten_leaf_merges: parsed.flatten_leaf_merges,
773 scoped_refs: parsed
774 .scoped_refs
775 .iter()
776 .map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
777 .collect::<Result<_, _>>()?,
778 })
779}
780
781fn write_persisted_state(
782 git_dir: &Path,
783 state: &PersistedRebaseState,
784) -> Result<(), RebaseSessionError> {
785 let dir = git_dir.join("rebase-merge");
786 fs::create_dir_all(&dir).map_err(map_lib)?;
787 let lock_path = dir.join("grit-plan.lock");
788 let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
789 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
790 let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
791 lock.write_all(&body).map_err(map_lib)?;
792 lock.commit_to(&dir.join("grit-plan"))
793 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
794 Ok(())
795}
796
797#[allow(clippy::too_many_arguments)]
798fn install_stopped_rebase(
799 repo: &Repository,
800 head_ref: &str,
801 orig_head: ObjectId,
802 request: &RebaseRequest,
803 rebase_opts: &RebaseOntoOptions,
804 apply: &ApplyOptions,
805 partial: super::plan::PartialRewritePlan,
806 progress: &mut dyn ProgressSink,
807 cancel: &dyn Cancel,
808) -> Result<RebaseStop, RebaseSessionError> {
809 poll_cancel(progress, cancel).map_err(map_lib)?;
810 let effective_onto = request.onto.unwrap_or(request.upstream);
811 let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
812 let persisted = state_from_partial(
813 &partial,
814 head_ref,
815 orig_head,
816 request,
817 rebase_opts,
818 apply,
819 effective_onto,
820 fork,
821 );
822 let conflict = partial.conflict;
823 let done = persisted
824 .done
825 .iter()
826 .map(|h| parse_oid(h))
827 .collect::<Result<Vec<_>, _>>()?;
828 let remaining = persisted
829 .remaining
830 .iter()
831 .map(|h| parse_oid(h))
832 .collect::<Result<Vec<_>, _>>()?;
833 write_git_rebase_merge_files(
834 repo,
835 head_ref,
836 orig_head,
837 effective_onto,
838 &done,
839 &remaining,
840 &conflict.commit,
841 )?;
842 write_persisted_state(repo.git_dir(), &persisted)?;
843
844 refs::update_direct_ref_on_store(
845 repo.refs(),
846 repo.git_dir(),
847 "HEAD",
848 &conflict.replayed_parent,
849 )
850 .map_err(map_lib)?;
851 let mut progress = UncancellableProgress::new(progress);
854 reset_index_worktree_to_head(repo, conflict.replayed_parent, &mut progress, &NeverCancel)?;
855 let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
856 apply_conflict_merge_worktree(
857 repo,
858 &ours_tree,
859 &conflict.merge,
860 &mut progress,
861 &NeverCancel,
862 )
863 .map_err(map_lib)?;
864
865 let message = load_commit(repo, conflict.commit)?.message;
866 fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
867
868 Ok(RebaseStop {
869 commit: conflict.commit,
870 conflicts: conflicts_from_tree_merge(&conflict.merge),
871 done,
872 remaining,
873 })
874}
875
876#[allow(clippy::too_many_arguments)]
877fn advance_after_plan(
878 repo: &Repository,
879 prev: &LoadedState,
880 apply: &ApplyOptions,
881 outcome: ResumablePlanOutcome,
882 head_before: ObjectId,
883 progress: &mut dyn ProgressSink,
884 cancel: &dyn Cancel,
885) -> Result<RebaseOutcome, RebaseSessionError> {
886 poll_cancel(progress, cancel).map_err(map_lib)?;
887 match outcome {
888 ResumablePlanOutcome::Complete(mut plan) => {
889 if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
890 if !plan.ref_updates.iter().any(|u| u.name == prev.head_ref) {
891 plan.ref_updates.push(PlannedRefUpdate {
892 name: prev.head_ref.clone(),
893 old: prev.orig_head,
894 new: new_tip,
895 });
896 }
897 }
898 let apply_without_hooks = ApplyOptions {
899 hooks: PostOperationHooks::Skip,
900 ..apply.clone()
901 };
902 let mut applied = apply_rewrite(repo, &plan, &apply_without_hooks, progress, cancel)?;
903 if let Some(branch) = plan
907 .ref_updates
908 .iter()
909 .find(|u| u.name == prev.head_ref && u.old != u.new)
910 {
911 log_head_move(repo, branch, apply)?;
912 }
913 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &prev.head_ref)
914 .map_err(map_lib)?;
915 let branch_tip = repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?;
916 reset_index_worktree_to_head(repo, branch_tip, &mut NullProgress, &NeverCancel)?;
918 applied.head = branch_tip;
919 clear_rebase_state(repo.git_dir())?;
920 if apply.hooks.enabled()
921 && run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
922 {
923 applied.hooks_run.push(HookName::PostRewrite);
924 }
925 Ok(RebaseOutcome::Done {
926 head_change: (head_before != applied.head).then_some((head_before, applied.head)),
927 applied,
928 plan,
929 })
930 }
931 ResumablePlanOutcome::Partial(partial) => {
932 let request = RebaseRequest {
933 upstream: prev.upstream,
934 onto: Some(prev.onto),
935 branch_tip: prev.branch_tip,
936 };
937 let stop = install_stopped_rebase(
938 repo,
939 &prev.head_ref,
940 prev.orig_head,
941 &request,
942 &prev.rebase,
943 apply,
944 *partial,
945 progress,
946 cancel,
947 )?;
948 Ok(RebaseOutcome::Stopped(stop))
949 }
950 }
951}
952
953fn write_git_rebase_merge_files(
954 repo: &Repository,
955 head_ref: &str,
956 orig_head: ObjectId,
957 onto: ObjectId,
958 done: &[ObjectId],
959 remaining: &[ObjectId],
960 current: &ObjectId,
961) -> Result<(), RebaseSessionError> {
962 let dir = repo.git_dir().join("rebase-merge");
963 fs::create_dir_all(&dir).map_err(map_lib)?;
964 fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
965 fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
966 fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
967 let todo_remaining: Vec<ObjectId> = if remaining.first() == Some(current) {
968 remaining.iter().skip(1).copied().collect()
969 } else {
970 remaining.to_vec()
971 };
972 fs::write(
973 dir.join("git-rebase-todo"),
974 todo_lines(repo, &todo_remaining)?,
975 )
976 .map_err(map_lib)?;
977 let done_with_current: Vec<ObjectId> = done.iter().chain([current]).copied().collect();
980 fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
981 let msgnum = done.len() + 1;
982 let end = done.len() + remaining.len();
983 fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
984 fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
985 fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
986 let commit = load_commit(repo, *current)?;
987 match author_script(&commit.author) {
988 Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
989 None => remove_if_exists(&dir.join("author-script"))?,
990 }
991 fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
992 fs::write(
993 repo.git_dir().join("ORIG_HEAD"),
994 format!("{}\n", orig_head.to_hex()),
995 )
996 .map_err(map_lib)?;
997 Ok(())
998}
999
1000fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
1001 let mut out = String::new();
1002 for &oid in commits {
1003 let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
1004 let abbrev = &oid.to_hex()[..7];
1005 out.push_str(&format!("pick {abbrev} {subject}\n"));
1006 }
1007 Ok(out)
1008}
1009
1010fn author_script(author: &str) -> Option<String> {
1014 let lt = author.find('<')?;
1015 let gt = lt + author[lt..].find('>')?;
1016 let name = author[..lt].trim_end();
1017 let email = &author[lt + 1..gt];
1018 let date = format!("@{}", author[gt + 1..].trim());
1019 let quote = crate::transport::sq_quote_shell_arg;
1020 Some(format!(
1021 "GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
1022 quote(name),
1023 quote(email),
1024 quote(&date)
1025 ))
1026}
1027
1028pub(super) fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
1029 for name in ["ORIG_HEAD", "MERGE_MSG"] {
1030 let path = git_dir.join(name);
1031 if path.exists() {
1032 fs::remove_file(&path).map_err(map_lib)?;
1033 }
1034 }
1035 let dir = git_dir.join("rebase-merge");
1036 if dir.is_dir() {
1037 fs::remove_dir_all(&dir).map_err(map_lib)?;
1038 }
1039 Ok(())
1040}
1041
1042fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
1043 match resolve_head_for_repository(repo).map_err(map_lib)? {
1044 HeadState::Branch {
1045 refname,
1046 oid: Some(oid),
1047 ..
1048 } => Ok((refname, oid)),
1049 HeadState::Branch { oid: None, .. } | HeadState::Detached { .. } | HeadState::Invalid => {
1050 Err(RebaseSessionError::NotOnBranch)
1051 }
1052 }
1053}
1054
1055fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
1056 resolve_head_for_repository(repo)
1057 .map_err(map_lib)?
1058 .oid()
1059 .copied()
1060 .ok_or(RebaseSessionError::NotOnBranch)
1061}
1062
1063fn commit_tree(repo: &Repository, commit: ObjectId) -> Result<ObjectId, RebaseSessionError> {
1064 repo.odb().commit_tree(&commit).map_err(map_lib)
1065}
1066
1067fn load_commit(
1068 repo: &Repository,
1069 oid: ObjectId,
1070) -> Result<crate::objects::CommitData, RebaseSessionError> {
1071 repo.odb().read_commit(&oid).map_err(map_lib)
1072}
1073
1074fn reset_index_worktree_to_head(
1075 repo: &Repository,
1076 head_oid: ObjectId,
1077 progress: &mut dyn ProgressSink,
1078 cancel: &dyn Cancel,
1079) -> Result<(), RebaseSessionError> {
1080 let head_tree = commit_tree(repo, head_oid)?;
1081 checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
1084 .map_err(map_lib)?;
1085 Ok(())
1086}
1087
1088fn hard_reset_branch(
1089 repo: &Repository,
1090 refname: &str,
1091 target: ObjectId,
1092 identity: &crate::ident::Signature,
1093) -> Result<(), RebaseSessionError> {
1094 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
1095 .map_err(map_lib)?;
1096 hard_reset_head(repo, target, identity)
1097}
1098
1099fn hard_reset_head(
1100 repo: &Repository,
1101 target: ObjectId,
1102 identity: &crate::ident::Signature,
1103) -> Result<(), RebaseSessionError> {
1104 reset_to_commit(
1105 repo,
1106 &ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
1107 &mut crate::progress::NullProgress,
1108 &crate::progress::NeverCancel,
1109 )
1110 .map_err(map_lib)?;
1111 Ok(())
1112}
1113
1114use std::io::Write;
1115
1116fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
1117 RebaseSessionError::Library(err.into())
1118}
1119
1120fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
1121 state.rewrite.clone()
1122}
1123
1124fn map_json(err: serde_json::Error) -> RebaseSessionError {
1125 RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::{author_script, edits_from_persist, edits_to_persist, PersistedEdit};
1131 use crate::history::types::{CommitEdit, HistoryEdits};
1132 use crate::objects::ObjectId;
1133
1134 fn oid(byte: u8) -> ObjectId {
1135 ObjectId::from_bytes(&[byte; 20]).unwrap_or_else(|_| ObjectId::zero())
1136 }
1137
1138 #[test]
1139 fn reuse_edit_round_trips_its_continuation_parent() {
1140 let mut edits = HistoryEdits::new();
1141 edits.insert(
1142 oid(1),
1143 CommitEdit::Reuse {
1144 existing: oid(2),
1145 parent: oid(3),
1146 },
1147 );
1148 let rows = edits_to_persist(&edits);
1149 let json = serde_json::to_string(&rows).unwrap_or_default();
1150 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1151 assert_eq!(edits_from_persist(&parsed).ok(), Some(edits));
1152 }
1153
1154 #[test]
1155 fn reuse_edit_saved_without_parent_continues_from_the_reused_commit() {
1156 let json = format!(
1157 r#"[{{"Reuse":{{"commit":"{}","existing":"{}"}}}}]"#,
1158 oid(1).to_hex(),
1159 oid(2).to_hex()
1160 );
1161 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1162 let edits = edits_from_persist(&parsed).ok();
1163 assert_eq!(
1164 edits.and_then(|edits| edits.get(&oid(1)).cloned()),
1165 Some(CommitEdit::Reuse {
1166 existing: oid(2),
1167 parent: oid(2),
1168 })
1169 );
1170 }
1171
1172 #[test]
1173 fn author_script_quotes_values_and_prefixes_date() {
1174 assert_eq!(
1175 author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
1176 Some(
1177 "GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
1178 )
1179 );
1180 assert_eq!(
1181 author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
1182 Some(
1183 "GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
1184 )
1185 );
1186 assert_eq!(author_script("no email here 1 +0000"), None);
1187 }
1188}