Skip to content

Commit b281a4e

Browse files
authored
Fix checkout skipping files with paths starting with '.git' (#2098)
2 parents 3056440 + dec35ce commit b281a4e

File tree

3 files changed

+62
-9
lines changed

3 files changed

+62
-9
lines changed

NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
* Support ``GIT_TRACE_PACKET`` in ``dulwich.cli``.
3434
(Jelmer Vernooij)
3535

36+
* Fix ``porcelain.checkout`` incorrectly skipping files whose paths start
37+
with ``.git`` (such as ``.github/``, ``.gitignore``, ``.gitattributes``)
38+
during working tree and index updates, leaving staged changes after a
39+
clean checkout. (Jelmer Vernooij)
40+
3641
* Fix cloning of SHA-256 repositories by including ``object-format`` and
3742
``agent`` capabilities in Git protocol v2 ``ls-refs`` and ``fetch``
3843
commands. (Jelmer Vernooij)

dulwich/index.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2736,9 +2736,7 @@ def update_working_tree(
27362736
if change.type in (CHANGE_MODIFY, CHANGE_DELETE) and change.old:
27372737
path = change.old.path
27382738
assert path is not None
2739-
if path.startswith(b".git") or not validate_path(
2740-
path, validate_path_element
2741-
):
2739+
if not validate_path(path, validate_path_element):
27422740
continue
27432741

27442742
full_path = _tree_to_fs_path(repo_path, path, tree_encoding)
@@ -2779,9 +2777,7 @@ def update_working_tree(
27792777
# Remove file/directory
27802778
assert change.old is not None and change.old.path is not None
27812779
path = change.old.path
2782-
if path.startswith(b".git") or not validate_path(
2783-
path, validate_path_element
2784-
):
2780+
if not validate_path(path, validate_path_element):
27852781
continue
27862782

27872783
full_path = _tree_to_fs_path(repo_path, path, tree_encoding)
@@ -2810,9 +2806,7 @@ def update_working_tree(
28102806
and change.new.mode is not None
28112807
)
28122808
path = change.new.path
2813-
if path.startswith(b".git") or not validate_path(
2814-
path, validate_path_element
2815-
):
2809+
if not validate_path(path, validate_path_element):
28162810
continue
28172811

28182812
full_path = _tree_to_fs_path(repo_path, path, tree_encoding)

tests/porcelain/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4547,6 +4547,60 @@ def test_checkout_to_commit_sha(self) -> None:
45474547
porcelain.checkout(self.repo, self._sha)
45484548
self.assertEqual(self._sha, self.repo.head())
45494549

4550+
# Working tree and index should be clean after checkout
4551+
status = list(porcelain.status(self.repo))
4552+
self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
4553+
4554+
def test_checkout_files_starting_with_dotgit(self) -> None:
4555+
# Regression test: paths starting with ".git" (like .github/, .gitignore,
4556+
# .gitattributes) were incorrectly skipped during checkout, leaving the
4557+
# index out of sync with HEAD.
4558+
github_dir = os.path.join(self.repo.path, ".github", "workflows")
4559+
os.makedirs(github_dir)
4560+
4561+
github_file = os.path.join(github_dir, "ci.yml")
4562+
gitignore_file = os.path.join(self.repo.path, ".gitignore")
4563+
4564+
with open(github_file, "w") as f:
4565+
f.write("# version 1\n")
4566+
with open(gitignore_file, "w") as f:
4567+
f.write("*.pyc\n")
4568+
4569+
porcelain.add(self.repo, paths=[github_file, gitignore_file])
4570+
sha1 = porcelain.commit(
4571+
self.repo,
4572+
message=b"add .github and .gitignore",
4573+
committer=b"Jane <jane@example.com>",
4574+
author=b"John <john@example.com>",
4575+
)
4576+
4577+
# Update those files in a second commit
4578+
with open(github_file, "w") as f:
4579+
f.write("# version 2\n")
4580+
with open(gitignore_file, "w") as f:
4581+
f.write("*.pyc\n*.pyo\n")
4582+
4583+
porcelain.add(self.repo, paths=[github_file, gitignore_file])
4584+
porcelain.commit(
4585+
self.repo,
4586+
message=b"update .github and .gitignore",
4587+
committer=b"Jane <jane@example.com>",
4588+
author=b"John <john@example.com>",
4589+
)
4590+
4591+
# Checkout the first commit (going back to v1)
4592+
porcelain.checkout(self.repo, sha1)
4593+
4594+
# Working tree and index should be clean (no staged changes)
4595+
status = list(porcelain.status(self.repo))
4596+
self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
4597+
4598+
# Working tree files should have v1 content
4599+
with open(github_file) as f:
4600+
self.assertEqual("# version 1\n", f.read())
4601+
with open(gitignore_file) as f:
4602+
self.assertEqual("*.pyc\n", f.read())
4603+
45504604
def test_checkout_to_head(self) -> None:
45514605
new_sha = self._commit_something_wrong()
45524606

0 commit comments

Comments
 (0)