1use 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
18pub 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 ¤t_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 ¤t_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, ¤t_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 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}