1use std::ffi::CString;
2use std::{marker, mem, ptr, str};
3
4use crate::build::CheckoutBuilder;
5use crate::util::Binding;
6use crate::{raw, Error, Index, MergeOptions, Oid, Signature};
7
8pub struct RebaseOptions<'cb> {
12    raw: raw::git_rebase_options,
13    rewrite_notes_ref: Option<CString>,
14    merge_options: Option<MergeOptions>,
15    checkout_options: Option<CheckoutBuilder<'cb>>,
16}
17
18impl<'cb> Default for RebaseOptions<'cb> {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl<'cb> RebaseOptions<'cb> {
25    pub fn new() -> RebaseOptions<'cb> {
27        let mut opts = RebaseOptions {
28            raw: unsafe { mem::zeroed() },
29            rewrite_notes_ref: None,
30            merge_options: None,
31            checkout_options: None,
32        };
33        assert_eq!(unsafe { raw::git_rebase_init_options(&mut opts.raw, 1) }, 0);
34        opts
35    }
36
37    pub fn quiet(&mut self, quiet: bool) -> &mut RebaseOptions<'cb> {
43        self.raw.quiet = quiet as i32;
44        self
45    }
46
47    pub fn inmemory(&mut self, inmemory: bool) -> &mut RebaseOptions<'cb> {
53        self.raw.inmemory = inmemory as i32;
54        self
55    }
56
57    pub fn rewrite_notes_ref(&mut self, rewrite_notes_ref: &str) -> &mut RebaseOptions<'cb> {
64        self.rewrite_notes_ref = Some(CString::new(rewrite_notes_ref).unwrap());
65        self
66    }
67
68    pub fn merge_options(&mut self, opts: MergeOptions) -> &mut RebaseOptions<'cb> {
70        self.merge_options = Some(opts);
71        self
72    }
73
74    pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut RebaseOptions<'cb> {
80        self.checkout_options = Some(opts);
81        self
82    }
83
84    pub fn raw(&mut self) -> *const raw::git_rebase_options {
86        unsafe {
87            if let Some(opts) = self.merge_options.as_mut().take() {
88                ptr::copy_nonoverlapping(opts.raw(), &mut self.raw.merge_options, 1);
89            }
90            if let Some(opts) = self.checkout_options.as_mut() {
91                opts.configure(&mut self.raw.checkout_options);
92            }
93            self.raw.rewrite_notes_ref = self
94                .rewrite_notes_ref
95                .as_ref()
96                .map(|s| s.as_ptr())
97                .unwrap_or(ptr::null());
98        }
99        &self.raw
100    }
101}
102
103pub struct Rebase<'repo> {
105    raw: *mut raw::git_rebase,
106    _marker: marker::PhantomData<&'repo raw::git_rebase>,
107}
108
109impl<'repo> Rebase<'repo> {
110    pub fn len(&self) -> usize {
112        unsafe { raw::git_rebase_operation_entrycount(self.raw) }
113    }
114
115    pub fn orig_head_name(&self) -> Option<&str> {
117        let name_bytes =
118            unsafe { crate::opt_bytes(self, raw::git_rebase_orig_head_name(self.raw)) };
119        name_bytes.and_then(|s| str::from_utf8(s).ok())
120    }
121
122    pub fn orig_head_id(&self) -> Option<Oid> {
124        unsafe { Oid::from_raw_opt(raw::git_rebase_orig_head_id(self.raw)) }
125    }
126
127    pub fn nth(&mut self, n: usize) -> Option<RebaseOperation<'_>> {
129        unsafe {
130            let op = raw::git_rebase_operation_byindex(self.raw, n);
131            if op.is_null() {
132                None
133            } else {
134                Some(RebaseOperation::from_raw(op))
135            }
136        }
137    }
138
139    pub fn operation_current(&mut self) -> Option<usize> {
143        let cur = unsafe { raw::git_rebase_operation_current(self.raw) };
144        if cur == raw::GIT_REBASE_NO_OPERATION {
145            None
146        } else {
147            Some(cur)
148        }
149    }
150
151    pub fn inmemory_index(&mut self) -> Result<Index, Error> {
159        let mut idx = ptr::null_mut();
160        unsafe {
161            try_call!(raw::git_rebase_inmemory_index(&mut idx, self.raw));
162            Ok(Binding::from_raw(idx))
163        }
164    }
165
166    pub fn commit(
171        &mut self,
172        author: Option<&Signature<'_>>,
173        committer: &Signature<'_>,
174        message: Option<&str>,
175    ) -> Result<Oid, Error> {
176        let mut id: raw::git_oid = unsafe { mem::zeroed() };
177        let message = crate::opt_cstr(message)?;
178        unsafe {
179            try_call!(raw::git_rebase_commit(
180                &mut id,
181                self.raw,
182                author.map(|a| a.raw()),
183                committer.raw(),
184                ptr::null(),
185                message
186            ));
187            Ok(Binding::from_raw(&id as *const _))
188        }
189    }
190
191    pub fn abort(&mut self) -> Result<(), Error> {
194        unsafe {
195            try_call!(raw::git_rebase_abort(self.raw));
196        }
197
198        Ok(())
199    }
200
201    pub fn finish(&mut self, signature: Option<&Signature<'_>>) -> Result<(), Error> {
204        unsafe {
205            try_call!(raw::git_rebase_finish(self.raw, signature.map(|s| s.raw())));
206        }
207
208        Ok(())
209    }
210}
211
212impl<'rebase> Iterator for Rebase<'rebase> {
213    type Item = Result<RebaseOperation<'rebase>, Error>;
214
215    fn next(&mut self) -> Option<Result<RebaseOperation<'rebase>, Error>> {
221        let mut out = ptr::null_mut();
222        unsafe {
223            try_call_iter!(raw::git_rebase_next(&mut out, self.raw));
224            Some(Ok(RebaseOperation::from_raw(out)))
225        }
226    }
227}
228
229impl<'repo> Binding for Rebase<'repo> {
230    type Raw = *mut raw::git_rebase;
231    unsafe fn from_raw(raw: *mut raw::git_rebase) -> Rebase<'repo> {
232        Rebase {
233            raw,
234            _marker: marker::PhantomData,
235        }
236    }
237    fn raw(&self) -> *mut raw::git_rebase {
238        self.raw
239    }
240}
241
242impl<'repo> Drop for Rebase<'repo> {
243    fn drop(&mut self) {
244        unsafe { raw::git_rebase_free(self.raw) }
245    }
246}
247
248#[derive(Debug, PartialEq)]
253pub enum RebaseOperationType {
254    Pick,
257
258    Reword,
261
262    Edit,
265
266    Squash,
269
270    Fixup,
273
274    Exec,
277}
278
279impl RebaseOperationType {
280    pub fn from_raw(raw: raw::git_rebase_operation_t) -> Option<RebaseOperationType> {
282        match raw {
283            raw::GIT_REBASE_OPERATION_PICK => Some(RebaseOperationType::Pick),
284            raw::GIT_REBASE_OPERATION_REWORD => Some(RebaseOperationType::Reword),
285            raw::GIT_REBASE_OPERATION_EDIT => Some(RebaseOperationType::Edit),
286            raw::GIT_REBASE_OPERATION_SQUASH => Some(RebaseOperationType::Squash),
287            raw::GIT_REBASE_OPERATION_FIXUP => Some(RebaseOperationType::Fixup),
288            raw::GIT_REBASE_OPERATION_EXEC => Some(RebaseOperationType::Exec),
289            _ => None,
290        }
291    }
292}
293
294#[derive(Debug)]
299pub struct RebaseOperation<'rebase> {
300    raw: *const raw::git_rebase_operation,
301    _marker: marker::PhantomData<Rebase<'rebase>>,
302}
303
304impl<'rebase> RebaseOperation<'rebase> {
305    pub fn kind(&self) -> Option<RebaseOperationType> {
307        unsafe { RebaseOperationType::from_raw((*self.raw).kind) }
308    }
309
310    pub fn id(&self) -> Oid {
313        unsafe { Binding::from_raw(&(*self.raw).id as *const _) }
314    }
315
316    pub fn exec(&self) -> Option<&str> {
319        unsafe { str::from_utf8(crate::opt_bytes(self, (*self.raw).exec).unwrap()).ok() }
320    }
321}
322
323impl<'rebase> Binding for RebaseOperation<'rebase> {
324    type Raw = *const raw::git_rebase_operation;
325    unsafe fn from_raw(raw: *const raw::git_rebase_operation) -> RebaseOperation<'rebase> {
326        RebaseOperation {
327            raw,
328            _marker: marker::PhantomData,
329        }
330    }
331    fn raw(&self) -> *const raw::git_rebase_operation {
332        self.raw
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use crate::{RebaseOperationType, RebaseOptions, Signature};
339    use std::{fs, path};
340
341    #[test]
342    fn smoke() {
343        let (_td, repo) = crate::test::repo_init();
344        let head_target = repo.head().unwrap().target().unwrap();
345        let tip = repo.find_commit(head_target).unwrap();
346        let sig = tip.author();
347        let tree = tip.tree().unwrap();
348
349        let c1 = repo
352            .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&tip])
353            .unwrap();
354        let c1 = repo.find_commit(c1).unwrap();
355        let c2 = repo
356            .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&c1])
357            .unwrap();
358
359        let head = repo.find_reference("refs/heads/main").unwrap();
360        let branch = repo.reference_to_annotated_commit(&head).unwrap();
361        let upstream = repo.find_annotated_commit(tip.id()).unwrap();
362        let mut rebase = repo
363            .rebase(Some(&branch), Some(&upstream), None, None)
364            .unwrap();
365
366        assert_eq!(Some("refs/heads/main"), rebase.orig_head_name());
367        assert_eq!(Some(c2), rebase.orig_head_id());
368
369        assert_eq!(rebase.len(), 2);
370        {
371            let op = rebase.next().unwrap().unwrap();
372            assert_eq!(op.kind(), Some(RebaseOperationType::Pick));
373            assert_eq!(op.id(), c1.id());
374        }
375        {
376            let op = rebase.next().unwrap().unwrap();
377            assert_eq!(op.kind(), Some(RebaseOperationType::Pick));
378            assert_eq!(op.id(), c2);
379        }
380        {
381            let op = rebase.next();
382            assert!(op.is_none());
383        }
384    }
385
386    #[test]
387    fn keeping_original_author_msg() {
388        let (td, repo) = crate::test::repo_init();
389        let head_target = repo.head().unwrap().target().unwrap();
390        let tip = repo.find_commit(head_target).unwrap();
391        let sig = Signature::now("testname", "testemail").unwrap();
392        let mut index = repo.index().unwrap();
393
394        fs::File::create(td.path().join("file_a")).unwrap();
395        index.add_path(path::Path::new("file_a")).unwrap();
396        index.write().unwrap();
397        let tree_id_a = index.write_tree().unwrap();
398        let tree_a = repo.find_tree(tree_id_a).unwrap();
399        let c1 = repo
400            .commit(Some("refs/heads/main"), &sig, &sig, "A", &tree_a, &[&tip])
401            .unwrap();
402        let c1 = repo.find_commit(c1).unwrap();
403
404        fs::File::create(td.path().join("file_b")).unwrap();
405        index.add_path(path::Path::new("file_b")).unwrap();
406        index.write().unwrap();
407        let tree_id_b = index.write_tree().unwrap();
408        let tree_b = repo.find_tree(tree_id_b).unwrap();
409        let c2 = repo
410            .commit(Some("refs/heads/main"), &sig, &sig, "B", &tree_b, &[&c1])
411            .unwrap();
412
413        let branch = repo.find_annotated_commit(c2).unwrap();
414        let upstream = repo.find_annotated_commit(tip.id()).unwrap();
415        let mut opts: RebaseOptions<'_> = Default::default();
416        let mut rebase = repo
417            .rebase(Some(&branch), Some(&upstream), None, Some(&mut opts))
418            .unwrap();
419
420        assert_eq!(rebase.len(), 2);
421
422        {
423            rebase.next().unwrap().unwrap();
424            let id = rebase.commit(None, &sig, None).unwrap();
425            let commit = repo.find_commit(id).unwrap();
426            assert_eq!(commit.message(), Some("A"));
427            assert_eq!(commit.author().name(), Some("testname"));
428            assert_eq!(commit.author().email(), Some("testemail"));
429        }
430
431        {
432            rebase.next().unwrap().unwrap();
433            let id = rebase.commit(None, &sig, None).unwrap();
434            let commit = repo.find_commit(id).unwrap();
435            assert_eq!(commit.message(), Some("B"));
436            assert_eq!(commit.author().name(), Some("testname"));
437            assert_eq!(commit.author().email(), Some("testemail"));
438        }
439        rebase.finish(None).unwrap();
440    }
441}