use std::ffi::CString;
use std::{marker, mem, ptr, str};
use crate::build::CheckoutBuilder;
use crate::util::Binding;
use crate::{raw, Error, Index, MergeOptions, Oid, Signature};
pub struct RebaseOptions<'cb> {
raw: raw::git_rebase_options,
rewrite_notes_ref: Option<CString>,
merge_options: Option<MergeOptions>,
checkout_options: Option<CheckoutBuilder<'cb>>,
}
impl<'cb> Default for RebaseOptions<'cb> {
fn default() -> Self {
Self::new()
}
}
impl<'cb> RebaseOptions<'cb> {
pub fn new() -> RebaseOptions<'cb> {
let mut opts = RebaseOptions {
raw: unsafe { mem::zeroed() },
rewrite_notes_ref: None,
merge_options: None,
checkout_options: None,
};
assert_eq!(unsafe { raw::git_rebase_init_options(&mut opts.raw, 1) }, 0);
opts
}
pub fn quiet(&mut self, quiet: bool) -> &mut RebaseOptions<'cb> {
self.raw.quiet = quiet as i32;
self
}
pub fn inmemory(&mut self, inmemory: bool) -> &mut RebaseOptions<'cb> {
self.raw.inmemory = inmemory as i32;
self
}
pub fn rewrite_notes_ref(&mut self, rewrite_notes_ref: &str) -> &mut RebaseOptions<'cb> {
self.rewrite_notes_ref = Some(CString::new(rewrite_notes_ref).unwrap());
self
}
pub fn merge_options(&mut self, opts: MergeOptions) -> &mut RebaseOptions<'cb> {
self.merge_options = Some(opts);
self
}
pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut RebaseOptions<'cb> {
self.checkout_options = Some(opts);
self
}
pub fn raw(&mut self) -> *const raw::git_rebase_options {
unsafe {
if let Some(opts) = self.merge_options.as_mut().take() {
ptr::copy_nonoverlapping(opts.raw(), &mut self.raw.merge_options, 1);
}
if let Some(opts) = self.checkout_options.as_mut() {
opts.configure(&mut self.raw.checkout_options);
}
self.raw.rewrite_notes_ref = self
.rewrite_notes_ref
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null());
}
&self.raw
}
}
pub struct Rebase<'repo> {
raw: *mut raw::git_rebase,
_marker: marker::PhantomData<&'repo raw::git_rebase>,
}
impl<'repo> Rebase<'repo> {
pub fn len(&self) -> usize {
unsafe { raw::git_rebase_operation_entrycount(self.raw) }
}
pub fn orig_head_name(&self) -> Option<&str> {
let name_bytes =
unsafe { crate::opt_bytes(self, raw::git_rebase_orig_head_name(self.raw)) };
name_bytes.and_then(|s| str::from_utf8(s).ok())
}
pub fn orig_head_id(&self) -> Option<Oid> {
unsafe { Oid::from_raw_opt(raw::git_rebase_orig_head_id(self.raw)) }
}
pub fn nth(&mut self, n: usize) -> Option<RebaseOperation<'_>> {
unsafe {
let op = raw::git_rebase_operation_byindex(self.raw, n);
if op.is_null() {
None
} else {
Some(RebaseOperation::from_raw(op))
}
}
}
pub fn operation_current(&mut self) -> Option<usize> {
let cur = unsafe { raw::git_rebase_operation_current(self.raw) };
if cur == raw::GIT_REBASE_NO_OPERATION {
None
} else {
Some(cur)
}
}
pub fn inmemory_index(&mut self) -> Result<Index, Error> {
let mut idx = ptr::null_mut();
unsafe {
try_call!(raw::git_rebase_inmemory_index(&mut idx, self.raw));
Ok(Binding::from_raw(idx))
}
}
pub fn commit(
&mut self,
author: Option<&Signature<'_>>,
committer: &Signature<'_>,
message: Option<&str>,
) -> Result<Oid, Error> {
let mut id: raw::git_oid = unsafe { mem::zeroed() };
let message = crate::opt_cstr(message)?;
unsafe {
try_call!(raw::git_rebase_commit(
&mut id,
self.raw,
author.map(|a| a.raw()),
committer.raw(),
ptr::null(),
message
));
Ok(Binding::from_raw(&id as *const _))
}
}
pub fn abort(&mut self) -> Result<(), Error> {
unsafe {
try_call!(raw::git_rebase_abort(self.raw));
}
Ok(())
}
pub fn finish(&mut self, signature: Option<&Signature<'_>>) -> Result<(), Error> {
unsafe {
try_call!(raw::git_rebase_finish(self.raw, signature.map(|s| s.raw())));
}
Ok(())
}
}
impl<'rebase> Iterator for Rebase<'rebase> {
type Item = Result<RebaseOperation<'rebase>, Error>;
fn next(&mut self) -> Option<Result<RebaseOperation<'rebase>, Error>> {
let mut out = ptr::null_mut();
unsafe {
try_call_iter!(raw::git_rebase_next(&mut out, self.raw));
Some(Ok(RebaseOperation::from_raw(out)))
}
}
}
impl<'repo> Binding for Rebase<'repo> {
type Raw = *mut raw::git_rebase;
unsafe fn from_raw(raw: *mut raw::git_rebase) -> Rebase<'repo> {
Rebase {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> *mut raw::git_rebase {
self.raw
}
}
impl<'repo> Drop for Rebase<'repo> {
fn drop(&mut self) {
unsafe { raw::git_rebase_free(self.raw) }
}
}
#[derive(Debug, PartialEq)]
pub enum RebaseOperationType {
Pick,
Reword,
Edit,
Squash,
Fixup,
Exec,
}
impl RebaseOperationType {
pub fn from_raw(raw: raw::git_rebase_operation_t) -> Option<RebaseOperationType> {
match raw {
raw::GIT_REBASE_OPERATION_PICK => Some(RebaseOperationType::Pick),
raw::GIT_REBASE_OPERATION_REWORD => Some(RebaseOperationType::Reword),
raw::GIT_REBASE_OPERATION_EDIT => Some(RebaseOperationType::Edit),
raw::GIT_REBASE_OPERATION_SQUASH => Some(RebaseOperationType::Squash),
raw::GIT_REBASE_OPERATION_FIXUP => Some(RebaseOperationType::Fixup),
raw::GIT_REBASE_OPERATION_EXEC => Some(RebaseOperationType::Exec),
_ => None,
}
}
}
#[derive(Debug)]
pub struct RebaseOperation<'rebase> {
raw: *const raw::git_rebase_operation,
_marker: marker::PhantomData<Rebase<'rebase>>,
}
impl<'rebase> RebaseOperation<'rebase> {
pub fn kind(&self) -> Option<RebaseOperationType> {
unsafe { RebaseOperationType::from_raw((*self.raw).kind) }
}
pub fn id(&self) -> Oid {
unsafe { Binding::from_raw(&(*self.raw).id as *const _) }
}
pub fn exec(&self) -> Option<&str> {
unsafe { str::from_utf8(crate::opt_bytes(self, (*self.raw).exec).unwrap()).ok() }
}
}
impl<'rebase> Binding for RebaseOperation<'rebase> {
type Raw = *const raw::git_rebase_operation;
unsafe fn from_raw(raw: *const raw::git_rebase_operation) -> RebaseOperation<'rebase> {
RebaseOperation {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> *const raw::git_rebase_operation {
self.raw
}
}
#[cfg(test)]
mod tests {
use crate::{RebaseOperationType, RebaseOptions, Signature};
use std::{fs, path};
#[test]
fn smoke() {
let (_td, repo) = crate::test::repo_init();
let head_target = repo.head().unwrap().target().unwrap();
let tip = repo.find_commit(head_target).unwrap();
let sig = tip.author();
let tree = tip.tree().unwrap();
let c1 = repo
.commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&tip])
.unwrap();
let c1 = repo.find_commit(c1).unwrap();
let c2 = repo
.commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&c1])
.unwrap();
let head = repo.find_reference("refs/heads/main").unwrap();
let branch = repo.reference_to_annotated_commit(&head).unwrap();
let upstream = repo.find_annotated_commit(tip.id()).unwrap();
let mut rebase = repo
.rebase(Some(&branch), Some(&upstream), None, None)
.unwrap();
assert_eq!(Some("refs/heads/main"), rebase.orig_head_name());
assert_eq!(Some(c2), rebase.orig_head_id());
assert_eq!(rebase.len(), 2);
{
let op = rebase.next().unwrap().unwrap();
assert_eq!(op.kind(), Some(RebaseOperationType::Pick));
assert_eq!(op.id(), c1.id());
}
{
let op = rebase.next().unwrap().unwrap();
assert_eq!(op.kind(), Some(RebaseOperationType::Pick));
assert_eq!(op.id(), c2);
}
{
let op = rebase.next();
assert!(op.is_none());
}
}
#[test]
fn keeping_original_author_msg() {
let (td, repo) = crate::test::repo_init();
let head_target = repo.head().unwrap().target().unwrap();
let tip = repo.find_commit(head_target).unwrap();
let sig = Signature::now("testname", "testemail").unwrap();
let mut index = repo.index().unwrap();
fs::File::create(td.path().join("file_a")).unwrap();
index.add_path(path::Path::new("file_a")).unwrap();
index.write().unwrap();
let tree_id_a = index.write_tree().unwrap();
let tree_a = repo.find_tree(tree_id_a).unwrap();
let c1 = repo
.commit(Some("refs/heads/main"), &sig, &sig, "A", &tree_a, &[&tip])
.unwrap();
let c1 = repo.find_commit(c1).unwrap();
fs::File::create(td.path().join("file_b")).unwrap();
index.add_path(path::Path::new("file_b")).unwrap();
index.write().unwrap();
let tree_id_b = index.write_tree().unwrap();
let tree_b = repo.find_tree(tree_id_b).unwrap();
let c2 = repo
.commit(Some("refs/heads/main"), &sig, &sig, "B", &tree_b, &[&c1])
.unwrap();
let branch = repo.find_annotated_commit(c2).unwrap();
let upstream = repo.find_annotated_commit(tip.id()).unwrap();
let mut opts: RebaseOptions<'_> = Default::default();
let mut rebase = repo
.rebase(Some(&branch), Some(&upstream), None, Some(&mut opts))
.unwrap();
assert_eq!(rebase.len(), 2);
{
rebase.next().unwrap().unwrap();
let id = rebase.commit(None, &sig, None).unwrap();
let commit = repo.find_commit(id).unwrap();
assert_eq!(commit.message(), Some("A"));
assert_eq!(commit.author().name(), Some("testname"));
assert_eq!(commit.author().email(), Some("testemail"));
}
{
rebase.next().unwrap().unwrap();
let id = rebase.commit(None, &sig, None).unwrap();
let commit = repo.find_commit(id).unwrap();
assert_eq!(commit.message(), Some("B"));
assert_eq!(commit.author().name(), Some("testname"));
assert_eq!(commit.author().email(), Some("testemail"));
}
rebase.finish(None).unwrap();
}
}