diff --git a/.gitignore b/.gitignore index 2a729c83fa..74bfa406dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ target Cargo.lock src/main.rs + +.vscode/ +.history/ +.idea/ \ No newline at end of file diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 259d5e8df1..d27744b225 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -8,6 +8,7 @@ use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t}; #[cfg(feature = "ssh")] use libssh2_sys as libssh2; use std::ffi::CStr; +use std::os::raw::c_ushort; pub const GIT_OID_RAWSZ: usize = 20; pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2; @@ -1348,6 +1349,65 @@ git_enum! { } } +#[repr(C)] +pub struct git_merge_file_options { + pub version: c_uint, + + /// Label for the ancestor file side of the conflict which will be prepended + /// to labels in diff3-format merge files. + pub ancestor_label: *const c_char, + + /// Label for our file side of the conflict which will be prepended + /// to labels in merge files. + pub our_label: *const c_char, + + /// Label for their file side of the conflict which will be prepended + /// to labels in merge files. + pub their_label: *const c_char, + + /// The file to favor in region conflicts. + pub favor: git_merge_file_favor_t, + + /// see `git_merge_file_flag_t` + pub flags: c_uint, + pub marker_size: c_ushort, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct git_merge_file_input { + pub version: c_uint, + /// Pointer to the contents of the file. + pub ptr: *const c_char, + /// Size of the contents pointed to in `ptr`. + pub size: size_t, + /// File name of the conflicted file, or `NULL` to not merge the path. + pub path: *const c_char, + /// File mode of the conflicted file, or `0` to not merge the mode. + pub mode: c_uint, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct git_merge_file_result { + /// True if the output was automerged, false if the output contains + /// conflict markers. + pub automergeable: c_uint, + + /// The path that the resultant merge file should use, or NULL if a + /// filename conflict would occur. + pub path: *const c_char, + + /// The mode that the resultant merge file should use. + pub mode: c_uint, + + /// The contents of the merge. + pub ptr: *const c_char, + + /// The length of the merge contents. + pub len: size_t, +} + pub type git_transport_cb = Option< extern "C" fn( out: *mut *mut git_transport, @@ -3242,7 +3302,6 @@ extern "C" { pub fn git_repository_state_cleanup(repo: *mut git_repository) -> c_int; // merge analysis - pub fn git_merge_analysis( analysis_out: *mut git_merge_analysis_t, pref_out: *mut git_merge_preference_t, @@ -3251,6 +3310,22 @@ extern "C" { their_heads_len: usize, ) -> c_int; + // For git_merge_file + pub fn git_merge_file_options_init(opts: *mut git_merge_file_options, version: c_uint) + -> c_int; + pub fn git_merge_file_input_init(opts: *mut git_merge_file_input, version: c_uint) -> c_int; + + pub fn git_merge_file( + out: *mut git_merge_file_result, + ancestor: *const git_merge_file_input, + ours: *const git_merge_file_input, + theirs: *const git_merge_file_input, + opts: *const git_merge_file_options, + ) -> c_int; + + // Not used? + pub fn git_merge_file_result_free(result: *mut git_merge_file_result); + pub fn git_merge_analysis_for_ref( analysis_out: *mut git_merge_analysis_t, pref_out: *mut git_merge_preference_t, diff --git a/src/lib.rs b/src/lib.rs index cdc3648d83..9166f080e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,9 @@ pub use crate::index::{ pub use crate::indexer::{IndexerProgress, Progress}; pub use crate::mailmap::Mailmap; pub use crate::mempack::Mempack; -pub use crate::merge::{AnnotatedCommit, MergeOptions}; +pub use crate::merge::{ + AnnotatedCommit, MergeFileInput, MergeFileOptions, MergeFileResult, MergeOptions, +}; pub use crate::message::{ message_prettify, message_trailers_bytes, message_trailers_strs, MessageTrailersBytes, MessageTrailersBytesIterator, MessageTrailersStrs, MessageTrailersStrsIterator, @@ -1102,6 +1104,34 @@ pub enum FileMode { Commit, } +impl FileMode { + #[cfg(target_os = "windows")] + fn from(mode: i32) -> Self { + match mode { + raw::GIT_FILEMODE_UNREADABLE => FileMode::Unreadable, + raw::GIT_FILEMODE_TREE => FileMode::Tree, + raw::GIT_FILEMODE_BLOB => FileMode::Blob, + raw::GIT_FILEMODE_BLOB_EXECUTABLE => FileMode::BlobExecutable, + raw::GIT_FILEMODE_LINK => FileMode::Link, + raw::GIT_FILEMODE_COMMIT => FileMode::Commit, + mode => panic!("unknown file mode: {}", mode), + } + } + + #[cfg(not(target_os = "windows"))] + fn from(mode: u32) -> Self { + match mode { + raw::GIT_FILEMODE_UNREADABLE => FileMode::Unreadable, + raw::GIT_FILEMODE_TREE => FileMode::Tree, + raw::GIT_FILEMODE_BLOB => FileMode::Blob, + raw::GIT_FILEMODE_BLOB_EXECUTABLE => FileMode::BlobExecutable, + raw::GIT_FILEMODE_LINK => FileMode::Link, + raw::GIT_FILEMODE_COMMIT => FileMode::Commit, + mode => panic!("unknown file mode: {}", mode), + } + } +} + impl From for i32 { fn from(mode: FileMode) -> i32 { match mode { diff --git a/src/merge.rs b/src/merge.rs index 6bd30c10d1..7cef6adfb5 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -5,7 +5,10 @@ use std::str; use crate::call::Convert; use crate::util::Binding; -use crate::{raw, Commit, FileFavor, Oid}; +use crate::{raw, Commit, FileFavor, FileMode, IntoCString, Oid}; +use core::{ptr, slice}; +use std::convert::TryInto; +use std::ffi::{CStr, CString}; /// A structure to represent an annotated commit, the input to merge and rebase. /// @@ -192,3 +195,276 @@ impl<'repo> Drop for AnnotatedCommit<'repo> { unsafe { raw::git_annotated_commit_free(self.raw) } } } + +/// Options for merging files +pub struct MergeFileOptions { + /// Label for the ancestor file side of the conflict which will be prepended + /// to labels in diff3-format merge files. + ancestor_label: Option, + + /// Label for our file side of the conflict which will be prepended + /// to labels in merge files. + our_label: Option, + + /// Label for their file side of the conflict which will be prepended + /// to labels in merge files. + their_label: Option, + + // raw data + raw: raw::git_merge_file_options, +} + +impl Default for MergeFileOptions { + fn default() -> Self { + Self::new() + } +} + +impl MergeFileOptions { + /// Creates a default set of merge 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 + } + + /// Specify ancestor label, default is "ancestor" + pub fn ancestor_label(&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 + } + + /// Specify ancestor label, default is "ours" + pub fn our_label(&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 + } + + /// Specify ancestor label, default is "theirs" + pub fn their_label(&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 file_favor(&mut self, favor: FileFavor) -> &mut MergeFileOptions { + self.raw.favor = favor.convert(); + self + } + + /// Specify marker size, default is 7: <<<<<<< ours + pub fn marker_size(&mut self, size: u16) -> &mut MergeFileOptions { + self.raw.marker_size = size; + self + } + + /// Acquire a pointer to the underlying raw options. + pub unsafe fn raw(&self) -> *const raw::git_merge_file_options { + &self.raw as *const _ + } +} + +/// For git_merge_file_input +pub struct MergeFileInput<'a> { + raw: raw::git_merge_file_input, + + /// File name of the conflicted file, or `NULL` to not merge the path. + /// + /// You can turn this value into a `std::ffi::CString` with + /// `CString::new(&entry.path[..]).unwrap()`. To turn a reference into a + /// `&std::path::Path`, see the `bytes2path()` function in the private, + /// internal `util` module in this crate’s source code. + path: Option, + + /// File content + content: Option<&'a [u8]>, +} + +impl Default for MergeFileInput<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> MergeFileInput<'a> { + /// Creates a new set of empty diff options. + pub fn new() -> MergeFileInput<'a> { + let mut input = MergeFileInput { + raw: unsafe { mem::zeroed() }, + path: None, + content: None, + }; + assert_eq!( + unsafe { raw::git_merge_file_input_init(&mut input.raw, 1) }, + 0 + ); + input + } + + /// File name of the conflicted file, or `None` to not merge the path. + pub fn path(&mut self, t: T) -> &mut MergeFileInput<'a> { + self.path = Some(t.into_c_string().unwrap()); + + self.raw.path = self + .path + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + self + } + + /// File mode of the conflicted file, or `0` to not merge the mode. + pub fn mode(&mut self, mode: Option) -> &mut MergeFileInput<'a> { + if let Some(mode) = mode { + self.raw.mode = mode as u32; + } + + self + } + + /// File content, text or binary + pub fn content(&mut self, content: Option<&'a [u8]>) -> &mut MergeFileInput<'a> { + self.content = content; + + self.raw.size = self.content.as_ref().map(|c| c.len()).unwrap_or(0); + self.raw.ptr = self + .content + .as_ref() + .map(|c| c.as_ptr() as *const _) + .unwrap_or(ptr::null()); + + self + } + + /// Get the raw struct in C + pub fn raw(&self) -> *const raw::git_merge_file_input { + &self.raw as *const _ + } +} + +impl std::fmt::Debug for MergeFileInput<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("MergeFileInput"); + if let Some(path) = &self.path { + ds.field("path", path); + } + ds.field("mode", &FileMode::from(self.raw.mode.try_into().unwrap())); + + match FileMode::from(self.raw.mode.try_into().unwrap()) { + FileMode::Unreadable => {} + FileMode::Tree => {} + FileMode::Blob => { + let content = self + .content + .as_ref() + .map(|s| String::from_utf8_lossy(&s).to_string()) + .unwrap_or("unknown content".to_string()); + + ds.field("content", &content); + } + FileMode::BlobExecutable => {} + FileMode::Link => {} + FileMode::Commit => {} + } + ds.finish() + } +} + +/// For git_merge_file_result +pub struct MergeFileResult { + raw: raw::git_merge_file_result, +} + +impl MergeFileResult { + /// Create MergeFileResult from C + pub unsafe fn from_raw(raw: raw::git_merge_file_result) -> MergeFileResult { + MergeFileResult { raw } + } + + /// True if the output was automerged, false if the output contains + /// conflict markers. + pub fn automergeable(&self) -> bool { + self.raw.automergeable > 0 + } + + /// The path that the resultant merge file should use, or NULL if a + /// filename conflict would occur. + pub unsafe fn path(&self) -> Option { + let c_str: &CStr = CStr::from_ptr(self.raw.path); + let str_slice: &str = c_str.to_str().unwrap(); + let path: String = str_slice.to_owned(); + Some(path) + } + + /// The mode that the resultant merge file should use. + pub fn mode(&self) -> FileMode { + FileMode::from(self.raw.mode.try_into().unwrap()) + } + + /// The contents of the merge. + pub unsafe fn content(&self) -> Option> { + let content = + slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.len as usize).to_vec(); + Some(content) + } +} + +impl std::fmt::Debug for MergeFileResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("MergeFileResult"); + unsafe { + if let Some(path) = &self.path() { + ds.field("path", path); + } + } + ds.field("mode", &self.mode()); + + match self.mode() { + FileMode::Unreadable => {} + FileMode::Tree => {} + FileMode::Blob => unsafe { + let content = self + .content() + .as_ref() + .map(|c| String::from_utf8_unchecked(c.clone())) + .unwrap_or("unknown content".to_string()); + ds.field("content", &content); + }, + FileMode::BlobExecutable => {} + FileMode::Link => {} + FileMode::Commit => {} + } + + ds.finish() + } +} diff --git a/src/repo.rs b/src/repo.rs index 92fa948305..addeba995a 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -11,6 +11,7 @@ use crate::build::{CheckoutBuilder, RepoBuilder}; use crate::diff::{ binary_cb_c, file_cb_c, hunk_cb_c, line_cb_c, BinaryCb, DiffCallbacks, FileCb, HunkCb, LineCb, }; +use crate::merge::{MergeFileInput, MergeFileOptions, MergeFileResult}; use crate::oid_array::OidArray; use crate::stash::{stash_cb, StashApplyOptions, StashCbData}; use crate::string_array::StringArray; @@ -1975,6 +1976,43 @@ impl Repository { Ok(()) } + /// Merge two files as they exist in the in-memory data structures, using + /// the given common ancestor as the baseline, producing a + /// `git_merge_file_result` that reflects the merge result. The + /// `git_merge_file_result` must be freed with `git_merge_file_result_free`. + /// + /// Note that this function does not reference a repository and any + /// configuration must be passed as `git_merge_file_options`. + pub fn merge_file( + &self, + ancestor: Option<&MergeFileInput<'_>>, + ours: Option<&MergeFileInput<'_>>, + theirs: Option<&MergeFileInput<'_>>, + options: Option<&MergeFileOptions>, + ) -> Result { + let mut ret = raw::git_merge_file_result { + automergeable: 0, + path: ptr::null(), + mode: 0, + ptr: ptr::null(), + len: 0, + }; + + unsafe { + try_call!(raw::git_merge_file( + &mut ret, + ancestor.map(|a| a.raw()).unwrap_or(ptr::null()), + ours.map(|a| a.raw()).unwrap_or(ptr::null()), + theirs.map(|a| a.raw()).unwrap_or(ptr::null()), + options.map(|o| o.raw()) + )); + + let result = MergeFileResult::from_raw(ret); + + Ok(result) + } + } + /// Merges the given commit(s) into HEAD, writing the results into the /// working directory. Any changes are staged for commit and any conflicts /// are written to the index. Callers should inspect the repository's index @@ -3280,12 +3318,14 @@ impl RepositoryInitOptions { #[cfg(test)] mod tests { use crate::build::CheckoutBuilder; - use crate::CherrypickOptions; + use crate::{CherrypickOptions, FileMode, MergeFileInput}; use crate::{ ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, }; + use std::convert::TryInto; use std::ffi::OsStr; use std::fs; + use std::io::Write; use std::path::Path; use tempfile::TempDir; @@ -3506,6 +3546,231 @@ mod tests { assert_eq!(repo.head().unwrap().target().unwrap(), main_oid); } + /// merge files then return the result + #[test] + fn smoke_merge_file() { + let (_temp_dir, repo) = graph_repo_init(); + let sig = repo.signature().unwrap(); + + // let oid1 = head + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + println!("created oid1 {:?}", oid1); + + repo.branch("branch_a", &commit1, true).unwrap(); + repo.branch("branch_b", &commit1, true).unwrap(); + + let file_on_branch_a_content_1 = "111\n222\n333\n"; + let file_on_branch_a_content_2 = "bbb\nccc\nxxx\nyyy\nzzz"; + let file_on_branch_b_content_1 = "aaa\nbbb\nccc\n"; + let file_on_branch_b_content_2 = "ooo\nppp\nqqq\nkkk"; + let merge_file_result_content = "<<<<<<< file_a\n111\n222\n333\nbbb\nccc\nxxx\nyyy\nzzz\n=======\naaa\nbbb\nccc\nooo\nppp\nqqq\nkkk\n>>>>>>> file_a\n"; + + // create commit oid2 on branchA + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + println!("using path {:?}", p); + let mut file_a = fs::File::create(&p).unwrap(); + file_a + .write_all(file_on_branch_a_content_1.as_bytes()) + .unwrap(); + drop(file_a); + index.add_path(Path::new("file_a")).unwrap(); + let id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(id_a).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_a, + &[&commit1], + ) + .unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid3 on branchB + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + let mut file_a = fs::File::create(&p).unwrap(); + file_a + .write_all(file_on_branch_b_content_1.as_bytes()) + .unwrap(); + drop(file_a); + index.add_path(Path::new("file_a")).unwrap(); + let id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(id_b).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 3", + &tree_b, + &[&commit1], + ) + .unwrap(); + let commit3 = repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + + t!(repo.reset(commit2.as_object(), ResetType::Hard, None)); + + // create commit oid4 on branchA + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + let mut file_a = fs::OpenOptions::new().append(true).open(&p).unwrap(); + file_a.write(file_on_branch_a_content_2.as_bytes()).unwrap(); + drop(file_a); + index.add_path(Path::new("file_a")).unwrap(); + let id_a_2 = index.write_tree().unwrap(); + let tree_a_2 = repo.find_tree(id_a_2).unwrap(); + let oid4 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 4", + &tree_a_2, + &[&commit2], + ) + .unwrap(); + let commit4 = repo.find_commit(oid4).unwrap(); + println!("created oid4 {:?}", oid4); + + t!(repo.reset(commit3.as_object(), ResetType::Hard, None)); + + // create commit oid5 on branchB + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + let mut file_a = fs::OpenOptions::new().append(true).open(&p).unwrap(); + file_a.write(file_on_branch_b_content_2.as_bytes()).unwrap(); + drop(file_a); + index.add_path(Path::new("file_a")).unwrap(); + let id_b_2 = index.write_tree().unwrap(); + let tree_b_2 = repo.find_tree(id_b_2).unwrap(); + let oid5 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 5", + &tree_b_2, + &[&commit3], + ) + .unwrap(); + let commit5 = repo.find_commit(oid5).unwrap(); + println!("created oid5 {:?}", oid5); + + // create merge commit oid4 on branchA with parents oid2 and oid3 + //let mut index4 = repo.merge_commits(&commit2, &commit3, None).unwrap(); + repo.set_head("refs/heads/branch_a").unwrap(); + repo.checkout_head(None).unwrap(); + + let index = repo.merge_commits(&commit4, &commit5, None).unwrap(); + + assert!(index.has_conflicts(), "index should have conflicts"); + + let mut conflict_count = 0; + + let index_conflicts = index.conflicts().unwrap(); + for conflict in index_conflicts { + let conflict = conflict.unwrap(); + + let ancestor_input; + let ours_input; + let theirs_input; + + let ancestor_blob; + let ours_blob; + let theirs_blob; + + if let Some(ancestor) = conflict.ancestor { + match repo.find_blob(ancestor.id.clone()) { + Ok(b) => { + ancestor_blob = b; + let ancestor_content = ancestor_blob.content(); + let mut input = MergeFileInput::new(); + input.path(String::from_utf8(ancestor.path).unwrap()); + input.mode(Some(FileMode::from(ancestor.mode.try_into().unwrap()))); + input.content(Some(&ancestor_content)); + ancestor_input = Some(input); + } + Err(_e) => { + ancestor_input = None; + } + } + } else { + ancestor_input = None; + } + if let Some(ours) = conflict.our { + match repo.find_blob(ours.id.clone()) { + Ok(b) => { + ours_blob = b; + let ours_content = ours_blob.content(); + let mut input = MergeFileInput::new(); + input.path(String::from_utf8(ours.path).unwrap()); + input.mode(Some(FileMode::from(ours.mode.try_into().unwrap()))); + input.content(Some(&ours_content)); + ours_input = Some(input); + } + Err(_e) => { + ours_input = None; + } + } + } else { + ours_input = None; + } + if let Some(theirs) = conflict.their { + match repo.find_blob(theirs.id.clone()) { + Ok(b) => { + theirs_blob = b; + let theirs_content = theirs_blob.content(); + let mut input = MergeFileInput::new(); + input.path(String::from_utf8(theirs.path).unwrap()); + input.mode(Some(FileMode::from(theirs.mode.try_into().unwrap()))); + input.content(Some(&theirs_content)); + theirs_input = Some(input); + } + Err(_e) => { + theirs_input = None; + } + } + } else { + theirs_input = None; + } + + let merge_file_result = repo + .merge_file( + ancestor_input.as_ref(), + ours_input.as_ref(), + theirs_input.as_ref(), + None, + ) + .unwrap(); + + assert_eq!(merge_file_result.mode(), FileMode::Blob); + assert_eq!( + unsafe { merge_file_result.path().unwrap().as_str() }, + "file_a" + ); + assert_eq!( + unsafe { + String::from_utf8(merge_file_result.content().unwrap()) + .unwrap() + .as_str() + }, + merge_file_result_content + ); + + conflict_count += 1; + } + assert_eq!(conflict_count, 1, "There should be one conflict!"); + } + /// create the following: /// /---o4 /// /---o3