Skip to content

Repository: get an instance of MergeFileResult from git_merge_file_from_index #1376

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

Open
wants to merge 1 commit 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
29 changes: 29 additions & 0 deletions pygit2/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.

import typing
import warnings
import weakref
from dataclasses import dataclass

# Import from pygit2
from ._pygit2 import Oid, Tree, Diff
Expand Down Expand Up @@ -349,6 +351,33 @@ def conflicts(self):
return self._conflicts()


@dataclass
class MergeFileResult:
automergeable: bool
'True if the output was automerged, false if the output contains conflict markers'

path: typing.Union[str, None]
'The path that the resultant merge file should use, or None if a filename conflict would occur'

mode: FileMode
'The mode that the resultant merge file should use'

contents: str
'Contents of the file, which might include conflict markers'

@classmethod
def _from_c(cls, centry):
if centry == ffi.NULL:
return None

automergeable = centry.automergeable != 0
path = to_str(ffi.string(centry.path)) if centry.path else None
mode = FileMode(centry.mode)
contents = ffi.string(centry.ptr, centry.len).decode('utf-8')

return MergeFileResult(automergeable, path, mode, contents)


class IndexEntry:
path: str
'The path of this entry'
Expand Down
29 changes: 23 additions & 6 deletions pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
)
from .errors import check_error
from .ffi import ffi, C
from .index import Index, IndexEntry
from .index import Index, IndexEntry, MergeFileResult
from .packbuilder import PackBuilder
from .references import References
from .remotes import RemoteCollection
Expand Down Expand Up @@ -682,9 +682,13 @@ def merge_file_from_index(
ancestor: typing.Union[None, IndexEntry],
ours: typing.Union[None, IndexEntry],
theirs: typing.Union[None, IndexEntry],
) -> str:
"""Merge files from index. Return a string with the merge result
containing possible conflicts.
use_deprecated: bool = True,
) -> typing.Union[str, typing.Union[MergeFileResult, None]]:
"""Merge files from index.

Returns: A string with the content of the file containing
possible conflicts if use_deprecated==True.
If use_deprecated==False then it returns an instance of MergeFileResult.

ancestor
The index entry which will be used as a common
Expand All @@ -693,6 +697,10 @@ def merge_file_from_index(
The index entry to take as "ours" or base.
theirs
The index entry which will be merged into "ours"
use_deprecated
This controls what will be returned. If use_deprecated==True (default),
a string with the contents of the file will be returned.
An instance of MergeFileResult will be returned otherwise.
"""
cmergeresult = ffi.new('git_merge_file_result *')

Expand All @@ -709,10 +717,19 @@ def merge_file_from_index(
)
check_error(err)

ret = ffi.string(cmergeresult.ptr, cmergeresult.len).decode('utf-8')
mergeFileResult = MergeFileResult._from_c(cmergeresult)
C.git_merge_file_result_free(cmergeresult)

return ret
if use_deprecated:
warnings.warn(
'Getting an str from Repository.merge_file_from_index is deprecated. '
'The method will later return an instance of MergeFileResult by default, instead. '
'Check parameter use_deprecated.',
DeprecationWarning,
)
return mergeFileResult.contents if mergeFileResult else ''

return mergeFileResult

def merge_commits(
self,
Expand Down
146 changes: 145 additions & 1 deletion test/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# pygit2
import pygit2
from pygit2 import init_repository, clone_repository, discover_repository
from pygit2 import init_repository, clone_repository, discover_repository, IndexEntry
from pygit2 import Oid
from pygit2.enums import (
CheckoutNotify,
Expand All @@ -42,7 +42,9 @@
RepositoryState,
ResetMode,
StashApplyProgress,
FileMode,
)
from pygit2.index import MergeFileResult
from . import utils


Expand Down Expand Up @@ -985,3 +987,145 @@ def test_repository_hashfile_filter(testrepo):
testrepo.config['core.safecrlf'] = 'fail'
with pytest.raises(pygit2.GitError):
h = testrepo.hashfile('hello.txt')


def test_merge_file_from_index_deprecated(testrepo):
hello_txt = testrepo.index['hello.txt']
hello_txt_executable = IndexEntry(
hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE
)
hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode)
other_file_blob = testrepo.create_blob('Data that will clash with hello.txt')

# no change
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt)
assert res == testrepo.get(hello_txt.id).data.decode()

# executable switch on ours
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt)
assert res == testrepo.get(hello_txt.id).data.decode()

# executable switch on theirs
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt_executable)
assert res == testrepo.get(hello_txt.id).data.decode()

# executable switch on both
res = testrepo.merge_file_from_index(
hello_txt, hello_txt_executable, hello_txt_executable
)
assert res == testrepo.get(hello_txt.id).data.decode()

# path switch on ours
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt)
assert res == testrepo.get(hello_txt.id).data.decode()

# path switch on theirs
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_world)
assert res == testrepo.get(hello_txt.id).data.decode()

# path switch on both
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_world)
assert res == testrepo.get(hello_txt.id).data.decode()

# path switch on ours, executable flag switch on theirs
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt_executable)
assert res == testrepo.get(hello_txt.id).data.decode()

# path switch on theirs, executable flag switch on ours
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_world)
assert res == testrepo.get(hello_txt.id).data.decode()


def test_merge_file_from_index_non_deprecated(testrepo):
hello_txt = testrepo.index['hello.txt']
hello_txt_executable = IndexEntry(
hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE
)
hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode)

# no change
res = testrepo.merge_file_from_index(
hello_txt, hello_txt, hello_txt, use_deprecated=False
)
assert res == MergeFileResult(
True, hello_txt.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
)

# executable switch on ours
res = testrepo.merge_file_from_index(
hello_txt, hello_txt_executable, hello_txt, use_deprecated=False
)
assert res == MergeFileResult(
True,
hello_txt.path,
hello_txt_executable.mode,
testrepo.get(hello_txt.id).data.decode(),
)

# executable switch on theirs
res = testrepo.merge_file_from_index(
hello_txt, hello_txt, hello_txt_executable, use_deprecated=False
)
assert res == MergeFileResult(
True,
hello_txt.path,
hello_txt_executable.mode,
testrepo.get(hello_txt.id).data.decode(),
)

# executable switch on both
res = testrepo.merge_file_from_index(
hello_txt, hello_txt_executable, hello_txt_executable, use_deprecated=False
)
assert res == MergeFileResult(
True,
hello_txt.path,
hello_txt_executable.mode,
testrepo.get(hello_txt.id).data.decode(),
)

# path switch on ours
res = testrepo.merge_file_from_index(
hello_txt, hello_world, hello_txt, use_deprecated=False
)
assert res == MergeFileResult(
True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
)

# path switch on theirs
res = testrepo.merge_file_from_index(
hello_txt, hello_txt, hello_world, use_deprecated=False
)
assert res == MergeFileResult(
True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
)

# path switch on both
res = testrepo.merge_file_from_index(
hello_txt, hello_world, hello_world, use_deprecated=False
)
assert res == MergeFileResult(
True, None, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
)

# path switch on ours, executable flag switch on theirs
res = testrepo.merge_file_from_index(
hello_txt, hello_world, hello_txt_executable, use_deprecated=False
)
assert res == MergeFileResult(
True,
hello_world.path,
hello_txt_executable.mode,
testrepo.get(hello_txt.id).data.decode(),
)

# path switch on theirs, executable flag switch on ours
res = testrepo.merge_file_from_index(
hello_txt, hello_txt_executable, hello_world, use_deprecated=False
)
assert res == MergeFileResult(
True,
hello_world.path,
hello_txt_executable.mode,
testrepo.get(hello_txt.id).data.decode(),
)