Skip to content

Commit 676a795

Browse files
committed
Repository: get an instance of MergeFileResult from git_merge_file_from_index
In Repository.merge_file_from_index, pygit2 is only returning the resulting content out of the merge, however other values might be relevant like the filemode. Add a new class named MergeFileResult that maps to libgit2's git_merge_file_result. Add a flag called "use_deprecated" in Repository.merge_file_from_index to control whether the caller wants to use the deprecated functionality receiving an string. In case use_deprecated==False, an instance of MergeFileResult will be used, instead. The default value for use_deprecated is True so not to break existing callers all of the sudden. Later, giving enough time for callers to notice the deprecation warning and adjust their calls to use use_deprecated=False, the default value can be switched to False. And even more time later the flag can disappear completely from the method once all callers have removed the parameter because they are already using the non-deprecated version by removing the parameter completely from their calls. Add a warning when using the deprecated implementation.
1 parent c4fc955 commit 676a795

File tree

3 files changed

+194
-8
lines changed

3 files changed

+194
-8
lines changed

pygit2/index.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
2424
# Boston, MA 02110-1301, USA.
2525

26+
import typing
2627
import warnings
2728
import weakref
2829

@@ -349,6 +350,63 @@ def conflicts(self):
349350
return self._conflicts()
350351

351352

353+
class MergeFileResult:
354+
automergeable: bool
355+
'True if the output was automerged, false if the output contains conflict markers'
356+
357+
path: typing.Union[str,None]
358+
'The path that the resultant merge file should use, or None if a filename conflict would occur'
359+
360+
mode: FileMode
361+
'The mode that the resultant merge file should use'
362+
363+
contents: str
364+
'Contents of the file, which might include conflict markers'
365+
366+
367+
def __init__(
368+
self,
369+
automergeable: int,
370+
path: typing.Union[str, None],
371+
mode: FileMode,
372+
contents: str,
373+
):
374+
self.automergeable = automergeable
375+
self.path = path
376+
self.mode = mode
377+
self.contents = contents
378+
379+
def __str__(self):
380+
# FIXME Should I include contents over here?
381+
return f"<automergeable={self.automergeable} path={self.path} mode={self.mode} content={len(self.contents)}>"
382+
383+
def __repr__(self):
384+
t = type(self)
385+
# FIXME should I include contents over here?
386+
return f'<{t.__module__}.{t.__qualname__} automergeable={self.automergeable} path={self.path} mode={self.mode}>'
387+
388+
def __eq__(self, other):
389+
if self is other:
390+
return True
391+
if not isinstance(other, MergeFileResult):
392+
return NotImplemented
393+
return (
394+
self.automergeable == other.automergeable and self.path == other.path
395+
and self.mode == other.mode and self.contents == other.contents
396+
)
397+
398+
@classmethod
399+
def _from_c(cls, centry):
400+
if centry == ffi.NULL:
401+
return None
402+
403+
automergeable = centry.automergeable != 0
404+
path = to_str(ffi.string(centry.path)) if centry.path else None
405+
mode = FileMode(centry.mode)
406+
contents = ffi.string(centry.ptr, centry.len).decode('utf-8')
407+
408+
return MergeFileResult(automergeable, path, mode, contents)
409+
352410
class IndexEntry:
353411
path: str
354412
'The path of this entry'

pygit2/repository.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
)
5757
from .errors import check_error
5858
from .ffi import ffi, C
59-
from .index import Index, IndexEntry
59+
from .index import Index, IndexEntry, MergeFileResult
6060
from .packbuilder import PackBuilder
6161
from .references import References
6262
from .remotes import RemoteCollection
@@ -682,9 +682,13 @@ def merge_file_from_index(
682682
ancestor: typing.Union[None, IndexEntry],
683683
ours: typing.Union[None, IndexEntry],
684684
theirs: typing.Union[None, IndexEntry],
685-
) -> str:
686-
"""Merge files from index. Return a string with the merge result
687-
containing possible conflicts.
685+
use_deprecated: bool = True,
686+
) -> typing.Union[str, typing.Union[MergeFileResult, None]]:
687+
"""Merge files from index.
688+
689+
Returns: A string with the content of the file containing
690+
possible conflicts if use_deprecated==True.
691+
If use_deprecated==False then it returns an instance of MergeFileResult.
688692
689693
ancestor
690694
The index entry which will be used as a common
@@ -693,6 +697,10 @@ def merge_file_from_index(
693697
The index entry to take as "ours" or base.
694698
theirs
695699
The index entry which will be merged into "ours"
700+
use_deprecated
701+
This controls what will be returned. If use_deprecated==True (default),
702+
a string with the contents of the file will be returned.
703+
An instance of MergeFileResult will be returned otherwise.
696704
"""
697705
cmergeresult = ffi.new('git_merge_file_result *')
698706

@@ -709,10 +717,19 @@ def merge_file_from_index(
709717
)
710718
check_error(err)
711719

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

715-
return ret
723+
if use_deprecated:
724+
warnings.warn(
725+
'Getting an str from Repository.merge_file_from_index is deprecated. '
726+
'The method will later return an instance of MergeFileResult by default, instead. '
727+
'Check parameter use_deprecated.',
728+
DeprecationWarning,
729+
)
730+
return mergeFileResult.contents if mergeFileResult else ""
731+
732+
return mergeFileResult
716733

717734
def merge_commits(
718735
self,

test/test_repository.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
# pygit2
3333
import pygit2
34-
from pygit2 import init_repository, clone_repository, discover_repository
34+
from pygit2 import init_repository, clone_repository, discover_repository, IndexEntry
3535
from pygit2 import Oid
3636
from pygit2.enums import (
3737
CheckoutNotify,
@@ -41,7 +41,7 @@
4141
RepositoryOpenFlag,
4242
RepositoryState,
4343
ResetMode,
44-
StashApplyProgress,
44+
StashApplyProgress, FileMode,
4545
)
4646
from . import utils
4747

@@ -985,3 +985,114 @@ def test_repository_hashfile_filter(testrepo):
985985
testrepo.config['core.safecrlf'] = 'fail'
986986
with pytest.raises(pygit2.GitError):
987987
h = testrepo.hashfile('hello.txt')
988+
989+
990+
def test_merge_file_from_index_deprecated(testrepo):
991+
hello_txt = testrepo.index['hello.txt']
992+
hello_txt_executable = IndexEntry(hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE)
993+
hello_world = IndexEntry("hello_world.txt", hello_txt.id, hello_txt.mode)
994+
other_file_blob = testrepo.create_blob("Data that will clash with hello.txt")
995+
996+
# no change
997+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt)
998+
assert res == testrepo.get(hello_txt.id).data.decode()
999+
1000+
# executable switch on ours
1001+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt)
1002+
assert res == testrepo.get(hello_txt.id).data.decode()
1003+
1004+
# executable switch on theirs
1005+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt_executable)
1006+
assert res == testrepo.get(hello_txt.id).data.decode()
1007+
1008+
# executable switch on both
1009+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt_executable)
1010+
assert res == testrepo.get(hello_txt.id).data.decode()
1011+
1012+
# path switch on ours
1013+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt)
1014+
assert res == testrepo.get(hello_txt.id).data.decode()
1015+
1016+
# path switch on theirs
1017+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_world)
1018+
assert res == testrepo.get(hello_txt.id).data.decode()
1019+
1020+
# path switch on both
1021+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_world)
1022+
assert res == testrepo.get(hello_txt.id).data.decode()
1023+
1024+
# path switch on ours, executable flag switch on theirs
1025+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt_executable)
1026+
assert res == testrepo.get(hello_txt.id).data.decode()
1027+
1028+
# path switch on theirs, executable flag switch on ours
1029+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_world)
1030+
assert res == testrepo.get(hello_txt.id).data.decode()
1031+
1032+
def test_merge_file_from_index_non_deprecated(testrepo):
1033+
hello_txt = testrepo.index['hello.txt']
1034+
hello_txt_executable = IndexEntry(hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE)
1035+
hello_world = IndexEntry("hello_world.txt", hello_txt.id, hello_txt.mode)
1036+
1037+
# no change
1038+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt, use_deprecated=False)
1039+
assert res.automergeable
1040+
assert res.path == hello_txt.path
1041+
assert res.mode == hello_txt.mode
1042+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1043+
1044+
# executable switch on ours
1045+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt, use_deprecated=False)
1046+
assert res.automergeable
1047+
assert res.path == hello_txt.path
1048+
assert res.mode == hello_txt_executable.mode
1049+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1050+
1051+
# executable switch on theirs
1052+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt_executable, use_deprecated=False)
1053+
assert res.automergeable
1054+
assert res.path == hello_txt.path
1055+
assert res.mode == hello_txt_executable.mode
1056+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1057+
1058+
# executable switch on both
1059+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt_executable, use_deprecated=False)
1060+
assert res.automergeable
1061+
assert res.path == hello_txt.path
1062+
assert res.mode == hello_txt_executable.mode
1063+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1064+
1065+
# path switch on ours
1066+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt, use_deprecated=False)
1067+
assert res.automergeable
1068+
assert res.path == hello_world.path
1069+
assert res.mode == hello_txt.mode
1070+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1071+
1072+
# path switch on theirs
1073+
res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_world, use_deprecated=False)
1074+
assert res.automergeable
1075+
assert res.path == hello_world.path
1076+
assert res.mode == hello_txt.mode
1077+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1078+
1079+
# path switch on both
1080+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_world, use_deprecated=False)
1081+
assert res.automergeable
1082+
assert res.path is None
1083+
assert res.mode == hello_txt.mode
1084+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1085+
1086+
# path switch on ours, executable flag switch on theirs
1087+
res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt_executable, use_deprecated=False)
1088+
assert res.automergeable
1089+
assert res.path == hello_world.path
1090+
assert res.mode == hello_txt_executable.mode
1091+
assert res.contents == testrepo.get(hello_txt.id).data.decode()
1092+
1093+
# path switch on theirs, executable flag switch on ours
1094+
res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_world, use_deprecated=False)
1095+
assert res.automergeable
1096+
assert res.path == hello_world.path
1097+
assert res.mode == hello_txt_executable.mode
1098+
assert res.contents == testrepo.get(hello_txt.id).data.decode()

0 commit comments

Comments
 (0)