Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/remote.py: 31%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2#
3# This module is part of GitPython and is released under the
4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
6"""Module implementing a remote object allowing easy access to git remotes."""
8__all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"]
10import contextlib
11import logging
12import re
14from git.cmd import Git, handle_process_output
15from git.compat import defenc, force_text
16from git.config import GitConfigParser, SectionConstraint, cp
17from git.exc import GitCommandError, UnsafeOptionError
18from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
19from git.util import (
20 CallableRemoteProgress,
21 IterableList,
22 IterableObj,
23 LazyMixin,
24 RemoteProgress,
25 join_path,
26)
28# typing-------------------------------------------------------
30from typing import (
31 Any,
32 Callable,
33 Dict,
34 Iterator,
35 List,
36 NoReturn,
37 Optional,
38 Sequence,
39 TYPE_CHECKING,
40 Type,
41 TypeVar,
42 Union,
43 cast,
44 overload,
45)
47from git.types import AnyGitObject, Literal, PathLike
49if TYPE_CHECKING:
50 from git.objects.commit import Commit
51 from git.objects.submodule.base import UpdateProgress
52 from git.repo.base import Repo
54_T_RemoteName = TypeVar("_T_RemoteName", bound=Union[str, "Remote"])
56flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
58# -------------------------------------------------------------
60_logger = logging.getLogger(__name__)
62# { Utilities
65def add_progress(
66 kwargs: Any,
67 git: Git,
68 progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None],
69) -> Any:
70 """Add the ``--progress`` flag to the given `kwargs` dict if supported by the git
71 command.
73 :note:
74 If the actual progress in the given progress instance is not given, we do not
75 request any progress.
77 :return:
78 Possibly altered `kwargs`
79 """
80 if progress is not None:
81 v = git.version_info[:2]
82 if v >= (1, 7):
83 kwargs["progress"] = True
84 # END handle --progress
85 # END handle progress
86 return kwargs
89# } END utilities
92@overload
93def to_progress_instance(progress: None) -> RemoteProgress: ...
96@overload
97def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ...
100@overload
101def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ...
104def to_progress_instance(
105 progress: Union[Callable[..., Any], RemoteProgress, None],
106) -> Union[RemoteProgress, CallableRemoteProgress]:
107 """Given the `progress` return a suitable object derived from
108 :class:`~git.util.RemoteProgress`."""
109 # New API only needs progress as a function.
110 if callable(progress):
111 return CallableRemoteProgress(progress)
113 # Where None is passed create a parser that eats the progress.
114 elif progress is None:
115 return RemoteProgress()
117 # Assume its the old API with an instance of RemoteProgress.
118 return progress
121class PushInfo(IterableObj):
122 """
123 Carries information about the result of a push operation of a single head::
125 info = remote.push()[0]
126 info.flags # bitflags providing more information about the result
127 info.local_ref # Reference pointing to the local reference that was pushed
128 # It is None if the ref was deleted.
129 info.remote_ref_string # path to the remote reference located on the remote side
130 info.remote_ref # Remote Reference on the local side corresponding to
131 # the remote_ref_string. It can be a TagReference as well.
132 info.old_commit # commit at which the remote_ref was standing before we pushed
133 # it to local_ref.commit. Will be None if an error was indicated
134 info.summary # summary line providing human readable english text about the push
135 """
137 __slots__ = (
138 "local_ref",
139 "remote_ref_string",
140 "flags",
141 "_old_commit_sha",
142 "_remote",
143 "summary",
144 )
146 _id_attribute_ = "pushinfo"
148 (
149 NEW_TAG,
150 NEW_HEAD,
151 NO_MATCH,
152 REJECTED,
153 REMOTE_REJECTED,
154 REMOTE_FAILURE,
155 DELETED,
156 FORCED_UPDATE,
157 FAST_FORWARD,
158 UP_TO_DATE,
159 ERROR,
160 ) = [1 << x for x in range(11)]
162 _flag_map = {
163 "X": NO_MATCH,
164 "-": DELETED,
165 "*": 0,
166 "+": FORCED_UPDATE,
167 " ": FAST_FORWARD,
168 "=": UP_TO_DATE,
169 "!": ERROR,
170 }
172 def __init__(
173 self,
174 flags: int,
175 local_ref: Union[SymbolicReference, None],
176 remote_ref_string: str,
177 remote: "Remote",
178 old_commit: Optional[str] = None,
179 summary: str = "",
180 ) -> None:
181 """Initialize a new instance.
183 local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None
184 """
185 self.flags = flags
186 self.local_ref = local_ref
187 self.remote_ref_string = remote_ref_string
188 self._remote = remote
189 self._old_commit_sha = old_commit
190 self.summary = summary
192 @property
193 def old_commit(self) -> Union["Commit", None]:
194 return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
196 @property
197 def remote_ref(self) -> Union[RemoteReference, TagReference]:
198 """
199 :return:
200 Remote :class:`~git.refs.reference.Reference` or
201 :class:`~git.refs.tag.TagReference` in the local repository corresponding to
202 the :attr:`remote_ref_string` kept in this instance.
203 """
204 # Translate heads to a local remote. Tags stay as they are.
205 if self.remote_ref_string.startswith("refs/tags"):
206 return TagReference(self._remote.repo, self.remote_ref_string)
207 elif self.remote_ref_string.startswith("refs/heads"):
208 remote_ref = Reference(self._remote.repo, self.remote_ref_string)
209 return RemoteReference(
210 self._remote.repo,
211 "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name),
212 )
213 else:
214 raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string)
215 # END
217 @classmethod
218 def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
219 """Create a new :class:`PushInfo` instance as parsed from line which is expected
220 to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
221 control_character, from_to, summary = line.split("\t", 3)
222 flags = 0
224 # Control character handling
225 try:
226 flags |= cls._flag_map[control_character]
227 except KeyError as e:
228 raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
229 # END handle control character
231 # from_to handling
232 from_ref_string, to_ref_string = from_to.split(":")
233 if flags & cls.DELETED:
234 from_ref: Union[SymbolicReference, None] = None
235 else:
236 if from_ref_string == "(delete)":
237 from_ref = None
238 else:
239 from_ref = Reference.from_path(remote.repo, from_ref_string)
241 # Commit handling, could be message or commit info
242 old_commit: Optional[str] = None
243 if summary.startswith("["):
244 if "[rejected]" in summary:
245 flags |= cls.REJECTED
246 elif "[remote rejected]" in summary:
247 flags |= cls.REMOTE_REJECTED
248 elif "[remote failure]" in summary:
249 flags |= cls.REMOTE_FAILURE
250 elif "[no match]" in summary:
251 flags |= cls.ERROR
252 elif "[new tag]" in summary:
253 flags |= cls.NEW_TAG
254 elif "[new branch]" in summary:
255 flags |= cls.NEW_HEAD
256 # `uptodate` encoded in control character
257 else:
258 # Fast-forward or forced update - was encoded in control character,
259 # but we parse the old and new commit.
260 split_token = "..."
261 if control_character == " ":
262 split_token = ".."
263 old_sha, _new_sha = summary.split(" ")[0].split(split_token)
264 # Have to use constructor here as the sha usually is abbreviated.
265 old_commit = old_sha
266 # END message handling
268 return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary)
270 @classmethod
271 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['PushInfo']:
272 raise NotImplementedError
275class PushInfoList(IterableList[PushInfo]):
276 """:class:`~git.util.IterableList` of :class:`PushInfo` objects."""
278 def __new__(cls) -> "PushInfoList":
279 return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
281 def __init__(self) -> None:
282 super().__init__("push_infos")
283 self.error: Optional[Exception] = None
285 def raise_if_error(self) -> None:
286 """Raise an exception if any ref failed to push."""
287 if self.error:
288 raise self.error
291class FetchInfo(IterableObj):
292 """
293 Carries information about the results of a fetch operation of a single head::
295 info = remote.fetch()[0]
296 info.ref # Symbolic Reference or RemoteReference to the changed
297 # remote head or FETCH_HEAD
298 info.flags # additional flags to be & with enumeration members,
299 # i.e. info.flags & info.REJECTED
300 # is 0 if ref is SymbolicReference
301 info.note # additional notes given by git-fetch intended for the user
302 info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD,
303 # field is set to the previous location of ref, otherwise None
304 info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref
305 """
307 __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path")
309 _id_attribute_ = "fetchinfo"
311 (
312 NEW_TAG,
313 NEW_HEAD,
314 HEAD_UPTODATE,
315 TAG_UPDATE,
316 REJECTED,
317 FORCED_UPDATE,
318 FAST_FORWARD,
319 ERROR,
320 ) = [1 << x for x in range(8)]
322 _re_fetch_result = re.compile(r"^ *(?:.{0,3})(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)( \(.*\)?$)?")
324 _flag_map: Dict[flagKeyLiteral, int] = {
325 "!": ERROR,
326 "+": FORCED_UPDATE,
327 "*": 0,
328 "=": HEAD_UPTODATE,
329 " ": FAST_FORWARD,
330 "-": TAG_UPDATE,
331 }
333 @classmethod
334 def refresh(cls) -> Literal[True]:
335 """Update information about which :manpage:`git-fetch(1)` flags are supported
336 by the git executable being used.
338 Called by the :func:`git.refresh` function in the top level ``__init__``.
339 """
340 # Clear the old values in _flag_map.
341 with contextlib.suppress(KeyError):
342 del cls._flag_map["t"]
343 with contextlib.suppress(KeyError):
344 del cls._flag_map["-"]
346 # Set the value given the git version.
347 if Git().version_info[:2] >= (2, 10):
348 cls._flag_map["t"] = cls.TAG_UPDATE
349 else:
350 cls._flag_map["-"] = cls.TAG_UPDATE
352 return True
354 def __init__(
355 self,
356 ref: SymbolicReference,
357 flags: int,
358 note: str = "",
359 old_commit: Union[AnyGitObject, None] = None,
360 remote_ref_path: Optional[PathLike] = None,
361 ) -> None:
362 """Initialize a new instance."""
363 self.ref = ref
364 self.flags = flags
365 self.note = note
366 self.old_commit = old_commit
367 self.remote_ref_path = remote_ref_path
369 def __str__(self) -> str:
370 return self.name
372 @property
373 def name(self) -> str:
374 """:return: Name of our remote ref"""
375 return self.ref.name
377 @property
378 def commit(self) -> "Commit":
379 """:return: Commit of our remote ref"""
380 return self.ref.commit
382 @classmethod
383 def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
384 """Parse information from the given line as returned by ``git-fetch -v`` and
385 return a new :class:`FetchInfo` object representing this information.
387 We can handle a line as follows::
389 %c %-*s %-*s -> %s%s
391 Where ``c`` is either a space, ``!``, ``+``, ``-``, ``*``, or ``=``:
393 - '!' means error
394 - '+' means success forcing update
395 - '-' means a tag was updated
396 - '*' means birth of new branch or tag
397 - '=' means the head was up to date (and not moved)
398 - ' ' means a fast-forward
400 `fetch_line` is the corresponding line from FETCH_HEAD, like::
402 acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
403 """
404 match = cls._re_fetch_result.match(line)
405 if match is None:
406 raise ValueError("Failed to parse line: %r" % line)
408 # Parse lines.
409 remote_local_ref_str: str
410 (
411 control_character,
412 operation,
413 local_remote_ref,
414 remote_local_ref_str,
415 note,
416 ) = match.groups()
417 control_character = cast(flagKeyLiteral, control_character)
418 try:
419 _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t")
420 ref_type_name, fetch_note = fetch_note.split(" ", 1)
421 except ValueError as e: # unpack error
422 raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e
424 # Parse flags from control_character.
425 flags = 0
426 try:
427 flags |= cls._flag_map[control_character]
428 except KeyError as e:
429 raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
430 # END control char exception handling
432 # Parse operation string for more info.
433 # This makes no sense for symbolic refs, but we parse it anyway.
434 old_commit: Union[AnyGitObject, None] = None
435 is_tag_operation = False
436 if "rejected" in operation:
437 flags |= cls.REJECTED
438 if "new tag" in operation:
439 flags |= cls.NEW_TAG
440 is_tag_operation = True
441 if "tag update" in operation:
442 flags |= cls.TAG_UPDATE
443 is_tag_operation = True
444 if "new branch" in operation:
445 flags |= cls.NEW_HEAD
446 if "..." in operation or ".." in operation:
447 split_token = "..."
448 if control_character == " ":
449 split_token = split_token[:-1]
450 old_commit = repo.rev_parse(operation.split(split_token)[0])
451 # END handle refspec
453 # Handle FETCH_HEAD and figure out ref type.
454 # If we do not specify a target branch like master:refs/remotes/origin/master,
455 # the fetch result is stored in FETCH_HEAD which destroys the rule we usually
456 # have. In that case we use a symbolic reference which is detached.
457 ref_type: Optional[Type[SymbolicReference]] = None
458 if remote_local_ref_str == "FETCH_HEAD":
459 ref_type = SymbolicReference
460 elif ref_type_name == "tag" or is_tag_operation:
461 # The ref_type_name can be branch, whereas we are still seeing a tag
462 # operation. It happens during testing, which is based on actual git
463 # operations.
464 ref_type = TagReference
465 elif ref_type_name in ("remote-tracking", "branch"):
466 # Note: remote-tracking is just the first part of the
467 # 'remote-tracking branch' token. We don't parse it correctly, but it's
468 # enough to know what to do, and it's new in git 1.7something.
469 ref_type = RemoteReference
470 elif "/" in ref_type_name:
471 # If the fetch spec look something like '+refs/pull/*:refs/heads/pull/*',
472 # and is thus pretty much anything the user wants, we will have trouble
473 # determining what's going on. For now, we assume the local ref is a Head.
474 ref_type = Head
475 else:
476 raise TypeError("Cannot handle reference type: %r" % ref_type_name)
477 # END handle ref type
479 # Create ref instance.
480 if ref_type is SymbolicReference:
481 remote_local_ref = ref_type(repo, "FETCH_HEAD")
482 else:
483 # Determine prefix. Tags are usually pulled into refs/tags; they may have
484 # subdirectories. It is not clear sometimes where exactly the item is,
485 # unless we have an absolute path as indicated by the 'ref/' prefix.
486 # Otherwise even a tag could be in refs/remotes, which is when it will have
487 # the 'tags/' subdirectory in its path. We don't want to test for actual
488 # existence, but try to figure everything out analytically.
489 ref_path: Optional[PathLike] = None
490 remote_local_ref_str = remote_local_ref_str.strip()
492 if remote_local_ref_str.startswith(Reference._common_path_default + "/"):
493 # Always use actual type if we get absolute paths. This will always be
494 # the case if something is fetched outside of refs/remotes (if its not a
495 # tag).
496 ref_path = remote_local_ref_str
497 if ref_type is not TagReference and not remote_local_ref_str.startswith(
498 RemoteReference._common_path_default + "/"
499 ):
500 ref_type = Reference
501 # END downgrade remote reference
502 elif ref_type is TagReference and "tags/" in remote_local_ref_str:
503 # Even though it's a tag, it is located in refs/remotes.
504 ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str)
505 else:
506 ref_path = join_path(ref_type._common_path_default, remote_local_ref_str)
507 # END obtain refpath
509 # Even though the path could be within the git conventions, we make sure we
510 # respect whatever the user wanted, and disabled path checking.
511 remote_local_ref = ref_type(repo, ref_path, check_path=False)
512 # END create ref instance
514 note = (note and note.strip()) or ""
516 return cls(remote_local_ref, flags, note, old_commit, local_remote_ref)
518 @classmethod
519 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['FetchInfo']:
520 raise NotImplementedError
523Progress = Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None]
526class Remote(LazyMixin, IterableObj):
527 """Provides easy read and write access to a git remote.
529 Everything not part of this interface is considered an option for the current
530 remote, allowing constructs like ``remote.pushurl`` to query the pushurl.
532 :note:
533 When querying configuration, the configuration accessor will be cached to speed
534 up subsequent accesses.
535 """
537 __slots__ = ("repo", "name", "_config_reader")
539 _id_attribute_ = "name"
541 unsafe_git_fetch_options = [
542 # This option allows users to execute arbitrary commands.
543 # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt
544 "--upload-pack",
545 ]
546 unsafe_git_pull_options = [
547 # This option allows users to execute arbitrary commands.
548 # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt
549 "--upload-pack"
550 ]
551 unsafe_git_push_options = [
552 # This option allows users to execute arbitrary commands.
553 # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt
554 "--receive-pack",
555 "--exec",
556 ]
558 url: str # Obtained dynamically from _config_reader. See __getattr__ below.
559 """The URL configured for the remote."""
561 def __init__(self, repo: "Repo", name: str) -> None:
562 """Initialize a remote instance.
564 :param repo:
565 The repository we are a remote of.
567 :param name:
568 The name of the remote, e.g. ``origin``.
569 """
570 self.repo = repo
571 self.name = name
573 def __getattr__(self, attr: str) -> Any:
574 """Allows to call this instance like ``remote.special(*args, **kwargs)`` to
575 call ``git remote special self.name``."""
576 if attr == "_config_reader":
577 return super().__getattr__(attr)
579 # Sometimes, probably due to a bug in Python itself, we are being called even
580 # though a slot of the same name exists.
581 try:
582 return self._config_reader.get(attr)
583 except cp.NoOptionError:
584 return super().__getattr__(attr)
585 # END handle exception
587 def _config_section_name(self) -> str:
588 return 'remote "%s"' % self.name
590 def _set_cache_(self, attr: str) -> None:
591 if attr == "_config_reader":
592 # NOTE: This is cached as __getattr__ is overridden to return remote config
593 # values implicitly, such as in print(r.pushurl).
594 self._config_reader = SectionConstraint(
595 self.repo.config_reader("repository"),
596 self._config_section_name(),
597 )
598 else:
599 super()._set_cache_(attr)
601 def __str__(self) -> str:
602 return self.name
604 def __repr__(self) -> str:
605 return '<git.%s "%s">' % (self.__class__.__name__, self.name)
607 def __eq__(self, other: object) -> bool:
608 return isinstance(other, type(self)) and self.name == other.name
610 def __ne__(self, other: object) -> bool:
611 return not (self == other)
613 def __hash__(self) -> int:
614 return hash(self.name)
616 def exists(self) -> bool:
617 """
618 :return:
619 ``True`` if this is a valid, existing remote.
620 Valid remotes have an entry in the repository's configuration.
621 """
622 try:
623 self.config_reader.get("url")
624 return True
625 except cp.NoOptionError:
626 # We have the section at least...
627 return True
628 except cp.NoSectionError:
629 return False
631 @classmethod
632 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]:
633 """:return: Iterator yielding :class:`Remote` objects of the given repository"""
634 for section in repo.config_reader("repository").sections():
635 if not section.lower().startswith("remote "):
636 continue
637 lbound = section.find('"')
638 rbound = section.rfind('"')
639 if lbound == -1 or rbound == -1:
640 raise ValueError("Remote-Section has invalid format: %r" % section)
641 yield Remote(repo, section[lbound + 1 : rbound])
642 # END for each configuration section
644 def set_url(
645 self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
646 ) -> "Remote":
647 """Configure URLs on current remote (cf. command ``git remote set-url``).
649 This command manages URLs on the remote.
651 :param new_url:
652 String being the URL to add as an extra remote URL.
654 :param old_url:
655 When set, replaces this URL with `new_url` for the remote.
657 :param allow_unsafe_protocols:
658 Allow unsafe protocols to be used, like ``ext``.
660 :return:
661 self
662 """
663 if not allow_unsafe_protocols:
664 Git.check_unsafe_protocols(new_url)
665 scmd = "set-url"
666 kwargs["insert_kwargs_after"] = scmd
667 if old_url:
668 self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs)
669 else:
670 self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs)
671 return self
673 def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
674 """Adds a new url on current remote (special case of ``git remote set-url``).
676 This command adds new URLs to a given remote, making it possible to have
677 multiple URLs for a single remote.
679 :param url:
680 String being the URL to add as an extra remote URL.
682 :param allow_unsafe_protocols:
683 Allow unsafe protocols to be used, like ``ext``.
685 :return:
686 self
687 """
688 return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
690 def delete_url(self, url: str, **kwargs: Any) -> "Remote":
691 """Deletes a new url on current remote (special case of ``git remote set-url``).
693 This command deletes new URLs to a given remote, making it possible to have
694 multiple URLs for a single remote.
696 :param url:
697 String being the URL to delete from the remote.
699 :return:
700 self
701 """
702 return self.set_url(url, delete=True)
704 @property
705 def urls(self) -> Iterator[str]:
706 """:return: Iterator yielding all configured URL targets on a remote as strings"""
707 try:
708 remote_details = self.repo.git.remote("get-url", "--all", self.name)
709 assert isinstance(remote_details, str)
710 for line in remote_details.split("\n"):
711 yield line
712 except GitCommandError as ex:
713 ## We are on git < 2.7 (i.e TravisCI as of Oct-2016),
714 # so `get-utl` command does not exist yet!
715 # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319
716 # and: http://stackoverflow.com/a/32991784/548792
717 #
718 if "Unknown subcommand: get-url" in str(ex):
719 try:
720 remote_details = self.repo.git.remote("show", self.name)
721 assert isinstance(remote_details, str)
722 for line in remote_details.split("\n"):
723 if " Push URL:" in line:
724 yield line.split(": ")[-1]
725 except GitCommandError as _ex:
726 if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]):
727 # If ssh is not setup to access this repository, see issue 694.
728 remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name)
729 assert isinstance(remote_details, str)
730 for line in remote_details.split("\n"):
731 yield line
732 else:
733 raise _ex
734 else:
735 raise ex
737 @property
738 def refs(self) -> IterableList[RemoteReference]:
739 """
740 :return:
741 :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
742 objects.
744 It is prefixed, allowing you to omit the remote path portion, e.g.::
746 remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')
747 """
748 out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
749 out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name))
750 return out_refs
752 @property
753 def stale_refs(self) -> IterableList[Reference]:
754 """
755 :return:
756 :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
757 objects that do not have a corresponding head in the remote reference
758 anymore as they have been deleted on the remote side, but are still
759 available locally.
761 The :class:`~git.util.IterableList` is prefixed, hence the 'origin' must be
762 omitted. See :attr:`refs` property for an example.
764 To make things more complicated, it can be possible for the list to include
765 other kinds of references, for example, tag references, if these are stale
766 as well. This is a fix for the issue described here:
767 https://github.com/gitpython-developers/GitPython/issues/260
768 """
769 out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
770 for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]:
771 # expecting
772 # * [would prune] origin/new_branch
773 token = " * [would prune] "
774 if not line.startswith(token):
775 continue
776 ref_name = line.replace(token, "")
777 # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260.
778 if ref_name.startswith(Reference._common_path_default + "/"):
779 out_refs.append(Reference.from_path(self.repo, ref_name))
780 else:
781 fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name)
782 out_refs.append(RemoteReference(self.repo, fqhn))
783 # END special case handling
784 # END for each line
785 return out_refs
787 @classmethod
788 def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
789 """Create a new remote to the given repository.
791 :param repo:
792 Repository instance that is to receive the new remote.
794 :param name:
795 Desired name of the remote.
797 :param url:
798 URL which corresponds to the remote's name.
800 :param allow_unsafe_protocols:
801 Allow unsafe protocols to be used, like ``ext``.
803 :param kwargs:
804 Additional arguments to be passed to the ``git remote add`` command.
806 :return:
807 New :class:`Remote` instance
809 :raise git.exc.GitCommandError:
810 In case an origin with that name already exists.
811 """
812 scmd = "add"
813 kwargs["insert_kwargs_after"] = scmd
814 url = Git.polish_url(url, expand_vars=False)
815 if not allow_unsafe_protocols:
816 Git.check_unsafe_protocols(url)
817 repo.git.remote(scmd, "--", name, url, **kwargs)
818 return cls(repo, name)
820 # `add` is an alias.
821 @classmethod
822 def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
823 return cls.create(repo, name, url, **kwargs)
825 @classmethod
826 def remove(cls, repo: "Repo", name: _T_RemoteName) -> _T_RemoteName:
827 """Remove the remote with the given name.
829 :return:
830 The passed remote name to remove
831 """
832 repo.git.remote("rm", name)
833 remote = name
834 if isinstance(remote, cls):
835 remote._clear_cache()
836 return name
838 @classmethod
839 def rm(cls, repo: "Repo", name: _T_RemoteName) -> _T_RemoteName:
840 """Alias of remove.
841 Remove the remote with the given name.
843 :return:
844 The passed remote name to remove
845 """
846 return cls.remove(repo, name)
848 def rename(self, new_name: str) -> "Remote":
849 """Rename self to the given `new_name`.
851 :return:
852 self
853 """
854 if self.name == new_name:
855 return self
857 self.repo.git.remote("rename", self.name, new_name)
858 self.name = new_name
859 self._clear_cache()
861 return self
863 def update(self, **kwargs: Any) -> "Remote":
864 """Fetch all changes for this remote, including new branches which will be
865 forced in (in case your local remote branch is not part the new remote branch's
866 ancestry anymore).
868 :param kwargs:
869 Additional arguments passed to ``git remote update``.
871 :return:
872 self
873 """
874 # Like pull, remote update forwards operands to fetch without `--`.
875 if self.name.startswith("-"):
876 raise UnsafeOptionError("Remote names used by update must not start with '-'.")
877 scmd = "update"
878 kwargs["insert_kwargs_after"] = scmd
879 self.repo.git.remote(scmd, self.name, **kwargs)
880 return self
882 def _get_fetch_info_from_stderr(
883 self,
884 proc: "Git.AutoInterrupt",
885 progress: Progress,
886 kill_after_timeout: Union[None, float] = None,
887 ) -> IterableList["FetchInfo"]:
888 progress = to_progress_instance(progress)
890 # Skip first line as it is some remote info we are not interested in.
891 output: IterableList["FetchInfo"] = IterableList("name")
893 # Lines which are no progress are fetch info lines.
894 # This also waits for the command to finish.
895 # Skip some progress lines that don't provide relevant information.
896 fetch_info_lines = []
897 # Basically we want all fetch info lines which appear to be in regular form, and
898 # thus have a command character. Everything else we ignore.
899 cmds = set(FetchInfo._flag_map.keys())
901 progress_handler = progress.new_message_handler()
902 handle_process_output(
903 proc,
904 None,
905 progress_handler,
906 finalizer=None,
907 decode_streams=False,
908 kill_after_timeout=kill_after_timeout,
909 )
911 stderr_text = "\n".join(progress.error_lines)
912 proc.wait(stderr=stderr_text)
913 if stderr_text:
914 _logger.warning("Error lines received while fetching: %s", stderr_text)
916 for line in progress.other_lines:
917 line = force_text(line)
918 for cmd in cmds:
919 if len(line) > 1 and line[0] == " " and line[1] == cmd:
920 fetch_info_lines.append(line)
921 continue
923 # Read head information.
924 fetch_head = SymbolicReference(self.repo, "FETCH_HEAD")
925 with open(fetch_head.abspath, "rb") as fp:
926 fetch_head_info = [line.decode(defenc) for line in fp.readlines()]
928 l_fil = len(fetch_info_lines)
929 l_fhi = len(fetch_head_info)
930 if l_fil != l_fhi:
931 msg = "Fetch head lines do not match lines provided via progress information\n"
932 msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n"
933 msg += "Will ignore extra progress lines or fetch head lines."
934 msg %= (l_fil, l_fhi)
935 _logger.debug(msg)
936 _logger.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8"))
937 _logger.debug(b"head info: " + str(fetch_head_info).encode("UTF-8"))
938 if l_fil < l_fhi:
939 fetch_head_info = fetch_head_info[:l_fil]
940 else:
941 fetch_info_lines = fetch_info_lines[:l_fhi]
942 # END truncate correct list
943 # END sanity check + sanitization
945 for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info):
946 try:
947 output.append(FetchInfo._from_line(self.repo, err_line, fetch_line))
948 except ValueError as exc:
949 _logger.debug("Caught error while parsing line: %s", exc)
950 _logger.warning("Git informed while fetching: %s", err_line.strip())
951 return output
953 def _get_push_info(
954 self,
955 proc: "Git.AutoInterrupt",
956 progress: Union[Callable[..., Any], RemoteProgress, None],
957 kill_after_timeout: Union[None, float] = None,
958 ) -> PushInfoList:
959 progress = to_progress_instance(progress)
961 # Read progress information from stderr.
962 # We hope stdout can hold all the data, it should...
963 # Read the lines manually as it will use carriage returns between the messages
964 # to override the previous one. This is why we read the bytes manually.
965 progress_handler = progress.new_message_handler()
966 output: PushInfoList = PushInfoList()
968 def stdout_handler(line: str) -> None:
969 try:
970 output.append(PushInfo._from_line(self, line))
971 except ValueError:
972 # If an error happens, additional info is given which we parse below.
973 pass
975 handle_process_output(
976 proc,
977 stdout_handler,
978 progress_handler,
979 finalizer=None,
980 decode_streams=False,
981 kill_after_timeout=kill_after_timeout,
982 )
983 stderr_text = "\n".join(progress.error_lines)
984 try:
985 proc.wait(stderr=stderr_text)
986 except Exception as e:
987 # This is different than fetch (which fails if there is any stderr
988 # even if there is an output).
989 if not output:
990 raise
991 elif stderr_text or proc._timeout_error:
992 if stderr_text:
993 _logger.warning("Error lines received while pushing: %s", stderr_text)
994 output.error = e
996 return output
998 def _assert_refspec(self) -> None:
999 """Turns out we can't deal with remotes if the refspec is missing."""
1000 config = self.config_reader
1001 unset = "placeholder"
1002 try:
1003 if config.get_value("fetch", default=unset) is unset:
1004 msg = "Remote '%s' has no refspec set.\n"
1005 msg += "You can set it as follows:"
1006 msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'."
1007 raise AssertionError(msg % (self.name, self.name))
1008 finally:
1009 config.release()
1011 def fetch(
1012 self,
1013 refspec: Union[str, List[str], None] = None,
1014 progress: Progress = None,
1015 verbose: bool = True,
1016 kill_after_timeout: Union[None, float] = None,
1017 allow_unsafe_protocols: bool = False,
1018 allow_unsafe_options: bool = False,
1019 **kwargs: Any,
1020 ) -> IterableList[FetchInfo]:
1021 """Fetch the latest changes for this remote.
1023 :param refspec:
1024 A "refspec" is used by fetch and push to describe the mapping
1025 between remote ref and local ref. They are combined with a colon in
1026 the format ``<src>:<dst>``, preceded by an optional plus sign, ``+``.
1027 For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means
1028 "grab the master branch head from the $URL and store it as my origin
1029 branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream``
1030 means "publish my master branch head as to-upstream branch at $URL".
1031 See also :manpage:`git-push(1)`.
1033 Taken from the git manual, :manpage:`gitglossary(7)`.
1035 Fetch supports multiple refspecs (as the underlying :manpage:`git-fetch(1)`
1036 does) - supplying a list rather than a string for 'refspec' will make use of
1037 this facility.
1039 :param progress:
1040 See the :meth:`push` method.
1042 :param verbose:
1043 Boolean for verbose output.
1045 :param kill_after_timeout:
1046 To specify a timeout in seconds for the git command, after which the process
1047 should be killed. It is set to ``None`` by default.
1049 :param allow_unsafe_protocols:
1050 Allow unsafe protocols to be used, like ``ext``.
1052 :param allow_unsafe_options:
1053 Allow unsafe options to be used, like ``--upload-pack``.
1055 :param kwargs:
1056 Additional arguments to be passed to :manpage:`git-fetch(1)`.
1058 :return:
1059 IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing
1060 detailed information about the fetch results
1062 :note:
1063 As fetch does not provide progress information to non-ttys, we cannot make
1064 it available here unfortunately as in the :meth:`push` method.
1065 """
1066 if refspec is None:
1067 # No argument refspec, then ensure the repo's config has a fetch refspec.
1068 self._assert_refspec()
1070 kwargs = add_progress(kwargs, self.repo.git, progress)
1071 if isinstance(refspec, list):
1072 args: Sequence[Optional[str]] = refspec
1073 else:
1074 args = [refspec]
1076 if not allow_unsafe_protocols:
1077 self.repo.git._check_unsafe_protocols_in_args([self, *args], kwargs)
1079 if not allow_unsafe_options:
1080 Git.check_unsafe_options(
1081 options=Git._option_candidates([], kwargs),
1082 unsafe_options=self.unsafe_git_fetch_options,
1083 )
1085 proc = self.repo.git.fetch(
1086 "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=bool(verbose), **kwargs
1087 )
1088 res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
1089 if hasattr(self.repo.odb, "update_cache"):
1090 self.repo.odb.update_cache()
1091 return res
1093 def pull(
1094 self,
1095 refspec: Union[str, List[str], None] = None,
1096 progress: Progress = None,
1097 kill_after_timeout: Union[None, float] = None,
1098 allow_unsafe_protocols: bool = False,
1099 allow_unsafe_options: bool = False,
1100 **kwargs: Any,
1101 ) -> IterableList[FetchInfo]:
1102 """Pull changes from the given branch, being the same as a fetch followed by a
1103 merge of branch with your local branch.
1105 :param refspec:
1106 See :meth:`fetch` method. Values starting with ``-`` are rejected,
1107 even when ``allow_unsafe_options`` is enabled. Pass options as keywords.
1109 :param progress:
1110 See :meth:`push` method.
1112 :param kill_after_timeout:
1113 See :meth:`fetch` method.
1115 :param allow_unsafe_protocols:
1116 Allow unsafe protocols to be used, like ``ext``.
1118 :param allow_unsafe_options:
1119 Allow unsafe options to be used, like ``--upload-pack``.
1121 :param kwargs:
1122 Additional arguments to be passed to :manpage:`git-pull(1)`.
1124 :return:
1125 Please see :meth:`fetch` method.
1126 """
1127 if refspec is None:
1128 # No argument refspec, then ensure the repo's config has a fetch refspec.
1129 self._assert_refspec()
1130 kwargs = add_progress(kwargs, self.repo.git, progress)
1132 refspec = Git._unpack_args(refspec or [])
1133 # Git pull forwards these operands to fetch without preserving `--`.
1134 # Reject every option-shaped operand, including with unsafe options enabled:
1135 # opting into an explicit option must not turn a refspec into an option.
1136 for operand in [self.name, *refspec]:
1137 if operand.startswith("-"):
1138 raise UnsafeOptionError("Remote names and pull refspecs must not start with '-'.")
1139 if not allow_unsafe_protocols:
1140 self.repo.git._check_unsafe_protocols_in_args([self, *refspec], kwargs)
1142 if not allow_unsafe_options:
1143 Git.check_unsafe_options(
1144 options=Git._option_candidates([], kwargs),
1145 unsafe_options=self.unsafe_git_pull_options,
1146 )
1148 proc = self.repo.git.pull(
1149 "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
1150 )
1151 res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
1152 if hasattr(self.repo.odb, "update_cache"):
1153 self.repo.odb.update_cache()
1154 return res
1156 def push(
1157 self,
1158 refspec: Union[str, List[str], None] = None,
1159 progress: Progress = None,
1160 kill_after_timeout: Union[None, float] = None,
1161 allow_unsafe_protocols: bool = False,
1162 allow_unsafe_options: bool = False,
1163 **kwargs: Any,
1164 ) -> PushInfoList:
1165 """Push changes from source branch in refspec to target branch in refspec.
1167 :param refspec:
1168 See :meth:`fetch` method.
1170 :param progress:
1171 Can take one of many value types:
1173 * ``None``, to discard progress information.
1174 * A function (callable) that is called with the progress information.
1175 Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
1176 See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a
1177 description of all arguments given to the function.
1178 * An instance of a class derived from :class:`~git.util.RemoteProgress` that
1179 overrides the
1180 :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method.
1182 :note:
1183 No further progress information is returned after push returns.
1185 :param kill_after_timeout:
1186 To specify a timeout in seconds for the git command, after which the process
1187 should be killed. It is set to ``None`` by default.
1189 :param allow_unsafe_protocols:
1190 Allow unsafe protocols to be used, like ``ext``.
1192 :param allow_unsafe_options:
1193 Allow unsafe options to be used, like ``--receive-pack``.
1195 :param kwargs:
1196 Additional arguments to be passed to :manpage:`git-push(1)`.
1198 :return:
1199 A :class:`PushInfoList` object, where each list member represents an
1200 individual head which had been updated on the remote side.
1202 If the push contains rejected heads, these will have the
1203 :const:`PushInfo.ERROR` bit set in their flags.
1205 If the operation fails completely, the length of the returned
1206 :class:`PushInfoList` will be 0.
1208 Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on
1209 any failure.
1210 """
1211 kwargs = add_progress(kwargs, self.repo.git, progress)
1213 refspec = Git._unpack_args(refspec or [])
1214 if not allow_unsafe_protocols:
1215 self.repo.git._check_unsafe_protocols_in_args([self, *refspec], kwargs)
1217 if not allow_unsafe_options:
1218 Git.check_unsafe_options(
1219 options=Git._option_candidates([], kwargs),
1220 unsafe_options=self.unsafe_git_push_options,
1221 )
1223 proc = self.repo.git.push(
1224 "--",
1225 self,
1226 refspec,
1227 porcelain=True,
1228 as_process=True,
1229 universal_newlines=True,
1230 kill_after_timeout=kill_after_timeout,
1231 **kwargs,
1232 )
1233 return self._get_push_info(proc, progress, kill_after_timeout=kill_after_timeout)
1235 @property
1236 def config_reader(self) -> SectionConstraint[GitConfigParser]:
1237 """
1238 :return:
1239 :class:`~git.config.GitConfigParser` compatible object able to read options
1240 for only our remote. Hence you may simply type ``config.get("pushurl")`` to
1241 obtain the information.
1242 """
1243 return self._config_reader
1245 def _clear_cache(self) -> None:
1246 try:
1247 del self._config_reader
1248 except AttributeError:
1249 pass
1250 # END handle exception
1252 @property
1253 def config_writer(self) -> SectionConstraint:
1254 """
1255 :return:
1256 :class:`~git.config.GitConfigParser`-compatible object able to write options
1257 for this remote.
1259 :note:
1260 You can only own one writer at a time - delete it to release the
1261 configuration file and make it usable by others.
1263 To assure consistent results, you should only query options through the
1264 writer. Once you are done writing, you are free to use the config reader
1265 once again.
1266 """
1267 writer = self.repo.config_writer()
1269 # Clear our cache to ensure we re-read the possibly changed configuration.
1270 self._clear_cache()
1271 return SectionConstraint(writer, self._config_section_name())