Skip to main content

nusy_arrow_git/
rebase.rs

1//! Rebase — replay a sequence of commits onto a new base.
2//!
3//! Walks from `start` to `end` following parent pointers, collects commits
4//! in order, then cherry-picks each onto the new base sequentially.
5
6use crate::cherry_pick::{CherryPickError, cherry_pick};
7use crate::commit::CommitsTable;
8use crate::history::log;
9use crate::object_store::GitObjectStore;
10
11/// Errors from rebase operations.
12#[derive(Debug, thiserror::Error)]
13pub enum RebaseError {
14    #[error("Cherry-pick failed: {0}")]
15    CherryPick(#[from] CherryPickError),
16
17    #[error("Commit not found: {0}")]
18    CommitNotFound(String),
19
20    #[error("Nothing to rebase (start equals onto)")]
21    NothingToRebase,
22}
23
24/// Result of a rebase operation.
25pub struct RebaseResult {
26    /// The new HEAD commit ID after rebase.
27    pub new_head: String,
28    /// Number of commits replayed.
29    pub replayed: usize,
30}
31
32/// Rebase commits from `start_commit_id` (exclusive) through `end_commit_id`
33/// (inclusive) onto `onto_commit_id`.
34///
35/// Walks the commit chain from end back to start, collects commits in
36/// chronological order, then cherry-picks each onto the new base.
37///
38/// Returns the new HEAD after all commits are replayed.
39pub fn rebase(
40    obj_store: &mut GitObjectStore,
41    commits_table: &mut CommitsTable,
42    start_commit_id: &str, // The old base (exclusive — commits after this)
43    end_commit_id: &str,   // The tip to rebase (inclusive)
44    onto_commit_id: &str,  // The new base to replay onto
45    author: &str,
46) -> Result<RebaseResult, RebaseError> {
47    if start_commit_id == onto_commit_id {
48        return Err(RebaseError::NothingToRebase);
49    }
50
51    // Collect commit IDs from end back to start (exclusive).
52    // We collect owned Strings so the borrow on commits_table is released
53    // before the cherry-pick loop needs a mutable borrow.
54    let all = log(commits_table, end_commit_id, 0);
55    let mut to_replay: Vec<String> = Vec::new();
56    for commit in &all {
57        if commit.commit_id == start_commit_id {
58            break;
59        }
60        to_replay.push(commit.commit_id.clone());
61    }
62
63    // log() returns newest-first; we need oldest-first for replay
64    to_replay.reverse();
65
66    if to_replay.is_empty() {
67        return Err(RebaseError::NothingToRebase);
68    }
69
70    // Cherry-pick each commit onto the new base
71    let mut current_head = onto_commit_id.to_string();
72    let mut replayed = 0;
73
74    for commit_id in &to_replay {
75        let new_id = cherry_pick(obj_store, commits_table, commit_id, &current_head, author)?;
76        current_head = new_id;
77        replayed += 1;
78    }
79
80    Ok(RebaseResult {
81        new_head: current_head,
82        replayed,
83    })
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::{CommitsTable, GitObjectStore, checkout, create_commit};
90    use nusy_arrow_core::{Namespace, Triple, YLayer};
91
92    fn make_triple(s: &str, p: &str, o: &str) -> Triple {
93        Triple {
94            subject: s.to_string(),
95            predicate: p.to_string(),
96            object: o.to_string(),
97            graph: None,
98            confidence: Some(1.0),
99            source_document: None,
100            source_chunk_id: None,
101            extracted_by: None,
102            caused_by: None,
103            derived_from: None,
104            consolidated_at: None,
105            certifiability_class: None,
106        }
107    }
108
109    #[test]
110    fn test_rebase_linear_chain() {
111        let tmp = tempfile::tempdir().unwrap();
112        let mut obj = GitObjectStore::with_snapshot_dir(tmp.path());
113        let mut commits = CommitsTable::new();
114
115        // Base commit
116        obj.store
117            .add_triple(
118                &make_triple("a", "r", "1"),
119                Namespace::World,
120                YLayer::Semantic,
121            )
122            .unwrap();
123        let base = create_commit(&obj, &mut commits, vec![], "base", "test").unwrap();
124
125        // Two commits on old branch
126        obj.store
127            .add_triple(
128                &make_triple("b", "r", "2"),
129                Namespace::World,
130                YLayer::Semantic,
131            )
132            .unwrap();
133        let c1 = create_commit(
134            &obj,
135            &mut commits,
136            vec![base.commit_id.clone()],
137            "c1",
138            "test",
139        )
140        .unwrap();
141
142        obj.store
143            .add_triple(
144                &make_triple("c", "r", "3"),
145                Namespace::World,
146                YLayer::Semantic,
147            )
148            .unwrap();
149        let c2 =
150            create_commit(&obj, &mut commits, vec![c1.commit_id.clone()], "c2", "test").unwrap();
151
152        // New base (divergent)
153        checkout(&mut obj, &commits, &base.commit_id).unwrap();
154        obj.store
155            .add_triple(
156                &make_triple("d", "r", "4"),
157                Namespace::World,
158                YLayer::Semantic,
159            )
160            .unwrap();
161        let new_base = create_commit(
162            &obj,
163            &mut commits,
164            vec![base.commit_id.clone()],
165            "new_base",
166            "test",
167        )
168        .unwrap();
169
170        // Rebase c1..c2 onto new_base
171        let result = rebase(
172            &mut obj,
173            &mut commits,
174            &base.commit_id,
175            &c2.commit_id,
176            &new_base.commit_id,
177            "test",
178        )
179        .unwrap();
180
181        assert_eq!(result.replayed, 2);
182        // New head should be different from c2
183        assert_ne!(result.new_head, c2.commit_id);
184    }
185
186    #[test]
187    fn test_rebase_nothing_to_rebase() {
188        let tmp = tempfile::tempdir().unwrap();
189        let mut obj = GitObjectStore::with_snapshot_dir(tmp.path());
190        let mut commits = CommitsTable::new();
191
192        obj.store
193            .add_triple(
194                &make_triple("a", "r", "1"),
195                Namespace::World,
196                YLayer::Semantic,
197            )
198            .unwrap();
199        let base = create_commit(&obj, &mut commits, vec![], "base", "test").unwrap();
200
201        // Rebase base onto itself
202        let result = rebase(
203            &mut obj,
204            &mut commits,
205            &base.commit_id,
206            &base.commit_id,
207            &base.commit_id,
208            "test",
209        );
210        assert!(result.is_err());
211    }
212
213    #[test]
214    fn test_rebase_single_commit() {
215        let tmp = tempfile::tempdir().unwrap();
216        let mut obj = GitObjectStore::with_snapshot_dir(tmp.path());
217        let mut commits = CommitsTable::new();
218
219        obj.store
220            .add_triple(
221                &make_triple("a", "r", "1"),
222                Namespace::World,
223                YLayer::Semantic,
224            )
225            .unwrap();
226        let base = create_commit(&obj, &mut commits, vec![], "base", "test").unwrap();
227
228        obj.store
229            .add_triple(
230                &make_triple("b", "r", "2"),
231                Namespace::World,
232                YLayer::Semantic,
233            )
234            .unwrap();
235        let c1 = create_commit(
236            &obj,
237            &mut commits,
238            vec![base.commit_id.clone()],
239            "c1",
240            "test",
241        )
242        .unwrap();
243
244        // New base
245        checkout(&mut obj, &commits, &base.commit_id).unwrap();
246        obj.store
247            .add_triple(
248                &make_triple("x", "r", "9"),
249                Namespace::World,
250                YLayer::Semantic,
251            )
252            .unwrap();
253        let new_base = create_commit(
254            &obj,
255            &mut commits,
256            vec![base.commit_id.clone()],
257            "new_base",
258            "test",
259        )
260        .unwrap();
261
262        let result = rebase(
263            &mut obj,
264            &mut commits,
265            &base.commit_id,
266            &c1.commit_id,
267            &new_base.commit_id,
268            "test",
269        )
270        .unwrap();
271
272        assert_eq!(result.replayed, 1);
273    }
274}