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