git_stack/legacy/graph/
ops.rs

1use std::collections::BTreeMap;
2use std::collections::HashSet;
3use std::collections::VecDeque;
4
5use crate::legacy::graph::Graph;
6use crate::legacy::graph::Node;
7
8pub fn protect_branches(
9    graph: &mut Graph,
10    repo: &dyn crate::legacy::git::Repo,
11    protected_branches: &crate::legacy::git::Branches,
12) {
13    let protected_oids: HashSet<_> = protected_branches
14        .iter()
15        .flat_map(|(_, branches)| branches.iter().map(|b| b.id))
16        .collect();
17
18    protect_commits(graph, repo, protected_oids);
19}
20
21pub fn protect_commits(
22    graph: &mut Graph,
23    repo: &dyn crate::legacy::git::Repo,
24    protected_oids: HashSet<git2::Oid>,
25) {
26    let root_id = graph.root_id();
27
28    for protected_oid in protected_oids.into_iter().filter(|protected_oid| {
29        repo.merge_base(root_id, *protected_oid)
30            .map(|merge_base_oid| merge_base_oid == root_id)
31            .unwrap_or(false)
32    }) {
33        for commit_id in crate::legacy::git::commit_range(repo, protected_oid..=root_id)
34            .expect("IDs already validated")
35        {
36            let commit = repo
37                .find_commit(commit_id)
38                .expect("commit_range returns valid commits");
39            if let Some(node) = graph.get_mut(commit.id) {
40                if node.action.is_protected() {
41                    break;
42                }
43                node.action = crate::legacy::graph::Action::Protected;
44            }
45            if commit.id == root_id {
46                break;
47            }
48        }
49    }
50}
51
52pub fn protect_large_branches(graph: &mut Graph, max: usize) -> Vec<String> {
53    let mut large_branches = Vec::new();
54
55    let mut protected_queue = VecDeque::new();
56    if graph.root().action.is_protected() {
57        protected_queue.push_back(graph.root_id());
58    }
59    while let Some(current_id) = protected_queue.pop_front() {
60        let current_children = graph
61            .get(current_id)
62            .expect("all children exist")
63            .children
64            .clone();
65
66        for child_id in current_children {
67            let child_action = graph.get(child_id).expect("all children exist").action;
68            if child_action.is_protected() {
69                protected_queue.push_back(child_id);
70            } else {
71                let protected =
72                    protect_large_branches_recursive(graph, child_id, 0, max, &mut large_branches);
73                if protected {
74                    protected_queue.push_back(child_id);
75                }
76            }
77        }
78    }
79
80    large_branches
81}
82
83fn protect_large_branches_recursive(
84    graph: &mut Graph,
85    node_id: git2::Oid,
86    count: usize,
87    max: usize,
88    large_branches: &mut Vec<String>,
89) -> bool {
90    let mut needs_protection = false;
91
92    if !graph
93        .get(node_id)
94        .expect("all children exist")
95        .branches
96        .is_empty()
97    {
98    } else if count <= max {
99        let current_children = graph
100            .get(node_id)
101            .expect("all children exist")
102            .children
103            .clone();
104
105        for child_id in current_children {
106            needs_protection |=
107                protect_large_branches_recursive(graph, child_id, count + 1, max, large_branches);
108        }
109        if needs_protection {
110            let node = graph.get_mut(node_id).expect("all children exist");
111            node.action = crate::legacy::graph::Action::Protected;
112        }
113    } else {
114        mark_branch_protected(graph, node_id, large_branches);
115        needs_protection = true;
116    }
117
118    needs_protection
119}
120
121fn mark_branch_protected(graph: &mut Graph, node_id: git2::Oid, branches: &mut Vec<String>) {
122    let mut protected_queue = VecDeque::new();
123    protected_queue.push_back(node_id);
124    while let Some(current_id) = protected_queue.pop_front() {
125        let current = graph.get_mut(current_id).expect("all children exist");
126        current.action = crate::legacy::graph::Action::Protected;
127
128        if current.branches.is_empty() {
129            protected_queue.extend(&graph.get(current_id).expect("all children exist").children);
130        } else {
131            branches.extend(
132                current
133                    .branches
134                    .iter()
135                    // Remote branches are implicitly protected, so we don't need to record them
136                    .filter_map(|b| b.local_name().map(String::from)),
137            );
138        }
139    }
140}
141
142pub fn protect_old_branches(
143    graph: &mut Graph,
144    earlier_than: std::time::SystemTime,
145    ignore: &[git2::Oid],
146) -> Vec<String> {
147    let mut old_branches = Vec::new();
148
149    let mut protected_queue = VecDeque::new();
150    if graph.root().action.is_protected() {
151        protected_queue.push_back(graph.root_id());
152    }
153    while let Some(current_id) = protected_queue.pop_front() {
154        let current_children = graph
155            .get(current_id)
156            .expect("all children exist")
157            .children
158            .clone();
159
160        for child_id in current_children {
161            let child_action = graph.get(child_id).expect("all children exist").action;
162            if child_action.is_protected() {
163                protected_queue.push_back(child_id);
164            } else {
165                if is_branch_old(graph, child_id, earlier_than, ignore) {
166                    mark_branch_protected(graph, child_id, &mut old_branches);
167                }
168            }
169        }
170    }
171
172    old_branches
173}
174
175pub fn trim_old_branches(
176    graph: &mut Graph,
177    earlier_than: std::time::SystemTime,
178    ignore: &[git2::Oid],
179) -> Vec<String> {
180    let mut old_branches = Vec::new();
181
182    let mut protected_queue = VecDeque::new();
183    if graph.root().action.is_protected() {
184        protected_queue.push_back(graph.root_id());
185    }
186    while let Some(current_id) = protected_queue.pop_front() {
187        let current_children = graph
188            .get(current_id)
189            .expect("all children exist")
190            .children
191            .clone();
192
193        for child_id in current_children {
194            let child_action = graph.get(child_id).expect("all children exist").action;
195            if child_action.is_protected() {
196                protected_queue.push_back(child_id);
197            } else {
198                if is_branch_old(graph, child_id, earlier_than, ignore) {
199                    let removed = graph
200                        .remove_child(current_id, child_id)
201                        .expect("all children exist");
202                    old_branches.extend(removed.breadth_first_iter().flat_map(|n| {
203                        n.branches
204                            .iter()
205                            // Remote branches are implicitly protected, so we don't need to record them
206                            .filter_map(|b| b.local_name().map(String::from))
207                    }));
208                }
209            }
210        }
211    }
212
213    old_branches
214}
215
216fn is_branch_old(
217    graph: &Graph,
218    node_id: git2::Oid,
219    earlier_than: std::time::SystemTime,
220    ignore: &[git2::Oid],
221) -> bool {
222    if ignore.contains(&node_id) {
223        return false;
224    }
225
226    let current = graph.get(node_id).expect("all children exist");
227
228    if earlier_than < current.commit.time {
229        return false;
230    }
231
232    for child_id in current.children.iter().copied() {
233        if !is_branch_old(graph, child_id, earlier_than, ignore) {
234            return false;
235        }
236    }
237
238    true
239}
240
241pub fn protect_foreign_branches(
242    graph: &mut Graph,
243    user: &str,
244    ignore: &[git2::Oid],
245) -> Vec<String> {
246    let mut foreign_branches = Vec::new();
247
248    let mut protected_queue = VecDeque::new();
249    if graph.root().action.is_protected() {
250        protected_queue.push_back(graph.root_id());
251    }
252    while let Some(current_id) = protected_queue.pop_front() {
253        let current_children = graph
254            .get(current_id)
255            .expect("all children exist")
256            .children
257            .clone();
258
259        for child_id in current_children {
260            let child_action = graph.get(child_id).expect("all children exist").action;
261            if child_action.is_protected() {
262                protected_queue.push_back(child_id);
263            } else {
264                if !is_personal_branch(graph, child_id, user, ignore) {
265                    mark_branch_protected(graph, child_id, &mut foreign_branches);
266                }
267            }
268        }
269    }
270
271    foreign_branches
272}
273
274pub fn trim_foreign_branches(graph: &mut Graph, user: &str, ignore: &[git2::Oid]) -> Vec<String> {
275    let mut foreign_branches = Vec::new();
276
277    let mut protected_queue = VecDeque::new();
278    if graph.root().action.is_protected() {
279        protected_queue.push_back(graph.root_id());
280    }
281    while let Some(current_id) = protected_queue.pop_front() {
282        let current_children = graph
283            .get(current_id)
284            .expect("all children exist")
285            .children
286            .clone();
287
288        for child_id in current_children {
289            let child_action = graph.get(child_id).expect("all children exist").action;
290            if child_action.is_protected() {
291                protected_queue.push_back(child_id);
292            } else {
293                if !is_personal_branch(graph, child_id, user, ignore) {
294                    let removed = graph
295                        .remove_child(current_id, child_id)
296                        .expect("all children exist");
297                    foreign_branches.extend(removed.breadth_first_iter().flat_map(|n| {
298                        n.branches
299                            .iter()
300                            // Remote branches are implicitly protected, so we don't need to record them
301                            .filter_map(|b| b.local_name().map(String::from))
302                    }));
303                }
304            }
305        }
306    }
307
308    foreign_branches
309}
310
311fn is_personal_branch(graph: &Graph, node_id: git2::Oid, user: &str, ignore: &[git2::Oid]) -> bool {
312    if ignore.contains(&node_id) {
313        return true;
314    }
315
316    let current = graph.get(node_id).expect("all children exist");
317
318    if current.commit.committer.as_deref() == Some(user)
319        || current.commit.author.as_deref() == Some(user)
320    {
321        return true;
322    }
323
324    for child_id in current.children.iter().copied() {
325        if is_personal_branch(graph, child_id, user, ignore) {
326            return true;
327        }
328    }
329
330    false
331}
332
333/// Pre-requisites:
334/// - Running `protect_branches`
335///
336/// # Panics
337///
338/// - If `new_base_id` doesn't exist
339pub fn rebase_development_branches(graph: &mut Graph, new_base_id: git2::Oid) {
340    debug_assert!(graph.get(new_base_id).is_some());
341
342    let mut protected_queue = VecDeque::new();
343    if graph.root().action.is_protected() {
344        protected_queue.push_back(graph.root_id());
345    }
346    while let Some(current_id) = protected_queue.pop_front() {
347        let current_children = graph
348            .get(current_id)
349            .expect("all children exist")
350            .children
351            .clone();
352
353        let mut rebaseable = Vec::new();
354        for child_id in current_children {
355            let child_action = graph.get(child_id).expect("all children exist").action;
356            if child_action.is_protected() {
357                protected_queue.push_back(child_id);
358            } else {
359                rebaseable.push(child_id);
360            }
361        }
362
363        if !rebaseable.is_empty() {
364            let current = graph.get_mut(current_id).expect("all children exist");
365            for child_id in rebaseable.iter() {
366                current.children.remove(child_id);
367            }
368            graph
369                .get_mut(new_base_id)
370                .expect("pre-asserted")
371                .children
372                .extend(rebaseable);
373        }
374    }
375}
376
377/// Update branches from `pull_start` to `pull_end`
378///
379/// A normal `rebase_development_branches` only looks at development commits.  If `main` is pristine or if the
380/// user has branches on the same commit as `main`, we should also update these to what we pulled.
381pub fn fast_forward_pulled_branches(graph: &mut Graph, pull_start: git2::Oid, pull_end: git2::Oid) {
382    if pull_start == pull_end {
383        return;
384    }
385
386    let branches = std::mem::take(
387        &mut graph
388            .get_mut(pull_start)
389            .expect("all children exist")
390            .branches,
391    );
392    let (start_branches, end_branches) = branches.into_iter().partition(|b| b.remote.is_some());
393    graph
394        .get_mut(pull_start)
395        .expect("all children exist")
396        .branches = start_branches;
397    graph
398        .get_mut(pull_end)
399        .expect("all children exist")
400        .branches
401        .extend(end_branches);
402}
403
404pub fn pushable(graph: &mut Graph) {
405    let mut node_queue: VecDeque<(git2::Oid, Option<&str>)> = VecDeque::new();
406
407    // No idea if a parent commit invalidates our results
408    if graph.root().action.is_protected() {
409        node_queue.push_back((graph.root_id(), None));
410    }
411    while let Some((current_id, mut cause)) = node_queue.pop_front() {
412        let current = graph.get_mut(current_id).expect("all children exist");
413        if current.action.is_protected() {
414            if !current.branches.is_empty() {
415                let branch = &current.branches[0];
416                log::debug!("{} isn't pushable, branch is protected", branch);
417                // Don't set `cause` as that will block descendants
418            }
419        } else {
420            if cause.is_some() {
421                // Preserve existing cause
422            } else if !current.branches.is_empty()
423                && current.branches.iter().all(|b| Some(b.id) == b.push_id)
424            {
425                cause = Some("already pushed");
426            } else if current.commit.wip_summary().is_some() {
427                cause = Some("contains WIP commit");
428            }
429
430            if !current.branches.is_empty() {
431                let branch = &current.branches[0];
432                if let Some(c) = cause {
433                    log::debug!("{} isn't pushable, {}", branch, c);
434                    cause = Some("parent isn't pushable");
435                } else {
436                    log::debug!("{} is pushable", branch);
437                    current.pushable = true;
438                    cause = Some("parent is pushable");
439                }
440            }
441        }
442
443        node_queue.extend(current.children.iter().copied().map(|id| (id, cause)));
444    }
445}
446
447/// Quick pass for what is droppable
448///
449/// We get into this state when a branch is squashed.  The id would be different due to metadata
450/// but the `tree_id`, associated with the repo, is the same if your branch is up-to-date.
451///
452/// The big risk is if a commit was reverted.  To protect against this, we only look at the final
453/// state of the branch and then check if it looks like a revert.
454///
455/// To avoid walking too much of the tree, we are going to assume only the first branch in a stack
456/// could have been squash-merged.
457///
458/// This assumes that the Node was rebased onto all of the new potentially squash-merged Nodes and
459/// we extract the potential `tree_id`'s from those protected commits.
460pub fn drop_squashed_by_tree_id(
461    graph: &mut Graph,
462    pulled_tree_ids: impl Iterator<Item = git2::Oid>,
463) {
464    let pulled_tree_ids: HashSet<_> = pulled_tree_ids.collect();
465
466    let mut protected_queue = VecDeque::new();
467    let root_action = graph.root().action;
468    if root_action.is_protected() {
469        protected_queue.push_back(graph.root_id());
470    }
471    while let Some(current_id) = protected_queue.pop_front() {
472        let current_children = graph
473            .get(current_id)
474            .expect("all children exist")
475            .children
476            .clone();
477
478        for child_id in current_children {
479            let child_action = graph.get(child_id).expect("all children exist").action;
480            if child_action.is_protected() || child_action.is_delete() {
481                protected_queue.push_back(child_id);
482            } else {
483                drop_first_branch_by_tree_id(graph, child_id, HashSet::new(), &pulled_tree_ids);
484            }
485        }
486    }
487}
488
489fn drop_first_branch_by_tree_id(
490    graph: &mut Graph,
491    node_id: git2::Oid,
492    mut branch_ids: HashSet<git2::Oid>,
493    pulled_tree_ids: &HashSet<git2::Oid>,
494) {
495    branch_ids.insert(node_id);
496
497    let node = graph.get(node_id).expect("all children exist");
498    debug_assert!(!node.action.is_protected());
499    if node.commit.revert_summary().is_some() {
500        // Might not *actually* be a revert or something more complicated might be going on.  Let's
501        // just be cautious.
502        return;
503    }
504
505    let is_branch = !node.branches.is_empty();
506    let node_tree_id = node.commit.tree_id;
507
508    if is_branch {
509        if pulled_tree_ids.contains(&node_tree_id) {
510            for branch_id in branch_ids {
511                graph.get_mut(branch_id).expect("all children exist").action =
512                    crate::legacy::graph::Action::Delete;
513            }
514        }
515    } else {
516        let node_children = graph
517            .get(node_id)
518            .expect("all children exist")
519            .children
520            .clone();
521        match node_children.len() {
522            0 => {}
523            1 => {
524                let child_id = node_children.into_iter().next().unwrap();
525                drop_first_branch_by_tree_id(graph, child_id, branch_ids, pulled_tree_ids);
526            }
527            _ => {
528                for child_id in node_children {
529                    drop_first_branch_by_tree_id(
530                        graph,
531                        child_id,
532                        branch_ids.clone(),
533                        pulled_tree_ids,
534                    );
535                }
536            }
537        }
538    }
539}
540
541/// Drop branches merged among the pulled IDs
542///
543/// The removal in `graph` is purely superficial since nothing can act on it.  The returned branch
544/// names is the important part.
545pub fn drop_merged_branches(
546    graph: &mut Graph,
547    pulled_ids: impl Iterator<Item = git2::Oid>,
548    protected_branches: &crate::legacy::git::Branches,
549) -> Vec<String> {
550    let mut removed = Vec::new();
551    let protected_branch_names: HashSet<_> = protected_branches
552        .iter()
553        .flat_map(|(_, b)| b.iter())
554        // We'll filter out remote branches lateer
555        .filter_map(|b| b.local_name())
556        .collect();
557
558    for pulled_id in pulled_ids {
559        // HACK: Depending on how merges in master worked out, not all commits will be present
560        if let Some(node) = graph.get_mut(pulled_id) {
561            if !node.branches.is_empty() {
562                for i in (node.branches.len() - 1)..=0 {
563                    let branch = &node.branches[i];
564                    if let Some(local_name) = branch.local_name() {
565                        if !protected_branch_names.contains(local_name) {
566                            let branch = node.branches.remove(i);
567                            assert!(branch.remote.is_none());
568                            removed.push(branch.name);
569                        }
570                    }
571                }
572            }
573        }
574    }
575
576    removed
577}
578
579pub fn fixup(graph: &mut Graph, effect: crate::config::Fixup) {
580    if effect == crate::config::Fixup::Ignore {
581        return;
582    }
583
584    let mut protected_queue = VecDeque::new();
585    let root_action = graph.root().action;
586    if root_action.is_protected() {
587        protected_queue.push_back(graph.root_id());
588    }
589    while let Some(current_id) = protected_queue.pop_front() {
590        let current_children = graph
591            .get(current_id)
592            .expect("all children exist")
593            .children
594            .clone();
595
596        for child_id in current_children {
597            let child_action = graph.get(child_id).expect("all children exist").action;
598            if child_action.is_protected() || child_action.is_delete() {
599                protected_queue.push_back(child_id);
600            } else {
601                fixup_branch(graph, current_id, child_id, effect);
602            }
603        }
604    }
605}
606
607fn fixup_branch(
608    graph: &mut Graph,
609    base_id: git2::Oid,
610    mut node_id: git2::Oid,
611    effect: crate::config::Fixup,
612) {
613    debug_assert_ne!(effect, crate::config::Fixup::Ignore);
614
615    let mut outstanding = BTreeMap::new();
616    let node_children = graph
617        .get(node_id)
618        .expect("all children exist")
619        .children
620        .clone();
621    for child_id in node_children {
622        fixup_node(graph, node_id, child_id, effect, &mut outstanding);
623    }
624    if !outstanding.is_empty() {
625        let node = graph.get_mut(node_id).expect("all children exist");
626        if let Some(fixup_ids) = outstanding.remove(&node.commit.summary) {
627            if effect == crate::config::Fixup::Squash {
628                for fixup_id in fixup_ids.iter().copied() {
629                    let fixup = graph.get_mut(fixup_id).expect("all children exist");
630                    assert!(fixup.action == crate::legacy::graph::Action::Pick);
631                    fixup.action = crate::legacy::graph::Action::Fixup;
632                }
633            }
634            splice_after(graph, node_id, fixup_ids);
635        }
636        debug_assert_ne!(
637            graph.get(node_id).expect("all children exist").action,
638            crate::legacy::graph::Action::Protected,
639            "Unexpected result for {base_id}"
640        );
641        for fixup_ids in outstanding.into_values() {
642            node_id = splice_between(graph, base_id, node_id, fixup_ids);
643        }
644    }
645}
646
647fn fixup_node(
648    graph: &mut Graph,
649    base_id: git2::Oid,
650    node_id: git2::Oid,
651    effect: crate::config::Fixup,
652    outstanding: &mut BTreeMap<bstr::BString, Vec<git2::Oid>>,
653) {
654    debug_assert_ne!(effect, crate::config::Fixup::Ignore);
655
656    let node_children = graph
657        .get(node_id)
658        .expect("all children exist")
659        .children
660        .clone();
661    for child_id in node_children {
662        fixup_node(graph, node_id, child_id, effect, outstanding);
663    }
664
665    let mut patch = None;
666    let mut fixup_ids = Vec::new();
667    {
668        let node = graph.get_mut(node_id).expect("all children exist");
669        debug_assert_ne!(node.action, crate::legacy::graph::Action::Protected);
670        debug_assert_ne!(node.action, crate::legacy::graph::Action::Delete);
671        if let Some(summary) = node.commit.fixup_summary() {
672            outstanding
673                .entry(summary.to_owned())
674                .or_default()
675                .push(node_id);
676
677            let mut children = Default::default();
678            std::mem::swap(&mut node.children, &mut children);
679            let mut branches = Default::default();
680            std::mem::swap(&mut node.branches, &mut branches);
681            patch = Some((children, branches));
682        } else if let Some(ids) = outstanding.remove(&node.commit.summary) {
683            fixup_ids = ids;
684        }
685    }
686
687    if let Some((children, branches)) = patch {
688        debug_assert!(fixup_ids.is_empty());
689
690        let base = graph.get_mut(base_id).expect("all children exist");
691        debug_assert_ne!(base.action, crate::legacy::graph::Action::Protected);
692        debug_assert_ne!(base.action, crate::legacy::graph::Action::Delete);
693        base.children.remove(&node_id);
694        base.children.extend(children);
695        base.branches.extend(branches);
696    } else if !fixup_ids.is_empty() {
697        if effect == crate::config::Fixup::Squash {
698            for fixup_id in fixup_ids.iter().copied() {
699                let fixup = graph.get_mut(fixup_id).expect("all children exist");
700                assert!(fixup.action == crate::legacy::graph::Action::Pick);
701                fixup.action = crate::legacy::graph::Action::Fixup;
702            }
703        }
704        splice_after(graph, node_id, fixup_ids);
705    }
706}
707
708// Does not update references
709fn splice_between(
710    graph: &mut Graph,
711    parent_id: git2::Oid,
712    child_id: git2::Oid,
713    node_ids: Vec<git2::Oid>,
714) -> git2::Oid {
715    let mut new_child_id = child_id;
716    for node_id in node_ids {
717        let node = graph.get_mut(node_id).expect("all children exist");
718        debug_assert!(node.children.is_empty());
719        node.children.insert(new_child_id);
720        new_child_id = node.commit.id;
721    }
722    let parent = graph.get_mut(parent_id).expect("all children exist");
723    parent.children.remove(&child_id);
724    parent.children.insert(new_child_id);
725    new_child_id
726}
727
728// Updates references
729fn splice_after(graph: &mut Graph, node_id: git2::Oid, fixup_ids: Vec<git2::Oid>) {
730    if fixup_ids.is_empty() {
731        return;
732    }
733
734    let mut new_children = Default::default();
735    let mut new_branches = Default::default();
736    {
737        let node = graph.get_mut(node_id).expect("all children exist");
738        std::mem::swap(&mut node.children, &mut new_children);
739        std::mem::swap(&mut node.branches, &mut new_branches);
740    }
741
742    let mut last_id = node_id;
743    for fixup_id in fixup_ids.into_iter().rev() {
744        let last = graph.get_mut(last_id).expect("all children exist");
745        last.children.insert(fixup_id);
746        last_id = fixup_id;
747    }
748
749    {
750        let last = graph.get_mut(last_id).expect("all children exist");
751        debug_assert!(last.children.is_empty());
752        debug_assert!(last.branches.is_empty());
753        std::mem::swap(&mut last.children, &mut new_children);
754        std::mem::swap(&mut last.branches, &mut new_branches);
755    }
756}
757
758/// When a branch has extra commits, update dependent branches to the latest
759pub fn realign_stacks(graph: &mut Graph) {
760    let mut protected_queue = VecDeque::new();
761    let root_action = graph.root().action;
762    if root_action.is_protected() {
763        protected_queue.push_back(graph.root_id());
764    }
765    while let Some(current_id) = protected_queue.pop_front() {
766        let current_children = graph
767            .get(current_id)
768            .expect("all children exist")
769            .children
770            .clone();
771
772        for child_id in current_children {
773            let child_action = graph.get(child_id).expect("all children exist").action;
774            if child_action.is_protected() || child_action.is_delete() {
775                protected_queue.push_back(child_id);
776            } else {
777                realign_stack(graph, child_id);
778            }
779        }
780    }
781}
782
783fn realign_stack(graph: &mut Graph, node_id: git2::Oid) {
784    let mut children = std::collections::BTreeSet::new();
785
786    let mut current_id = node_id;
787    loop {
788        let current = graph.get_mut(current_id).expect("all children exist");
789        if current.branches.is_empty() {
790            let mut current_children: Vec<_> = current.children.iter().copied().collect();
791            match current_children.len() {
792                0 => {
793                    current.children.extend(children);
794                    return;
795                }
796                1 => {
797                    current_id = current_children.into_iter().next().unwrap();
798                }
799                _ => {
800                    // Assuming the more recent work is a continuation of the existing stack and
801                    // aligning the other stacks to be on top of it
802                    //
803                    // This should be safe in light of our rebases since we don't preserve the time
804                    current_children.sort_unstable_by_key(|id| {
805                        graph.get(*id).expect("all children exist").commit.time
806                    });
807                    let newest = current_children.pop().unwrap();
808                    {
809                        let current = graph.get_mut(current_id).expect("all children exist");
810                        for child_id in &current_children {
811                            current.children.remove(child_id);
812                        }
813                    }
814                    children.extend(current_children);
815                    current_id = newest;
816                }
817            }
818        } else {
819            if !children.is_empty() {
820                current.children.extend(children);
821            }
822            return;
823        }
824    }
825}
826
827/// When a rebase has split stack, re-combine them
828pub fn merge_stacks(graph: &mut Graph) {
829    let mut protected_queue = VecDeque::new();
830    let root_action = graph.root().action;
831    if root_action.is_protected() {
832        protected_queue.push_back(graph.root_id());
833    }
834    while let Some(current_id) = protected_queue.pop_front() {
835        let current_children = graph
836            .get(current_id)
837            .expect("all children exist")
838            .children
839            .clone();
840
841        let mut unprotected_children = CommitTimesByTreeId::new();
842        for child_id in current_children {
843            let child_action = graph.get(child_id).expect("all children exist").action;
844            if child_action.is_protected() || child_action.is_delete() {
845                protected_queue.push_back(child_id);
846            } else {
847                let child = graph.get(child_id).expect("all children exist");
848
849                unprotected_children
850                    .entry(child.commit.tree_id)
851                    .or_default()
852                    .push((child.commit.time, child_id));
853            }
854        }
855        if !unprotected_children.is_empty() {
856            merge_child_stacks(graph, current_id, unprotected_children);
857        }
858    }
859}
860
861type CommitTimesByTreeId = BTreeMap<git2::Oid, Vec<(std::time::SystemTime, git2::Oid)>>;
862
863fn merge_child_stacks(graph: &mut Graph, node_id: git2::Oid, children: CommitTimesByTreeId) {
864    let mut queue = VecDeque::new();
865    queue.push_back((node_id, children));
866    while let Some((current_id, children)) = queue.pop_front() {
867        for (_, mut children) in children {
868            match children.len() {
869                0 => unreachable!("each entry has at least one commit"),
870                1 => {
871                    enqueue_merge_stack(&mut queue, graph, children[0].1);
872                }
873                _ => {
874                    children.sort_unstable();
875                    let last_index = children.len() - 1;
876                    let last_child_id = children[last_index].1;
877
878                    for (_, child_id) in &children[0..last_index] {
879                        let mut grandchildren = Default::default();
880                        let mut branches = Default::default();
881
882                        let child = graph.get_mut(*child_id).expect("all children exist");
883                        std::mem::swap(&mut child.branches, &mut branches);
884                        std::mem::swap(&mut child.children, &mut grandchildren);
885
886                        let last_child = graph.get_mut(last_child_id).expect("all children exist");
887                        last_child.branches.extend(branches);
888                        last_child.children.extend(grandchildren);
889
890                        graph.remove_child(current_id, *child_id);
891                    }
892
893                    enqueue_merge_stack(&mut queue, graph, last_child_id);
894                }
895            }
896        }
897    }
898}
899
900fn enqueue_merge_stack(
901    queue: &mut VecDeque<(git2::Oid, CommitTimesByTreeId)>,
902    graph: &Graph,
903    node_id: git2::Oid,
904) {
905    let mut unprotected_children = CommitTimesByTreeId::new();
906    for child_id in graph
907        .get(node_id)
908        .expect("all children exist")
909        .children
910        .iter()
911        .copied()
912    {
913        let child = graph.get(child_id).expect("all children exist");
914
915        unprotected_children
916            .entry(child.commit.tree_id)
917            .or_default()
918            .push((child.commit.time, child_id));
919    }
920    if !unprotected_children.is_empty() {
921        queue.push_back((node_id, unprotected_children));
922    }
923}
924
925pub fn to_script(graph: &Graph) -> crate::legacy::git::Script {
926    let mut script = crate::legacy::git::Script::new();
927
928    let mut protected_queue = VecDeque::new();
929    if graph.root().action.is_protected() {
930        protected_queue.push_back(graph.root_id());
931    }
932    while let Some(current_id) = protected_queue.pop_front() {
933        let current = graph.get(current_id).expect("all children exist");
934
935        for child_id in current.children.iter().copied() {
936            let child = graph.get(child_id).expect("all children exist");
937            let child_action = child.action;
938            if child_action.is_protected() {
939                if !child.branches.is_empty() {
940                    // We might be updating protected branches as part of a `pull --rebase`,
941                    let stack_mark = child.commit.id;
942                    script
943                        .commands
944                        .push(crate::legacy::git::Command::SwitchCommit(stack_mark));
945                    for branch in child.branches.iter() {
946                        if let Some(local_name) = branch.local_name() {
947                            script
948                                .commands
949                                .push(crate::legacy::git::Command::CreateBranch(
950                                    local_name.to_owned(),
951                                ));
952                        }
953                    }
954                }
955                protected_queue.push_back(child_id);
956            } else if let Some(mut dependent) = node_to_script(graph, child_id) {
957                dependent
958                    .commands
959                    .insert(0, crate::legacy::git::Command::SwitchCommit(current_id));
960                script.dependents.push(dependent);
961            }
962        }
963    }
964
965    script
966}
967
968fn node_to_script(graph: &Graph, node_id: git2::Oid) -> Option<crate::legacy::git::Script> {
969    let mut script = crate::legacy::git::Script::new();
970
971    let node = graph.get(node_id).expect("all children exist");
972    match node.action {
973        crate::legacy::graph::Action::Pick => {
974            script
975                .commands
976                .push(crate::legacy::git::Command::CherryPick(node.commit.id));
977            for branch in node.branches.iter() {
978                if let Some(local_name) = branch.local_name() {
979                    script
980                        .commands
981                        .push(crate::legacy::git::Command::CreateBranch(
982                            local_name.to_owned(),
983                        ));
984                }
985            }
986
987            let node_dependents: Vec<_> = node
988                .children
989                .iter()
990                .copied()
991                .filter_map(|child_id| node_to_script(graph, child_id))
992                .collect();
993            if !node_dependents.is_empty() {
994                // End the transaction on branch boundaries
995                let transaction = !node.branches.is_empty();
996                extend_dependents(node, &mut script, node_dependents, transaction);
997            }
998        }
999        crate::legacy::graph::Action::Fixup => {
1000            script
1001                .commands
1002                .push(crate::legacy::git::Command::Fixup(node.commit.id));
1003            // We can't re-target the branches of the commit we are squashing into, so the ops that
1004            // creates a `Fixup` option has to handle that.
1005            for branch in node.branches.iter() {
1006                if let Some(local_name) = branch.local_name() {
1007                    script
1008                        .commands
1009                        .push(crate::legacy::git::Command::CreateBranch(
1010                            local_name.to_owned(),
1011                        ));
1012                }
1013            }
1014
1015            let node_dependents: Vec<_> = node
1016                .children
1017                .iter()
1018                .copied()
1019                .filter_map(|child_id| node_to_script(graph, child_id))
1020                .collect();
1021            if !node_dependents.is_empty() {
1022                // End the transaction on branch boundaries
1023                let transaction = !node.branches.is_empty();
1024                extend_dependents(node, &mut script, node_dependents, transaction);
1025            }
1026        }
1027        crate::legacy::graph::Action::Protected => {
1028            let node_dependents: Vec<_> = node
1029                .children
1030                .iter()
1031                .copied()
1032                .filter_map(|child_id| node_to_script(graph, child_id))
1033                .collect();
1034            if !node_dependents.is_empty() || !node.branches.is_empty() {
1035                let stack_mark = node.commit.id;
1036                script
1037                    .commands
1038                    .push(crate::legacy::git::Command::SwitchCommit(stack_mark));
1039                // We might be updating protected branches as part of a `pull --rebase`,
1040                for branch in node.branches.iter() {
1041                    if let Some(local_name) = branch.local_name() {
1042                        script
1043                            .commands
1044                            .push(crate::legacy::git::Command::CreateBranch(
1045                                local_name.to_owned(),
1046                            ));
1047                    }
1048                }
1049
1050                // No transactions needed for protected commits
1051                let transaction = false;
1052                extend_dependents(node, &mut script, node_dependents, transaction);
1053            }
1054        }
1055        crate::legacy::graph::Action::Delete => {
1056            for branch in node.branches.iter() {
1057                if let Some(local_name) = branch.local_name() {
1058                    script
1059                        .commands
1060                        .push(crate::legacy::git::Command::CreateBranch(
1061                            local_name.to_owned(),
1062                        ));
1063                }
1064            }
1065
1066            let node_dependents: Vec<_> = node
1067                .children
1068                .iter()
1069                .copied()
1070                .filter_map(|child_id| node_to_script(graph, child_id))
1071                .collect();
1072            if !node_dependents.is_empty() {
1073                // End the transaction on branch boundaries
1074                let transaction = !node.branches.is_empty();
1075                extend_dependents(node, &mut script, node_dependents, transaction);
1076            }
1077        }
1078    }
1079
1080    if script.is_empty() {
1081        None
1082    } else {
1083        Some(script)
1084    }
1085}
1086
1087fn extend_dependents(
1088    node: &Node,
1089    script: &mut crate::legacy::git::Script,
1090    mut dependents: Vec<crate::legacy::git::Script>,
1091    transaction: bool,
1092) {
1093    // Create transactions at the branch boundaries
1094    if !transaction && dependents.len() == 1 {
1095        let dependent = dependents.remove(0);
1096        script.commands.extend(dependent.commands);
1097        script.dependents.extend(dependent.dependents);
1098    } else {
1099        // Ensure each dependent can pick up where needed
1100        let stack_mark = node.commit.id;
1101        script
1102            .commands
1103            .push(crate::legacy::git::Command::RegisterMark(stack_mark));
1104        for dependent in dependents.iter_mut() {
1105            dependent
1106                .commands
1107                .insert(0, crate::legacy::git::Command::SwitchMark(stack_mark));
1108        }
1109        script.dependents.extend(dependents);
1110    }
1111}