Skip to content

Commit c81f4e0

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 c81f4e0

File tree

3 files changed

+196
-7
lines changed

3 files changed

+196
-7
lines changed

pygit2/index.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
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
29+
from dataclasses import dataclass
2830

2931
# Import from pygit2
3032
from ._pygit2 import Oid, Tree, Diff
@@ -349,6 +351,33 @@ def conflicts(self):
349351
return self._conflicts()
350352

351353

354+
@dataclass
355+
class MergeFileResult:
356+
automergeable: bool
357+
'True if the output was automerged, false if the output contains conflict markers'
358+
359+
path: typing.Union[str, None]
360+
'The path that the resultant merge file should use, or None if a filename conflict would occur'
361+
362+
mode: FileMode
363+
'The mode that the resultant merge file should use'
364+
365+
contents: str
366+
'Contents of the file, which might include conflict markers'
367+
368+
@classmethod
369+
def _from_c(cls, centry):
370+
if centry == ffi.NULL:
371+
return None
372+
373+
automergeable = centry.automergeable != 0
374+
path = to_str(ffi.string(centry.path)) if centry.path else None
375+
mode = FileMode(centry.mode)
376+
contents = ffi.string(centry.ptr, centry.len).decode('utf-8')
377+
378+
return MergeFileResult(automergeable, path, mode, contents)
379+
380+
352381
class IndexEntry:
353382
path: str
354383
'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: 144 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,7 +42,9 @@
4242
RepositoryState,
4343
ResetMode,
4444
StashApplyProgress,
45+
FileMode,
4546
)
47+
from pygit2.index import MergeFileResult
4648
from . import utils
4749

4850

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

0 commit comments

Comments
 (0)