lit/commands/
cherry_pick.rs1use crate::core::{
2 find_repo_root, get_current_branch, read_head, write_ref, Commit, Object, ObjectHash, Tree,
3};
4use crate::response::CherryPickResponse;
5use crate::storage::ObjectStore;
6
7pub fn execute(target: String) -> Result<CherryPickResponse, crate::errors::LitError> {
8 let repo_root = find_repo_root()?;
9 let store = ObjectStore::new(&repo_root);
10
11 let commit_hash = resolve_rev(&repo_root, &target)?;
13 let hash_obj = ObjectHash::from_hex(commit_hash.clone());
14
15 let commit = match store.read(&hash_obj)? {
16 Object::Commit(c) => c,
17 _ => return Err(format!("'{}' is not a commit", target).into()),
18 };
19
20 let parent_hash = commit
22 .parents
23 .first()
24 .ok_or("Cannot cherry-pick a root commit")?;
25 let parent_commit = match store.read(parent_hash)? {
26 Object::Commit(c) => c,
27 _ => return Err("Parent is not a commit".into()),
28 };
29
30 let head_hash_str = read_head(&repo_root)?;
32 let head_hash = ObjectHash::from_hex(head_hash_str.clone());
33 let head_commit = match store.read(&head_hash)? {
34 Object::Commit(c) => c,
35 _ => return Err("HEAD is not a commit".into()),
36 };
37
38 let parent_files = load_tree_files(&store, &parent_commit.tree)?;
40 let commit_files = load_tree_files(&store, &commit.tree)?;
41 let head_files = load_tree_files(&store, &head_commit.tree)?;
42
43 let mut new_tree = Tree::new();
44 let mut changed_files = Vec::new();
45
46 for (path, (hash, mode)) in &head_files {
48 let in_parent = parent_files.get(path);
49 let in_commit = commit_files.get(path);
50
51 match (in_parent, in_commit) {
52 (Some(pv), Some(cv)) if pv.0 != cv.0 => {
54 changed_files.push(path.clone());
55 new_tree.add_entry(
56 cv.1.clone(),
57 path.clone(),
58 ObjectHash::from_hex(cv.0.clone()),
59 "blob".to_string(),
60 );
61 }
62 (Some(_), None) => {
64 changed_files.push(path.clone());
65 continue; }
67 _ => {
69 new_tree.add_entry(
70 mode.clone(),
71 path.clone(),
72 ObjectHash::from_hex(hash.clone()),
73 "blob".to_string(),
74 );
75 }
76 }
77 }
78
79 for (path, (hash, mode)) in &commit_files {
81 if !parent_files.contains_key(path) && !head_files.contains_key(path) {
82 changed_files.push(path.clone());
83 new_tree.add_entry(
84 mode.clone(),
85 path.clone(),
86 ObjectHash::from_hex(hash.clone()),
87 "blob".to_string(),
88 );
89 }
90 }
91
92 let tree_hash = store.write(&Object::Tree(new_tree))?;
93
94 let author = std::env::var("USER")
95 .or_else(|_| std::env::var("USERNAME"))
96 .unwrap_or_else(|_| "Unknown".to_string());
97
98 let new_commit = Commit::new(
99 tree_hash,
100 vec![ObjectHash::from_hex(head_hash_str)],
101 author,
102 commit.message.clone(),
103 );
104
105 let new_hash = store.write(&Object::Commit(new_commit))?;
106
107 let branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
108 write_ref(&repo_root, &format!("heads/{}", branch), new_hash.as_str())?;
109
110 Ok(CherryPickResponse {
111 source_commit: commit_hash[..16.min(commit_hash.len())].to_string(),
112 new_commit: new_hash.short(),
113 files_changed: changed_files.len(),
114 message: format!(
115 "Cherry-picked {} as {}",
116 &commit_hash[..16.min(commit_hash.len())],
117 new_hash.short()
118 ),
119 })
120}
121
122fn load_tree_files(
123 store: &ObjectStore,
124 tree_hash: &ObjectHash,
125) -> Result<std::collections::HashMap<String, (String, String)>, String> {
126 let tree = match store.read(tree_hash)? {
127 Object::Tree(t) => t,
128 _ => return Err("Not a tree".into()),
129 };
130 let mut files = std::collections::HashMap::new();
131 for entry in &tree.entries {
132 files.insert(
133 entry.name.clone(),
134 (entry.hash.to_string(), entry.mode.clone()),
135 );
136 }
137 Ok(files)
138}
139
140fn resolve_rev(
141 repo_root: &std::path::Path,
142 target: &str,
143) -> Result<String, crate::errors::LitError> {
144 if target.starts_with("HEAD~") || target.starts_with("HEAD^") {
145 let count: usize = target[5..].parse().unwrap_or(1);
146 let mut current = read_head(repo_root)?;
147 let store = ObjectStore::new(repo_root);
148 for _ in 0..count {
149 let hash = ObjectHash::from_hex(current);
150 let commit = match store.read(&hash)? {
151 Object::Commit(c) => c,
152 _ => return Err("Not a commit in history".into()),
153 };
154 current = commit
155 .parents
156 .first()
157 .ok_or("No parent commit")?
158 .to_string();
159 }
160 return Ok(current);
161 }
162 if target == "HEAD" {
163 return Ok(read_head(repo_root)?);
164 }
165 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("heads/{}", target)) {
166 return Ok(hash);
167 }
168 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("tags/{}", target)) {
169 return Ok(hash);
170 }
171 if target.len() >= 16 && target.chars().all(|c| c.is_ascii_hexdigit()) {
172 return Ok(target.to_string());
173 }
174 Err(format!("Cannot resolve '{}' to a commit", target).into())
175}