| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248 |
- # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
- #
- # This module is part of GitPython and is released under the
- # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
- """Module implementing a remote object allowing easy access to git remotes."""
- __all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"]
- import contextlib
- import logging
- import re
- from git.cmd import Git, handle_process_output
- from git.compat import defenc, force_text
- from git.config import GitConfigParser, SectionConstraint, cp
- from git.exc import GitCommandError
- from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
- from git.util import (
- CallableRemoteProgress,
- IterableList,
- IterableObj,
- LazyMixin,
- RemoteProgress,
- join_path,
- )
- # typing-------------------------------------------------------
- from typing import (
- Any,
- Callable,
- Dict,
- Iterator,
- List,
- NoReturn,
- Optional,
- Sequence,
- TYPE_CHECKING,
- Type,
- Union,
- cast,
- overload,
- )
- from git.types import AnyGitObject, Literal, PathLike
- if TYPE_CHECKING:
- from git.objects.commit import Commit
- from git.objects.submodule.base import UpdateProgress
- from git.repo.base import Repo
- flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
- # -------------------------------------------------------------
- _logger = logging.getLogger(__name__)
- # { Utilities
- def add_progress(
- kwargs: Any,
- git: Git,
- progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None],
- ) -> Any:
- """Add the ``--progress`` flag to the given `kwargs` dict if supported by the git
- command.
- :note:
- If the actual progress in the given progress instance is not given, we do not
- request any progress.
- :return:
- Possibly altered `kwargs`
- """
- if progress is not None:
- v = git.version_info[:2]
- if v >= (1, 7):
- kwargs["progress"] = True
- # END handle --progress
- # END handle progress
- return kwargs
- # } END utilities
- @overload
- def to_progress_instance(progress: None) -> RemoteProgress: ...
- @overload
- def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ...
- @overload
- def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ...
- def to_progress_instance(
- progress: Union[Callable[..., Any], RemoteProgress, None],
- ) -> Union[RemoteProgress, CallableRemoteProgress]:
- """Given the `progress` return a suitable object derived from
- :class:`~git.util.RemoteProgress`."""
- # New API only needs progress as a function.
- if callable(progress):
- return CallableRemoteProgress(progress)
- # Where None is passed create a parser that eats the progress.
- elif progress is None:
- return RemoteProgress()
- # Assume its the old API with an instance of RemoteProgress.
- return progress
- class PushInfo(IterableObj):
- """
- Carries information about the result of a push operation of a single head::
- info = remote.push()[0]
- info.flags # bitflags providing more information about the result
- info.local_ref # Reference pointing to the local reference that was pushed
- # It is None if the ref was deleted.
- info.remote_ref_string # path to the remote reference located on the remote side
- info.remote_ref # Remote Reference on the local side corresponding to
- # the remote_ref_string. It can be a TagReference as well.
- info.old_commit # commit at which the remote_ref was standing before we pushed
- # it to local_ref.commit. Will be None if an error was indicated
- info.summary # summary line providing human readable english text about the push
- """
- __slots__ = (
- "local_ref",
- "remote_ref_string",
- "flags",
- "_old_commit_sha",
- "_remote",
- "summary",
- )
- _id_attribute_ = "pushinfo"
- (
- NEW_TAG,
- NEW_HEAD,
- NO_MATCH,
- REJECTED,
- REMOTE_REJECTED,
- REMOTE_FAILURE,
- DELETED,
- FORCED_UPDATE,
- FAST_FORWARD,
- UP_TO_DATE,
- ERROR,
- ) = [1 << x for x in range(11)]
- _flag_map = {
- "X": NO_MATCH,
- "-": DELETED,
- "*": 0,
- "+": FORCED_UPDATE,
- " ": FAST_FORWARD,
- "=": UP_TO_DATE,
- "!": ERROR,
- }
- def __init__(
- self,
- flags: int,
- local_ref: Union[SymbolicReference, None],
- remote_ref_string: str,
- remote: "Remote",
- old_commit: Optional[str] = None,
- summary: str = "",
- ) -> None:
- """Initialize a new instance.
- local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None
- """
- self.flags = flags
- self.local_ref = local_ref
- self.remote_ref_string = remote_ref_string
- self._remote = remote
- self._old_commit_sha = old_commit
- self.summary = summary
- @property
- def old_commit(self) -> Union["Commit", None]:
- return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
- @property
- def remote_ref(self) -> Union[RemoteReference, TagReference]:
- """
- :return:
- Remote :class:`~git.refs.reference.Reference` or
- :class:`~git.refs.tag.TagReference` in the local repository corresponding to
- the :attr:`remote_ref_string` kept in this instance.
- """
- # Translate heads to a local remote. Tags stay as they are.
- if self.remote_ref_string.startswith("refs/tags"):
- return TagReference(self._remote.repo, self.remote_ref_string)
- elif self.remote_ref_string.startswith("refs/heads"):
- remote_ref = Reference(self._remote.repo, self.remote_ref_string)
- return RemoteReference(
- self._remote.repo,
- "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name),
- )
- else:
- raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string)
- # END
- @classmethod
- def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
- """Create a new :class:`PushInfo` instance as parsed from line which is expected
- to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
- control_character, from_to, summary = line.split("\t", 3)
- flags = 0
- # Control character handling
- try:
- flags |= cls._flag_map[control_character]
- except KeyError as e:
- raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
- # END handle control character
- # from_to handling
- from_ref_string, to_ref_string = from_to.split(":")
- if flags & cls.DELETED:
- from_ref: Union[SymbolicReference, None] = None
- else:
- if from_ref_string == "(delete)":
- from_ref = None
- else:
- from_ref = Reference.from_path(remote.repo, from_ref_string)
- # Commit handling, could be message or commit info
- old_commit: Optional[str] = None
- if summary.startswith("["):
- if "[rejected]" in summary:
- flags |= cls.REJECTED
- elif "[remote rejected]" in summary:
- flags |= cls.REMOTE_REJECTED
- elif "[remote failure]" in summary:
- flags |= cls.REMOTE_FAILURE
- elif "[no match]" in summary:
- flags |= cls.ERROR
- elif "[new tag]" in summary:
- flags |= cls.NEW_TAG
- elif "[new branch]" in summary:
- flags |= cls.NEW_HEAD
- # `uptodate` encoded in control character
- else:
- # Fast-forward or forced update - was encoded in control character,
- # but we parse the old and new commit.
- split_token = "..."
- if control_character == " ":
- split_token = ".."
- old_sha, _new_sha = summary.split(" ")[0].split(split_token)
- # Have to use constructor here as the sha usually is abbreviated.
- old_commit = old_sha
- # END message handling
- return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary)
- @classmethod
- def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['PushInfo']:
- raise NotImplementedError
- class PushInfoList(IterableList[PushInfo]):
- """:class:`~git.util.IterableList` of :class:`PushInfo` objects."""
- def __new__(cls) -> "PushInfoList":
- return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
- def __init__(self) -> None:
- super().__init__("push_infos")
- self.error: Optional[Exception] = None
- def raise_if_error(self) -> None:
- """Raise an exception if any ref failed to push."""
- if self.error:
- raise self.error
- class FetchInfo(IterableObj):
- """
- Carries information about the results of a fetch operation of a single head::
- info = remote.fetch()[0]
- info.ref # Symbolic Reference or RemoteReference to the changed
- # remote head or FETCH_HEAD
- info.flags # additional flags to be & with enumeration members,
- # i.e. info.flags & info.REJECTED
- # is 0 if ref is SymbolicReference
- info.note # additional notes given by git-fetch intended for the user
- info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD,
- # field is set to the previous location of ref, otherwise None
- info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref
- """
- __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path")
- _id_attribute_ = "fetchinfo"
- (
- NEW_TAG,
- NEW_HEAD,
- HEAD_UPTODATE,
- TAG_UPDATE,
- REJECTED,
- FORCED_UPDATE,
- FAST_FORWARD,
- ERROR,
- ) = [1 << x for x in range(8)]
- _re_fetch_result = re.compile(r"^ *(?:.{0,3})(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)( \(.*\)?$)?")
- _flag_map: Dict[flagKeyLiteral, int] = {
- "!": ERROR,
- "+": FORCED_UPDATE,
- "*": 0,
- "=": HEAD_UPTODATE,
- " ": FAST_FORWARD,
- "-": TAG_UPDATE,
- }
- @classmethod
- def refresh(cls) -> Literal[True]:
- """Update information about which :manpage:`git-fetch(1)` flags are supported
- by the git executable being used.
- Called by the :func:`git.refresh` function in the top level ``__init__``.
- """
- # Clear the old values in _flag_map.
- with contextlib.suppress(KeyError):
- del cls._flag_map["t"]
- with contextlib.suppress(KeyError):
- del cls._flag_map["-"]
- # Set the value given the git version.
- if Git().version_info[:2] >= (2, 10):
- cls._flag_map["t"] = cls.TAG_UPDATE
- else:
- cls._flag_map["-"] = cls.TAG_UPDATE
- return True
- def __init__(
- self,
- ref: SymbolicReference,
- flags: int,
- note: str = "",
- old_commit: Union[AnyGitObject, None] = None,
- remote_ref_path: Optional[PathLike] = None,
- ) -> None:
- """Initialize a new instance."""
- self.ref = ref
- self.flags = flags
- self.note = note
- self.old_commit = old_commit
- self.remote_ref_path = remote_ref_path
- def __str__(self) -> str:
- return self.name
- @property
- def name(self) -> str:
- """:return: Name of our remote ref"""
- return self.ref.name
- @property
- def commit(self) -> "Commit":
- """:return: Commit of our remote ref"""
- return self.ref.commit
- @classmethod
- def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
- """Parse information from the given line as returned by ``git-fetch -v`` and
- return a new :class:`FetchInfo` object representing this information.
- We can handle a line as follows::
- %c %-*s %-*s -> %s%s
- Where ``c`` is either a space, ``!``, ``+``, ``-``, ``*``, or ``=``:
- - '!' means error
- - '+' means success forcing update
- - '-' means a tag was updated
- - '*' means birth of new branch or tag
- - '=' means the head was up to date (and not moved)
- - ' ' means a fast-forward
- `fetch_line` is the corresponding line from FETCH_HEAD, like::
- acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
- """
- match = cls._re_fetch_result.match(line)
- if match is None:
- raise ValueError("Failed to parse line: %r" % line)
- # Parse lines.
- remote_local_ref_str: str
- (
- control_character,
- operation,
- local_remote_ref,
- remote_local_ref_str,
- note,
- ) = match.groups()
- control_character = cast(flagKeyLiteral, control_character)
- try:
- _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t")
- ref_type_name, fetch_note = fetch_note.split(" ", 1)
- except ValueError as e: # unpack error
- raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e
- # Parse flags from control_character.
- flags = 0
- try:
- flags |= cls._flag_map[control_character]
- except KeyError as e:
- raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
- # END control char exception handling
- # Parse operation string for more info.
- # This makes no sense for symbolic refs, but we parse it anyway.
- old_commit: Union[AnyGitObject, None] = None
- is_tag_operation = False
- if "rejected" in operation:
- flags |= cls.REJECTED
- if "new tag" in operation:
- flags |= cls.NEW_TAG
- is_tag_operation = True
- if "tag update" in operation:
- flags |= cls.TAG_UPDATE
- is_tag_operation = True
- if "new branch" in operation:
- flags |= cls.NEW_HEAD
- if "..." in operation or ".." in operation:
- split_token = "..."
- if control_character == " ":
- split_token = split_token[:-1]
- old_commit = repo.rev_parse(operation.split(split_token)[0])
- # END handle refspec
- # Handle FETCH_HEAD and figure out ref type.
- # If we do not specify a target branch like master:refs/remotes/origin/master,
- # the fetch result is stored in FETCH_HEAD which destroys the rule we usually
- # have. In that case we use a symbolic reference which is detached.
- ref_type: Optional[Type[SymbolicReference]] = None
- if remote_local_ref_str == "FETCH_HEAD":
- ref_type = SymbolicReference
- elif ref_type_name == "tag" or is_tag_operation:
- # The ref_type_name can be branch, whereas we are still seeing a tag
- # operation. It happens during testing, which is based on actual git
- # operations.
- ref_type = TagReference
- elif ref_type_name in ("remote-tracking", "branch"):
- # Note: remote-tracking is just the first part of the
- # 'remote-tracking branch' token. We don't parse it correctly, but it's
- # enough to know what to do, and it's new in git 1.7something.
- ref_type = RemoteReference
- elif "/" in ref_type_name:
- # If the fetch spec look something like '+refs/pull/*:refs/heads/pull/*',
- # and is thus pretty much anything the user wants, we will have trouble
- # determining what's going on. For now, we assume the local ref is a Head.
- ref_type = Head
- else:
- raise TypeError("Cannot handle reference type: %r" % ref_type_name)
- # END handle ref type
- # Create ref instance.
- if ref_type is SymbolicReference:
- remote_local_ref = ref_type(repo, "FETCH_HEAD")
- else:
- # Determine prefix. Tags are usually pulled into refs/tags; they may have
- # subdirectories. It is not clear sometimes where exactly the item is,
- # unless we have an absolute path as indicated by the 'ref/' prefix.
- # Otherwise even a tag could be in refs/remotes, which is when it will have
- # the 'tags/' subdirectory in its path. We don't want to test for actual
- # existence, but try to figure everything out analytically.
- ref_path: Optional[PathLike] = None
- remote_local_ref_str = remote_local_ref_str.strip()
- if remote_local_ref_str.startswith(Reference._common_path_default + "/"):
- # Always use actual type if we get absolute paths. This will always be
- # the case if something is fetched outside of refs/remotes (if its not a
- # tag).
- ref_path = remote_local_ref_str
- if ref_type is not TagReference and not remote_local_ref_str.startswith(
- RemoteReference._common_path_default + "/"
- ):
- ref_type = Reference
- # END downgrade remote reference
- elif ref_type is TagReference and "tags/" in remote_local_ref_str:
- # Even though it's a tag, it is located in refs/remotes.
- ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str)
- else:
- ref_path = join_path(ref_type._common_path_default, remote_local_ref_str)
- # END obtain refpath
- # Even though the path could be within the git conventions, we make sure we
- # respect whatever the user wanted, and disabled path checking.
- remote_local_ref = ref_type(repo, ref_path, check_path=False)
- # END create ref instance
- note = (note and note.strip()) or ""
- return cls(remote_local_ref, flags, note, old_commit, local_remote_ref)
- @classmethod
- def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['FetchInfo']:
- raise NotImplementedError
- class Remote(LazyMixin, IterableObj):
- """Provides easy read and write access to a git remote.
- Everything not part of this interface is considered an option for the current
- remote, allowing constructs like ``remote.pushurl`` to query the pushurl.
- :note:
- When querying configuration, the configuration accessor will be cached to speed
- up subsequent accesses.
- """
- __slots__ = ("repo", "name", "_config_reader")
- _id_attribute_ = "name"
- unsafe_git_fetch_options = [
- # This option allows users to execute arbitrary commands.
- # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt
- "--upload-pack",
- ]
- unsafe_git_pull_options = [
- # This option allows users to execute arbitrary commands.
- # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt
- "--upload-pack"
- ]
- unsafe_git_push_options = [
- # This option allows users to execute arbitrary commands.
- # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt
- "--receive-pack",
- "--exec",
- ]
- url: str # Obtained dynamically from _config_reader. See __getattr__ below.
- """The URL configured for the remote."""
- def __init__(self, repo: "Repo", name: str) -> None:
- """Initialize a remote instance.
- :param repo:
- The repository we are a remote of.
- :param name:
- The name of the remote, e.g. ``origin``.
- """
- self.repo = repo
- self.name = name
- def __getattr__(self, attr: str) -> Any:
- """Allows to call this instance like ``remote.special(*args, **kwargs)`` to
- call ``git remote special self.name``."""
- if attr == "_config_reader":
- return super().__getattr__(attr)
- # Sometimes, probably due to a bug in Python itself, we are being called even
- # though a slot of the same name exists.
- try:
- return self._config_reader.get(attr)
- except cp.NoOptionError:
- return super().__getattr__(attr)
- # END handle exception
- def _config_section_name(self) -> str:
- return 'remote "%s"' % self.name
- def _set_cache_(self, attr: str) -> None:
- if attr == "_config_reader":
- # NOTE: This is cached as __getattr__ is overridden to return remote config
- # values implicitly, such as in print(r.pushurl).
- self._config_reader = SectionConstraint(
- self.repo.config_reader("repository"),
- self._config_section_name(),
- )
- else:
- super()._set_cache_(attr)
- def __str__(self) -> str:
- return self.name
- def __repr__(self) -> str:
- return '<git.%s "%s">' % (self.__class__.__name__, self.name)
- def __eq__(self, other: object) -> bool:
- return isinstance(other, type(self)) and self.name == other.name
- def __ne__(self, other: object) -> bool:
- return not (self == other)
- def __hash__(self) -> int:
- return hash(self.name)
- def exists(self) -> bool:
- """
- :return:
- ``True`` if this is a valid, existing remote.
- Valid remotes have an entry in the repository's configuration.
- """
- try:
- self.config_reader.get("url")
- return True
- except cp.NoOptionError:
- # We have the section at least...
- return True
- except cp.NoSectionError:
- return False
- @classmethod
- def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]:
- """:return: Iterator yielding :class:`Remote` objects of the given repository"""
- for section in repo.config_reader("repository").sections():
- if not section.startswith("remote "):
- continue
- lbound = section.find('"')
- rbound = section.rfind('"')
- if lbound == -1 or rbound == -1:
- raise ValueError("Remote-Section has invalid format: %r" % section)
- yield Remote(repo, section[lbound + 1 : rbound])
- # END for each configuration section
- def set_url(
- self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
- ) -> "Remote":
- """Configure URLs on current remote (cf. command ``git remote set-url``).
- This command manages URLs on the remote.
- :param new_url:
- String being the URL to add as an extra remote URL.
- :param old_url:
- When set, replaces this URL with `new_url` for the remote.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :return:
- self
- """
- if not allow_unsafe_protocols:
- Git.check_unsafe_protocols(new_url)
- scmd = "set-url"
- kwargs["insert_kwargs_after"] = scmd
- if old_url:
- self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs)
- else:
- self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs)
- return self
- def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
- """Adds a new url on current remote (special case of ``git remote set-url``).
- This command adds new URLs to a given remote, making it possible to have
- multiple URLs for a single remote.
- :param url:
- String being the URL to add as an extra remote URL.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :return:
- self
- """
- return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
- def delete_url(self, url: str, **kwargs: Any) -> "Remote":
- """Deletes a new url on current remote (special case of ``git remote set-url``).
- This command deletes new URLs to a given remote, making it possible to have
- multiple URLs for a single remote.
- :param url:
- String being the URL to delete from the remote.
- :return:
- self
- """
- return self.set_url(url, delete=True)
- @property
- def urls(self) -> Iterator[str]:
- """:return: Iterator yielding all configured URL targets on a remote as strings"""
- try:
- remote_details = self.repo.git.remote("get-url", "--all", self.name)
- assert isinstance(remote_details, str)
- for line in remote_details.split("\n"):
- yield line
- except GitCommandError as ex:
- ## We are on git < 2.7 (i.e TravisCI as of Oct-2016),
- # so `get-utl` command does not exist yet!
- # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319
- # and: http://stackoverflow.com/a/32991784/548792
- #
- if "Unknown subcommand: get-url" in str(ex):
- try:
- remote_details = self.repo.git.remote("show", self.name)
- assert isinstance(remote_details, str)
- for line in remote_details.split("\n"):
- if " Push URL:" in line:
- yield line.split(": ")[-1]
- except GitCommandError as _ex:
- if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]):
- # If ssh is not setup to access this repository, see issue 694.
- remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name)
- assert isinstance(remote_details, str)
- for line in remote_details.split("\n"):
- yield line
- else:
- raise _ex
- else:
- raise ex
- @property
- def refs(self) -> IterableList[RemoteReference]:
- """
- :return:
- :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
- objects.
- It is prefixed, allowing you to omit the remote path portion, e.g.::
- remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')
- """
- out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
- out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name))
- return out_refs
- @property
- def stale_refs(self) -> IterableList[Reference]:
- """
- :return:
- :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
- objects that do not have a corresponding head in the remote reference
- anymore as they have been deleted on the remote side, but are still
- available locally.
- The :class:`~git.util.IterableList` is prefixed, hence the 'origin' must be
- omitted. See :attr:`refs` property for an example.
- To make things more complicated, it can be possible for the list to include
- other kinds of references, for example, tag references, if these are stale
- as well. This is a fix for the issue described here:
- https://github.com/gitpython-developers/GitPython/issues/260
- """
- out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
- for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]:
- # expecting
- # * [would prune] origin/new_branch
- token = " * [would prune] "
- if not line.startswith(token):
- continue
- ref_name = line.replace(token, "")
- # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260.
- if ref_name.startswith(Reference._common_path_default + "/"):
- out_refs.append(Reference.from_path(self.repo, ref_name))
- else:
- fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name)
- out_refs.append(RemoteReference(self.repo, fqhn))
- # END special case handling
- # END for each line
- return out_refs
- @classmethod
- def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
- """Create a new remote to the given repository.
- :param repo:
- Repository instance that is to receive the new remote.
- :param name:
- Desired name of the remote.
- :param url:
- URL which corresponds to the remote's name.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :param kwargs:
- Additional arguments to be passed to the ``git remote add`` command.
- :return:
- New :class:`Remote` instance
- :raise git.exc.GitCommandError:
- In case an origin with that name already exists.
- """
- scmd = "add"
- kwargs["insert_kwargs_after"] = scmd
- url = Git.polish_url(url)
- if not allow_unsafe_protocols:
- Git.check_unsafe_protocols(url)
- repo.git.remote(scmd, "--", name, url, **kwargs)
- return cls(repo, name)
- # `add` is an alias.
- @classmethod
- def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
- return cls.create(repo, name, url, **kwargs)
- @classmethod
- def remove(cls, repo: "Repo", name: str) -> str:
- """Remove the remote with the given name.
- :return:
- The passed remote name to remove
- """
- repo.git.remote("rm", name)
- if isinstance(name, cls):
- name._clear_cache()
- return name
- @classmethod
- def rm(cls, repo: "Repo", name: str) -> str:
- """Alias of remove.
- Remove the remote with the given name.
- :return:
- The passed remote name to remove
- """
- return cls.remove(repo, name)
- def rename(self, new_name: str) -> "Remote":
- """Rename self to the given `new_name`.
- :return:
- self
- """
- if self.name == new_name:
- return self
- self.repo.git.remote("rename", self.name, new_name)
- self.name = new_name
- self._clear_cache()
- return self
- def update(self, **kwargs: Any) -> "Remote":
- """Fetch all changes for this remote, including new branches which will be
- forced in (in case your local remote branch is not part the new remote branch's
- ancestry anymore).
- :param kwargs:
- Additional arguments passed to ``git remote update``.
- :return:
- self
- """
- scmd = "update"
- kwargs["insert_kwargs_after"] = scmd
- self.repo.git.remote(scmd, self.name, **kwargs)
- return self
- def _get_fetch_info_from_stderr(
- self,
- proc: "Git.AutoInterrupt",
- progress: Union[Callable[..., Any], RemoteProgress, None],
- kill_after_timeout: Union[None, float] = None,
- ) -> IterableList["FetchInfo"]:
- progress = to_progress_instance(progress)
- # Skip first line as it is some remote info we are not interested in.
- output: IterableList["FetchInfo"] = IterableList("name")
- # Lines which are no progress are fetch info lines.
- # This also waits for the command to finish.
- # Skip some progress lines that don't provide relevant information.
- fetch_info_lines = []
- # Basically we want all fetch info lines which appear to be in regular form, and
- # thus have a command character. Everything else we ignore.
- cmds = set(FetchInfo._flag_map.keys())
- progress_handler = progress.new_message_handler()
- handle_process_output(
- proc,
- None,
- progress_handler,
- finalizer=None,
- decode_streams=False,
- kill_after_timeout=kill_after_timeout,
- )
- stderr_text = progress.error_lines and "\n".join(progress.error_lines) or ""
- proc.wait(stderr=stderr_text)
- if stderr_text:
- _logger.warning("Error lines received while fetching: %s", stderr_text)
- for line in progress.other_lines:
- line = force_text(line)
- for cmd in cmds:
- if len(line) > 1 and line[0] == " " and line[1] == cmd:
- fetch_info_lines.append(line)
- continue
- # Read head information.
- fetch_head = SymbolicReference(self.repo, "FETCH_HEAD")
- with open(fetch_head.abspath, "rb") as fp:
- fetch_head_info = [line.decode(defenc) for line in fp.readlines()]
- l_fil = len(fetch_info_lines)
- l_fhi = len(fetch_head_info)
- if l_fil != l_fhi:
- msg = "Fetch head lines do not match lines provided via progress information\n"
- msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n"
- msg += "Will ignore extra progress lines or fetch head lines."
- msg %= (l_fil, l_fhi)
- _logger.debug(msg)
- _logger.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8"))
- _logger.debug(b"head info: " + str(fetch_head_info).encode("UTF-8"))
- if l_fil < l_fhi:
- fetch_head_info = fetch_head_info[:l_fil]
- else:
- fetch_info_lines = fetch_info_lines[:l_fhi]
- # END truncate correct list
- # END sanity check + sanitization
- for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info):
- try:
- output.append(FetchInfo._from_line(self.repo, err_line, fetch_line))
- except ValueError as exc:
- _logger.debug("Caught error while parsing line: %s", exc)
- _logger.warning("Git informed while fetching: %s", err_line.strip())
- return output
- def _get_push_info(
- self,
- proc: "Git.AutoInterrupt",
- progress: Union[Callable[..., Any], RemoteProgress, None],
- kill_after_timeout: Union[None, float] = None,
- ) -> PushInfoList:
- progress = to_progress_instance(progress)
- # Read progress information from stderr.
- # We hope stdout can hold all the data, it should...
- # Read the lines manually as it will use carriage returns between the messages
- # to override the previous one. This is why we read the bytes manually.
- progress_handler = progress.new_message_handler()
- output: PushInfoList = PushInfoList()
- def stdout_handler(line: str) -> None:
- try:
- output.append(PushInfo._from_line(self, line))
- except ValueError:
- # If an error happens, additional info is given which we parse below.
- pass
- handle_process_output(
- proc,
- stdout_handler,
- progress_handler,
- finalizer=None,
- decode_streams=False,
- kill_after_timeout=kill_after_timeout,
- )
- stderr_text = progress.error_lines and "\n".join(progress.error_lines) or ""
- try:
- proc.wait(stderr=stderr_text)
- except Exception as e:
- # This is different than fetch (which fails if there is any stderr
- # even if there is an output).
- if not output:
- raise
- elif stderr_text:
- _logger.warning("Error lines received while fetching: %s", stderr_text)
- output.error = e
- return output
- def _assert_refspec(self) -> None:
- """Turns out we can't deal with remotes if the refspec is missing."""
- config = self.config_reader
- unset = "placeholder"
- try:
- if config.get_value("fetch", default=unset) is unset:
- msg = "Remote '%s' has no refspec set.\n"
- msg += "You can set it as follows:"
- msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'."
- raise AssertionError(msg % (self.name, self.name))
- finally:
- config.release()
- def fetch(
- self,
- refspec: Union[str, List[str], None] = None,
- progress: Union[RemoteProgress, None, "UpdateProgress"] = None,
- verbose: bool = True,
- kill_after_timeout: Union[None, float] = None,
- allow_unsafe_protocols: bool = False,
- allow_unsafe_options: bool = False,
- **kwargs: Any,
- ) -> IterableList[FetchInfo]:
- """Fetch the latest changes for this remote.
- :param refspec:
- A "refspec" is used by fetch and push to describe the mapping
- between remote ref and local ref. They are combined with a colon in
- the format ``<src>:<dst>``, preceded by an optional plus sign, ``+``.
- For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means
- "grab the master branch head from the $URL and store it as my origin
- branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream``
- means "publish my master branch head as to-upstream branch at $URL".
- See also :manpage:`git-push(1)`.
- Taken from the git manual, :manpage:`gitglossary(7)`.
- Fetch supports multiple refspecs (as the underlying :manpage:`git-fetch(1)`
- does) - supplying a list rather than a string for 'refspec' will make use of
- this facility.
- :param progress:
- See the :meth:`push` method.
- :param verbose:
- Boolean for verbose output.
- :param kill_after_timeout:
- To specify a timeout in seconds for the git command, after which the process
- should be killed. It is set to ``None`` by default.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :param allow_unsafe_options:
- Allow unsafe options to be used, like ``--upload-pack``.
- :param kwargs:
- Additional arguments to be passed to :manpage:`git-fetch(1)`.
- :return:
- IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing
- detailed information about the fetch results
- :note:
- As fetch does not provide progress information to non-ttys, we cannot make
- it available here unfortunately as in the :meth:`push` method.
- """
- if refspec is None:
- # No argument refspec, then ensure the repo's config has a fetch refspec.
- self._assert_refspec()
- kwargs = add_progress(kwargs, self.repo.git, progress)
- if isinstance(refspec, list):
- args: Sequence[Optional[str]] = refspec
- else:
- args = [refspec]
- if not allow_unsafe_protocols:
- for ref in args:
- if ref:
- Git.check_unsafe_protocols(ref)
- if not allow_unsafe_options:
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
- proc = self.repo.git.fetch(
- "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
- )
- res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
- if hasattr(self.repo.odb, "update_cache"):
- self.repo.odb.update_cache()
- return res
- def pull(
- self,
- refspec: Union[str, List[str], None] = None,
- progress: Union[RemoteProgress, "UpdateProgress", None] = None,
- kill_after_timeout: Union[None, float] = None,
- allow_unsafe_protocols: bool = False,
- allow_unsafe_options: bool = False,
- **kwargs: Any,
- ) -> IterableList[FetchInfo]:
- """Pull changes from the given branch, being the same as a fetch followed by a
- merge of branch with your local branch.
- :param refspec:
- See :meth:`fetch` method.
- :param progress:
- See :meth:`push` method.
- :param kill_after_timeout:
- See :meth:`fetch` method.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :param allow_unsafe_options:
- Allow unsafe options to be used, like ``--upload-pack``.
- :param kwargs:
- Additional arguments to be passed to :manpage:`git-pull(1)`.
- :return:
- Please see :meth:`fetch` method.
- """
- if refspec is None:
- # No argument refspec, then ensure the repo's config has a fetch refspec.
- self._assert_refspec()
- kwargs = add_progress(kwargs, self.repo.git, progress)
- refspec = Git._unpack_args(refspec or [])
- if not allow_unsafe_protocols:
- for ref in refspec:
- Git.check_unsafe_protocols(ref)
- if not allow_unsafe_options:
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
- proc = self.repo.git.pull(
- "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
- )
- res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
- if hasattr(self.repo.odb, "update_cache"):
- self.repo.odb.update_cache()
- return res
- def push(
- self,
- refspec: Union[str, List[str], None] = None,
- progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None,
- kill_after_timeout: Union[None, float] = None,
- allow_unsafe_protocols: bool = False,
- allow_unsafe_options: bool = False,
- **kwargs: Any,
- ) -> PushInfoList:
- """Push changes from source branch in refspec to target branch in refspec.
- :param refspec:
- See :meth:`fetch` method.
- :param progress:
- Can take one of many value types:
- * ``None``, to discard progress information.
- * A function (callable) that is called with the progress information.
- Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
- See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a
- description of all arguments given to the function.
- * An instance of a class derived from :class:`~git.util.RemoteProgress` that
- overrides the
- :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method.
- :note:
- No further progress information is returned after push returns.
- :param kill_after_timeout:
- To specify a timeout in seconds for the git command, after which the process
- should be killed. It is set to ``None`` by default.
- :param allow_unsafe_protocols:
- Allow unsafe protocols to be used, like ``ext``.
- :param allow_unsafe_options:
- Allow unsafe options to be used, like ``--receive-pack``.
- :param kwargs:
- Additional arguments to be passed to :manpage:`git-push(1)`.
- :return:
- A :class:`PushInfoList` object, where each list member represents an
- individual head which had been updated on the remote side.
- If the push contains rejected heads, these will have the
- :const:`PushInfo.ERROR` bit set in their flags.
- If the operation fails completely, the length of the returned
- :class:`PushInfoList` will be 0.
- Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on
- any failure.
- """
- kwargs = add_progress(kwargs, self.repo.git, progress)
- refspec = Git._unpack_args(refspec or [])
- if not allow_unsafe_protocols:
- for ref in refspec:
- Git.check_unsafe_protocols(ref)
- if not allow_unsafe_options:
- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)
- proc = self.repo.git.push(
- "--",
- self,
- refspec,
- porcelain=True,
- as_process=True,
- universal_newlines=True,
- kill_after_timeout=kill_after_timeout,
- **kwargs,
- )
- return self._get_push_info(proc, progress, kill_after_timeout=kill_after_timeout)
- @property
- def config_reader(self) -> SectionConstraint[GitConfigParser]:
- """
- :return:
- :class:`~git.config.GitConfigParser` compatible object able to read options
- for only our remote. Hence you may simply type ``config.get("pushurl")`` to
- obtain the information.
- """
- return self._config_reader
- def _clear_cache(self) -> None:
- try:
- del self._config_reader
- except AttributeError:
- pass
- # END handle exception
- @property
- def config_writer(self) -> SectionConstraint:
- """
- :return:
- :class:`~git.config.GitConfigParser`-compatible object able to write options
- for this remote.
- :note:
- You can only own one writer at a time - delete it to release the
- configuration file and make it usable by others.
- To assure consistent results, you should only query options through the
- writer. Once you are done writing, you are free to use the config reader
- once again.
- """
- writer = self.repo.config_writer()
- # Clear our cache to ensure we re-read the possibly changed configuration.
- self._clear_cache()
- return SectionConstraint(writer, self._config_section_name())
|