Skip to content

Commit 6bcb25b

Browse files
Byroncodex
andcommitted
Clarify detached HEAD access and attach writable test fixtures (#2230)
<!-- agent --> Reading head.reference or active_branch raises TypeError when HEAD points directly to a commit, but the public documentation did not clearly explain how to access that commit. Document head.commit.hexsha for attached and detached HEADs, explain the reference setter/getter asymmetry, and clarify that active_branch requires an attached HEAD. Preserve the exception type and existing message prefix while adding a hint to use .commit or .object. Writable test fixtures assumed cloning produced an attached HEAD, so their branch access could fail when the source checkout was detached. Have with_rw_repo create and attach master at the requested revision when its clone is detached, retaining the clone's branch and tracking configuration otherwise. Explicitly attach the temporary bare remote to its own master branch before cloning it for remote tests. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
1 parent b62e91b commit 6bcb25b

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)