Skip to content

Commit 4905b4a

Browse files
authored
Merge pull request #2231 from gitpython-developers/detached-head
Clarify detached HEAD access and isolate test branch state
2 parents b62e91b + 6bcb25b commit 4905b4a

4 files changed

Lines changed: 52 additions & 10 deletions

File tree

doc/source/tutorial.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ Query relevant repository paths ...
7878

7979
:class:`Heads <git.refs.head.Head>` Heads are branches in git-speak. :class:`References <git.refs.reference.Reference>` are pointers to a specific commit or to other references. Heads and :class:`Tags <git.refs.tag.TagReference>` are a kind of references. GitPython allows you to query them rather intuitively.
8080

81+
To obtain the current commit ID, use ``repo.head.commit.hexsha``. This works both
82+
on a branch and with a detached HEAD, provided HEAD resolves to an existing commit.
83+
When ``repo.head.is_detached`` is true, HEAD points directly to a commit and there
84+
is no active branch: reading ``repo.head.reference`` or ``repo.active_branch``
85+
raises :exc:`TypeError`. The branch examples below assume an attached HEAD.
86+
8187
.. literalinclude:: ../../test/test_docs.py
8288
:language: python
8389
:dedent: 8
@@ -152,7 +158,7 @@ Examining References
152158
:start-after: # [2-test_references_and_objects]
153159
:end-before: # ![2-test_references_and_objects]
154160

155-
A :class:`symbolic reference <git.refs.symbolic.SymbolicReference>` is a special case of a reference as it points to another reference instead of a commit.
161+
A :class:`symbolic reference <git.refs.symbolic.SymbolicReference>` can point to another reference. When detached, it points directly to a commit instead. Reading its ``commit`` property resolves the commit in either state. Assigning a commit to ``reference`` detaches it; reading ``reference`` then raises :exc:`TypeError`.
156162

157163
.. literalinclude:: ../../test/test_docs.py
158164
:language: python

git/refs/symbolic.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
5959

6060

6161
class SymbolicReference:
62-
"""Special case of a reference that is symbolic.
62+
"""A reference that can point to another reference or be detached.
6363
64-
This does not point to a specific commit, but to another
65-
:class:`~git.refs.head.Head`, which itself specifies a commit.
64+
An attached :class:`~git.refs.head.HEAD` usually points to a
65+
:class:`~git.refs.head.Head`, which itself specifies a commit. A detached
66+
:class:`~git.refs.head.HEAD` points directly to a commit instead.
6667
67-
A typical example for a symbolic reference is :class:`~git.refs.head.HEAD`.
68+
Use :attr:`commit` to access the commit in either case, and :attr:`reference`
69+
to access the target reference when attached.
6870
"""
6971

7072
__slots__ = ("repo", "path")
@@ -416,7 +418,15 @@ def set_object(
416418

417419
@property
418420
def commit(self) -> "Commit":
419-
"""Query or set commits directly"""
421+
"""The commit this reference resolves to, whether detached or symbolic.
422+
423+
For example, ``repo.head.commit.hexsha`` returns the current commit ID
424+
both on a branch and with a detached HEAD. HEAD must resolve to an
425+
existing commit; an unborn branch in an empty repository has none.
426+
427+
Assigning updates the commit without changing whether this reference
428+
is detached.
429+
"""
420430
return self._get_commit()
421431

422432
@commit.setter
@@ -443,7 +453,10 @@ def _get_reference(self) -> "Reference":
443453
"""
444454
sha, target_ref_path = self._get_ref_info(self.repo, self.path)
445455
if target_ref_path is None:
446-
raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha))
456+
raise TypeError(
457+
"%s is a detached symbolic reference as it points to %r. "
458+
"Use .commit or .object to access the target directly." % (self, sha)
459+
)
447460
return cast("Reference", self.from_path(self.repo, target_ref_path))
448461

449462
def set_reference(
@@ -531,6 +544,18 @@ def set_reference(
531544
# Aliased reference
532545
@property
533546
def reference(self) -> "Reference":
547+
"""The reference we point to, available only when not detached.
548+
549+
Check :attr:`is_detached` before reading this property if a target
550+
reference is required. To access the target commit or object in either
551+
state, use :attr:`commit` or :attr:`object` instead.
552+
553+
Assigning a reference keeps this reference symbolic. Assigning a git
554+
object or revision string detaches it; reading this property then raises.
555+
556+
:raise TypeError:
557+
If this reference is detached when reading the property.
558+
"""
534559
return self._get_reference()
535560

536561
@reference.setter

git/repo/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,11 @@ def ignored(self, *paths: PathLike) -> List[str]:
11501150

11511151
@property
11521152
def active_branch(self) -> Head:
1153-
"""The name of the currently active branch.
1153+
"""The currently active branch.
1154+
1155+
Check ``repo.head.is_detached`` before accessing this property if HEAD
1156+
may be detached. To access the current commit in either state, use
1157+
``repo.head.commit`` instead.
11541158
11551159
:raise TypeError:
11561160
If HEAD is detached.

test/lib/helper.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def wrapper(self, *args, **kwargs):
139139

140140
def with_rw_repo(working_tree_ref, bare=False):
141141
"""Same as with_bare_repo, but clones the rorepo as non-bare repository, checking
142-
out the working tree at the given working_tree_ref.
142+
out the working tree at the given working_tree_ref with an attached HEAD,
143+
regardless of the source repository's HEAD state.
143144
144145
This repository type is more costly due to the working copy checkout.
145146
@@ -158,7 +159,12 @@ def repo_creator(self):
158159
repo_dir = tempfile.mktemp(prefix="%sbare_%s" % (prefix, func.__name__))
159160
rw_repo = self.rorepo.clone(repo_dir, shared=True, bare=bare, n=True)
160161

161-
rw_repo.head.commit = rw_repo.commit(working_tree_ref)
162+
if rw_repo.head.is_detached:
163+
rw_repo.head.reference = rw_repo.create_head(
164+
"master", working_tree_ref, force=True, logmsg="Create test branch"
165+
)
166+
else:
167+
rw_repo.head.commit = rw_repo.commit(working_tree_ref)
162168
if not bare:
163169
rw_repo.head.reference.checkout()
164170
# END handle checkout
@@ -294,6 +300,7 @@ def remote_repo_creator(self):
294300
rw_repo_dir = tempfile.mktemp(prefix="daemon_cloned_repo-%s-" % func.__name__)
295301

296302
rw_daemon_repo = self.rorepo.clone(rw_daemon_repo_dir, shared=True, bare=True)
303+
rw_daemon_repo.head.reference = rw_daemon_repo.create_head("master", force=True)
297304
# Recursive alternates info?
298305
rw_repo = rw_daemon_repo.clone(rw_repo_dir, shared=True, bare=False, n=True)
299306
try:

0 commit comments

Comments
 (0)