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