Skip to content

Commit eaaa546

Browse files
authored
Update head.py
1 parent 1a360c8 commit eaaa546

File tree

1 file changed

+263
-0
lines changed

1 file changed

+263
-0
lines changed

git/refs/head.py

+263
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111

1212
from git.types import PathLike, Commit_ish
1313

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+
1429
if TYPE_CHECKING:
1530
from git.repo import Repo
1631
from git.objects import Commit
@@ -106,6 +121,254 @@ def reset(self, commit: Union[Commit_ish, SymbolicReference, str] = 'HEAD',
106121
return self
107122

108123

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+
109372
class Head(Reference):
110373

111374
"""A Head is a named reference to a Commit. Every Head instance contains a name

0 commit comments

Comments
 (0)