reference.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from git.util import (
  2. LazyMixin,
  3. IterableObj,
  4. )
  5. from .symbolic import SymbolicReference, T_References
  6. # typing ------------------------------------------------------------------
  7. from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING # NOQA
  8. from git.types import Commit_ish, PathLike, _T # NOQA
  9. if TYPE_CHECKING:
  10. from git.repo import Repo
  11. # ------------------------------------------------------------------------------
  12. __all__ = ["Reference"]
  13. # { Utilities
  14. def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
  15. """A decorator raising a TypeError if we are not a valid remote, based on the path"""
  16. def wrapper(self: T_References, *args: Any) -> _T:
  17. if not self.is_remote():
  18. raise ValueError("ref path does not point to a remote reference: %s" % self.path)
  19. return func(self, *args)
  20. # END wrapper
  21. wrapper.__name__ = func.__name__
  22. return wrapper
  23. # }END utilities
  24. class Reference(SymbolicReference, LazyMixin, IterableObj):
  25. """Represents a named reference to any object. Subclasses may apply restrictions though,
  26. i.e. Heads can only point to commits."""
  27. __slots__ = ()
  28. _points_to_commits_only = False
  29. _resolve_ref_on_create = True
  30. _common_path_default = "refs"
  31. def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> None:
  32. """Initialize this instance
  33. :param repo: Our parent repository
  34. :param path:
  35. Path relative to the .git/ directory pointing to the ref in question, i.e.
  36. refs/heads/master
  37. :param check_path: if False, you can provide any path. Otherwise the path must start with the
  38. default path prefix of this type."""
  39. if check_path and not str(path).startswith(self._common_path_default + "/"):
  40. raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
  41. self.path: str # SymbolicReference converts to string atm
  42. super(Reference, self).__init__(repo, path)
  43. def __str__(self) -> str:
  44. return self.name
  45. # { Interface
  46. # @ReservedAssignment
  47. def set_object(
  48. self,
  49. object: Union[Commit_ish, "SymbolicReference", str],
  50. logmsg: Union[str, None] = None,
  51. ) -> "Reference":
  52. """Special version which checks if the head-log needs an update as well
  53. :return: self"""
  54. oldbinsha = None
  55. if logmsg is not None:
  56. head = self.repo.head
  57. if not head.is_detached and head.ref == self:
  58. oldbinsha = self.commit.binsha
  59. # END handle commit retrieval
  60. # END handle message is set
  61. super(Reference, self).set_object(object, logmsg)
  62. if oldbinsha is not None:
  63. # /* from refs.c in git-source
  64. # * Special hack: If a branch is updated directly and HEAD
  65. # * points to it (may happen on the remote side of a push
  66. # * for example) then logically the HEAD reflog should be
  67. # * updated too.
  68. # * A generic solution implies reverse symref information,
  69. # * but finding all symrefs pointing to the given branch
  70. # * would be rather costly for this rare event (the direct
  71. # * update of a branch) to be worth it. So let's cheat and
  72. # * check with HEAD only which should cover 99% of all usage
  73. # * scenarios (even 100% of the default ones).
  74. # */
  75. self.repo.head.log_append(oldbinsha, logmsg)
  76. # END check if the head
  77. return self
  78. # NOTE: Don't have to overwrite properties as the will only work without a the log
  79. @property
  80. def name(self) -> str:
  81. """:return: (shortest) Name of this reference - it may contain path components"""
  82. # first two path tokens are can be removed as they are
  83. # refs/heads or refs/tags or refs/remotes
  84. tokens = self.path.split("/")
  85. if len(tokens) < 3:
  86. return self.path # could be refs/HEAD
  87. return "/".join(tokens[2:])
  88. @classmethod
  89. def iter_items(
  90. cls: Type[T_References],
  91. repo: "Repo",
  92. common_path: Union[PathLike, None] = None,
  93. *args: Any,
  94. **kwargs: Any,
  95. ) -> Iterator[T_References]:
  96. """Equivalent to SymbolicReference.iter_items, but will return non-detached
  97. references as well."""
  98. return cls._iter_items(repo, common_path)
  99. # }END interface
  100. # { Remote Interface
  101. @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21)
  102. @require_remote_ref_path
  103. def remote_name(self) -> str:
  104. """
  105. :return:
  106. Name of the remote we are a reference of, such as 'origin' for a reference
  107. named 'origin/master'"""
  108. tokens = self.path.split("/")
  109. # /refs/remotes/<remote name>/<branch_name>
  110. return tokens[2]
  111. @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21)
  112. @require_remote_ref_path
  113. def remote_head(self) -> str:
  114. """:return: Name of the remote head itself, i.e. master.
  115. :note: The returned name is usually not qualified enough to uniquely identify
  116. a branch"""
  117. tokens = self.path.split("/")
  118. return "/".join(tokens[3:])
  119. # } END remote interface