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