Skip to content

Commit 387ad8c

Browse files
codexByron
authored andcommitted
docs(repo): demonstrate closing repositories before deletion (#2101)
Reading `repo.head.commit` can open persistent object readers or retain a native repository handle. Deleting the repository while its handles remain open is not a supported sequence on Windows. Git's read-only object files can independently prevent plain `shutil.rmtree()` from removing a clone. `Repo.close()` and its context-manager support already release the retained handle and terminate persistent Git readers synchronously. Document that contract in the API reference, tutorial and README, replacing the old advice to invoke destructors manually. Demonstrate closing the repository first, then using the existing strict `git.util.rmtree()` helper for read-only files. All instances and separately opened readers must be closed by their owners. Add a packed-repository fixture and a tutorial regression that reads HEAD and blob data, verifies reader processes have exited and caches are released, and removes the directory without suppressing deletion errors. The test passes with both CLI and Gix backends on macOS with Git 2.54.0. Sphinx's strict HTML build, Ruff and ShellCheck pass. Windows execution is left to CI; no automatic cleanup of a still-live repository is introduced.
1 parent a5163e8 commit 387ad8c

5 files changed

Lines changed: 69 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,12 @@ GitPython is not suited for long-running processes (like daemons) as it tends to
161161
leak system resources. It was written in a time where destructors (as implemented
162162
in the `__del__` method) still ran deterministically.
163163

164-
In case you still want to use it in such a context, you will want to search the
165-
codebase for `__del__` implementations and call these yourself when you see fit.
164+
Use `Repo` as a context manager (`with Repo(path) as repo:`), or call
165+
`repo.close()` explicitly when finished, to release its native repository handle
166+
and persistent Git object readers. Close every repository instance and any
167+
separately opened readers before removing its directory. On Windows,
168+
`git.util.rmtree(path)` also handles Git's read-only files, which plain
169+
`shutil.rmtree()` may be unable to remove even after handles are closed.
166170

167171
Another way assure proper cleanup of resources is to factor out GitPython into a
168172
separate process which can be dropped periodically.

‎doc/source/tutorial.rst‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ Archive the repository contents to a tar file.
6363
:start-after: # [6-test_init_repo_object]
6464
:end-before: # ![6-test_init_repo_object]
6565

66+
Closing a repository
67+
====================
68+
69+
Use a :class:`~git.repo.base.Repo` context manager, or call
70+
:meth:`~git.repo.base.Repo.close` explicitly when finished. Exiting the context
71+
releases its native handle and persistent Git object readers, including when an
72+
exception is raised.
73+
74+
.. literalinclude:: ../../test/test_docs.py
75+
:language: python
76+
:dedent: 8
77+
:start-after: # [close-repo-context]
78+
:end-before: # ![close-repo-context]
79+
80+
Close every repository instance and any separately opened readers before deleting
81+
the directory. Windows prevents deletion while files remain open; Git's read-only
82+
object files can also make plain ``shutil.rmtree()`` fail. The existing
83+
:func:`git.util.rmtree` helper handles those read-only files while still reporting
84+
deletion failures:
85+
86+
.. literalinclude:: ../../test/test_docs.py
87+
:language: python
88+
:dedent: 8
89+
:start-after: # [remove-repo-directory]
90+
:end-before: # ![remove-repo-directory]
91+
6692
Advanced Repo Usage
6793
===================
6894

‎git/repo/base.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,13 @@ def __del__(self) -> None:
535535
pass
536536

537537
def close(self) -> None:
538+
"""Release native repository state and persistent Git object readers.
539+
540+
Use ``with Repo(path) as repo:`` to close automatically, including on errors.
541+
Close every repository instance and any separately opened readers before
542+
removing its directory, especially on Windows where open handles prevent
543+
deletion. :func:`git.util.rmtree` also handles Git's read-only files.
544+
"""
538545
self._gix_repository = self._gix_state = None
539546
if self.git:
540547
self.git.clear_cache()

‎test/test_docs.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@
1212
from test.lib.fixtures import writable_fixture
1313

1414

15+
def test_repo_context_releases_resources_before_removal():
16+
with writable_fixture("repo-close.sh") as temporary:
17+
path = temporary.path() / "repo"
18+
# [close-repo-context]
19+
from git import Repo
20+
21+
with Repo(path) as repo:
22+
print(repo.head.commit.hexsha)
23+
# ![close-repo-context]
24+
assert repo.head.commit.tree["file"].data_stream.read() == b"contents\n"
25+
processes = [reader.proc for reader in (repo.git.cat_file_header, repo.git.cat_file_all) if reader]
26+
27+
assert repo._gix_repository is None
28+
assert repo.git.cat_file_header is repo.git.cat_file_all is None
29+
assert all(process.poll() is not None for process in processes)
30+
# [remove-repo-directory]
31+
from git.util import rmtree
32+
33+
rmtree(path)
34+
# ![remove-repo-directory]
35+
assert not path.exists()
36+
37+
1538
class Tutorials(TestBase):
1639
def tearDown(self):
1740
gc.collect()

‎tests/fixtures/repo-close.sh‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init --initial-branch=main repo
4+
printf 'contents\n' >repo/file
5+
git -C repo add file
6+
git -C repo commit -m initial
7+
git -C repo gc

0 commit comments

Comments
 (0)