Skip to main content

verbs/merge/
plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared merge planning seam for preview and apply flows.
3
4use anyhow::{Result, anyhow};
5use merge::{ConflictLabels, TreeMergeResult, merge_trees};
6use objects::{
7    object::{StateId, StructuredConflict},
8    store::ObjectStore,
9};
10use repo::{CommitGraphIndex, Repository};
11
12use super::{
13    RepositoryMergeBlobSource, advice, map_tree_merge_error,
14    relation::{MergeRelation, MergeRelationKind},
15    tree_merge_options,
16};
17
18/// Planned merge relationship plus optional 3-way tree merge result.
19pub struct MergePlan {
20    relation: MergeRelation,
21    merge_result: Option<TreeMergeResult>,
22    structured_conflicts: Option<StructuredConflict>,
23}
24
25impl MergePlan {
26    pub fn for_merge_command(
27        repo: &Repository,
28        graph: &mut CommitGraphIndex<'_>,
29        current_state_id: &StateId,
30        target_state_id: &StateId,
31        labels: ConflictLabels<'_>,
32    ) -> Result<Self> {
33        Self::build(
34            repo,
35            graph,
36            current_state_id,
37            target_state_id,
38            MergeRelationKind::AlreadyUpToDate,
39            labels,
40        )
41    }
42
43    pub fn for_thread_preview(
44        repo: &Repository,
45        graph: &mut CommitGraphIndex<'_>,
46        target_state_id: &StateId,
47        thread_state_id: &StateId,
48        labels: ConflictLabels<'_>,
49    ) -> Result<Self> {
50        Self::build(
51            repo,
52            graph,
53            target_state_id,
54            thread_state_id,
55            MergeRelationKind::AlreadyIntegrated,
56            labels,
57        )
58    }
59
60    pub fn relation(&self) -> &MergeRelation {
61        &self.relation
62    }
63
64    pub fn merge_result(&self) -> Option<&TreeMergeResult> {
65        self.merge_result.as_ref()
66    }
67
68    pub fn structured_conflicts(&self) -> Option<&StructuredConflict> {
69        self.structured_conflicts.as_ref()
70    }
71
72    fn build(
73        repo: &Repository,
74        graph: &mut CommitGraphIndex<'_>,
75        current_state_id: &StateId,
76        target_state_id: &StateId,
77        integrated_kind: MergeRelationKind,
78        labels: ConflictLabels<'_>,
79    ) -> Result<Self> {
80        if graph.is_ancestor(target_state_id, current_state_id)? {
81            return Ok(Self {
82                relation: MergeRelation::new(
83                    integrated_kind,
84                    *current_state_id,
85                    *target_state_id,
86                    None,
87                    0,
88                ),
89                merge_result: None,
90                structured_conflicts: None,
91            });
92        }
93
94        if graph.is_ancestor(current_state_id, target_state_id)? {
95            return Ok(Self {
96                relation: MergeRelation::new(
97                    MergeRelationKind::FastForward,
98                    *current_state_id,
99                    *target_state_id,
100                    None,
101                    0,
102                ),
103                merge_result: None,
104                structured_conflicts: None,
105            });
106        }
107
108        let merge_base_id = graph
109            .find_merge_base(current_state_id, target_state_id)?
110            .ok_or_else(|| {
111                anyhow!(advice::merge_no_common_ancestor(
112                    &current_state_id.short(),
113                    &target_state_id.short(),
114                ))
115            })?;
116        let base_tree = load_tree(repo, &merge_base_id)?;
117        let current_tree = load_tree(repo, current_state_id)?;
118        let target_tree = load_tree(repo, target_state_id)?;
119        let blob_source = RepositoryMergeBlobSource { repo };
120        let merge_result = merge_trees(
121            repo.store(),
122            &blob_source,
123            &base_tree,
124            &current_tree,
125            &target_tree,
126            tree_merge_options(labels),
127        )
128        .map_err(map_tree_merge_error)?;
129        let relation_kind = if merge_result.conflicts.is_empty() {
130            MergeRelationKind::CleanApply
131        } else {
132            MergeRelationKind::Conflicted
133        };
134        let structured_conflicts = if merge_result.conflicts.is_empty() {
135            None
136        } else {
137            Some(super::structured::build_conflict_payload(
138                repo,
139                (merge_base_id, &base_tree),
140                (*current_state_id, &current_tree),
141                (*target_state_id, &target_tree),
142                &merge_result.tree,
143                &merge_result.conflicts,
144            )?)
145        };
146
147        Ok(Self {
148            relation: MergeRelation::new(
149                relation_kind,
150                *current_state_id,
151                *target_state_id,
152                Some(merge_base_id),
153                merge_result.conflicts.len(),
154            ),
155            merge_result: Some(merge_result),
156            structured_conflicts,
157        })
158    }
159}
160
161fn load_tree(repo: &Repository, state_id: &StateId) -> Result<objects::object::Tree> {
162    let state = repo
163        .store()
164        .get_state(state_id)?
165        .ok_or_else(|| anyhow!("State '{}' not found", state_id.short()))?;
166    // Surface missing trees as integrity refusals instead of silently treating
167    // a corrupt store as an empty side.
168    repo.store().get_tree(&state.tree)?.ok_or_else(|| {
169        anyhow!(advice::merge_integrity_refusal(
170            format!(
171                "State {} references missing tree {}",
172                state_id.short(),
173                state.tree
174            ),
175            format!(
176                "state {} references tree {} but the object store has no such tree",
177                state_id.short(),
178                state.tree
179            ),
180            "merging against a missing tree would silently treat that side as empty and could erase tracked files",
181            "merge stopped before writing refs, metadata, or worktree files",
182        ))
183    })
184}