1use std::collections::BTreeMap;
2use std::collections::HashSet;
3
4use crate::graph::Graph;
5use crate::graph::Resource;
6
7pub fn protect_branches(graph: &mut Graph) {
8 let protected_oids: Vec<_> = graph
9 .branches
10 .iter()
11 .filter_map(|(oid, branches)| {
12 branches
13 .iter()
14 .find(|b| b.kind() == crate::graph::BranchKind::Protected)
15 .map(|_| oid)
16 })
17 .flat_map(|protected_oid| graph.ancestors_of(protected_oid))
18 .collect();
19 for protected_oid in protected_oids {
20 graph.commit_set(protected_oid, crate::graph::Action::Protected);
21 }
22}
23
24pub fn protect_large_branches(graph: &mut Graph, max: usize) -> Vec<String> {
25 let mut large_branches = Vec::new();
26
27 'branch: for branch_id in graph.branches.oids().collect::<Vec<_>>() {
28 let mut ancestors = graph.ancestors_of(branch_id).into_cursor();
29 let mut count = 0;
30 while let Some(ancestor_id) = ancestors.next(graph) {
31 count += 1;
32 for branch in graph.branches.get(ancestor_id).into_iter().flatten() {
33 match branch.kind() {
34 crate::graph::BranchKind::Deleted => {
35 }
37 crate::graph::BranchKind::Mutable | crate::graph::BranchKind::Mixed => {
38 continue 'branch;
40 }
41 crate::graph::BranchKind::Protected => {
42 ancestors.stop();
43 }
44 }
45 }
46
47 let action = graph
48 .commit_get::<crate::graph::Action>(ancestor_id)
49 .copied()
50 .unwrap_or_default();
51 if action.is_protected() {
52 ancestors.stop();
53 }
54 }
55 if max <= count {
56 mark_branch_protected(graph, branch_id);
57 large_branches.extend(
58 graph
59 .branches
60 .get(branch_id)
61 .unwrap_or(&[])
62 .iter()
63 .filter(|&branch| branch.kind().has_user_commits())
64 .map(|branch| branch.name()),
65 );
66 }
67 }
68
69 large_branches
70}
71
72fn mark_branch_protected(graph: &mut Graph, commit_id: git2::Oid) {
73 let protected_oids: Vec<_> = graph.ancestors_of(commit_id).collect();
74 for protected_oid in protected_oids {
75 graph.commit_set(protected_oid, crate::graph::Action::Protected);
76 }
77}
78
79pub fn tag_commits_while(
80 graph: &mut Graph,
81 tag: impl Fn(&Graph, git2::Oid) -> Option<crate::any::BoxedEntry>,
82) {
83 let mut cursor = graph.descendants().into_cursor();
84 while let Some(descendant_id) = cursor.next(graph) {
85 if let Some(resource) = tag(graph, descendant_id) {
86 graph.commit_set(descendant_id, resource);
87 } else {
88 cursor.stop();
89 }
90 }
91}
92
93pub fn trim_tagged_branch<R: Resource + Eq>(
94 graph: &mut Graph,
95 template: R,
96) -> Vec<crate::graph::Branch> {
97 let mut trimmed = Vec::new();
98
99 let mut branches = graph
100 .branches
101 .iter()
102 .map(|(id, _branches)| id)
103 .collect::<Vec<_>>();
104 let mut made_progress = true;
105 while made_progress {
106 made_progress = false;
107 branches.retain(|id| {
108 if graph.commit_get::<R>(*id) != Some(&template) {
109 return false;
111 }
112
113 if graph.children_of(*id).count() != 0 {
114 return true;
116 }
117
118 let mut to_remove = Vec::new();
119 let mut cursor = graph.ancestors_of(*id).into_cursor();
120 while let Some(candidate_id) = cursor.next(graph) {
121 if candidate_id == graph.root_id() {
122 cursor.stop();
124 } else if 1 < graph.children_of(*id).count() {
125 cursor.stop();
127 } else if graph
128 .commit_get::<crate::graph::Action>(candidate_id)
129 .copied()
130 .unwrap_or_default()
131 .is_protected()
132 {
133 cursor.stop();
135 } else if candidate_id != *id && graph.branches.contains_oid(candidate_id) {
136 cursor.stop();
139 } else {
140 trimmed.extend(graph.branches.remove(candidate_id).unwrap_or_default());
141 to_remove.push(candidate_id);
142 }
143 }
144 for id in to_remove {
145 graph.remove(id);
146 }
147 made_progress = true;
148
149 false
150 });
151 }
152
153 trimmed
154}
155
156pub fn protect_tagged_branch<R: Resource + Eq>(graph: &mut Graph, template: R) {
157 let branches = graph
158 .branches
159 .iter()
160 .map(|(id, _branches)| id)
161 .filter(|id| graph.commit_get::<R>(*id) == Some(&template))
162 .collect::<Vec<_>>();
163 for branch_id in branches {
164 mark_branch_protected(graph, branch_id);
165 }
166}
167
168pub fn tag_stale_commits(
169 graph: &mut Graph,
170 repo: &dyn crate::git::Repo,
171 earlier_than: std::time::SystemTime,
172 ignore: &[git2::Oid],
173) {
174 tag_commits_while(graph, |_graph, id| {
175 if ignore.contains(&id) {
176 return None;
177 }
178 let commit = repo.find_commit(id)?;
179 (commit.time < earlier_than).then(|| StaleCommit.into())
180 });
181}
182
183pub fn trim_stale_branches(
184 graph: &mut Graph,
185 repo: &dyn crate::git::Repo,
186 earlier_than: std::time::SystemTime,
187 ignore: &[git2::Oid],
188) -> Vec<crate::graph::Branch> {
189 tag_stale_commits(graph, repo, earlier_than, ignore);
190 trim_tagged_branch(graph, StaleCommit)
191}
192
193pub fn protect_stale_branches(
194 graph: &mut Graph,
195 repo: &dyn crate::git::Repo,
196 earlier_than: std::time::SystemTime,
197 ignore: &[git2::Oid],
198) {
199 tag_stale_commits(graph, repo, earlier_than, ignore);
200 protect_tagged_branch(graph, StaleCommit);
201}
202
203#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
204pub struct StaleCommit;
205
206impl crate::any::ResourceTag for StaleCommit {}
207
208pub fn tag_foreign_commits(
209 graph: &mut Graph,
210 repo: &dyn crate::git::Repo,
211 user: &str,
212 ignore: &[git2::Oid],
213) {
214 tag_commits_while(graph, |_graph, id| {
215 if ignore.contains(&id) {
216 return None;
217 }
218 let commit = repo.find_commit(id)?;
219 (commit.committer.as_deref() != Some(user) && commit.author.as_deref() != Some(user))
220 .then(|| ForeignCommit.into())
221 });
222}
223
224pub fn trim_foreign_branches(
225 graph: &mut Graph,
226 repo: &dyn crate::git::Repo,
227 user: &str,
228 ignore: &[git2::Oid],
229) -> Vec<crate::graph::Branch> {
230 tag_foreign_commits(graph, repo, user, ignore);
231 trim_tagged_branch(graph, ForeignCommit)
232}
233
234pub fn protect_foreign_branches(
235 graph: &mut Graph,
236 repo: &dyn crate::git::Repo,
237 user: &str,
238 ignore: &[git2::Oid],
239) {
240 tag_foreign_commits(graph, repo, user, ignore);
241 protect_tagged_branch(graph, ForeignCommit);
242}
243
244#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
245pub struct ForeignCommit;
246
247impl crate::any::ResourceTag for ForeignCommit {}
248
249pub fn rebase_development_branches(graph: &mut Graph, onto_id: git2::Oid) {
256 let mut descendants = graph.descendants().into_cursor();
257 while let Some(descendant_id) = descendants.next(graph) {
258 let action = graph
259 .commit_get::<crate::graph::Action>(descendant_id)
260 .copied()
261 .unwrap_or_default();
262 if action.is_protected() {
263 continue;
264 }
265
266 let bases: Vec<_> = graph
267 .parents_of(descendant_id)
268 .filter(|id| *id != onto_id)
269 .filter(|id| {
270 let action = graph
271 .commit_get::<crate::graph::Action>(*id)
272 .copied()
273 .unwrap_or_default();
274 action.is_protected()
275 })
276 .collect();
277 for base in bases {
278 graph.rebase(descendant_id, base, onto_id);
279 }
280
281 descendants.stop();
282 }
283}
284
285pub fn fast_forward_pulled_branches(graph: &mut Graph, pull_start: git2::Oid, pull_end: git2::Oid) {
290 if pull_start == pull_end {
291 return;
292 }
293
294 let branches = if let Some(branches) = graph.branches.remove(pull_start) {
295 branches
296 } else {
297 return;
298 };
299 let (mut end_branches, start_branches): (Vec<_>, Vec<_>) = branches
300 .into_iter()
301 .partition(|b| b.kind().has_user_commits());
302 graph.branches.extend(start_branches);
303
304 for end_branch in &mut end_branches {
305 end_branch.set_id(pull_end);
306 }
307 graph.branches.extend(end_branches);
308}
309
310pub fn mark_wip(graph: &mut Graph, repo: &dyn crate::git::Repo) {
311 let mut cursor = graph.descendants().into_cursor();
312 while let Some(current_id) = cursor.next(graph) {
313 if graph
314 .commit_get::<crate::graph::Action>(current_id)
315 .copied()
316 .unwrap_or_default()
317 .is_protected()
318 {
319 continue;
320 }
321
322 let commit = repo
323 .find_commit(current_id)
324 .expect("all commits in graph present in git");
325 if commit.wip_summary().is_some() {
326 graph.commit_set(current_id, Wip);
327 }
328 }
329}
330
331#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
332pub struct Wip;
333
334impl crate::any::ResourceTag for Wip {}
335
336pub fn mark_fixup(graph: &mut Graph, repo: &dyn crate::git::Repo) {
337 let mut cursor = graph.descendants().into_cursor();
338 while let Some(current_id) = cursor.next(graph) {
339 if graph
340 .commit_get::<crate::graph::Action>(current_id)
341 .copied()
342 .unwrap_or_default()
343 .is_protected()
344 {
345 continue;
346 }
347
348 let commit = repo
349 .find_commit(current_id)
350 .expect("all commits in graph present in git");
351 if commit.fixup_summary().is_some() {
352 graph.commit_set(current_id, Fixup);
353 }
354 }
355}
356
357#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
358pub struct Fixup;
359
360impl crate::any::ResourceTag for Fixup {}
361
362pub fn pushable(graph: &mut Graph) {
363 let branches = graph
364 .branches
365 .iter()
366 .map(|(id, _branches)| id)
367 .collect::<Vec<_>>();
368 for branch_id in branches {
369 mark_push_status(graph, branch_id);
370 }
371}
372
373fn mark_push_status(graph: &mut Graph, branch_id: git2::Oid) -> Option<PushStatus> {
374 if let Some(status) = graph.commit_get::<PushStatus>(branch_id) {
375 return Some(*status);
376 }
377
378 let mut status = Some(PushStatus::Pushable);
379
380 if graph
381 .commit_get::<crate::graph::Action>(branch_id)
382 .copied()
383 .unwrap_or_default()
384 .is_protected()
385 {
386 log::debug!(
387 "Branches at {} aren't pushable, the commit is protected",
388 branch_id
389 );
390 status = None;
391 } else {
392 let mut ancestors = graph.ancestors_of(branch_id).into_cursor();
393 while let Some(parent_id) = ancestors.next(graph) {
394 if graph
395 .commit_get::<crate::graph::Action>(parent_id)
396 .copied()
397 .unwrap_or_default()
398 .is_protected()
399 {
400 ancestors.stop();
401 } else if graph.commit_get::<Wip>(parent_id).is_some() {
402 log::debug!(
403 "Branches at {} aren't pushable, commit {} is WIP",
404 branch_id,
405 parent_id,
406 );
407 status = Some(PushStatus::Blocked("wip"));
408 break;
409 } else if branch_id != parent_id && graph.branches.contains_oid(parent_id) {
410 let parent_status = mark_push_status(graph, parent_id);
411 match parent_status {
412 Some(PushStatus::Blocked(reason)) => {
413 log::debug!(
414 "Branches at {} aren't pushable, parent commit {} is blocked for {}",
415 branch_id,
416 parent_id,
417 reason
418 );
419 status = Some(PushStatus::Blocked("parent branch"));
420 break;
421 }
422 Some(PushStatus::Pushed) | Some(PushStatus::Pushable) => {
423 log::debug!(
424 "Branches at {} aren't pushable, parent branch at {} should be pushed first",
425 branch_id,
426 parent_id
427 );
428 status = Some(PushStatus::Blocked("parent branch"));
429 break;
430 }
431 None => {
432 }
434 }
435 ancestors.stop();
436 }
437 }
438 }
439
440 if graph
441 .branches
442 .get(branch_id)
443 .into_iter()
444 .flatten()
445 .any(|b| Some(b.id()) == b.push_id())
446 {
447 log::debug!("A branch at {} is already pushed", branch_id);
449 status = Some(PushStatus::Pushed);
450 }
451
452 if let Some(status) = status {
453 graph.commit_set(branch_id, status);
454 }
455 status
456}
457
458#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
459pub enum PushStatus {
460 Blocked(&'static str),
461 Pushed,
462 Pushable,
463}
464
465impl Default for PushStatus {
466 fn default() -> Self {
467 Self::Blocked("protected")
468 }
469}
470
471impl crate::any::ResourceTag for PushStatus {}
472
473pub fn hide_protected_branch_commits(graph: &mut Graph, visible: &[git2::Oid]) {
474 let hidden_oids: Vec<_> = graph
475 .branches
476 .iter()
477 .filter_map(|(oid, branches)| {
478 branches
479 .iter()
480 .find(|b| b.kind() == crate::graph::BranchKind::Protected)
481 .map(|_| oid)
482 })
483 .flat_map(|branch_oid| graph.ancestors_of(branch_oid))
484 .filter(|oid| {
485 let is_branch = graph.branches.get(*oid).is_some();
486 let is_forced = visible.contains(oid);
487 let is_root = *oid == graph.root_id();
488 let is_leaf = graph.children_of(*oid).count() == 0;
489 !(is_branch || is_forced || is_root || is_leaf)
490 })
491 .collect();
492 for hidden_oid in hidden_oids {
493 graph.commit_set(hidden_oid, Hidden);
494 }
495}
496
497pub fn hide_branch_commits(graph: &mut Graph, visible: &[git2::Oid]) {
498 let hidden_oids: Vec<_> = graph
499 .branches
500 .oids()
501 .flat_map(|branch_oid| graph.ancestors_of(branch_oid))
502 .filter(|oid| {
503 let is_branch = graph.branches.get(*oid).is_some();
504 let is_forced = visible.contains(oid);
505 let is_root = *oid == graph.root_id();
506 let is_leaf = graph.children_of(*oid).count() == 0;
507 !(is_branch || is_forced || is_root || is_leaf)
508 })
509 .collect();
510 for hidden_oid in hidden_oids {
511 graph.commit_set(hidden_oid, Hidden);
512 }
513}
514
515#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
516pub struct Hidden;
517
518impl crate::any::ResourceTag for Hidden {}
519
520pub fn delete_squashed_branches_by_tree_id(
534 graph: &mut Graph,
535 repo: &dyn crate::git::Repo,
536 pull_start_id: git2::Oid,
537 pulled_tree_ids: impl Iterator<Item = git2::Oid>,
538) -> Vec<crate::graph::Branch> {
539 let mut removed = Vec::new();
540
541 let pulled_tree_ids: HashSet<_> = pulled_tree_ids.collect();
542 let mut descendants = graph.descendants_of(pull_start_id).into_cursor();
543 while let Some(descendant_id) = descendants.next(graph) {
544 if pulled_tree_ids.contains(&descendant_id) {
545 descendants.stop();
546 continue;
547 }
548
549 let branches = if let Some(branches) = graph.branches.get_mut(descendant_id) {
550 branches
551 } else {
552 continue;
553 };
554 let mut stop = false;
555 for branch in branches {
556 match branch.kind() {
557 crate::graph::BranchKind::Deleted => {
558 }
560 crate::graph::BranchKind::Mutable => {
561 stop = true;
562
563 let commit = repo
564 .find_commit(descendant_id)
565 .expect("graph entries are valid");
566 if pulled_tree_ids.contains(&commit.tree_id) {
567 branch.set_kind(crate::graph::BranchKind::Deleted);
568 removed.push(branch.clone());
569 }
570 }
571 crate::graph::BranchKind::Mixed | crate::graph::BranchKind::Protected => {
572 stop = true;
573 }
574 }
575 }
576 if stop {
577 descendants.stop();
578 }
579 }
580
581 removed
582}
583
584pub fn delete_merged_branches(
589 graph: &mut Graph,
590 pulled_ids: impl Iterator<Item = git2::Oid>,
591) -> Vec<crate::graph::Branch> {
592 let mut removed = Vec::new();
593 for pulled_id in pulled_ids {
594 let branches = if let Some(branches) = graph.branches.get_mut(pulled_id) {
595 branches
596 } else {
597 continue;
598 };
599 for branch in branches {
600 if branch.kind() == crate::graph::BranchKind::Mutable {
601 branch.set_kind(crate::graph::BranchKind::Deleted);
602 removed.push(branch.clone());
603 }
604 }
605 }
606 removed
607}
608
609pub fn fixup(graph: &mut Graph, repo: &dyn crate::git::Repo, effect: crate::config::Fixup) {
610 if effect == crate::config::Fixup::Ignore {
611 return;
612 }
613
614 let mut fixups = Vec::new();
615
616 let mut descendants = graph.descendants().into_cursor();
617 while let Some(descendant_id) = descendants.next(graph) {
618 let action = graph
619 .commit_get::<crate::graph::Action>(descendant_id)
620 .copied()
621 .unwrap_or_default();
622 if action.is_protected() {
623 continue;
624 }
625
626 let commit = repo
627 .find_commit(descendant_id)
628 .expect("all commits in graph present in git");
629 if let Some(summary) = commit.fixup_summary() {
630 fixups.push((descendant_id, summary.to_owned()));
631 }
632 }
633
634 for (fixup_id, summary) in fixups {
635 let mut ancestors = graph.ancestors_of(fixup_id).into_cursor();
636 let _self = ancestors.next(graph);
637 assert_eq!(_self, Some(fixup_id));
638 let mut fixed = false;
639 while let Some(ancestor_id) = ancestors.next(graph) {
640 let action = graph
641 .commit_get::<crate::graph::Action>(ancestor_id)
642 .copied()
643 .unwrap_or_default();
644 if action.is_protected() {
645 ancestors.stop();
646 continue;
647 }
648
649 let anc_commit = repo
650 .find_commit(ancestor_id)
651 .expect("all commits in graph present in git");
652 if let Some(anc_summary) = anc_commit.fixup_summary() {
653 if anc_summary == summary {
654 fixup_commit(graph, fixup_id, ancestor_id, effect);
655 fixed = true;
656 break;
657 }
658 }
659 if anc_commit.summary == summary {
660 fixup_commit(graph, fixup_id, ancestor_id, effect);
661 fixed = true;
662 break;
663 }
664 }
665 if !fixed {
666 log::trace!(
667 "Could not find base commit for fixup {} ({})",
668 fixup_id,
669 summary
670 );
671 }
672 }
673}
674
675fn fixup_commit(
676 graph: &mut Graph,
677 fixup_id: git2::Oid,
678 target_id: git2::Oid,
679 effect: crate::config::Fixup,
680) {
681 debug_assert_ne!(fixup_id, target_id);
682 let branches = graph.branches.remove(fixup_id);
684 let fixup_parent_id = graph
685 .primary_parent_of(fixup_id)
686 .expect("if there is a target, there is a parent");
687 for mut branch in branches.into_iter().flatten() {
688 branch.set_id(fixup_parent_id);
689 graph.branches.insert(branch);
690 }
691
692 let node = graph.remove(fixup_id).expect("fixup is always valid");
694 graph.insert(node, target_id);
695
696 for target_child_id in graph.children_of(target_id).collect::<Vec<_>>() {
698 if target_child_id != fixup_id {
699 graph.rebase(target_child_id, target_id, fixup_id);
700 }
701 }
702
703 let branches = graph.branches.remove(target_id);
705 for mut branch in branches.into_iter().flatten() {
706 if branch.kind().has_user_commits() {
707 branch.set_id(fixup_id);
708 }
709 graph.branches.insert(branch);
710 }
711
712 match effect {
713 crate::config::Fixup::Ignore => unreachable!(),
714 crate::config::Fixup::Squash => {
715 graph.commit_set(fixup_id, crate::graph::Action::Fixup);
716 }
717 crate::config::Fixup::Move => {
718 }
720 }
721}
722
723pub fn realign_stacks(graph: &mut Graph, repo: &dyn crate::git::Repo) {
725 let mut descendants = graph.descendants().into_cursor();
726 while let Some(descendant_id) = descendants.next(graph) {
727 let action = graph
728 .commit_get::<crate::graph::Action>(descendant_id)
729 .copied()
730 .unwrap_or_default();
731 if action.is_protected() {
732 continue;
733 }
734
735 realign_stack(graph, repo, descendant_id);
736 descendants.stop();
737 }
738}
739
740fn realign_stack(graph: &mut Graph, repo: &dyn crate::git::Repo, base_id: git2::Oid) {
741 let mut old_edges = Vec::new();
742
743 let mut current_id = base_id;
744 loop {
745 if graph
746 .branches
747 .get(current_id)
748 .unwrap_or_default()
749 .is_empty()
750 {
751 let mut children = graph.children_of(current_id).collect::<Vec<_>>();
752 match children.len() {
753 0 => {
754 break;
756 }
757 1 => {
758 current_id = children[0];
759 }
760 _ => {
761 children.sort_unstable_by_key(|id| {
766 repo.find_commit(*id)
767 .expect("all commits in graph present in git")
768 .time
769 });
770 let newest = children.pop().unwrap();
771 current_id = newest;
772 old_edges.extend(children.into_iter().map(|child_id| (current_id, child_id)));
773 }
774 }
775 } else {
776 break;
778 }
779 }
780
781 for (parent_id, child_id) in old_edges {
782 graph.rebase(child_id, parent_id, current_id);
783 }
784 let children = graph.children_of(current_id).collect::<Vec<_>>();
785 for child_id in children {
786 realign_stack(graph, repo, child_id);
787 }
788}
789
790pub fn merge_stacks_by_tree_id(graph: &mut Graph, repo: &dyn crate::git::Repo) {
792 let mut descendants = graph.descendants().into_cursor();
793 while let Some(descendant_id) = descendants.next(graph) {
794 let mut unprotected_children = CommitTimesByTreeId::new();
795 for child_id in graph.children_of(descendant_id) {
796 let action = graph
797 .commit_get::<crate::graph::Action>(descendant_id)
798 .copied()
799 .unwrap_or_default();
800 if action.is_protected() {
801 continue;
802 }
803
804 let commit = repo
805 .find_commit(child_id)
806 .expect("all commits in graph present in git");
807 unprotected_children
808 .entry(commit.tree_id)
809 .or_default()
810 .push((commit.time, child_id));
811 }
812 for mut commits in unprotected_children.into_values() {
813 if commits.len() < 2 {
814 continue;
815 }
816
817 commits.sort_unstable();
818 let (_, winner_id) = commits.pop().expect("checked len earlier");
819
820 for (_, from_id) in commits {
821 let rebased_grandchildren = graph.children_of(from_id).collect::<Vec<_>>();
822 for grandchild_id in rebased_grandchildren {
823 graph.rebase(grandchild_id, from_id, winner_id);
824 }
825 for mut branch in graph.branches.remove(from_id).into_iter().flatten() {
826 branch.set_id(winner_id);
827 graph.branches.insert(branch);
828 }
829 graph.remove(from_id);
830 }
831 }
832 }
833}
834
835type CommitTimesByTreeId = BTreeMap<git2::Oid, Vec<(std::time::SystemTime, git2::Oid)>>;
836
837pub fn reword_commit(
838 graph: &mut Graph,
839 repo: &dyn crate::git::Repo,
840 id: git2::Oid,
841 message: String,
842) -> Result<(), eyre::Error> {
843 eyre::ensure!(
844 graph.contains_id(id),
845 "cannot rewrite commit {}, not present",
846 id
847 );
848
849 let commit = repo
850 .find_commit(id)
851 .expect("graph.contains_id ensures commit exists");
852 for descendant_id in graph.descendants_of(id) {
853 let descendant_commit = repo
854 .find_commit(descendant_id)
855 .expect("graph.descendants_of ensures commit exists");
856 if Some(commit.summary.as_ref()) == descendant_commit.fixup_summary() {
857 eyre::bail!("cannot reword; first squash dependent fixups")
858 }
859 }
860
861 graph.commit_set(id, Reword(message));
862
863 Ok(())
864}
865
866#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
867pub struct Reword(String);
868
869impl crate::any::ResourceTag for Reword {}
870
871pub fn to_scripts(
872 graph: &Graph,
873 dropped_branches: Vec<super::Branch>,
874) -> Vec<crate::rewrite::Script> {
875 let mut scripts = Vec::new();
876 let mut dropped_branches = dropped_branches
877 .into_iter()
878 .map(|b| {
879 (
880 b.id(),
881 b.local_name()
882 .expect("only local branches passed in")
883 .to_owned(),
884 )
885 })
886 .collect::<std::collections::HashMap<_, _>>();
887
888 let root_id = graph.root_id();
889 let action = graph
890 .commit_get::<crate::graph::Action>(root_id)
891 .copied()
892 .unwrap_or_default();
893 if action.is_protected() {
894 protected_branches_to_scripts(graph, root_id, &mut dropped_branches, &mut scripts);
895 }
896
897 let mut descendants = graph.descendants().into_cursor();
898 let mut seen = HashSet::new();
899 while let Some(descendant_id) = descendants.next(graph) {
900 let action = graph
901 .commit_get::<crate::graph::Action>(descendant_id)
902 .copied()
903 .unwrap_or_default();
904 if !seen.insert(descendant_id) {
905 descendants.stop();
906 } else if action.is_protected() {
907 protected_branches_to_scripts(
908 graph,
909 descendant_id,
910 &mut dropped_branches,
911 &mut scripts,
912 );
913 } else {
914 descendants.stop();
915 if let Some(parent_id) = graph.primary_parent_of(descendant_id) {
916 let mut script = Vec::new();
917 gather_script(
918 graph,
919 parent_id,
920 descendant_id,
921 &mut dropped_branches,
922 &mut script,
923 );
924 scripts.push(script.into());
925 }
926 }
927 }
928
929 if !dropped_branches.is_empty() {
930 let mut batch = crate::rewrite::Batch::new(graph.root_id());
931 for (id, branch) in dropped_branches {
932 batch.push(id, crate::rewrite::Command::DeleteBranch(branch));
933 }
934 scripts.insert(0, vec![batch].into());
935 }
936
937 scripts
938}
939
940fn protected_branches_to_scripts(
941 graph: &Graph,
942 start_id: git2::Oid,
943 dropped_branches: &mut std::collections::HashMap<git2::Oid, String>,
944 scripts: &mut Vec<crate::rewrite::Script>,
945) {
946 let mut batch = crate::rewrite::Batch::new(start_id);
947 if let Some(dropped) = dropped_branches.remove(&start_id) {
948 batch.push(start_id, crate::rewrite::Command::DeleteBranch(dropped));
949 }
950 for branch in graph.branches.get(start_id).into_iter().flatten() {
951 if branch.kind().has_user_commits() {
952 if let Some(local_name) = branch.local_name() {
953 batch.push(
954 start_id,
955 crate::rewrite::Command::CreateBranch(local_name.to_owned()),
956 );
957 }
958 }
959 }
960 if !batch.is_empty() {
961 scripts.push(vec![batch].into());
962 }
963}
964
965fn gather_script(
966 graph: &Graph,
967 onto_id: git2::Oid,
968 start_id: git2::Oid,
969 dropped_branches: &mut std::collections::HashMap<git2::Oid, String>,
970 script: &mut Vec<crate::rewrite::Batch>,
971) {
972 let mut batch = crate::rewrite::Batch::new(onto_id);
973
974 let mut current_id = Some(start_id);
975 while let Some(id) = current_id {
976 if let Some(dropped) = dropped_branches.remove(&id) {
977 batch.push(id, crate::rewrite::Command::DeleteBranch(dropped));
978 }
979 let action = graph
980 .commit_get::<crate::graph::Action>(id)
981 .copied()
982 .unwrap_or_default();
983 match action {
984 crate::graph::Action::Pick => {
985 batch.push(id, crate::rewrite::Command::CherryPick(id));
986 if let Some(Reword(message)) = graph.commit_get::<Reword>(id) {
987 batch.push(id, crate::rewrite::Command::Reword(message.clone()));
988 }
989 for branch in graph.branches.get(id).into_iter().flatten() {
990 if branch.kind().has_user_commits() {
991 if let Some(local_name) = branch.local_name() {
992 batch.push(
993 id,
994 crate::rewrite::Command::CreateBranch(local_name.to_owned()),
995 );
996 }
997 }
998 }
999 }
1000 crate::graph::Action::Fixup => {
1001 batch.push(id, crate::rewrite::Command::Fixup(id));
1002 for branch in graph.branches.get(id).into_iter().flatten() {
1003 if branch.kind().has_user_commits() {
1004 if let Some(local_name) = branch.local_name() {
1005 batch.push(
1006 id,
1007 crate::rewrite::Command::CreateBranch(local_name.to_owned()),
1008 );
1009 }
1010 }
1011 }
1012 }
1013 crate::graph::Action::Protected => {
1014 unreachable!(
1015 "Rebasing {} (via {}) onto {} should not have protected commits",
1016 id, start_id, onto_id
1017 );
1018 }
1019 }
1020
1021 current_id = None;
1022 for (i, child_id) in graph.children_of(id).enumerate() {
1023 match i {
1024 0 if 1 < graph.parents_of(child_id).count() => {
1025 current_id = None;
1026 gather_script(graph, id, child_id, dropped_branches, script);
1027 }
1028 0 => {
1029 current_id = Some(child_id);
1030 }
1031 _ => {
1032 gather_script(graph, id, child_id, dropped_branches, script);
1033 }
1034 }
1035 }
1036 }
1037
1038 script.push(batch);
1039}