Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function: git_merge_file_from_index #1062

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion libgit2-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// This is required to link libz when libssh2-sys is not included.
extern crate libz_sys as libz;

use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t};
use libc::{c_char, c_int, c_uchar, c_uint, c_ushort, c_void, size_t};
#[cfg(feature = "ssh")]
use libssh2_sys as libssh2;
use std::ffi::CStr;
Expand Down Expand Up @@ -1350,6 +1350,27 @@ pub struct git_merge_options {
pub file_flags: u32,
}

#[repr(C)]
pub struct git_merge_file_options {
pub version: c_uint,
pub ancestor_label: *const c_char,
pub our_label: *const c_char,
pub their_label: *const c_char,
pub favor: git_merge_file_favor_t,
pub flags: u32,
pub marker_size: c_ushort,
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct git_merge_file_result {
pub automergeable: c_uint,
pub path: *const c_char,
pub mode: c_uint,
pub ptr: *const c_char,
pub len: size_t,
}

git_enum! {
pub enum git_merge_flag_t {
GIT_MERGE_FIND_RENAMES = 1 << 0,
Expand Down Expand Up @@ -1379,6 +1400,8 @@ git_enum! {
GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL = 1 << 5,
GIT_MERGE_FILE_DIFF_PATIENCE = 1 << 6,
GIT_MERGE_FILE_DIFF_MINIMAL = 1 << 7,
GIT_MERGE_FILE_STYLE_ZDIFF3 = 1 << 8,
GIT_MERGE_FILE_ACCEPT_CONFLICTS = 1 << 9,
}
}

Expand Down Expand Up @@ -3378,6 +3401,8 @@ extern "C" {
their_tree: *const git_tree,
opts: *const git_merge_options,
) -> c_int;
pub fn git_merge_file_options_init(opts: *mut git_merge_file_options, version: c_uint)
-> c_int;
pub fn git_repository_state_cleanup(repo: *mut git_repository) -> c_int;

// merge analysis
Expand Down Expand Up @@ -3519,6 +3544,17 @@ extern "C" {
input_array: *const git_oid,
) -> c_int;

pub fn git_merge_file_from_index(
out: *mut git_merge_file_result,
repo: *mut git_repository,
ancestor: *const git_index_entry,
ours: *const git_index_entry,
theirs: *const git_index_entry,
opts: *const git_merge_file_options,
) -> c_int;

pub fn git_merge_file_result_free(file_result: *mut git_merge_file_result);

// pathspec
pub fn git_pathspec_free(ps: *mut git_pathspec);
pub fn git_pathspec_match_diff(
Expand Down
46 changes: 46 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,52 @@ impl Index {
}
}

impl IndexEntry {
/// Create a raw index entry.
///
/// The returned `raw::git_index_entry` contains a pointer to a `CString` path, which is also
/// returned because it's lifetime must exceed the lifetime of the `raw::git_index_entry`.
pub(crate) unsafe fn to_raw(&self) -> Result<(raw::git_index_entry, CString), Error> {
let path = CString::new(&self.path[..])?;

// libgit2 encodes the length of the path in the lower bits of the
// `flags` entry, so mask those out and recalculate here to ensure we
// don't corrupt anything.
let mut flags = self.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK;

if self.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize {
flags |= self.path.len() as u16;
} else {
flags |= raw::GIT_INDEX_ENTRY_NAMEMASK;
}

unsafe {
let raw = raw::git_index_entry {
dev: self.dev,
ino: self.ino,
mode: self.mode,
uid: self.uid,
gid: self.gid,
file_size: self.file_size,
id: *self.id.raw(),
flags,
flags_extended: self.flags_extended,
path: path.as_ptr(),
mtime: raw::git_index_time {
seconds: self.mtime.seconds(),
nanoseconds: self.mtime.nanoseconds(),
},
ctime: raw::git_index_time {
seconds: self.ctime.seconds(),
nanoseconds: self.ctime.nanoseconds(),
},
};

Ok((raw, path))
}
}
}

impl Binding for Index {
type Raw = *mut raw::git_index;
unsafe fn from_raw(raw: *mut raw::git_index) -> Index {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub use crate::index::{
pub use crate::indexer::{Indexer, IndexerProgress, Progress};
pub use crate::mailmap::Mailmap;
pub use crate::mempack::Mempack;
pub use crate::merge::{AnnotatedCommit, MergeOptions};
pub use crate::merge::{AnnotatedCommit, MergeFileOptions, MergeFileResult, MergeOptions};
pub use crate::message::{
message_prettify, message_trailers_bytes, message_trailers_strs, MessageTrailersBytes,
MessageTrailersBytesIterator, MessageTrailersStrs, MessageTrailersStrsIterator,
Expand Down
223 changes: 222 additions & 1 deletion src/merge.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use libc::c_uint;
use libc::{c_uint, c_ushort};
use std::ffi::CString;
use std::marker;
use std::mem;
use std::ptr;
use std::str;

use crate::call::Convert;
use crate::util::Binding;
use crate::IntoCString;
use crate::{raw, Commit, FileFavor, Oid};

/// A structure to represent an annotated commit, the input to merge and rebase.
Expand All @@ -22,6 +25,20 @@ pub struct MergeOptions {
raw: raw::git_merge_options,
}

/// Options for merging a file.
pub struct MergeFileOptions {
ancestor_label: Option<CString>,
our_label: Option<CString>,
their_label: Option<CString>,
raw: raw::git_merge_file_options,
}

/// Information about file-level merging.
pub struct MergeFileResult<'repo> {
raw: raw::git_merge_file_result,
_marker: marker::PhantomData<&'repo str>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have a PhantomData?

From what I can tell, the struct owns its own pointers (which are freed with git_merge_file_result_free).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be unnecessary, see comment below.


impl<'repo> AnnotatedCommit<'repo> {
/// Gets the commit ID that the given git_annotated_commit refers to
pub fn id(&self) -> Oid {
Expand Down Expand Up @@ -192,3 +209,207 @@ impl<'repo> Drop for AnnotatedCommit<'repo> {
unsafe { raw::git_annotated_commit_free(self.raw) }
}
}

impl Default for MergeFileOptions {
fn default() -> Self {
Self::new()
}
}

impl MergeFileOptions {
/// Creates a default set of merge file options.
pub fn new() -> MergeFileOptions {
let mut opts = MergeFileOptions {
ancestor_label: None,
our_label: None,
their_label: None,
raw: unsafe { mem::zeroed() },
};
assert_eq!(
unsafe { raw::git_merge_file_options_init(&mut opts.raw, 1) },
0
);
opts
}

/// Label for the ancestor file side of the conflict which will be prepended
/// to labels in diff3-format merge files.
pub fn ancestor_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
self.ancestor_label = Some(t.into_c_string().unwrap());

self.raw.ancestor_label = self
.ancestor_label
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null());

self
}

/// Label for our file side of the conflict which will be prepended to labels
/// in merge files.
pub fn our_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
self.our_label = Some(t.into_c_string().unwrap());

self.raw.our_label = self
.our_label
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null());

self
}

/// Label for their file side of the conflict which will be prepended to labels
/// in merge files.
pub fn their_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
self.their_label = Some(t.into_c_string().unwrap());

self.raw.their_label = self
.their_label
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null());

self
}

/// Specify a side to favor for resolving conflicts
pub fn favor(&mut self, favor: FileFavor) -> &mut MergeFileOptions {
self.raw.favor = favor.convert();
self
}

fn flag(&mut self, opt: raw::git_merge_file_flag_t, val: bool) -> &mut MergeFileOptions {
if val {
self.raw.flags |= opt as u32;
} else {
self.raw.flags &= !opt as u32;
}
self
}

/// Create standard conflicted merge files
pub fn style_standard(&mut self, standard: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_STYLE_MERGE, standard)
}

/// Create diff3-style file
pub fn style_diff3(&mut self, diff3: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_STYLE_DIFF3, diff3)
}

/// Condense non-alphanumeric regions for simplified diff file
pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM, simplify)
}

/// Ignore all whitespace
pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE, ignore)
}

/// Ignore changes in amount of whitespace
pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE, ignore)
}

/// Ignore whitespace at end of line
pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL, ignore)
}

/// Use the "patience diff" algorithm
pub fn patience(&mut self, patience: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE, patience)
}

/// Take extra time to find minimal diff
pub fn minimal(&mut self, minimal: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL, minimal)
}

/// Create zdiff3 ("zealous diff3")-style files
pub fn style_zdiff3(&mut self, zdiff3: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_STYLE_ZDIFF3, zdiff3)
}

/// Do not produce file conflicts when common regions have changed
pub fn accept_conflicts(&mut self, accept: bool) -> &mut MergeFileOptions {
self.flag(raw::GIT_MERGE_FILE_ACCEPT_CONFLICTS, accept)
}

/// The size of conflict markers (eg, "<<<<<<<"). Default is 7.
pub fn marker_size(&mut self, size: u16) -> &mut MergeFileOptions {
self.raw.marker_size = size as c_ushort;
self
}

/// Acquire a pointer to the underlying raw options.
pub unsafe fn raw(&mut self) -> *const raw::git_merge_file_options {
&self.raw as *const _
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be pub?

Also, it seems like this could be done as a Binding impl. Is there any reason that was not done that way?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, there could well be cargo cult issues going on here - I believe I copied this from MergeOptions. I'm not clear on the difference with implementing Binding, but it does need to be pub, since it is used in the merge_commits func in repo.rs. If impl Binding is the preferred way of doing this I can change over to use that?

More generally, I think the reasoning in my head was that structs like AnnotatedCommit or MergeFileResult are returned via libgit2 functions, and could be linked to the repository and should have a lifetime as such. Structs like MergeOptions or MergeFileOptions are created manually and consumed by the libgit2 functions, and as such would not have those same lifetime requirements.
I could be wrong, I am not all that familiar with how things work, but that is why I have copied from AnnotatedCommit to write MergeFileResult, and from MergeOptions to write MergeFileOptions.

}
}

impl<'repo> MergeFileResult<'repo> {
/// True if the output was automerged, false if the output contains
/// conflict markers.
pub fn is_automergeable(&self) -> bool {
self.raw.automergeable > 0
}

/// The path that the resultant merge file should use.
///
/// returns `None` if a filename conflict would occur,
/// or if the path is not valid utf-8
pub fn path(&self) -> Option<&str> {
self.path_bytes()
.and_then(|bytes| str::from_utf8(bytes).ok())
}

/// Gets the path as a byte slice.
pub fn path_bytes(&self) -> Option<&[u8]> {
unsafe { crate::opt_bytes(self, self.raw.path) }
}

/// The mode that the resultant merge file should use.
pub fn mode(&self) -> u32 {
self.raw.mode as u32
}

/// The contents of the merge.
pub fn content(&self) -> &'repo [u8] {
unsafe { std::slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.len as usize) }
}
}

impl<'repo> Binding for MergeFileResult<'repo> {
type Raw = raw::git_merge_file_result;
unsafe fn from_raw(raw: raw::git_merge_file_result) -> MergeFileResult<'repo> {
MergeFileResult {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> raw::git_merge_file_result {
self.raw
}
}
Comment on lines +394 to +396
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally doesn't look correct to me, or at least looks concerning. I don't think git_merge_file_result should be copy/clone, since this looks like it is erasing the lifetime.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raw func is required to impl Binding? Not sure what you mean here


impl<'repo> Drop for MergeFileResult<'repo> {
fn drop(&mut self) {
unsafe { raw::git_merge_file_result_free(&mut self.raw) }
}
}

impl<'repo> std::fmt::Debug for MergeFileResult<'repo> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_struct("MergeFileResult");
if let Some(path) = &self.path() {
ds.field("path", path);
}
ds.field("automergeable", &self.is_automergeable());
ds.field("mode", &self.mode());
ds.finish()
}
}
Loading
Loading