stash.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # stash.py
  2. # Copyright (C) 2018 Jelmer Vernooij <jelmer@samba.org>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Stash handling."""
  22. import os
  23. from typing import TYPE_CHECKING, Optional, TypedDict
  24. from .file import GitFile
  25. from .index import commit_tree, iter_fresh_objects
  26. from .objects import ObjectID
  27. from .reflog import drop_reflog_entry, read_reflog
  28. from .refs import Ref
  29. if TYPE_CHECKING:
  30. from .reflog import Entry
  31. from .repo import Repo
  32. class CommitKwargs(TypedDict, total=False):
  33. """Keyword arguments for do_commit."""
  34. committer: bytes
  35. author: bytes
  36. DEFAULT_STASH_REF = b"refs/stash"
  37. class Stash:
  38. """A Git stash.
  39. Note that this doesn't currently update the working tree.
  40. """
  41. def __init__(self, repo: "Repo", ref: Ref = DEFAULT_STASH_REF) -> None:
  42. self._ref = ref
  43. self._repo = repo
  44. @property
  45. def _reflog_path(self) -> str:
  46. return os.path.join(self._repo.commondir(), "logs", os.fsdecode(self._ref))
  47. def stashes(self) -> list["Entry"]:
  48. try:
  49. with GitFile(self._reflog_path, "rb") as f:
  50. return list(reversed(list(read_reflog(f))))
  51. except FileNotFoundError:
  52. return []
  53. @classmethod
  54. def from_repo(cls, repo: "Repo") -> "Stash":
  55. """Create a new stash from a Repo object."""
  56. return cls(repo)
  57. def drop(self, index: int) -> None:
  58. """Drop entry with specified index."""
  59. with open(self._reflog_path, "rb+") as f:
  60. drop_reflog_entry(f, index, rewrite=True)
  61. if len(self) == 0:
  62. os.remove(self._reflog_path)
  63. del self._repo.refs[self._ref]
  64. return
  65. if index == 0:
  66. self._repo.refs[self._ref] = self[0].new_sha
  67. def pop(self, index: int) -> "Entry":
  68. raise NotImplementedError(self.pop)
  69. def push(
  70. self,
  71. committer: Optional[bytes] = None,
  72. author: Optional[bytes] = None,
  73. message: Optional[bytes] = None,
  74. ) -> ObjectID:
  75. """Create a new stash.
  76. Args:
  77. committer: Optional committer name to use
  78. author: Optional author name to use
  79. message: Optional commit message
  80. """
  81. # First, create the index commit.
  82. commit_kwargs = CommitKwargs()
  83. if committer is not None:
  84. commit_kwargs["committer"] = committer
  85. if author is not None:
  86. commit_kwargs["author"] = author
  87. index = self._repo.open_index()
  88. index_tree_id = index.commit(self._repo.object_store)
  89. index_commit_id = self._repo.do_commit(
  90. tree=index_tree_id,
  91. message=b"Index stash",
  92. merge_heads=[self._repo.head()],
  93. no_verify=True,
  94. **commit_kwargs,
  95. )
  96. # Then, the working tree one.
  97. # Filter out entries with None values since commit_tree expects non-None values
  98. fresh_objects = [
  99. (path, sha, mode)
  100. for path, sha, mode in iter_fresh_objects(
  101. index,
  102. os.fsencode(self._repo.path),
  103. object_store=self._repo.object_store,
  104. )
  105. if sha is not None and mode is not None
  106. ]
  107. stash_tree_id = commit_tree(
  108. self._repo.object_store,
  109. fresh_objects,
  110. )
  111. if message is None:
  112. message = b"A stash on " + self._repo.head()
  113. # TODO(jelmer): Just pass parents into do_commit()?
  114. self._repo.refs[self._ref] = self._repo.head()
  115. cid = self._repo.do_commit(
  116. ref=self._ref,
  117. tree=stash_tree_id,
  118. message=message,
  119. merge_heads=[index_commit_id],
  120. no_verify=True,
  121. **commit_kwargs,
  122. )
  123. return cid
  124. def __getitem__(self, index: int) -> "Entry":
  125. return list(self.stashes())[index]
  126. def __len__(self) -> int:
  127. return len(list(self.stashes()))