@@ -371,10 +371,46 @@ def clone(url: str, target_dir: str, object_format: str | None = None) -> Reposi
371371
372372
373373# ---------------------------------------------------------------------------
374- # fetch — like clone, but updates remote-tracking refs only.
374+ # fetch — like clone; default fetch updates remote-tracking refs, while
375+ # explicit branch refspecs are fetched to FETCH_HEAD unless a destination is
376+ # given.
375377
376378
377- def fetch (repo : Repository , remote : str = "origin" ) -> dict [str , str ]:
379+ def _resolve_remote_fetch_ref (remote_refs : dict [str , str ], source : str ) -> tuple [str , str ]:
380+ candidates = [source ]
381+ if not source .startswith ("refs/" ):
382+ candidates .extend ((f"refs/heads/{ source } " , f"refs/tags/{ source } " ))
383+ for name in candidates :
384+ sha = remote_refs .get (name )
385+ if sha :
386+ return name , sha
387+ raise RuntimeError (f"couldn't find remote ref { source } " )
388+
389+
390+ def _normalize_fetch_destination (remote_name : str , destination : str ) -> str :
391+ if destination .startswith ("refs/" ):
392+ return destination
393+ if remote_name .startswith ("refs/tags/" ):
394+ return f"refs/tags/{ destination } "
395+ return f"refs/heads/{ destination } "
396+
397+
398+ def _fetch_head_note (remote_name : str , url : str ) -> str :
399+ if remote_name == "HEAD" :
400+ return f"'{ remote_name } ' of { url } "
401+ if remote_name .startswith ("refs/heads/" ):
402+ return f"branch '{ remote_name [len ('refs/heads/' ):]} ' of { url } "
403+ if remote_name .startswith ("refs/tags/" ):
404+ return f"tag '{ remote_name [len ('refs/tags/' ):]} ' of { url } "
405+ return f"'{ remote_name } ' of { url } "
406+
407+
408+ def _write_fetch_head (repo : Repository , entries : list [tuple [str , str ]], url : str ) -> None :
409+ lines = [f"{ sha } \t \t { _fetch_head_note (name , url )} \n " for name , sha in entries ]
410+ (repo .gitdir / "FETCH_HEAD" ).write_text ("" .join (lines ), encoding = "utf-8" )
411+
412+
413+ def fetch (repo : Repository , remote : str = "origin" , refspecs : list [str ] | None = None ) -> dict [str , str ]:
378414 from . import pack as pack_mod
379415 from . import refs as refs_mod
380416 cp = repo .config ()
@@ -389,7 +425,19 @@ def fetch(repo: Repository, remote: str = "origin") -> dict[str, str]:
389425 s = refs_mod .read_ref (repo , f"refs/heads/{ b } " )
390426 if s :
391427 haves .append (s )
392- wants = sorted (set (remote_refs .values ()) - set (haves ))
428+ explicit : list [tuple [str , str , str | None ]] = []
429+ if refspecs :
430+ for raw in refspecs :
431+ spec = raw [1 :] if raw .startswith ("+" ) else raw
432+ source , has_destination , destination = spec .partition (":" )
433+ if not source :
434+ continue
435+ remote_name , sha = _resolve_remote_fetch_ref (remote_refs , source )
436+ dst_ref = _normalize_fetch_destination (remote_name , destination ) if has_destination and destination else None
437+ explicit .append ((remote_name , sha , dst_ref ))
438+ wants = sorted ({sha for _name , sha , _dst in explicit } - set (haves ))
439+ else :
440+ wants = sorted (set (remote_refs .values ()) - set (haves ))
393441 updated : dict [str , str ] = {}
394442 if wants :
395443 tmp_pack = None
@@ -409,15 +457,28 @@ def fetch(repo: Repository, remote: str = "origin") -> dict[str, str]:
409457 os .unlink (str (Path (tmp_pack ).with_suffix (".idx" )))
410458 except OSError :
411459 pass
460+ if explicit :
461+ fetch_head_entries = []
462+ for name , sha , dst_ref in explicit :
463+ if dst_ref :
464+ cur = refs_mod .read_ref (repo , dst_ref )
465+ if cur != sha :
466+ refs_mod .update_ref (repo , dst_ref , sha , message = f"fetch { remote } " )
467+ updated [dst_ref ] = sha
468+ else :
469+ fetch_head_entries .append ((name , sha ))
470+ updated ["FETCH_HEAD" ] = sha
471+ if fetch_head_entries :
472+ _write_fetch_head (repo , fetch_head_entries , url )
473+ return updated
412474 for name , sha in remote_refs .items ():
413- if not name .startswith ("refs/heads/" ):
414- continue
415- branch = name [len ("refs/heads/" ):]
416- ref = f"refs/remotes/{ remote } /{ branch } "
417- cur = refs_mod .read_ref (repo , ref )
418- if cur != sha :
419- refs_mod .update_ref (repo , ref , sha , message = f"fetch { remote } " )
420- updated [ref ] = sha
475+ if name .startswith ("refs/heads/" ):
476+ branch = name [len ("refs/heads/" ):]
477+ ref = f"refs/remotes/{ remote } /{ branch } "
478+ cur = refs_mod .read_ref (repo , ref )
479+ if cur != sha :
480+ refs_mod .update_ref (repo , ref , sha , message = f"fetch { remote } " )
481+ updated [ref ] = sha
421482 return updated
422483
423484
0 commit comments