Skip to main content

git2/
merge.rs

1use libc::{c_char, c_uint, c_ushort, size_t};
2use std::ffi::CString;
3use std::marker;
4use std::mem;
5use std::ptr;
6use std::str;
7
8use crate::call::Convert;
9use crate::util::Binding;
10use crate::{raw, Commit, Error, FileFavor, FileMode, IntoCString, Oid};
11
12/// A structure to represent an annotated commit, the input to merge and rebase.
13///
14/// An annotated commit contains information about how it was looked up, which
15/// may be useful for functions like merge or rebase to provide context to the
16/// operation.
17pub struct AnnotatedCommit<'repo> {
18    raw: *mut raw::git_annotated_commit,
19    _marker: marker::PhantomData<Commit<'repo>>,
20}
21
22/// Options to specify when merging.
23pub struct MergeOptions {
24    raw: raw::git_merge_options,
25}
26
27/// Options for merging a file.
28pub struct MergeFileOptions {
29    ancestor_label: Option<CString>,
30    our_label: Option<CString>,
31    their_label: Option<CString>,
32    raw: raw::git_merge_file_options,
33}
34
35/// Information about file-level merging.
36pub struct MergeFileResult {
37    raw: raw::git_merge_file_result,
38}
39
40impl<'repo> AnnotatedCommit<'repo> {
41    /// Gets the commit ID that the given git_annotated_commit refers to
42    pub fn id(&self) -> Oid {
43        unsafe { Binding::from_raw(raw::git_annotated_commit_id(self.raw)) }
44    }
45
46    /// Get the refname that the given git_annotated_commit refers to
47    pub fn refname(&self) -> Result<&str, Error> {
48        str::from_utf8(self.refname_bytes()).map_err(|e| e.into())
49    }
50
51    /// Get the refname that the given git_annotated_commit refers to.
52    pub fn refname_bytes(&self) -> &[u8] {
53        unsafe { crate::opt_bytes(self, raw::git_annotated_commit_ref(&*self.raw)).unwrap() }
54    }
55}
56
57impl Default for MergeOptions {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl MergeOptions {
64    /// Creates a default set of merge options.
65    pub fn new() -> MergeOptions {
66        let mut opts = MergeOptions {
67            raw: unsafe { mem::zeroed() },
68        };
69        assert_eq!(unsafe { raw::git_merge_init_options(&mut opts.raw, 1) }, 0);
70        opts
71    }
72
73    fn flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions {
74        if val {
75            self.raw.flags |= opt;
76        } else {
77            self.raw.flags &= !opt;
78        }
79        self
80    }
81
82    /// Detect file renames
83    pub fn find_renames(&mut self, find: bool) -> &mut MergeOptions {
84        self.flag(raw::GIT_MERGE_FIND_RENAMES as u32, find)
85    }
86
87    /// If a conflict occurs, exit immediately instead of attempting to continue
88    /// resolving conflicts
89    pub fn fail_on_conflict(&mut self, fail: bool) -> &mut MergeOptions {
90        self.flag(raw::GIT_MERGE_FAIL_ON_CONFLICT as u32, fail)
91    }
92
93    /// Do not write the REUC extension on the generated index
94    pub fn skip_reuc(&mut self, skip: bool) -> &mut MergeOptions {
95        self.flag(raw::GIT_MERGE_SKIP_REUC as u32, skip)
96    }
97
98    /// If the commits being merged have multiple merge bases, do not build a
99    /// recursive merge base (by merging the multiple merge bases), instead
100    /// simply use the first base.
101    pub fn no_recursive(&mut self, disable: bool) -> &mut MergeOptions {
102        self.flag(raw::GIT_MERGE_NO_RECURSIVE as u32, disable)
103    }
104
105    /// Similarity to consider a file renamed (default 50)
106    pub fn rename_threshold(&mut self, thresh: u32) -> &mut MergeOptions {
107        self.raw.rename_threshold = thresh;
108        self
109    }
110
111    ///  Maximum similarity sources to examine for renames (default 200).
112    /// If the number of rename candidates (add / delete pairs) is greater
113    /// than this value, inexact rename detection is aborted. This setting
114    /// overrides the `merge.renameLimit` configuration value.
115    pub fn target_limit(&mut self, limit: u32) -> &mut MergeOptions {
116        self.raw.target_limit = limit as c_uint;
117        self
118    }
119
120    /// Maximum number of times to merge common ancestors to build a
121    /// virtual merge base when faced with criss-cross merges.  When
122    /// this limit is reached, the next ancestor will simply be used
123    /// instead of attempting to merge it.  The default is unlimited.
124    pub fn recursion_limit(&mut self, limit: u32) -> &mut MergeOptions {
125        self.raw.recursion_limit = limit as c_uint;
126        self
127    }
128
129    /// Specify a side to favor for resolving conflicts
130    pub fn file_favor(&mut self, favor: FileFavor) -> &mut MergeOptions {
131        self.raw.file_favor = favor.convert();
132        self
133    }
134
135    fn file_flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions {
136        if val {
137            self.raw.file_flags |= opt;
138        } else {
139            self.raw.file_flags &= !opt;
140        }
141        self
142    }
143
144    /// Create standard conflicted merge files
145    pub fn standard_style(&mut self, standard: bool) -> &mut MergeOptions {
146        self.file_flag(raw::GIT_MERGE_FILE_STYLE_MERGE as u32, standard)
147    }
148
149    /// Create diff3-style file
150    pub fn diff3_style(&mut self, diff3: bool) -> &mut MergeOptions {
151        self.file_flag(raw::GIT_MERGE_FILE_STYLE_DIFF3 as u32, diff3)
152    }
153
154    /// Condense non-alphanumeric regions for simplified diff file
155    pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeOptions {
156        self.file_flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM as u32, simplify)
157    }
158
159    /// Ignore all whitespace
160    pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeOptions {
161        self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE as u32, ignore)
162    }
163
164    /// Ignore changes in amount of whitespace
165    pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeOptions {
166        self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE as u32, ignore)
167    }
168
169    /// Ignore whitespace at end of line
170    pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeOptions {
171        self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL as u32, ignore)
172    }
173
174    /// Use the "patience diff" algorithm
175    pub fn patience(&mut self, patience: bool) -> &mut MergeOptions {
176        self.file_flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE as u32, patience)
177    }
178
179    /// Take extra time to find minimal diff
180    pub fn minimal(&mut self, minimal: bool) -> &mut MergeOptions {
181        self.file_flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL as u32, minimal)
182    }
183
184    /// Acquire a pointer to the underlying raw options.
185    pub unsafe fn raw(&self) -> *const raw::git_merge_options {
186        &self.raw as *const _
187    }
188}
189
190impl<'repo> Binding for AnnotatedCommit<'repo> {
191    type Raw = *mut raw::git_annotated_commit;
192    unsafe fn from_raw(raw: *mut raw::git_annotated_commit) -> AnnotatedCommit<'repo> {
193        AnnotatedCommit {
194            raw,
195            _marker: marker::PhantomData,
196        }
197    }
198    fn raw(&self) -> *mut raw::git_annotated_commit {
199        self.raw
200    }
201}
202
203impl<'repo> Drop for AnnotatedCommit<'repo> {
204    fn drop(&mut self) {
205        unsafe { raw::git_annotated_commit_free(self.raw) }
206    }
207}
208
209impl Default for MergeFileOptions {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215impl MergeFileOptions {
216    /// Creates a default set of merge file options.
217    pub fn new() -> MergeFileOptions {
218        let mut opts = MergeFileOptions {
219            ancestor_label: None,
220            our_label: None,
221            their_label: None,
222            raw: unsafe { mem::zeroed() },
223        };
224        assert_eq!(
225            unsafe { raw::git_merge_file_options_init(&mut opts.raw, 1) },
226            0
227        );
228        opts
229    }
230
231    /// Label for the ancestor file side of the conflict which will be prepended
232    /// to labels in diff3-format merge files.
233    pub fn ancestor_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
234        self.ancestor_label = Some(t.into_c_string().unwrap());
235
236        self.raw.ancestor_label = self
237            .ancestor_label
238            .as_ref()
239            .map(|s| s.as_ptr())
240            .unwrap_or(ptr::null());
241
242        self
243    }
244
245    /// Label for our file side of the conflict which will be prepended to labels
246    /// in merge files.
247    pub fn our_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
248        self.our_label = Some(t.into_c_string().unwrap());
249
250        self.raw.our_label = self
251            .our_label
252            .as_ref()
253            .map(|s| s.as_ptr())
254            .unwrap_or(ptr::null());
255
256        self
257    }
258
259    /// Label for their file side of the conflict which will be prepended to labels
260    /// in merge files.
261    pub fn their_label<T: IntoCString>(&mut self, t: T) -> &mut MergeFileOptions {
262        self.their_label = Some(t.into_c_string().unwrap());
263
264        self.raw.their_label = self
265            .their_label
266            .as_ref()
267            .map(|s| s.as_ptr())
268            .unwrap_or(ptr::null());
269
270        self
271    }
272
273    /// Specify a side to favor for resolving conflicts
274    pub fn favor(&mut self, favor: FileFavor) -> &mut MergeFileOptions {
275        self.raw.favor = favor.convert();
276        self
277    }
278
279    fn flag(&mut self, opt: raw::git_merge_file_flag_t, val: bool) -> &mut MergeFileOptions {
280        if val {
281            self.raw.flags |= opt as u32;
282        } else {
283            self.raw.flags &= !opt as u32;
284        }
285        self
286    }
287
288    /// Create standard conflicted merge files
289    pub fn style_standard(&mut self, standard: bool) -> &mut MergeFileOptions {
290        self.flag(raw::GIT_MERGE_FILE_STYLE_MERGE, standard)
291    }
292
293    /// Create diff3-style file
294    pub fn style_diff3(&mut self, diff3: bool) -> &mut MergeFileOptions {
295        self.flag(raw::GIT_MERGE_FILE_STYLE_DIFF3, diff3)
296    }
297
298    /// Condense non-alphanumeric regions for simplified diff file
299    pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeFileOptions {
300        self.flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM, simplify)
301    }
302
303    /// Ignore all whitespace
304    pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeFileOptions {
305        self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE, ignore)
306    }
307
308    /// Ignore changes in amount of whitespace
309    pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeFileOptions {
310        self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE, ignore)
311    }
312
313    /// Ignore whitespace at end of line
314    pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeFileOptions {
315        self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL, ignore)
316    }
317
318    /// Use the "patience diff" algorithm
319    pub fn patience(&mut self, patience: bool) -> &mut MergeFileOptions {
320        self.flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE, patience)
321    }
322
323    /// Take extra time to find minimal diff
324    pub fn minimal(&mut self, minimal: bool) -> &mut MergeFileOptions {
325        self.flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL, minimal)
326    }
327
328    /// Create zdiff3 ("zealous diff3")-style files
329    pub fn style_zdiff3(&mut self, zdiff3: bool) -> &mut MergeFileOptions {
330        self.flag(raw::GIT_MERGE_FILE_STYLE_ZDIFF3, zdiff3)
331    }
332
333    /// Do not produce file conflicts when common regions have changed
334    pub fn accept_conflicts(&mut self, accept: bool) -> &mut MergeFileOptions {
335        self.flag(raw::GIT_MERGE_FILE_ACCEPT_CONFLICTS, accept)
336    }
337
338    /// The size of conflict markers (eg, "<<<<<<<"). Default is 7.
339    pub fn marker_size(&mut self, size: u16) -> &mut MergeFileOptions {
340        self.raw.marker_size = size as c_ushort;
341        self
342    }
343
344    /// Acquire a pointer to the underlying raw options.
345    ///
346    /// # Safety
347    /// The pointer used here (or its contents) should not outlive self.
348    pub(crate) unsafe fn raw(&mut self) -> *const raw::git_merge_file_options {
349        &self.raw
350    }
351}
352
353impl MergeFileResult {
354    /// True if the output was automerged, false if the output contains
355    /// conflict markers.
356    pub fn is_automergeable(&self) -> bool {
357        self.raw.automergeable > 0
358    }
359
360    /// The path that the resultant merge file should use.
361    ///
362    /// returns `Ok(None)` if a filename conflict would occur
363    pub fn path(&self) -> Result<Option<&str>, Error> {
364        match self.path_bytes() {
365            Some(pb) => str::from_utf8(pb).map(|s| Some(s)).map_err(|e| e.into()),
366            None => Ok(None),
367        }
368    }
369
370    /// Gets the path as a byte slice.
371    pub fn path_bytes(&self) -> Option<&[u8]> {
372        unsafe { crate::opt_bytes(self, self.raw.path) }
373    }
374
375    /// The mode that the resultant merge file should use.
376    pub fn mode(&self) -> u32 {
377        self.raw.mode as u32
378    }
379
380    /// The contents of the merge.
381    pub fn content(&self) -> &[u8] {
382        unsafe { std::slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.len as usize) }
383    }
384}
385
386impl Binding for MergeFileResult {
387    type Raw = raw::git_merge_file_result;
388    unsafe fn from_raw(raw: raw::git_merge_file_result) -> MergeFileResult {
389        MergeFileResult { raw }
390    }
391    fn raw(&self) -> raw::git_merge_file_result {
392        unimplemented!()
393    }
394}
395
396impl Drop for MergeFileResult {
397    fn drop(&mut self) {
398        unsafe { raw::git_merge_file_result_free(&mut self.raw) }
399    }
400}
401
402impl std::fmt::Debug for MergeFileResult {
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        let mut ds = f.debug_struct("MergeFileResult");
405        if let Ok(Some(path)) = &self.path() {
406            ds.field("path", path);
407        }
408        ds.field("automergeable", &self.is_automergeable());
409        ds.field("mode", &self.mode());
410        ds.finish()
411    }
412}
413
414/// Input structure for merging a file.
415pub struct MergeFileInput<'a> {
416    raw: raw::git_merge_file_input,
417    path: Option<CString>,
418    content: Option<&'a [u8]>,
419}
420
421impl Default for MergeFileInput<'_> {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427impl Binding for MergeFileInput<'_> {
428    type Raw = *const raw::git_merge_file_input;
429
430    unsafe fn from_raw(_raw: *const raw::git_merge_file_input) -> MergeFileInput<'static> {
431        panic!("unimplemened")
432    }
433
434    fn raw(&self) -> *const raw::git_merge_file_input {
435        &self.raw as *const _
436    }
437}
438
439impl<'a> MergeFileInput<'a> {
440    /// Creates a new, empty merge file input.
441    pub fn new() -> MergeFileInput<'a> {
442        let mut input = MergeFileInput {
443            raw: unsafe { mem::zeroed() },
444            path: None,
445            content: None,
446        };
447        assert_eq!(
448            unsafe { raw::git_merge_file_input_init(&mut input.raw, 1) },
449            0
450        );
451        input
452    }
453
454    /// Sets the content of the file.
455    pub fn content(&mut self, content: &'a [u8]) -> &mut MergeFileInput<'a> {
456        self.content = Some(content);
457
458        self.raw.ptr = content.as_ptr() as *const c_char;
459        self.raw.size = content.len() as size_t;
460
461        self
462    }
463
464    /// Sets the path of the file.
465    pub fn path<T: IntoCString>(&mut self, t: T) -> &mut MergeFileInput<'a> {
466        self.path = Some(t.into_c_string().unwrap());
467
468        self.raw.path = self
469            .path
470            .as_ref()
471            .map(|s| s.as_ptr())
472            .unwrap_or(ptr::null());
473
474        self
475    }
476
477    /// Sets the mode of the file.
478    pub fn mode(&mut self, mode: Option<FileMode>) -> &mut MergeFileInput<'a> {
479        self.raw.mode = mode.map_or(0, u32::from);
480        self
481    }
482}
483
484/// Merges three versions of a file: an ancestor version, our version, and their version.
485pub fn merge_file(
486    ancestor: &MergeFileInput<'_>,
487    ours: &MergeFileInput<'_>,
488    theirs: &MergeFileInput<'_>,
489    opts: Option<&mut MergeFileOptions>,
490) -> Result<MergeFileResult, Error> {
491    unsafe {
492        let ancestor = ancestor.raw();
493        let ours = ours.raw();
494        let theirs = theirs.raw();
495
496        let mut ret = mem::zeroed();
497        try_call!(raw::git_merge_file(
498            &mut ret,
499            ancestor,
500            ours,
501            theirs,
502            opts.map(|o| o.raw()).unwrap_or(ptr::null())
503        ));
504        Ok(Binding::from_raw(ret))
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use crate::{MergeFileInput, MergeFileOptions};
511
512    use std::path::Path;
513
514    #[test]
515    fn smoke_merge_file() {
516        let file_path = Path::new("file");
517        let base = {
518            let mut input = MergeFileInput::new();
519            input.content(b"base").path(file_path);
520            input
521        };
522
523        let ours = {
524            let mut input = MergeFileInput::new();
525            input.content(b"foo").path(file_path);
526            input
527        };
528
529        let theirs = {
530            let mut input = MergeFileInput::new();
531            input.content(b"bar").path(file_path);
532            input
533        };
534
535        let mut opts = MergeFileOptions::new();
536        opts.ancestor_label("ancestor");
537        opts.our_label("ours");
538        opts.their_label("theirs");
539        opts.style_diff3(true);
540        let merge_file_result = crate::merge_file(&base, &ours, &theirs, Some(&mut opts)).unwrap();
541
542        assert!(!merge_file_result.is_automergeable());
543        assert_eq!(merge_file_result.path(), Ok(Some("file")));
544        assert_eq!(
545            String::from_utf8_lossy(merge_file_result.content()).to_string(),
546            r"<<<<<<< ours
547foo
548||||||| ancestor
549base
550=======
551bar
552>>>>>>> theirs
553",
554        );
555    }
556}