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

Allow setting merge options in Repository.merge #1008

Merged
merged 2 commits into from
May 19, 2020
Merged
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
145 changes: 123 additions & 22 deletions pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,32 +559,81 @@ def index(self):
#
# Merging
#
_FAVOR_TO_ENUM = {
'normal': C.GIT_MERGE_FILE_FAVOR_NORMAL,
'ours': C.GIT_MERGE_FILE_FAVOR_OURS,
'theirs': C.GIT_MERGE_FILE_FAVOR_THEIRS,
'union': C.GIT_MERGE_FILE_FAVOR_UNION,
}

_MERGE_FLAG_TO_ENUM = {
'find_renames': C.GIT_MERGE_FIND_RENAMES,
'fail_on_conflict': C.GIT_MERGE_FAIL_ON_CONFLICT,
'skip_reuc': C.GIT_MERGE_SKIP_REUC,
'no_recursive': C.GIT_MERGE_NO_RECURSIVE,
}

_MERGE_FLAG_DEFAULTS = {
'find_renames': True,
}

_MERGE_FILE_FLAG_TO_ENUM = {
'standard_style': C.GIT_MERGE_FILE_STYLE_MERGE,
'diff3_style': C.GIT_MERGE_FILE_STYLE_DIFF3,
'simplify_alnum': C.GIT_MERGE_FILE_SIMPLIFY_ALNUM,
'ignore_whitespace': C.GIT_MERGE_FILE_IGNORE_WHITESPACE,
'ignore_whitespace_change': C.GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE,
'ignore_whitespace_eol': C.GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL,
'patience': C.GIT_MERGE_FILE_DIFF_PATIENCE,
'minimal': C.GIT_MERGE_FILE_DIFF_MINIMAL,
}

_MERGE_FILE_FLAG_DEFAULTS = {}

@staticmethod
def _merge_options(favor):
"""Return a 'git_merge_opts *'"""
@classmethod
def _flag_dict_to_bitmask(cls, flag_dict, flag_defaults, mapping, label):
"""
Converts a dict eg {"find_renames": True, "skip_reuc": True} to
a bitmask eg C.GIT_MERGE_FIND_RENAMES | C.GIT_MERGE_SKIP_REUC.
"""
merged_dict = {**flag_defaults, **flag_dict}
bitmask = 0
for k, v in merged_dict.items():
enum = mapping.get(k, None)
if enum is None:
raise ValueError("unknown %s: %s" % (label, k))
if v:
bitmask |= enum
return bitmask

def favor_to_enum(favor):
if favor == 'normal':
return C.GIT_MERGE_FILE_FAVOR_NORMAL
elif favor == 'ours':
return C.GIT_MERGE_FILE_FAVOR_OURS
elif favor == 'theirs':
return C.GIT_MERGE_FILE_FAVOR_THEIRS
elif favor == 'union':
return C.GIT_MERGE_FILE_FAVOR_UNION
else:
return None
@classmethod
def _merge_options(cls, favor='normal', flags={}, file_flags={}):
"""Return a 'git_merge_opts *'"""

favor_val = favor_to_enum(favor)
favor_val = cls._FAVOR_TO_ENUM.get(favor, None)
if favor_val is None:
raise ValueError("unkown favor value %s" % favor)
raise ValueError("unknown favor: %s" % favor)

flags_bitmask = Repository._flag_dict_to_bitmask(
flags,
cls._MERGE_FLAG_DEFAULTS,
cls._MERGE_FLAG_TO_ENUM,
"merge flag"
)
file_flags_bitmask = cls._flag_dict_to_bitmask(
file_flags,
cls._MERGE_FILE_FLAG_DEFAULTS,
cls._MERGE_FILE_FLAG_TO_ENUM,
"merge file_flag"
)

opts = ffi.new('git_merge_options *')
err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION)
check_error(err)

opts.file_favor = favor_val
opts.flags = flags_bitmask
opts.file_flags = file_flags_bitmask

return opts

Expand Down Expand Up @@ -621,7 +670,7 @@ def merge_file_from_index(self, ancestor, ours, theirs):

return ret

def merge_commits(self, ours, theirs, favor='normal'):
def merge_commits(self, ours, theirs, favor='normal', flags={}, file_flags={}):
"""
Merge two arbitrary commits.

Expand All @@ -645,8 +694,34 @@ def merge_commits(self, ours, theirs, favor='normal'):

For all but NORMAL, the index will not record a conflict.

Both "ours" and "theirs" can be any object which peels to a commit or the id
(string or Oid) of an object which peels to a commit.
flags
A dict of str: bool to turn on or off functionality while merging.
If a key is not present, the default will be used. The keys are:

* find_renames. Detect file renames. Defaults to True.
* fail_on_conflict. If a conflict occurs, exit immediately instead
of attempting to continue resolving conflicts.
* skip_reuc. Do not write the REUC extension on the generated index.
* no_recursive. If the commits being merged have multiple merge
bases, do not build a recursive merge base (by merging the
multiple merge bases), instead simply use the first base.

file_flags
A dict of str: bool to turn on or off functionality while merging.
If a key is not present, the default will be used. The keys are:

* standard_style. Create standard conflicted merge files.
* diff3_style. Create diff3-style file.
* simplify_alnum. Condense non-alphanumeric regions for simplified
diff file.
* ignore_whitespace. Ignore all whitespace.
* ignore_whitespace_change. Ignore changes in amount of whitespace.
* ignore_whitespace_eol. Ignore whitespace at end of line.
* patience. Use the "patience diff" algorithm
* minimal. Take extra time to find minimal diff

Both "ours" and "theirs" can be any object which peels to a commit or
the id (string or Oid) of an object which peels to a commit.
"""

ours_ptr = ffi.new('git_commit **')
Expand All @@ -661,7 +736,7 @@ def merge_commits(self, ours, theirs, favor='normal'):
ours = ours.peel(Commit)
theirs = theirs.peel(Commit)

opts = self._merge_options(favor)
opts = self._merge_options(favor, flags, file_flags)

ffi.buffer(ours_ptr)[:] = ours._pointer[:]
ffi.buffer(theirs_ptr)[:] = theirs._pointer[:]
Expand All @@ -671,7 +746,7 @@ def merge_commits(self, ours, theirs, favor='normal'):

return Index.from_c(self, cindex)

def merge_trees(self, ancestor, ours, theirs, favor='normal'):
def merge_trees(self, ancestor, ours, theirs, favor='normal', flags={}, file_flags={}):
"""
Merge two trees.

Expand All @@ -697,6 +772,32 @@ def merge_trees(self, ancestor, ours, theirs, favor='normal'):
* union. Unique lines from each side will be used.

For all but NORMAL, the index will not record a conflict.

flags
A dict of str: bool to turn on or off functionality while merging.
If a key is not present, the default will be used. The keys are:

* find_renames. Detect file renames. Defaults to True.
* fail_on_conflict. If a conflict occurs, exit immediately instead
of attempting to continue resolving conflicts.
* skip_reuc. Do not write the REUC extension on the generated index.
* no_recursive. If the commits being merged have multiple merge
bases, do not build a recursive merge base (by merging the
multiple merge bases), instead simply use the first base.

file_flags
A dict of str: bool to turn on or off functionality while merging.
If a key is not present, the default will be used. The keys are:

* standard_style. Create standard conflicted merge files.
* diff3_style. Create diff3-style file.
* simplify_alnum. Condense non-alphanumeric regions for simplified
diff file.
* ignore_whitespace. Ignore all whitespace.
* ignore_whitespace_change. Ignore changes in amount of whitespace.
* ignore_whitespace_eol. Ignore whitespace at end of line.
* patience. Use the "patience diff" algorithm
* minimal. Take extra time to find minimal diff
"""

ancestor_ptr = ffi.new('git_tree **')
Expand All @@ -715,7 +816,7 @@ def merge_trees(self, ancestor, ours, theirs, favor='normal'):
ours = ours.peel(Tree)
theirs = theirs.peel(Tree)

opts = self._merge_options(favor)
opts = self._merge_options(favor, flags, file_flags)

ffi.buffer(ancestor_ptr)[:] = ancestor._pointer[:]
ffi.buffer(ours_ptr)[:] = ours._pointer[:]
Expand Down
37 changes: 37 additions & 0 deletions test/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,40 @@ def test_merge_trees_favor(mergerepo):

with pytest.raises(ValueError):
mergerepo.merge_trees(ancestor_id, mergerepo.head.target, branch_head_hex, favor='foo')


def test_merge_options():
from pygit2.ffi import C

# Default
o = pygit2.Repository._merge_options()
assert o.file_favor == C.GIT_MERGE_FILE_FAVOR_NORMAL
assert o.flags == C.GIT_MERGE_FIND_RENAMES
assert o.file_flags == 0

o = pygit2.Repository._merge_options(
favor='ours', flags={'fail_on_conflict': True}, file_flags={'ignore_whitespace': True}
)
assert o.file_favor == C.GIT_MERGE_FILE_FAVOR_OURS
assert o.flags == C.GIT_MERGE_FIND_RENAMES | C.GIT_MERGE_FAIL_ON_CONFLICT
assert o.file_flags == C.GIT_MERGE_FILE_IGNORE_WHITESPACE

o = pygit2.Repository._merge_options(
favor='theirs', flags={'find_renames': False}, file_flags={'ignore_whitespace': False}
)
assert o.file_favor == C.GIT_MERGE_FILE_FAVOR_THEIRS
assert o.flags == 0
assert o.file_flags == 0

o = pygit2.Repository._merge_options(
favor='union',
flags={'find_renames': True, 'no_recursive': True},
file_flags={'diff3_style': True, 'ignore_whitespace': True, 'patience': True}
)
assert o.file_favor == C.GIT_MERGE_FILE_FAVOR_UNION
assert o.flags == C.GIT_MERGE_FIND_RENAMES | C.GIT_MERGE_NO_RECURSIVE
assert o.file_flags == (
C.GIT_MERGE_FILE_STYLE_DIFF3
| C.GIT_MERGE_FILE_IGNORE_WHITESPACE
| C.GIT_MERGE_FILE_DIFF_PATIENCE
)