Skip to content

Commit f29c606

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 f29c606

File tree

3 files changed

+221
-7
lines changed

3 files changed

+221
-7
lines changed

pygit2/index.py

Lines changed: 60 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,65 @@ 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+
def __init__(
367+
self,
368+
automergeable: int,
369+
path: typing.Union[str, None],
370+
mode: FileMode,
371+
contents: str,
372+
):
373+
self.automergeable = automergeable
374+
self.path = path
375+
self.mode = mode
376+
self.contents = contents
377+
378+
def __str__(self):
379+
# FIXME Should I include contents over here?
380+
return f'<automergeable={self.automergeable} path={self.path} mode={self.mode} content={len(self.contents)}>'
381+
382+
def __repr__(self):
383+
t = type(self)
384+
# FIXME should I include contents over here?
385+
return f'<{t.__module__}.{t.__qualname__} automergeable={self.automergeable} path={self.path} mode={self.mode}>'
386+
387+
def __eq__(self, other):
388+
if self is other:
389+
return True
390+
if not isinstance(other, MergeFileResult):
391+
return NotImplemented
392+
return (
393+
self.automergeable == other.automergeable
394+
and self.path == other.path
395+
and self.mode == other.mode
396+
and self.contents == other.contents
397+
)
398+
399+
@classmethod
400+
def _from_c(cls, centry):
401+
if centry == ffi.NULL:
402+
return None
403+
404+
automergeable = centry.automergeable != 0
405+
path = to_str(ffi.string(centry.path)) if centry.path else None
406+
mode = FileMode(centry.mode)
407+
contents = ffi.string(centry.ptr, centry.len).decode('utf-8')
408+
409+
return MergeFileResult(automergeable, path, mode, contents)
410+
411+
352412
class IndexEntry:
353413
path: str
354414
'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: 138 additions & 1 deletion
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,
@@ -42,6 +42,7 @@
4242
RepositoryState,
4343
ResetMode,
4444
StashApplyProgress,
45+
FileMode,
4546
)
4647
from . import utils
4748

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

0 commit comments

Comments
 (0)