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::refs;
20use crate::repo::Repository;
21use crate::state::{resolve_head_for_repository, HeadState};
22use crate::write_tree::{write_tree_update_index, WriteTreeFlags};
23
24use super::apply::apply_rewrite;
25use super::error::HistoryError;
26use super::plan::{plan_rewrite_with_order_resumable_seeded, ResumablePlanOutcome};
27use super::rebase::{plan_rebase_resumable, RebaseOntoOptions, RebasePlan, RebaseRequest};
28use super::types::{
29 AppliedRewrite, ApplyOptions, CommitEdit, HistoryEdits, PlannedRefUpdate, RewriteOptions,
30 RewritePlan, RewrittenCommit,
31};
32use crate::operation::Conflict;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct StartRebaseOptions {
37 pub request: RebaseRequest,
39 pub rebase: RebaseOntoOptions,
41 pub apply: ApplyOptions,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct RebaseStop {
48 pub commit: ObjectId,
50 pub conflicts: Vec<Conflict>,
52 pub done: Vec<ObjectId>,
54 pub remaining: Vec<ObjectId>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum RebaseOutcome {
61 Done {
63 applied: AppliedRewrite,
65 plan: RewritePlan,
67 },
68 Stopped(RebaseStop),
70}
71
72#[derive(Debug, thiserror::Error)]
74#[non_exhaustive]
75pub enum RebaseSessionError {
76 #[error("no rebase in progress")]
78 NoRebaseInProgress,
79
80 #[error("rebase is already in progress")]
82 RebaseAlreadyInProgress,
83
84 #[error("a rebase started by git is in progress")]
90 ForeignRebaseInProgress,
91
92 #[error("cannot continue: unmerged paths remain in the index")]
94 UnmergedPaths,
95
96 #[error("nothing to commit after conflict resolution")]
98 NothingToCommit,
99
100 #[error("rebase session requires HEAD on a branch")]
102 NotOnBranch,
103
104 #[error(transparent)]
106 History(#[from] HistoryError),
107
108 #[error(transparent)]
110 Library(#[from] crate::error::Error),
111}
112
113impl RebaseSessionError {
114 #[must_use]
116 pub fn is_cancelled(&self) -> bool {
117 match self {
118 Self::Library(e) => e.is_cancelled(),
119 Self::History(e) => e.is_cancelled(),
120 Self::NoRebaseInProgress
121 | Self::RebaseAlreadyInProgress
122 | Self::ForeignRebaseInProgress
123 | Self::UnmergedPaths
124 | Self::NothingToCommit
125 | Self::NotOnBranch => false,
126 }
127 }
128}
129
130pub fn start_rebase(
146 repo: &Repository,
147 opts: &StartRebaseOptions,
148) -> Result<RebaseOutcome, RebaseSessionError> {
149 super::ensure_rewrite_apply_ready(repo)?;
150 if active_rebase(repo.git_dir()).is_some() {
151 return Err(RebaseSessionError::RebaseAlreadyInProgress);
152 }
153 let (head_ref, orig_head) = branch_head(repo)?;
154 match plan_rebase_resumable(repo, &opts.request, &opts.rebase)? {
155 Err(RebasePlan::UpToDate) => Ok(RebaseOutcome::Done {
156 applied: AppliedRewrite {
157 ref_updates: Vec::new(),
158 head: orig_head,
159 hooks_run: Vec::new(),
160 },
161 plan: RewritePlan {
162 commits: Vec::new(),
163 ref_updates: Vec::new(),
164 head: super::types::HeadEffect::default(),
165 },
166 }),
167 Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
168 message: "unexpected non-resumable rebase plan".into(),
169 }
170 .into()),
171 Ok(ResumablePlanOutcome::Complete(plan)) => {
172 let applied = apply_rewrite(
173 repo,
174 &plan,
175 &opts.apply,
176 &mut crate::progress::NullProgress,
177 &crate::progress::NeverCancel,
178 )?;
179 Ok(RebaseOutcome::Done {
180 applied,
181 plan: *plan,
182 })
183 }
184 Ok(ResumablePlanOutcome::Partial(partial)) => {
185 let orig_tree = commit_tree(repo, orig_head)?;
186 let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
187 ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
188 let stop = install_stopped_rebase(
189 repo,
190 &head_ref,
191 orig_head,
192 &opts.request,
193 &opts.rebase,
194 &opts.apply,
195 *partial,
196 )?;
197 if opts.apply.hooks.enabled() {
198 let onto = opts.request.onto.unwrap_or(opts.request.upstream);
199 run_post_checkout(repo, Some(orig_head), onto, true);
200 }
201 Ok(RebaseOutcome::Stopped(stop))
202 }
203 }
204}
205
206pub fn continue_rebase(
214 repo: &Repository,
215 opts: &StartRebaseOptions,
216) -> Result<RebaseOutcome, RebaseSessionError> {
217 super::ensure_rewrite_apply_ready(repo)?;
218 let state = load_persisted_state(repo.git_dir())?;
219 if repo.load_index()?.has_unmerged_entries() {
220 return Err(RebaseSessionError::UnmergedPaths);
221 }
222 let current = state
223 .remaining
224 .first()
225 .copied()
226 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
227 let new_tree = {
228 let mut index = repo.load_index()?;
229 write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
230 .map_err(map_lib)?
231 };
232 let head_oid = resolve_head_oid(repo)?;
233 let head_tree = commit_tree(repo, head_oid)?;
234 if new_tree == head_tree {
235 return Err(RebaseSessionError::NothingToCommit);
236 }
237
238 let mut edits = state.edits.clone();
239 edits.insert(
240 current,
241 CommitEdit::Rewrite {
242 tree: Some(new_tree),
243 message: None,
244 author: None,
245 parents: Some(vec![head_oid]),
246 },
247 );
248
249 let seed = (state.mapping.clone(), state.rewritten.clone());
250 let rewrite = replan_rewrite_options(&state);
251 let outcome = plan_rewrite_with_order_resumable_seeded(
252 repo,
253 &edits,
254 &rewrite,
255 Some(&state.replay_order),
256 state.flatten_leaf_merges,
257 Some(seed),
258 Some(state.scoped_refs.clone()),
259 )?;
260 advance_after_plan(repo, &state, &opts.apply, outcome, edits)
261}
262
263pub fn skip_rebase(
271 repo: &Repository,
272 opts: &StartRebaseOptions,
273) -> Result<RebaseOutcome, RebaseSessionError> {
274 let state = load_persisted_state(repo.git_dir())?;
275 let current = state
276 .remaining
277 .first()
278 .copied()
279 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
280 let head_oid = resolve_head_oid(repo)?;
281 reset_index_worktree_to_head(repo, head_oid)?;
282
283 let replayed_parent = head_oid;
284 let mut mapping = state.mapping.clone();
285 mapping.insert(current, replayed_parent);
286 let mut rewritten = state.rewritten.clone();
287 rewritten.push(RewrittenCommit {
288 old: current,
289 new: None,
290 change_id: None,
291 subject: None,
292 reused: false,
293 });
294
295 let rewrite = replan_rewrite_options(&state);
296 let outcome = plan_rewrite_with_order_resumable_seeded(
297 repo,
298 &state.edits,
299 &rewrite,
300 Some(&state.replay_order),
301 state.flatten_leaf_merges,
302 Some((mapping, rewritten)),
303 Some(state.scoped_refs.clone()),
304 )?;
305 advance_after_plan(repo, &state, &opts.apply, outcome, state.edits.clone())
306}
307
308pub fn abort_rebase(
322 repo: &Repository,
323 identity: &crate::ident::Signature,
324) -> Result<(), RebaseSessionError> {
325 identity
326 .validate()
327 .map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
328 let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
329 if had_journal {
330 super::abort_interrupted_rewrite_apply(repo)?;
331 }
332 match active_rebase(repo.git_dir()) {
333 None => {
334 if had_journal {
335 Ok(())
336 } else {
337 Err(RebaseSessionError::NoRebaseInProgress)
338 }
339 }
340 Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
341 Some(ActiveRebase::Grit) => {
342 let state = load_persisted_state(repo.git_dir())?;
343 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
344 hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?;
345 clear_rebase_state(repo.git_dir())?;
346 Ok(())
347 }
348 }
349}
350
351fn abort_foreign_rebase(
352 repo: &Repository,
353 state_dir: &Path,
354 identity: &crate::ident::Signature,
355) -> Result<(), RebaseSessionError> {
356 if state_dir.join("autostash").exists() {
357 return Err(RebaseSessionError::ForeignRebaseInProgress);
358 }
359 let orig_head = first_line(&state_dir.join("orig-head"))
360 .and_then(|hex| ObjectId::from_hex(&hex).ok())
361 .ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
362 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
363 match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
364 Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
365 None => hard_reset_head(repo, orig_head, identity)?,
366 }
367 for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
368 remove_if_exists(&repo.git_dir().join(name))?;
369 }
370 fs::remove_dir_all(state_dir).map_err(map_lib)?;
371 Ok(())
372}
373
374enum ActiveRebase {
376 Grit,
378 Foreign(std::path::PathBuf),
380}
381
382fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
383 let merge = git_dir.join("rebase-merge");
384 if merge.join("grit-plan").is_file() {
385 return Some(ActiveRebase::Grit);
386 }
387 if merge.is_dir() {
388 return Some(ActiveRebase::Foreign(merge));
389 }
390 let apply = git_dir.join("rebase-apply");
391 (apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
392}
393
394fn first_line(path: &Path) -> Option<String> {
395 let raw = fs::read_to_string(path).ok()?;
396 let line = raw.lines().next()?.trim();
397 (!line.is_empty()).then(|| line.to_owned())
398}
399
400fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
401 match fs::remove_file(path) {
402 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
403 _ => Ok(()),
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
410struct PersistedRebaseState {
411 mapping: HashMap<String, String>,
412 rewritten: Vec<PersistedRewrittenRow>,
413 edits: Vec<PersistedEdit>,
414 rewrite: RewriteOptions,
415 rebase: RebaseOntoOptions,
416 apply: ApplyOptions,
417 replay_order: Vec<String>,
418 remaining: Vec<String>,
419 done: Vec<String>,
420 fork: String,
421 onto: String,
422 upstream: String,
423 head_ref: String,
424 orig_head: String,
425 branch_tip: String,
426 flatten_leaf_merges: bool,
427 scoped_refs: Vec<(String, String)>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
431struct PersistedRewrittenRow {
432 old: String,
433 new: Option<String>,
434 #[serde(default)]
435 reused: bool,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439enum PersistedEdit {
440 Reparent {
441 commit: String,
442 parent: String,
443 },
444 Reuse {
445 commit: String,
446 existing: String,
447 },
448 Drop {
449 commit: String,
450 },
451 Rewrite {
452 commit: String,
453 tree: Option<String>,
454 message: Option<String>,
455 author: Option<String>,
456 },
457}
458
459fn oid_hex(oid: &ObjectId) -> String {
460 oid.to_hex()
461}
462
463fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
464 ObjectId::from_hex(hex).map_err(|e| {
465 HistoryError::Planning {
466 message: e.to_string(),
467 }
468 .into()
469 })
470}
471
472fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
473 edits
474 .iter()
475 .map(|(commit, edit)| match edit {
476 CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
477 commit: oid_hex(commit),
478 parent: oid_hex(parent),
479 },
480 CommitEdit::Reuse(existing) => PersistedEdit::Reuse {
481 commit: oid_hex(commit),
482 existing: oid_hex(existing),
483 },
484 CommitEdit::Drop => PersistedEdit::Drop {
485 commit: oid_hex(commit),
486 },
487 CommitEdit::Rewrite {
488 tree,
489 message,
490 author,
491 parents: _,
492 } => PersistedEdit::Rewrite {
493 commit: oid_hex(commit),
494 tree: tree.as_ref().map(oid_hex),
495 message: message.clone(),
496 author: author.clone(),
497 },
498 CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
499 commit: oid_hex(commit),
500 },
501 })
502 .collect()
503}
504
505fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
506 let mut edits = HistoryEdits::new();
507 for row in rows {
508 match row {
509 PersistedEdit::Reparent { commit, parent } => {
510 edits.insert(
511 parse_oid(commit)?,
512 CommitEdit::Reparent {
513 parent: parse_oid(parent)?,
514 },
515 );
516 }
517 PersistedEdit::Reuse { commit, existing } => {
518 edits.insert(parse_oid(commit)?, CommitEdit::Reuse(parse_oid(existing)?));
519 }
520 PersistedEdit::Drop { commit } => {
521 edits.insert(parse_oid(commit)?, CommitEdit::Drop);
522 }
523 PersistedEdit::Rewrite {
524 commit,
525 tree,
526 message,
527 author,
528 } => {
529 edits.insert(
530 parse_oid(commit)?,
531 CommitEdit::Rewrite {
532 tree: tree.as_deref().map(parse_oid).transpose()?,
533 message: message.clone(),
534 author: author.clone(),
535 parents: None,
536 },
537 );
538 }
539 }
540 }
541 Ok(edits)
542}
543
544fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
545 map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
546}
547
548fn mapping_from_persist(
549 map: &HashMap<String, String>,
550) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
551 map.iter()
552 .map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
553 .collect()
554}
555
556#[allow(clippy::too_many_arguments)]
557fn state_from_partial(
558 partial: &super::plan::PartialRewritePlan,
559 head_ref: &str,
560 orig_head: ObjectId,
561 request: &RebaseRequest,
562 rebase: &RebaseOntoOptions,
563 apply: &ApplyOptions,
564 effective_onto: ObjectId,
565 fork: ObjectId,
566) -> PersistedRebaseState {
567 let done: Vec<ObjectId> = partial
568 .commits_to_replay
569 .iter()
570 .copied()
571 .filter(|c| partial.mapping.contains_key(c))
572 .collect();
573 let remaining: Vec<ObjectId> = partial
574 .commits_to_replay
575 .iter()
576 .copied()
577 .filter(|c| !partial.mapping.contains_key(c))
578 .collect();
579 PersistedRebaseState {
580 mapping: mapping_to_persist(&partial.mapping),
581 rewritten: partial
582 .rewritten
583 .iter()
584 .map(|r| PersistedRewrittenRow {
585 old: oid_hex(&r.old),
586 new: r.new.as_ref().map(oid_hex),
587 reused: r.reused,
588 })
589 .collect(),
590 edits: edits_to_persist(&partial.edits),
591 rewrite: partial.opts.clone(),
592 rebase: rebase.clone(),
593 apply: apply.clone(),
594 replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
595 remaining: remaining.iter().map(oid_hex).collect(),
596 done: done.iter().map(oid_hex).collect(),
597 fork: oid_hex(&fork),
598 onto: oid_hex(&effective_onto),
599 upstream: oid_hex(&request.upstream),
600 head_ref: head_ref.to_owned(),
601 orig_head: oid_hex(&orig_head),
602 branch_tip: oid_hex(&request.branch_tip),
603 flatten_leaf_merges: partial.flatten_leaf_merges,
604 scoped_refs: partial
605 .scoped_refs
606 .iter()
607 .map(|(n, o)| (n.clone(), oid_hex(o)))
608 .collect(),
609 }
610}
611
612struct LoadedState {
613 mapping: HashMap<ObjectId, ObjectId>,
614 rewritten: Vec<RewrittenCommit>,
615 edits: HistoryEdits,
616 rewrite: RewriteOptions,
617 rebase: RebaseOntoOptions,
618 replay_order: Vec<ObjectId>,
619 remaining: Vec<ObjectId>,
620 head_ref: String,
621 orig_head: ObjectId,
622 onto: ObjectId,
623 upstream: ObjectId,
624 branch_tip: ObjectId,
625 flatten_leaf_merges: bool,
626 scoped_refs: Vec<(String, ObjectId)>,
627}
628
629fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
630 match active_rebase(git_dir) {
631 None => return Err(RebaseSessionError::NoRebaseInProgress),
632 Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
633 Some(ActiveRebase::Grit) => {}
634 }
635 let path = git_dir.join("rebase-merge").join("grit-plan");
636 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
637 let parsed: PersistedRebaseState =
638 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
639 Ok(LoadedState {
640 mapping: mapping_from_persist(&parsed.mapping)?,
641 rewritten: parsed
642 .rewritten
643 .iter()
644 .map(|r| {
645 Ok(RewrittenCommit {
646 old: parse_oid(&r.old)?,
647 new: r.new.as_deref().map(parse_oid).transpose()?,
648 change_id: None,
649 subject: None,
650 reused: r.reused,
651 })
652 })
653 .collect::<Result<_, RebaseSessionError>>()?,
654 edits: edits_from_persist(&parsed.edits)?,
655 rewrite: parsed.rewrite,
656 rebase: parsed.rebase,
657 replay_order: parsed
658 .replay_order
659 .iter()
660 .map(|h| parse_oid(h))
661 .collect::<Result<_, _>>()?,
662 remaining: parsed
663 .remaining
664 .iter()
665 .map(|h| parse_oid(h))
666 .collect::<Result<_, _>>()?,
667 head_ref: parsed.head_ref,
668 orig_head: parse_oid(&parsed.orig_head)?,
669 onto: parse_oid(&parsed.onto)?,
670 upstream: parse_oid(&parsed.upstream)?,
671 branch_tip: parse_oid(&parsed.branch_tip)?,
672 flatten_leaf_merges: parsed.flatten_leaf_merges,
673 scoped_refs: parsed
674 .scoped_refs
675 .iter()
676 .map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
677 .collect::<Result<_, _>>()?,
678 })
679}
680
681fn write_persisted_state(
682 git_dir: &Path,
683 state: &PersistedRebaseState,
684) -> Result<(), RebaseSessionError> {
685 let dir = git_dir.join("rebase-merge");
686 fs::create_dir_all(&dir).map_err(map_lib)?;
687 let lock_path = dir.join("grit-plan.lock");
688 let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
689 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
690 let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
691 lock.write_all(&body).map_err(map_lib)?;
692 lock.commit_to(&dir.join("grit-plan"))
693 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
694 Ok(())
695}
696
697fn install_stopped_rebase(
698 repo: &Repository,
699 head_ref: &str,
700 orig_head: ObjectId,
701 request: &RebaseRequest,
702 rebase_opts: &RebaseOntoOptions,
703 apply: &ApplyOptions,
704 partial: super::plan::PartialRewritePlan,
705) -> Result<RebaseStop, RebaseSessionError> {
706 let effective_onto = request.onto.unwrap_or(request.upstream);
707 let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
708 let persisted = state_from_partial(
709 &partial,
710 head_ref,
711 orig_head,
712 request,
713 rebase_opts,
714 apply,
715 effective_onto,
716 fork,
717 );
718 let conflict = partial.conflict;
719 let done = persisted
720 .done
721 .iter()
722 .map(|h| parse_oid(h))
723 .collect::<Result<Vec<_>, _>>()?;
724 let remaining = persisted
725 .remaining
726 .iter()
727 .map(|h| parse_oid(h))
728 .collect::<Result<Vec<_>, _>>()?;
729 write_git_rebase_merge_files(
730 repo,
731 head_ref,
732 orig_head,
733 effective_onto,
734 &done,
735 &remaining,
736 &conflict.commit,
737 )?;
738 write_persisted_state(repo.git_dir(), &persisted)?;
739
740 refs::update_direct_ref_on_store(
741 repo.refs(),
742 repo.git_dir(),
743 "HEAD",
744 &conflict.replayed_parent,
745 )
746 .map_err(map_lib)?;
747 reset_index_worktree_to_head(repo, conflict.replayed_parent)?;
748 let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
749 apply_conflict_merge_worktree(repo, &ours_tree, &conflict.merge).map_err(map_lib)?;
750
751 let message = load_commit(repo, conflict.commit)?.message;
752 fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
753
754 Ok(RebaseStop {
755 commit: conflict.commit,
756 conflicts: conflicts_from_tree_merge(&conflict.merge),
757 done,
758 remaining,
759 })
760}
761
762fn advance_after_plan(
763 repo: &Repository,
764 prev: &LoadedState,
765 apply: &ApplyOptions,
766 outcome: ResumablePlanOutcome,
767 edits: HistoryEdits,
768) -> Result<RebaseOutcome, RebaseSessionError> {
769 match outcome {
770 ResumablePlanOutcome::Complete(mut plan) => {
771 if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
772 if !plan.ref_updates.iter().any(|u| u.name == prev.head_ref) {
773 plan.ref_updates.push(PlannedRefUpdate {
774 name: prev.head_ref.clone(),
775 old: prev.orig_head,
776 new: new_tip,
777 });
778 }
779 }
780 let apply_without_hooks = ApplyOptions {
781 hooks: PostOperationHooks::Skip,
782 ..apply.clone()
783 };
784 let mut applied = apply_rewrite(
785 repo,
786 &plan,
787 &apply_without_hooks,
788 &mut crate::progress::NullProgress,
789 &crate::progress::NeverCancel,
790 )?;
791 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &prev.head_ref)
792 .map_err(map_lib)?;
793 let branch_tip = repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?;
794 reset_index_worktree_to_head(repo, branch_tip)?;
795 applied.head = branch_tip;
796 clear_rebase_state(repo.git_dir())?;
797 if apply.hooks.enabled()
798 && run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
799 {
800 applied.hooks_run.push(HookName::PostRewrite);
801 }
802 Ok(RebaseOutcome::Done {
803 applied,
804 plan: *plan,
805 })
806 }
807 ResumablePlanOutcome::Partial(partial) => {
808 let request = RebaseRequest {
809 upstream: prev.upstream,
810 onto: Some(prev.onto),
811 branch_tip: prev.branch_tip,
812 };
813 let stop = install_stopped_rebase(
814 repo,
815 &prev.head_ref,
816 prev.orig_head,
817 &request,
818 &prev.rebase,
819 apply,
820 *partial,
821 )?;
822 let _ = edits;
823 Ok(RebaseOutcome::Stopped(stop))
824 }
825 }
826}
827
828fn write_git_rebase_merge_files(
829 repo: &Repository,
830 head_ref: &str,
831 orig_head: ObjectId,
832 onto: ObjectId,
833 done: &[ObjectId],
834 remaining: &[ObjectId],
835 current: &ObjectId,
836) -> Result<(), RebaseSessionError> {
837 let dir = repo.git_dir().join("rebase-merge");
838 fs::create_dir_all(&dir).map_err(map_lib)?;
839 fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
840 fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
841 fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
842 let todo_remaining: Vec<ObjectId> = if remaining.first() == Some(current) {
843 remaining.iter().skip(1).copied().collect()
844 } else {
845 remaining.to_vec()
846 };
847 fs::write(
848 dir.join("git-rebase-todo"),
849 todo_lines(repo, &todo_remaining)?,
850 )
851 .map_err(map_lib)?;
852 let done_with_current: Vec<ObjectId> = done.iter().chain([current]).copied().collect();
855 fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
856 let msgnum = done.len() + 1;
857 let end = done.len() + remaining.len();
858 fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
859 fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
860 fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
861 let commit = load_commit(repo, *current)?;
862 match author_script(&commit.author) {
863 Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
864 None => remove_if_exists(&dir.join("author-script"))?,
865 }
866 fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
867 fs::write(
868 repo.git_dir().join("ORIG_HEAD"),
869 format!("{}\n", orig_head.to_hex()),
870 )
871 .map_err(map_lib)?;
872 Ok(())
873}
874
875fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
876 let mut out = String::new();
877 for &oid in commits {
878 let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
879 let abbrev = &oid.to_hex()[..7];
880 out.push_str(&format!("pick {abbrev} {subject}\n"));
881 }
882 Ok(out)
883}
884
885fn author_script(author: &str) -> Option<String> {
889 let lt = author.find('<')?;
890 let gt = lt + author[lt..].find('>')?;
891 let name = author[..lt].trim_end();
892 let email = &author[lt + 1..gt];
893 let date = format!("@{}", author[gt + 1..].trim());
894 let quote = crate::transport::sq_quote_shell_arg;
895 Some(format!(
896 "GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
897 quote(name),
898 quote(email),
899 quote(&date)
900 ))
901}
902
903fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
904 for name in ["ORIG_HEAD", "MERGE_MSG"] {
905 let path = git_dir.join(name);
906 if path.exists() {
907 fs::remove_file(&path).map_err(map_lib)?;
908 }
909 }
910 let dir = git_dir.join("rebase-merge");
911 if dir.is_dir() {
912 fs::remove_dir_all(&dir).map_err(map_lib)?;
913 }
914 Ok(())
915}
916
917fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
918 match resolve_head_for_repository(repo).map_err(map_lib)? {
919 HeadState::Branch {
920 refname,
921 oid: Some(oid),
922 ..
923 } => Ok((refname, oid)),
924 HeadState::Branch { oid: None, .. } | HeadState::Detached { .. } | HeadState::Invalid => {
925 Err(RebaseSessionError::NotOnBranch)
926 }
927 }
928}
929
930fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
931 resolve_head_for_repository(repo)
932 .map_err(map_lib)?
933 .oid()
934 .copied()
935 .ok_or(RebaseSessionError::NotOnBranch)
936}
937
938fn commit_tree(repo: &Repository, commit: ObjectId) -> Result<ObjectId, RebaseSessionError> {
939 repo.odb().commit_tree(&commit).map_err(map_lib)
940}
941
942fn load_commit(
943 repo: &Repository,
944 oid: ObjectId,
945) -> Result<crate::objects::CommitData, RebaseSessionError> {
946 repo.odb().read_commit(&oid).map_err(map_lib)
947}
948
949fn reset_index_worktree_to_head(
950 repo: &Repository,
951 head_oid: ObjectId,
952) -> Result<(), RebaseSessionError> {
953 let head_tree = commit_tree(repo, head_oid)?;
954 checkout_hard_to_tree_from_head_tree(
957 repo,
958 &head_tree,
959 &head_tree,
960 &mut crate::progress::NullProgress,
961 &crate::progress::NeverCancel,
962 )
963 .map_err(map_lib)?;
964 Ok(())
965}
966
967fn hard_reset_branch(
968 repo: &Repository,
969 refname: &str,
970 target: ObjectId,
971 identity: &crate::ident::Signature,
972) -> Result<(), RebaseSessionError> {
973 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
974 .map_err(map_lib)?;
975 hard_reset_head(repo, target, identity)
976}
977
978fn hard_reset_head(
979 repo: &Repository,
980 target: ObjectId,
981 identity: &crate::ident::Signature,
982) -> Result<(), RebaseSessionError> {
983 reset_to_commit(
984 repo,
985 &ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
986 &mut crate::progress::NullProgress,
987 &crate::progress::NeverCancel,
988 )
989 .map_err(map_lib)?;
990 Ok(())
991}
992
993use std::io::Write;
994
995fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
996 RebaseSessionError::Library(err.into())
997}
998
999fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
1000 state.rewrite.clone()
1001}
1002
1003fn map_json(err: serde_json::Error) -> RebaseSessionError {
1004 RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::author_script;
1010
1011 #[test]
1012 fn author_script_quotes_values_and_prefixes_date() {
1013 assert_eq!(
1014 author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
1015 Some(
1016 "GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
1017 )
1018 );
1019 assert_eq!(
1020 author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
1021 Some(
1022 "GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
1023 )
1024 );
1025 assert_eq!(author_script("no email here 1 +0000"), None);
1026 }
1027}