|
11 | 11 |
|
12 | 12 | from git.types import PathLike, Commit_ish
|
13 | 13 |
|
| 14 | +if TYPE_CHECKING: |
| 15 | + from git.repo import Repo |
| 16 | + from git.objects import Commitfrom git.config import GitConfigParser, SectionConstraint |
| 17 | +from git.util import join_path |
| 18 | +from git.exc import GitCommandError |
| 19 | + |
| 20 | +from .symbolic import SymbolicReference |
| 21 | +from .reference import Reference |
| 22 | + |
| 23 | +# typinng --------------------------------------------------- |
| 24 | + |
| 25 | +from typing import Any, Sequence, Union, TYPE_CHECKING |
| 26 | + |
| 27 | +from git.types import PathLike, Commit_ish |
| 28 | + |
14 | 29 | if TYPE_CHECKING:
|
15 | 30 | from git.repo import Repo
|
16 | 31 | from git.objects import Commit
|
@@ -106,6 +121,254 @@ def reset(self, commit: Union[Commit_ish, SymbolicReference, str] = 'HEAD',
|
106 | 121 | return self
|
107 | 122 |
|
108 | 123 |
|
| 124 | +class Head(Reference): |
| 125 | + |
| 126 | + """A Head is a named reference to a Commit. Every Head instance contains a name |
| 127 | + and a Commit object. |
| 128 | +
|
| 129 | + Examples:: |
| 130 | +
|
| 131 | + >>> repo = Repo("/path/to/repo") |
| 132 | + >>> head = repo.heads[0] |
| 133 | +
|
| 134 | + >>> head.name |
| 135 | + 'master' |
| 136 | +
|
| 137 | + >>> head.commit |
| 138 | + <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> |
| 139 | +
|
| 140 | + >>> head.commit.hexsha |
| 141 | + '1c09f116cbc2cb4100fb6935bb162daa4723f455'""" |
| 142 | + _common_path_default = "refs/heads" |
| 143 | + k_config_remote = "remote" |
| 144 | + k_config_remote_ref = "merge" # branch to merge from remote |
| 145 | + |
| 146 | + @classmethod |
| 147 | + def delete(cls, repo: 'Repo', *heads: 'Head', **kwargs: Any): |
| 148 | + """Delete the given heads |
| 149 | +
|
| 150 | + :param force: |
| 151 | + If True, the heads will be deleted even if they are not yet merged into |
| 152 | + the main development stream. |
| 153 | + Default False""" |
| 154 | + force = kwargs.get("force", False) |
| 155 | + flag = "-d" |
| 156 | + if force: |
| 157 | + flag = "-D" |
| 158 | + repo.git.branch(flag, *heads) |
| 159 | + |
| 160 | + def set_tracking_branch(self, remote_reference: Union['RemoteReference', None]) -> 'Head': |
| 161 | + """ |
| 162 | + Configure this branch to track the given remote reference. This will alter |
| 163 | + this branch's configuration accordingly. |
| 164 | +
|
| 165 | + :param remote_reference: The remote reference to track or None to untrack |
| 166 | + any references |
| 167 | + :return: self""" |
| 168 | + from .remote import RemoteReference |
| 169 | + if remote_reference is not None and not isinstance(remote_reference, RemoteReference): |
| 170 | + raise ValueError("Incorrect parameter type: %r" % remote_reference) |
| 171 | + # END handle type |
| 172 | + |
| 173 | + with self.config_writer() as writer: |
| 174 | + if remote_reference is None: |
| 175 | + writer.remove_option(self.k_config_remote) |
| 176 | + writer.remove_option(self.k_config_remote_ref) |
| 177 | + if len(writer.options()) == 0: |
| 178 | + writer.remove_section() |
| 179 | + else: |
| 180 | + writer.set_value(self.k_config_remote, remote_reference.remote_name) |
| 181 | + writer.set_value(self.k_config_remote_ref, Head.to_full_path(remote_reference.remote_head)) |
| 182 | + |
| 183 | + return self |
| 184 | + |
| 185 | + def tracking_branch(self) -> Union['RemoteReference', None]: |
| 186 | + """ |
| 187 | + :return: The remote_reference we are tracking, or None if we are |
| 188 | + not a tracking branch""" |
| 189 | + from .remote import RemoteReference |
| 190 | + reader = self.config_reader() |
| 191 | + if reader.has_option(self.k_config_remote) and reader.has_option(self.k_config_remote_ref): |
| 192 | + ref = Head(self.repo, Head.to_full_path(strip_quotes(reader.get_value(self.k_config_remote_ref)))) |
| 193 | + remote_refpath = RemoteReference.to_full_path(join_path(reader.get_value(self.k_config_remote), ref.name)) |
| 194 | + return RemoteReference(self.repo, remote_refpath) |
| 195 | + # END handle have tracking branch |
| 196 | + |
| 197 | + # we are not a tracking branch |
| 198 | + return None |
| 199 | + |
| 200 | + def rename(self, new_path: PathLike, force: bool = False) -> 'Head': |
| 201 | + """Rename self to a new path |
| 202 | +
|
| 203 | + :param new_path: |
| 204 | + Either a simple name or a path, i.e. new_name or features/new_name. |
| 205 | + The prefix refs/heads is implied |
| 206 | +
|
| 207 | + :param force: |
| 208 | + If True, the rename will succeed even if a head with the target name |
| 209 | + already exists. |
| 210 | +
|
| 211 | + :return: self |
| 212 | + :note: respects the ref log as git commands are used""" |
| 213 | + flag = "-m" |
| 214 | + if force: |
| 215 | + flag = "-M" |
| 216 | + |
| 217 | + self.repo.git.branch(flag, self, new_path) |
| 218 | + self.path = "%s/%s" % (self._common_path_default, new_path) |
| 219 | + return self |
| 220 | + |
| 221 | + def checkout(self, force: bool = False, **kwargs: Any) -> Union['HEAD', 'Head']: |
| 222 | + """Checkout this head by setting the HEAD to this reference, by updating the index |
| 223 | + to reflect the tree we point to and by updating the working tree to reflect |
| 224 | + the latest index. |
| 225 | +
|
| 226 | + The command will fail if changed working tree files would be overwritten. |
| 227 | +
|
| 228 | + :param force: |
| 229 | + If True, changes to the index and the working tree will be discarded. |
| 230 | + If False, GitCommandError will be raised in that situation. |
| 231 | +
|
| 232 | + :param kwargs: |
| 233 | + Additional keyword arguments to be passed to git checkout, i.e. |
| 234 | + b='new_branch' to create a new branch at the given spot. |
| 235 | +
|
| 236 | + :return: |
| 237 | + The active branch after the checkout operation, usually self unless |
| 238 | + a new branch has been created. |
| 239 | + If there is no active branch, as the HEAD is now detached, the HEAD |
| 240 | + reference will be returned instead. |
| 241 | +
|
| 242 | + :note: |
| 243 | + By default it is only allowed to checkout heads - everything else |
| 244 | + will leave the HEAD detached which is allowed and possible, but remains |
| 245 | + a special state that some tools might not be able to handle.""" |
| 246 | + kwargs['f'] = force |
| 247 | + if kwargs['f'] is False: |
| 248 | + kwargs.pop('f') |
| 249 | + |
| 250 | + self.repo.git.checkout(self, **kwargs) |
| 251 | + if self.repo.head.is_detached: |
| 252 | + return self.repo.head |
| 253 | + else: |
| 254 | + return self.repo.active_branch |
| 255 | + |
| 256 | + #{ Configuration |
| 257 | + def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]: |
| 258 | + if read_only: |
| 259 | + parser = self.repo.config_reader() |
| 260 | + else: |
| 261 | + parser = self.repo.config_writer() |
| 262 | + # END handle parser instance |
| 263 | + |
| 264 | + return SectionConstraint(parser, 'branch "%s"' % self.name) |
| 265 | + |
| 266 | + def config_reader(self) -> SectionConstraint[GitConfigParser]: |
| 267 | + """ |
| 268 | + :return: A configuration parser instance constrained to only read |
| 269 | + this instance's values""" |
| 270 | + return self._config_parser(read_only=True) |
| 271 | + |
| 272 | + def config_writer(self) -> SectionConstraint[GitConfigParser]: |
| 273 | + """ |
| 274 | + :return: A configuration writer instance with read-and write access |
| 275 | + to options of this head""" |
| 276 | + return self._config_parser(read_only=False) |
| 277 | + |
| 278 | + #} END configuration |
| 279 | + |
| 280 | + from git.refs import RemoteReference |
| 281 | + |
| 282 | +# ------------------------------------------------------------------- |
| 283 | + |
| 284 | +__all__ = ["HEAD", "Head"] |
| 285 | + |
| 286 | + |
| 287 | +def strip_quotes(string): |
| 288 | + if string.startswith('"') and string.endswith('"'): |
| 289 | + return string[1:-1] |
| 290 | + return string |
| 291 | + |
| 292 | + |
| 293 | +class HEAD(SymbolicReference): |
| 294 | + |
| 295 | + """Special case of a Symbolic Reference as it represents the repository's |
| 296 | + HEAD reference.""" |
| 297 | + _HEAD_NAME = 'HEAD' |
| 298 | + _ORIG_HEAD_NAME = 'ORIG_HEAD' |
| 299 | + __slots__ = () |
| 300 | + |
| 301 | + def __init__(self, repo: 'Repo', path: PathLike = _HEAD_NAME): |
| 302 | + if path != self._HEAD_NAME: |
| 303 | + raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) |
| 304 | + super(HEAD, self).__init__(repo, path) |
| 305 | + self.commit: 'Commit' |
| 306 | + |
| 307 | + def orig_head(self) -> SymbolicReference: |
| 308 | + """ |
| 309 | + :return: SymbolicReference pointing at the ORIG_HEAD, which is maintained |
| 310 | + to contain the previous value of HEAD""" |
| 311 | + return SymbolicReference(self.repo, self._ORIG_HEAD_NAME) |
| 312 | + |
| 313 | + def reset(self, commit: Union[Commit_ish, SymbolicReference, str] = 'HEAD', |
| 314 | + index: bool = True, working_tree: bool = False, |
| 315 | + paths: Union[PathLike, Sequence[PathLike], None] = None, **kwargs: Any) -> 'HEAD': |
| 316 | + """Reset our HEAD to the given commit optionally synchronizing |
| 317 | + the index and working tree. The reference we refer to will be set to |
| 318 | + commit as well. |
| 319 | +
|
| 320 | + :param commit: |
| 321 | + Commit object, Reference Object or string identifying a revision we |
| 322 | + should reset HEAD to. |
| 323 | +
|
| 324 | + :param index: |
| 325 | + If True, the index will be set to match the given commit. Otherwise |
| 326 | + it will not be touched. |
| 327 | +
|
| 328 | + :param working_tree: |
| 329 | + If True, the working tree will be forcefully adjusted to match the given |
| 330 | + commit, possibly overwriting uncommitted changes without warning. |
| 331 | + If working_tree is True, index must be true as well |
| 332 | +
|
| 333 | + :param paths: |
| 334 | + Single path or list of paths relative to the git root directory |
| 335 | + that are to be reset. This allows to partially reset individual files. |
| 336 | +
|
| 337 | + :param kwargs: |
| 338 | + Additional arguments passed to git-reset. |
| 339 | +
|
| 340 | + :return: self""" |
| 341 | + mode: Union[str, None] |
| 342 | + mode = "--soft" |
| 343 | + if index: |
| 344 | + mode = "--mixed" |
| 345 | + |
| 346 | + # it appears, some git-versions declare mixed and paths deprecated |
| 347 | + # see http://github.com/Byron/GitPython/issues#issue/2 |
| 348 | + if paths: |
| 349 | + mode = None |
| 350 | + # END special case |
| 351 | + # END handle index |
| 352 | + |
| 353 | + if working_tree: |
| 354 | + mode = "--hard" |
| 355 | + if not index: |
| 356 | + raise ValueError("Cannot reset the working tree if the index is not reset as well") |
| 357 | + |
| 358 | + # END working tree handling |
| 359 | + |
| 360 | + try: |
| 361 | + self.repo.git.reset(mode, commit, '--', paths, **kwargs) |
| 362 | + except GitCommandError as e: |
| 363 | + # git nowadays may use 1 as status to indicate there are still unstaged |
| 364 | + # modifications after the reset |
| 365 | + if e.status != 1: |
| 366 | + raise |
| 367 | + # END handle exception |
| 368 | + |
| 369 | + return self |
| 370 | + |
| 371 | + |
109 | 372 | class Head(Reference):
|
110 | 373 |
|
111 | 374 | """A Head is a named reference to a Commit. Every Head instance contains a name
|
|
0 commit comments