Skip to content

Commit 9b78a14

Browse files
committed
pygit2: handle alternate merge strategies
1 parent 76380df commit 9b78a14

File tree

1 file changed

+65
-16
lines changed

1 file changed

+65
-16
lines changed

scmrepo/git/backend/pygit2.py

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -596,37 +596,86 @@ def merge(
596596
msg: Optional[str] = None,
597597
squash: bool = False,
598598
) -> Optional[str]:
599-
from pygit2 import GIT_RESET_MIXED, GitError
599+
from pygit2 import (
600+
GIT_MERGE_ANALYSIS_FASTFORWARD,
601+
GIT_MERGE_ANALYSIS_NONE,
602+
GIT_MERGE_ANALYSIS_UNBORN,
603+
GIT_MERGE_ANALYSIS_UP_TO_DATE,
604+
GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY,
605+
GIT_MERGE_PREFERENCE_NO_FASTFORWARD,
606+
GitError,
607+
)
600608

601609
if commit and squash:
602610
raise SCMError("Cannot merge with 'squash' and 'commit'")
603611

604-
if commit and not msg:
605-
raise SCMError("Merge commit message is required")
606-
607612
with self.release_odb_handles():
613+
self.repo.index.read(False)
614+
obj, _ref = self.repo.resolve_refish(rev)
615+
analysis, ff_pref = self.repo.merge_analysis(obj.id)
616+
617+
if analysis == GIT_MERGE_ANALYSIS_NONE:
618+
raise SCMError(f"'{rev}' cannot be merged into HEAD")
619+
if analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE:
620+
return None
621+
608622
try:
609-
self.repo.index.read(False)
610-
self.repo.merge(rev)
623+
self.repo.merge(obj.id)
611624
self.repo.index.write()
612625
except GitError as exc:
613626
raise SCMError("Merge failed") from exc
614627

615628
if self.repo.index.conflicts:
616629
raise MergeConflictError("Merge contained conflicts")
617630

618-
if commit:
619-
user = self.default_signature
620-
tree = self.repo.index.write_tree()
621-
merge_commit = self.repo.create_commit(
622-
"HEAD", user, user, msg, tree, [self.repo.head.target, rev]
623-
)
624-
return str(merge_commit)
625-
if squash:
626-
self.repo.reset(self.repo.head.target, GIT_RESET_MIXED)
631+
try:
632+
if not (
633+
squash or ff_pref & GIT_MERGE_PREFERENCE_NO_FASTFORWARD
634+
):
635+
if analysis & GIT_MERGE_ANALYSIS_FASTFORWARD:
636+
return self._merge_ff(rev, obj)
637+
638+
if analysis & GIT_MERGE_ANALYSIS_UNBORN:
639+
self.repo.set_head(obj.id)
640+
return str(obj.id)
641+
642+
if ff_pref & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY:
643+
raise SCMError("Cannot fast-forward HEAD to '{rev}'")
644+
645+
if commit:
646+
if not msg:
647+
raise SCMError("Merge commit message is required")
648+
user = self.default_signature
649+
tree = self.repo.index.write_tree()
650+
merge_commit = self.repo.create_commit(
651+
"HEAD",
652+
user,
653+
user,
654+
msg,
655+
tree,
656+
[self.repo.head.target, obj.id],
657+
)
658+
return str(merge_commit)
659+
660+
# --squash merge:
661+
# HEAD is not moved and merge changes stay in index
662+
return None
663+
finally:
627664
self.repo.state_cleanup()
628665
self.repo.index.write()
629-
return None
666+
667+
def _merge_ff(self, rev: str, obj) -> str:
668+
if self.repo.head_is_detached:
669+
self.repo.set_head(obj.id)
670+
else:
671+
branch = self.get_ref("HEAD", follow=False)
672+
assert branch
673+
self.set_ref(
674+
branch,
675+
str(obj.id),
676+
message=f"merge {rev}: Fast-forward",
677+
)
678+
return str(obj.id)
630679

631680
def validate_git_remote(self, url: str, **kwargs):
632681
raise NotImplementedError

0 commit comments

Comments
 (0)