git_stack/graph/
ops.rs

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                        // Pretend it doesn't exist
36                    }
37                    crate::graph::BranchKind::Mutable | crate::graph::BranchKind::Mixed => {
38                        // Let the parent branch take care of things
39                        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                // Not relevant, no more processing needed
112                return false;
113            }
114
115            if graph.children_of(*id).count() != 0 {
116                // Children might get removed in another pass
117                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                    // Always must have at least one commit, don't remove
125                    cursor.stop();
126                } else if 1 < graph.children_of(*id).count() {
127                    // Shared commit, don't remove
128                    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                    // Protected commit, don't remove
136                    cursor.stop();
137                } else if candidate_id != *id && graph.branches.contains_oid(candidate_id) {
138                    // Hit another branch which needs its own evaluation for whether we should
139                    // remove it
140                    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
251/// Pre-requisites:
252/// - Running `protect_branches`
253///
254/// # Panics
255///
256/// - If `new_base_id` doesn't exist
257pub 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
287/// Update branches from `pull_start` to `pull_end`
288///
289/// A normal `rebase_development_branches` only looks at development commits.  If `main` is pristine or if the
290/// user has branches on the same commit as `main`, we should also update these to what we pulled.
291pub 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                        // Must be a protected branch, safe for us to push
431                    }
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        // User pushed, so trust them.  Consider all other branches as empty branches
446        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
518/// Quick pass for what is droppable
519///
520/// We get into this state when a branch is squashed.  The id would be different due to metadata
521/// but the `tree_id`, associated with the repo, is the same if your branch is up-to-date.
522///
523/// The big risk is if a commit was reverted.  To protect against this, we only look at the final
524/// state of the branch and then check if it looks like a revert.
525///
526/// To avoid walking too much of the tree, we are going to assume only the first branch in a stack
527/// could have been squash-merged.
528///
529/// This assumes that the Node was rebased onto all of the new potentially squash-merged Nodes and
530/// we extract the potential `tree_id`'s from those protected commits.
531pub 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                    // Pretend it doesn't exist
557                }
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
582/// Drop branches merged among the pulled IDs
583///
584/// Marking it deleted in `graph` is purely superficial since nothing can act on it.  The returned
585/// branches are the important part.
586pub 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    // Re-target all branches from the fixup commit to the next most-recent commit
681    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    // Move the fixup commit
691    let node = graph.remove(fixup_id).expect("fixup is always valid");
692    graph.insert(node, target_id);
693
694    // Re-parent all commits to the fixup commit
695    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    // Re-target all branches from the target to the fixup
702    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            // Happened above
717        }
718    }
719}
720
721/// When a branch has extra commits, update dependent branches to the latest
722pub 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                    // Can't continue
753                    break;
754                }
755                1 => {
756                    current_id = children[0];
757                }
758                _ => {
759                    // Assuming the more recent work is a continuation of the existing stack and
760                    // aligning the other stacks to be on top of it
761                    //
762                    // This should be safe in light of our rebases since we don't preserve the time
763                    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            // Alignment point found
775            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
788/// When a rebase has split stack, re-combine them
789pub 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}