Skip to main content

cherry_pick/
cherry_pick.rs

1//! Minimal cherry-pick: apply another commit's tree onto `HEAD` using a three-way merge
2//! ([`merge_trees::merge_trees_three_way`]), then create a new commit.
3//!
4//! This mirrors Git’s merge setup for a pick: **base** = the picked commit’s parent tree,
5//! **ours** = `HEAD`’s tree, **theirs** = the picked commit’s tree.
6//!
7//! Run: `cargo run -p grit-lib --example cherry_pick`
8
9use grit_lib::commit_trailers;
10use grit_lib::config::ConfigSet;
11use grit_lib::merge_file::MergeFavor;
12use grit_lib::merge_trees::{merge_trees_three_way, WhitespaceMergeOptions};
13use grit_lib::objects::{parse_commit, serialize_commit, CommitData, ObjectKind};
14use grit_lib::refs;
15use grit_lib::repo::init_repository;
16use grit_lib::rev_parse::resolve_revision;
17use grit_lib::write_tree::write_tree_from_index;
18
19fn commit_from_tree(
20    repo: &grit_lib::repo::Repository,
21    tree: grit_lib::objects::ObjectId,
22    parents: &[grit_lib::objects::ObjectId],
23    message: &str,
24) -> grit_lib::error::Result<grit_lib::objects::ObjectId> {
25    let commit = CommitData {
26        tree,
27        parents: parents.to_vec(),
28        author: "Example <example@example.com> 1700000000 +0000".to_owned(),
29        committer: "Example <example@example.com> 1700000000 +0000".to_owned(),
30        author_raw: Vec::new(),
31        committer_raw: Vec::new(),
32        encoding: None,
33        message: message.to_owned(),
34        raw_message: None,
35    };
36    repo.odb
37        .write(ObjectKind::Commit, &serialize_commit(&commit))
38}
39
40fn tree_of_commit(
41    repo: &grit_lib::repo::Repository,
42    commit_oid: grit_lib::objects::ObjectId,
43) -> grit_lib::error::Result<grit_lib::objects::ObjectId> {
44    let obj = repo.odb.read(&commit_oid)?;
45    Ok(parse_commit(&obj.data)?.tree)
46}
47
48fn main() -> grit_lib::error::Result<()> {
49    let root = tempfile::tempdir()?;
50    let repo = init_repository(root.path(), false, "main", None, "files")?;
51
52    use grit_lib::index::{Index, IndexEntry, MODE_REGULAR};
53
54    // Base commit on main: one file.
55    let blob_a = repo.odb.write(ObjectKind::Blob, b"base\n")?;
56    let mut index = Index::new();
57    index.add_or_replace(IndexEntry {
58        ctime_sec: 0,
59        ctime_nsec: 0,
60        mtime_sec: 0,
61        mtime_nsec: 0,
62        dev: 0,
63        ino: 0,
64        mode: MODE_REGULAR,
65        uid: 0,
66        gid: 0,
67        size: 0,
68        oid: blob_a,
69        flags: 7,
70        flags_extended: None,
71        path: b"base.txt".to_vec(),
72        base_index_pos: 0,
73    });
74    repo.write_index(&mut index)?;
75    let index = repo.load_index()?;
76    let tree_a = write_tree_from_index(&repo.odb, &index, "")?;
77    let commit_a = commit_from_tree(&repo, tree_a, &[], "initial\n")?;
78    refs::write_ref(&repo.git_dir, "refs/heads/main", &commit_a)?;
79
80    // Topic commit: parent A, adds picked.txt (not on main yet).
81    let blob_pick = repo.odb.write(ObjectKind::Blob, b"hello from topic\n")?;
82    let mut index = Index::new();
83    index.add_or_replace(IndexEntry {
84        ctime_sec: 0,
85        ctime_nsec: 0,
86        mtime_sec: 0,
87        mtime_nsec: 0,
88        dev: 0,
89        ino: 0,
90        mode: MODE_REGULAR,
91        uid: 0,
92        gid: 0,
93        size: 0,
94        oid: blob_a,
95        flags: 7,
96        flags_extended: None,
97        path: b"base.txt".to_vec(),
98        base_index_pos: 0,
99    });
100    index.add_or_replace(IndexEntry {
101        ctime_sec: 0,
102        ctime_nsec: 0,
103        mtime_sec: 0,
104        mtime_nsec: 0,
105        dev: 0,
106        ino: 0,
107        mode: MODE_REGULAR,
108        uid: 0,
109        gid: 0,
110        size: 0,
111        oid: blob_pick,
112        flags: 9,
113        flags_extended: None,
114        path: b"picked.txt".to_vec(),
115        base_index_pos: 0,
116    });
117    repo.write_index(&mut index)?;
118    let index = repo.load_index()?;
119    let tree_b = write_tree_from_index(&repo.odb, &index, "")?;
120    let commit_b = commit_from_tree(&repo, tree_b, &[commit_a], "add picked file\n")?;
121    refs::write_ref(&repo.git_dir, "refs/heads/topic", &commit_b)?;
122
123    // Cherry-pick `topic` onto `main` (still at A).
124    let head = resolve_revision(&repo, "main")?;
125    let picked = resolve_revision(&repo, "topic")?;
126    let picked_obj = repo.odb.read(&picked)?;
127    let picked_data = parse_commit(&picked_obj.data)?;
128    let parent = picked_data.parents.first().copied().ok_or_else(|| {
129        grit_lib::error::Error::CorruptObject("picked commit has no parent".into())
130    })?;
131
132    let base_tree = tree_of_commit(&repo, parent)?;
133    let ours_tree = tree_of_commit(&repo, head)?;
134    let theirs_tree = picked_data.tree;
135
136    let merged = merge_trees_three_way(
137        &repo,
138        base_tree,
139        ours_tree,
140        theirs_tree,
141        MergeFavor::default(),
142        WhitespaceMergeOptions::default(),
143        grit_lib::merge_trees::TreeMergeConflictPresentation {
144            label_ours: "HEAD",
145            label_theirs: grit_lib::merge_trees::TheirsConflictLabel::Fixed("picked"),
146            label_base: "parent of picked commit",
147            style: grit_lib::merge_file::ConflictStyle::Merge,
148            checkout_merge: false,
149        },
150    )?;
151
152    if !merged.conflict_content.is_empty() {
153        return Err(grit_lib::error::Error::Message(format!(
154            "merge produced {} conflict path(s); this example expects a clean pick",
155            merged.conflict_content.len()
156        )));
157    }
158
159    let new_tree = write_tree_from_index(&repo.odb, &merged.index, "")?;
160    let config = ConfigSet::load_repo_local_only(&repo.git_dir)?;
161    let msg = commit_trailers::finalize_cherry_pick_message(
162        &picked_data.message,
163        true,
164        false,
165        "Example",
166        "example@example.com",
167        &config,
168        &picked.to_hex(),
169    );
170    let new_commit = commit_from_tree(&repo, new_tree, &[head], &msg)?;
171    refs::write_ref(&repo.git_dir, "refs/heads/main", &new_commit)?;
172
173    println!("cherry-picked {} onto {}", picked, head);
174    println!("new main: {new_commit}");
175    let out = repo.odb.read(&new_commit)?;
176    println!("message:\n{}", parse_commit(&out.data)?.message);
177
178    Ok(())
179}