77from typing import (
88 TYPE_CHECKING ,
99 Callable ,
10+ Dict ,
11+ Generator ,
1012 Iterable ,
1113 List ,
1214 Mapping ,
1517 Union ,
1618)
1719
18- from funcy import cached_property
20+ from funcy import cached_property , reraise
21+ from shortuuid import uuid
1922
2023from scmrepo .exceptions import CloneError , MergeConflictError , RevError , SCMError
2124from scmrepo .utils import relpath
2730
2831
2932if TYPE_CHECKING :
33+ from pygit2 .remote import Remote # type: ignore
34+
3035 from scmrepo .progress import GitProgressEvent
3136
3237
@@ -412,6 +417,52 @@ def push_refspecs(
412417 ) -> Mapping [str , SyncStatus ]:
413418 raise NotImplementedError
414419
420+ def _merge_remote_branch (
421+ self ,
422+ rh : str ,
423+ lh : str ,
424+ force : bool = False ,
425+ on_diverged : Optional [Callable [[str , str ], bool ]] = None ,
426+ ) -> SyncStatus :
427+ import pygit2
428+
429+ rh_rev = self .resolve_rev (rh )
430+
431+ if force :
432+ self .set_ref (lh , rh_rev )
433+ return SyncStatus .SUCCESS
434+
435+ try :
436+ merge_result , _ = self .repo .merge_analysis (rh_rev , lh )
437+ except KeyError :
438+ self .set_ref (lh , rh_rev )
439+ return SyncStatus .SUCCESS
440+
441+ if merge_result & pygit2 .GIT_MERGE_ANALYSIS_UP_TO_DATE :
442+ return SyncStatus .UP_TO_DATE
443+ if merge_result & pygit2 .GIT_MERGE_ANALYSIS_FASTFORWARD :
444+ self .set_ref (lh , rh_rev )
445+ return SyncStatus .SUCCESS
446+ if merge_result & pygit2 .GIT_MERGE_ANALYSIS_NORMAL :
447+ if on_diverged and on_diverged (lh , rh_rev ):
448+ return SyncStatus .SUCCESS
449+ return SyncStatus .DIVERGED
450+ logger .debug ("Unexpected merge result: %s" , pygit2 .GIT_MERGE_ANALYSIS_NORMAL )
451+ raise SCMError ("Unknown merge analysis result" )
452+
453+ @contextmanager
454+ def get_remote (self , url : str ) -> Generator ["Remote" , None , None ]:
455+ try :
456+ yield self .repo .remotes [url ]
457+ except ValueError :
458+ try :
459+ remote_name = uuid ()
460+ yield self .repo .remotes .create (remote_name , url )
461+ finally :
462+ self .repo .remotes .delete (remote_name )
463+ except KeyError :
464+ raise SCMError (f"'{ url } ' is not a valid Git remote or URL" )
465+
415466 def fetch_refspecs (
416467 self ,
417468 url : str ,
@@ -421,7 +472,58 @@ def fetch_refspecs(
421472 progress : Callable [["GitProgressEvent" ], None ] = None ,
422473 ** kwargs ,
423474 ) -> Mapping [str , SyncStatus ]:
424- raise NotImplementedError
475+ from pygit2 import GitError
476+
477+ if isinstance (refspecs , str ):
478+ refspecs = [refspecs ]
479+
480+ with self .get_remote (url ) as remote :
481+ if os .name == "nt" and remote .url .startswith ("ssh://" ):
482+ raise NotImplementedError
483+
484+ if os .name == "nt" and remote .url .startswith ("file://" ):
485+ url = remote .url [len ("file://" ) :]
486+ self .repo .remotes .set_url (remote .name , url )
487+ remote = self .repo .remotes [remote .name ]
488+
489+ fetch_refspecs : List [str ] = []
490+ for refspec in refspecs :
491+ if ":" in refspec :
492+ lh , rh = refspec .split (":" )
493+ else :
494+ lh = rh = refspec
495+ if not rh .startswith ("refs/" ):
496+ rh = f"refs/heads/{ rh } "
497+ if not lh .startswith ("refs/" ):
498+ lh = f"refs/heads/{ lh } "
499+ rh = rh [len ("refs/" ) :]
500+ refspec = f"+{ lh } :refs/remotes/{ remote .name } /{ rh } "
501+ fetch_refspecs .append (refspec )
502+
503+ logger .debug ("fetch_refspecs: %s" , fetch_refspecs )
504+ with reraise (
505+ GitError ,
506+ SCMError (f"Git failed to fetch ref from '{ url } '" ),
507+ ):
508+ remote .fetch (refspecs = fetch_refspecs )
509+
510+ result : Dict [str , "SyncStatus" ] = {}
511+ for refspec in fetch_refspecs :
512+ _ , rh = refspec .split (":" )
513+ if not rh .endswith ("*" ):
514+ refname = rh .split ("/" , 3 )[- 1 ]
515+ refname = f"refs/{ refname } "
516+ result [refname ] = self ._merge_remote_branch (
517+ rh , refname , force , on_diverged
518+ )
519+ continue
520+ rh = rh .rstrip ("*" ).rstrip ("/" ) + "/"
521+ for branch in self .iter_refs (base = rh ):
522+ refname = f"refs/{ branch [len (rh ):]} "
523+ result [refname ] = self ._merge_remote_branch (
524+ branch , refname , force , on_diverged
525+ )
526+ return result
425527
426528 def _stash_iter (self , ref : str ):
427529 raise NotImplementedError
0 commit comments