1010import contextlib
1111import logging
1212import re
13+ from urllib .parse import urlsplit , urlunsplit
1314
1415from git .cmd import Git , handle_process_output
1516from git .compat import defenc , force_text
@@ -1008,6 +1009,9 @@ def fetch(
10081009 kill_after_timeout : Union [None , float ] = None ,
10091010 allow_unsafe_protocols : bool = False ,
10101011 allow_unsafe_options : bool = False ,
1012+ url : Optional [str ] = None ,
1013+ token : Optional [str ] = None ,
1014+ dest_ref : Union [bool , str , None ] = None ,
10111015 ** kwargs : Any ,
10121016 ) -> IterableList [FetchInfo ]:
10131017 """Fetch the latest changes for this remote.
@@ -1028,6 +1032,16 @@ def fetch(
10281032 does) - supplying a list rather than a string for 'refspec' will make use of
10291033 this facility.
10301034
1035+ :note:
1036+ A *bare* source name with no ``:<dst>`` (e.g. ``"main"``) only updates
1037+ ``FETCH_HEAD`` when fetching from a remote by name, because git can
1038+ complete the destination from that remote's configured fetch refspec.
1039+ When fetching from an explicit ``url`` instead (no such config exists
1040+ for an anonymous URL), a bare name updates ``FETCH_HEAD`` only and
1041+ creates or updates no local ref at all. Use ``dest_ref`` below, or
1042+ spell out ``"<branch>:refs/remotes/<name>/<branch>"`` yourself, to get
1043+ a normal tracking ref out of a ``url=`` fetch.
1044+
10311045 :param progress:
10321046 See the :meth:`push` method.
10331047
@@ -1044,6 +1058,33 @@ def fetch(
10441058 :param allow_unsafe_options:
10451059 Allow unsafe options to be used, like ``--upload-pack``.
10461060
1061+ :param url:
1062+ Fetch from this URL instead of the remote's configured URL, for this
1063+ call only. The remote's stored config (``.git/config``) is never
1064+ written to or read for the transport target when this is given.
1065+ Takes precedence over the remote's name/configured url.
1066+
1067+ :param token:
1068+ Optional credential to embed in ``url`` for this call only (e.g. a
1069+ personal access token). Only used together with ``url``, and only
1070+ for ``http://``/``https://`` URLs. Never written to config, never
1071+ logged; kept out of :class:`FetchInfo`/exception text on failure.
1072+
1073+ :param dest_ref:
1074+ Only meaningful together with ``url``. Expands any *bare* (no ``:dst``)
1075+ entry in ``refspec`` into a full ``<branch>:<dst>`` mapping before
1076+ fetching, so the fetch updates a real local ref instead of only
1077+ ``FETCH_HEAD``. Entries that already contain a ``:`` are left untouched.
1078+
1079+ - ``True``: derive ``dst`` as ``refs/remotes/<this-remote's-name>/<branch>``
1080+ for each bare entry -- i.e. behave like a normal named-remote fetch would.
1081+ - A string containing ``{branch}``: used as a template for ``dst``, e.g.
1082+ ``"refs/remotes/upstream/{branch}"``.
1083+ - A plain string with no ``{branch}``: used verbatim as ``dst``. Only valid
1084+ when ``refspec`` is a single bare name, not a list.
1085+ - ``None`` (default): no expansion; bare names behave as plain git would
1086+ (``FETCH_HEAD`` only).
1087+
10471088 :param kwargs:
10481089 Additional arguments to be passed to :manpage:`git-fetch(1)`.
10491090
@@ -1055,16 +1096,41 @@ def fetch(
10551096 As fetch does not provide progress information to non-ttys, we cannot make
10561097 it available here unfortunately as in the :meth:`push` method.
10571098 """
1058- if refspec is None :
1099+ if url is None and refspec is None :
10591100 # No argument refspec, then ensure the repo's config has a fetch refspec.
10601101 self ._assert_refspec ()
10611102
1103+ if dest_ref is not None and url is None :
1104+ raise ValueError ("dest_ref is only meaningful together with url=" )
1105+
10621106 kwargs = add_progress (kwargs , self .repo .git , progress )
10631107 if isinstance (refspec , list ):
10641108 args : Sequence [Optional [str ]] = refspec
10651109 else :
10661110 args = [refspec ]
10671111
1112+ if dest_ref is not None :
1113+
1114+ def _expand (ref : Optional [str ]) -> Optional [str ]:
1115+ if not ref or ":" in ref :
1116+ return ref
1117+ if dest_ref is True :
1118+ dst = f"refs/remotes/{ self .name } /{ ref } "
1119+ elif isinstance (dest_ref , str ) and "{branch}" in dest_ref :
1120+ dst = dest_ref .format (branch = ref )
1121+ elif isinstance (dest_ref , str ):
1122+ if isinstance (refspec , list ) and len (refspec ) > 1 :
1123+ raise ValueError (
1124+ "dest_ref as a plain string only supports a single bare refspec entry; "
1125+ "use '{branch}' as a template, or pass True, for multiple entries"
1126+ )
1127+ dst = dest_ref
1128+ else :
1129+ raise TypeError ("dest_ref must be True, a string, or None" )
1130+ return f"{ ref } :{ dst } "
1131+
1132+ args = [_expand (ref ) for ref in args ]
1133+
10681134 if not allow_unsafe_protocols :
10691135 for ref in args :
10701136 if ref :
@@ -1076,10 +1142,41 @@ def fetch(
10761142 unsafe_options = self .unsafe_git_fetch_options ,
10771143 )
10781144
1079- proc = self .repo .git .fetch (
1080- "--" , self , * args , as_process = True , with_stdout = False , universal_newlines = True , v = verbose , ** kwargs
1081- )
1082- res = self ._get_fetch_info_from_stderr (proc , progress , kill_after_timeout = kill_after_timeout )
1145+ # Determine the fetch target: an explicit one-off `url` (optionally with an
1146+ # embedded `token`) takes precedence over the remote's configured name/url.
1147+ # Neither is ever written to .git/config -- it only affects this invocation.
1148+ fetch_target : Union [str , "Remote" ] = self
1149+ if url is not None :
1150+ if not allow_unsafe_protocols :
1151+ Git .check_unsafe_protocols (url )
1152+ if token is not None :
1153+ parsed = urlsplit (url )
1154+ if parsed .scheme not in ("http" , "https" ):
1155+ raise ValueError ("token= is only supported with http:// or https:// urls" )
1156+ netloc = f"{ token } @{ parsed .hostname } "
1157+ if parsed .port :
1158+ netloc += f":{ parsed .port } "
1159+ fetch_target = urlunsplit ((parsed .scheme , netloc , parsed .path , parsed .query , parsed .fragment ))
1160+ else :
1161+ fetch_target = url
1162+
1163+ try :
1164+ proc = self .repo .git .fetch (
1165+ "--" ,
1166+ fetch_target ,
1167+ * args ,
1168+ as_process = True ,
1169+ with_stdout = False ,
1170+ universal_newlines = True ,
1171+ v = verbose ,
1172+ ** kwargs ,
1173+ )
1174+ res = self ._get_fetch_info_from_stderr (proc , progress , kill_after_timeout = kill_after_timeout )
1175+ except GitCommandError as err :
1176+ # Never let a token embedded in the URL leak into an exception message.
1177+ if token is not None and token in str (err ):
1178+ err .args = tuple (a .replace (token , "***" ) if isinstance (a , str ) else a for a in err .args )
1179+ raise
10831180 if hasattr (self .repo .odb , "update_cache" ):
10841181 self .repo .odb .update_cache ()
10851182 return res
@@ -1091,6 +1188,8 @@ def pull(
10911188 kill_after_timeout : Union [None , float ] = None ,
10921189 allow_unsafe_protocols : bool = False ,
10931190 allow_unsafe_options : bool = False ,
1191+ url : Optional [str ] = None ,
1192+ token : Optional [str ] = None ,
10941193 ** kwargs : Any ,
10951194 ) -> IterableList [FetchInfo ]:
10961195 """Pull changes from the given branch, being the same as a fetch followed by a
@@ -1111,13 +1210,28 @@ def pull(
11111210 :param allow_unsafe_options:
11121211 Allow unsafe options to be used, like ``--upload-pack``.
11131212
1213+ :param url:
1214+ Pull from this URL instead of the remote's configured URL, for this
1215+ call only. The remote's stored config (``.git/config``) is never
1216+ written to or read for the transport target when this is given.
1217+ Takes precedence over the remote's name/configured url. As with plain
1218+ ``git pull``, the fetched commit is merged into your currently checked
1219+ out branch regardless of the source, so use with the same care you
1220+ would use interactively.
1221+
1222+ :param token:
1223+ Optional credential to embed in ``url`` for this call only (e.g. a
1224+ personal access token). Only used together with ``url``, and only
1225+ for ``http://``/``https://`` URLs. Never written to config, never
1226+ logged; kept out of exception text on failure.
1227+
11141228 :param kwargs:
11151229 Additional arguments to be passed to :manpage:`git-pull(1)`.
11161230
11171231 :return:
11181232 Please see :meth:`fetch` method.
11191233 """
1120- if refspec is None :
1234+ if url is None and refspec is None :
11211235 # No argument refspec, then ensure the repo's config has a fetch refspec.
11221236 self ._assert_refspec ()
11231237 kwargs = add_progress (kwargs , self .repo .git , progress )
@@ -1133,10 +1247,40 @@ def pull(
11331247 unsafe_options = self .unsafe_git_pull_options ,
11341248 )
11351249
1136- proc = self .repo .git .pull (
1137- "--" , self , refspec , with_stdout = False , as_process = True , universal_newlines = True , v = True , ** kwargs
1138- )
1139- res = self ._get_fetch_info_from_stderr (proc , progress , kill_after_timeout = kill_after_timeout )
1250+ # Same one-off-target mechanism as fetch(): an explicit `url` (optionally
1251+ # with an embedded `token`) takes precedence over the remote's configured
1252+ # name/url, and neither is ever written to .git/config.
1253+ pull_target : Union [str , "Remote" ] = self
1254+ if url is not None :
1255+ if not allow_unsafe_protocols :
1256+ Git .check_unsafe_protocols (url )
1257+ if token is not None :
1258+ parsed = urlsplit (url )
1259+ if parsed .scheme not in ("http" , "https" ):
1260+ raise ValueError ("token= is only supported with http:// or https:// urls" )
1261+ netloc = f"{ token } @{ parsed .hostname } "
1262+ if parsed .port :
1263+ netloc += f":{ parsed .port } "
1264+ pull_target = urlunsplit ((parsed .scheme , netloc , parsed .path , parsed .query , parsed .fragment ))
1265+ else :
1266+ pull_target = url
1267+
1268+ try :
1269+ proc = self .repo .git .pull (
1270+ "--" ,
1271+ pull_target ,
1272+ refspec ,
1273+ with_stdout = False ,
1274+ as_process = True ,
1275+ universal_newlines = True ,
1276+ v = True ,
1277+ ** kwargs ,
1278+ )
1279+ res = self ._get_fetch_info_from_stderr (proc , progress , kill_after_timeout = kill_after_timeout )
1280+ except GitCommandError as err :
1281+ if token is not None and token in str (err ):
1282+ err .args = tuple (a .replace (token , "***" ) if isinstance (a , str ) else a for a in err .args )
1283+ raise
11401284 if hasattr (self .repo .odb , "update_cache" ):
11411285 self .repo .odb .update_cache ()
11421286 return res
@@ -1148,6 +1292,8 @@ def push(
11481292 kill_after_timeout : Union [None , float ] = None ,
11491293 allow_unsafe_protocols : bool = False ,
11501294 allow_unsafe_options : bool = False ,
1295+ url : Optional [str ] = None ,
1296+ token : Optional [str ] = None ,
11511297 ** kwargs : Any ,
11521298 ) -> PushInfoList :
11531299 """Push changes from source branch in refspec to target branch in refspec.
@@ -1180,6 +1326,18 @@ def push(
11801326 :param allow_unsafe_options:
11811327 Allow unsafe options to be used, like ``--receive-pack``.
11821328
1329+ :param url:
1330+ Push to this URL instead of the remote's configured URL, for this call
1331+ only. The remote's stored config (``.git/config``) is never written to
1332+ or read for the transport target when this is given. Takes precedence
1333+ over the remote's name/configured url.
1334+
1335+ :param token:
1336+ Optional credential to embed in ``url`` for this call only (e.g. a
1337+ personal access token). Only used together with ``url``, and only for
1338+ ``http://``/``https://`` URLs. Never written to config, never logged;
1339+ kept out of exception text on failure.
1340+
11831341 :param kwargs:
11841342 Additional arguments to be passed to :manpage:`git-push(1)`.
11851343
@@ -1209,16 +1367,39 @@ def push(
12091367 unsafe_options = self .unsafe_git_push_options ,
12101368 )
12111369
1212- proc = self .repo .git .push (
1213- "--" ,
1214- self ,
1215- refspec ,
1216- porcelain = True ,
1217- as_process = True ,
1218- universal_newlines = True ,
1219- kill_after_timeout = kill_after_timeout ,
1220- ** kwargs ,
1221- )
1370+ # Same one-off-target mechanism as fetch(): an explicit `url` (optionally
1371+ # with an embedded `token`) takes precedence over the remote's configured
1372+ # name/url, and neither is ever written to .git/config.
1373+ push_target : Union [str , "Remote" ] = self
1374+ if url is not None :
1375+ if not allow_unsafe_protocols :
1376+ Git .check_unsafe_protocols (url )
1377+ if token is not None :
1378+ parsed = urlsplit (url )
1379+ if parsed .scheme not in ("http" , "https" ):
1380+ raise ValueError ("token= is only supported with http:// or https:// urls" )
1381+ netloc = f"{ token } @{ parsed .hostname } "
1382+ if parsed .port :
1383+ netloc += f":{ parsed .port } "
1384+ push_target = urlunsplit ((parsed .scheme , netloc , parsed .path , parsed .query , parsed .fragment ))
1385+ else :
1386+ push_target = url
1387+
1388+ try :
1389+ proc = self .repo .git .push (
1390+ "--" ,
1391+ push_target ,
1392+ refspec ,
1393+ porcelain = True ,
1394+ as_process = True ,
1395+ universal_newlines = True ,
1396+ kill_after_timeout = kill_after_timeout ,
1397+ ** kwargs ,
1398+ )
1399+ except GitCommandError as err :
1400+ if token is not None and token in str (err ):
1401+ err .args = tuple (a .replace (token , "***" ) if isinstance (a , str ) else a for a in err .args )
1402+ raise
12221403 return self ._get_push_info (proc , progress , kill_after_timeout = kill_after_timeout )
12231404
12241405 @property
0 commit comments