1use crate::hash::Hash;
13use crate::object::Object;
14use crate::store::{ObjectStore, StoreError};
15
16use super::merge::{self, Conflict};
17
18#[derive(Debug, thiserror::Error)]
22pub enum CherryPickError {
23 #[error("target hash does not refer to a commit object")]
24 NotACommit,
25 #[error("target commit's first parent does not refer to a commit object")]
26 ParentNotACommit,
27 #[error(transparent)]
28 Store(#[from] StoreError),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CherryPickResult {
38 pub tree_hash: Hash,
39 pub conflicts: Vec<Conflict>,
40 pub original_message: Vec<u8>,
41}
42
43impl CherryPickResult {
44 #[must_use]
45 pub fn has_conflicts(&self) -> bool {
46 !self.conflicts.is_empty()
47 }
48}
49
50pub fn cherry_pick(
70 store: &ObjectStore,
71 target_hash: Hash,
72 ours_tree: Hash,
73) -> Result<CherryPickResult, CherryPickError> {
74 let Object::Commit(target_commit) = store.read_object(&target_hash)? else {
75 return Err(CherryPickError::NotACommit);
76 };
77
78 let parent_tree: Option<Hash> = if target_commit.parents.is_empty() {
79 None
80 } else {
81 let Object::Commit(parent_commit) = store.read_object(&target_commit.parents[0])? else {
82 return Err(CherryPickError::ParentNotACommit);
83 };
84 Some(parent_commit.tree_hash)
85 };
86
87 let original_message = target_commit.message.clone();
88 let merge_result = merge::merge_trees(
89 store,
90 parent_tree,
91 Some(ours_tree),
92 Some(target_commit.tree_hash),
93 )?;
94
95 Ok(CherryPickResult {
96 tree_hash: merge_result.tree_hash,
97 conflicts: merge_result.conflicts,
98 original_message,
99 })
100}
101
102#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::object::{Blob, Commit, EntryMode, Identity, Object, Tree, TreeEntry};
110 use crate::ops::merge::ConflictKind;
111 use crate::serialize;
112 use tempfile::TempDir;
113
114 fn store() -> (TempDir, ObjectStore) {
115 let d = TempDir::new().unwrap();
116 let s = ObjectStore::init(d.path()).unwrap();
117 (d, s)
118 }
119 fn put_blob(s: &ObjectStore, data: &[u8]) -> Hash {
120 let bytes = serialize::serialize(&Object::Blob(Blob {
121 data: data.to_vec(),
122 }))
123 .unwrap();
124 s.write(&bytes).unwrap()
125 }
126 fn make_tree(s: &ObjectStore, entries: Vec<TreeEntry>) -> Hash {
127 let bytes = serialize::serialize(&Object::Tree(Tree { entries })).unwrap();
128 s.write(&bytes).unwrap()
129 }
130 fn entry(name: &[u8], mode: EntryMode, h: Hash) -> TreeEntry {
131 TreeEntry {
132 name: name.to_vec(),
133 mode,
134 object_hash: h,
135 }
136 }
137 fn make_commit(s: &ObjectStore, tree: Hash, parents: &[Hash], message: &str) -> Hash {
138 let c = Commit {
139 tree_hash: tree,
140 parents: parents.to_vec(),
141 author: Identity::ed25519([0; 32]),
142 signer: [0; 32],
143 message: message.as_bytes().to_vec(),
144 timestamp: message.len() as u64,
145 message_hash: [0; 32],
146 content_digest: [0; 32],
147 signature: [0; 64],
148 };
149 s.write(&serialize::serialize(&Object::Commit(c)).unwrap())
150 .unwrap()
151 }
152 fn tree_entries(s: &ObjectStore, h: Hash) -> Vec<TreeEntry> {
153 match s.read_object(&h).unwrap() {
154 Object::Tree(t) => t.entries,
155 other => panic!("expected tree, got {other}"),
156 }
157 }
158
159 #[test]
160 fn adds_a_file_onto_branch_missing_it() {
161 let (_d, s) = store();
162 let blob_a = put_blob(&s, b"aaa");
163 let base_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
164 let base_commit = make_commit(&s, base_tree, &[], "initial");
165 let blob_b = put_blob(&s, b"bbb");
166 let target_tree = make_tree(
167 &s,
168 vec![
169 entry(b"a.txt", EntryMode::Blob, blob_a),
170 entry(b"b.txt", EntryMode::Blob, blob_b),
171 ],
172 );
173 let target_commit = make_commit(&s, target_tree, &[base_commit], "add b.txt");
174
175 let r = cherry_pick(&s, target_commit, base_tree).unwrap();
176 assert!(!r.has_conflicts());
177 assert_eq!(r.original_message, b"add b.txt");
178 let merged = tree_entries(&s, r.tree_hash);
179 assert_eq!(merged.len(), 2);
180 }
181
182 #[test]
183 fn modify_modify_conflict() {
184 let (_d, s) = store();
185 let blob_orig = put_blob(&s, b"original");
186 let base_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_orig)]);
187 let base_commit = make_commit(&s, base_tree, &[], "initial");
188 let blob_theirs = put_blob(&s, b"theirs-change");
189 let target_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_theirs)]);
190 let target_commit = make_commit(&s, target_tree, &[base_commit], "change a.txt");
191 let blob_ours = put_blob(&s, b"ours-change");
192 let ours_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_ours)]);
193
194 let r = cherry_pick(&s, target_commit, ours_tree).unwrap();
195 assert!(r.has_conflicts());
196 assert_eq!(r.conflicts.len(), 1);
197 assert_eq!(r.conflicts[0].path, "a.txt");
198 assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
199 assert_eq!(r.original_message, b"change a.txt");
200 }
201
202 #[test]
203 fn root_commit_no_parent() {
204 let (_d, s) = store();
205 let blob_a = put_blob(&s, b"aaa");
206 let root_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
207 let root_commit = make_commit(&s, root_tree, &[], "root commit");
208 let blob_b = put_blob(&s, b"bbb");
209 let ours_tree = make_tree(&s, vec![entry(b"b.txt", EntryMode::Blob, blob_b)]);
210 let r = cherry_pick(&s, root_commit, ours_tree).unwrap();
211 assert!(!r.has_conflicts());
212 assert_eq!(r.original_message, b"root commit");
213 assert_eq!(tree_entries(&s, r.tree_hash).len(), 2);
214 }
215
216 #[test]
217 fn delete_modify_conflict() {
218 let (_d, s) = store();
219 let blob_a = put_blob(&s, b"original");
220 let base_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
221 let base_commit = make_commit(&s, base_tree, &[], "initial");
222 let target_tree = make_tree(&s, vec![]);
223 let target_commit = make_commit(&s, target_tree, &[base_commit], "remove a.txt");
224 let blob_modified = put_blob(&s, b"modified content");
225 let ours_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_modified)]);
226 let r = cherry_pick(&s, target_commit, ours_tree).unwrap();
227 assert!(r.has_conflicts());
228 assert_eq!(r.conflicts[0].kind, ConflictKind::DeleteModify);
229 assert_eq!(r.conflicts[0].path, "a.txt");
230 }
231
232 #[test]
233 fn adds_multiple_files() {
234 let (_d, s) = store();
235 let blob_a = put_blob(&s, b"aaa");
236 let base_tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
237 let base_commit = make_commit(&s, base_tree, &[], "initial");
238 let blob_b = put_blob(&s, b"bbb");
239 let blob_c = put_blob(&s, b"ccc");
240 let blob_d = put_blob(&s, b"ddd");
241 let target_tree = make_tree(
242 &s,
243 vec![
244 entry(b"a.txt", EntryMode::Blob, blob_a),
245 entry(b"b.txt", EntryMode::Blob, blob_b),
246 entry(b"c.txt", EntryMode::Blob, blob_c),
247 entry(b"d.txt", EntryMode::Blob, blob_d),
248 ],
249 );
250 let target_commit = make_commit(&s, target_tree, &[base_commit], "add b, c, d");
251 let r = cherry_pick(&s, target_commit, base_tree).unwrap();
252 assert!(!r.has_conflicts());
253 assert_eq!(tree_entries(&s, r.tree_hash).len(), 4);
254 }
255
256 #[test]
257 fn non_commit_input_returns_error() {
258 let (_d, s) = store();
259 let blob_hash = put_blob(&s, b"just a blob");
260 let empty_tree = make_tree(&s, vec![]);
261 let err = cherry_pick(&s, blob_hash, empty_tree).unwrap_err();
262 assert!(matches!(err, CherryPickError::NotACommit));
263 }
264
265 #[test]
266 fn root_commit_onto_empty_ours() {
267 let (_d, s) = store();
268 let blob_a = put_blob(&s, b"aaa");
269 let blob_b = put_blob(&s, b"bbb");
270 let root_tree = make_tree(
271 &s,
272 vec![
273 entry(b"a.txt", EntryMode::Blob, blob_a),
274 entry(b"b.txt", EntryMode::Blob, blob_b),
275 ],
276 );
277 let root_commit = make_commit(&s, root_tree, &[], "root");
278 let empty_tree = make_tree(&s, vec![]);
279 let r = cherry_pick(&s, root_commit, empty_tree).unwrap();
280 assert!(!r.has_conflicts());
281 assert_eq!(tree_entries(&s, r.tree_hash).len(), 2);
282 }
283}