← 输入框|github.com|刷新
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ API changes
happy paths, Python glue failures and representative compatibility regressions.
Historical timing-only tests are removed; the maintained backend benchmark
checks result parity and CLI budgets.
* ``IndexFile.remove()`` matches the paths of index entries, blobs, and submodules
literally, so an entry named ``*.txt`` no longer removes every ``.txt`` file.
Path strings keep their pathspec semantics. ``Submodule.move()`` and
``Submodule.remove()`` remove the submodule's own index entry the same way.
* Commit/tag metadata uses structured Gix decoders when the Gix backend supports
the repository. The CLI backend retains raw decoding because Git has no faithful
formatted query for all commit headers or arbitrary tag objects. ``gpgsig``
Expand Down
18 changes: 14 additions & 4 deletions git/index/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,11 +1066,12 @@ def remove(

- :class:`~git.objects.blob.Blob` object

Only the path portion is used in this case.
Only the path portion is used in this case. It is matched literally.

- :class:`~git.index.typ.BaseIndexEntry` or compatible type

The only relevant information here is the path. The stage is ignored.
The only relevant information here is the path, which is matched
literally. The stage is ignored.

:param working_tree:
If ``True``, the entry will also be removed from the working tree,
Expand Down Expand Up @@ -1107,8 +1108,17 @@ def remove(
args.append("--cached")
args.append("--")

# Preprocess paths.
paths = list(map(os.fspath, self._items_to_rela_paths(items))) # type: ignore[arg-type]
# Preprocess paths. Entries, blobs and submodules name exact paths, which Git
# must not expand as patterns. Path strings keep their pathspec semantics.
if isinstance(items, (str, os.PathLike)):
items = [items]
item_list = list(items)
paths = [
":(literal)" + os.fspath(rela_path)
if isinstance(item, (BaseIndexEntry, Blob, Submodule))
else os.fspath(rela_path)
for item, rela_path in zip(item_list, self._items_to_rela_paths(item_list))
]
with self._materialized_index() as path:
removed_paths = self.repo.git._call_process_safe(
"rm", args, paths, env={"GIT_INDEX_FILE": path}, **kwargs
Expand Down
5 changes: 2 additions & 3 deletions git/objects/submodule/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
if configuration:
try:
entry = index.entry(self.path)
index.remove(self.path, write=False, force=True)
index.remove([entry], write=False, force=True)
nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
index.add([nentry], write=False)
except KeyError as e:
Expand Down Expand Up @@ -1572,8 +1572,7 @@ def remove(
# First the index-entry.
parent_index = self.repo.index
try:
parent_index.entry(self.path)
parent_index.remove(self.path, write=False, force=True)
parent_index.remove([parent_index.entry(self.path)], write=False, force=True)
except KeyError:
pass
# END delete entry
Expand Down
18 changes: 18 additions & 0 deletions test/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,24 @@ def test_checkout_sends_each_path_as_one_record(self):
# Neither "nested/first" nor "outside" was requested.
assert (root / "outside").read_bytes() == b"local"

@pytest.mark.skipif(os.name == "nt", reason="'*' and ':' are not valid Windows filenames")
def test_remove_matches_entry_paths_literally(self):
with fixture_repo("index.sh", "remove-literal", write=True) as repo:
root = Path(repo.working_tree_dir)
index = repo.index
entries = [index.entry("*.txt"), index.entry(":(glob)**")]
assert index.remove(entries, write=False) == ["*.txt", ":(glob)**"]
assert {entry.path for entry in index.iter_entries()} == {"a.txt", "b.txt", "src/app.py"}

entry = entries[0]
blob = Blob(repo, entry.binsha, entry.mode, entry.path)
assert repo.index.remove([blob], working_tree=True) == ["*.txt"]
assert not (root / "*.txt").exists()
assert (root / "a.txt").exists() and (root / "b.txt").exists()

# Path strings keep their pathspec semantics.
assert repo.index.remove(["*.txt"], write=False) == ["a.txt", "b.txt"]


class TestIndexUtils:
@pytest.mark.parametrize("file_path_type", [str, Path])
Expand Down
15 changes: 15 additions & 0 deletions test/test_submodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,21 @@ def test_submodule_cli_lifecycle(monkeypatch, object_format, ref_format):
assert not parent.submodules


@pytest.mark.skipif(os.name == "nt", reason="'*' is not a valid Windows filename")
@pytest.mark.parametrize("operation", ["remove", "move"])
def test_submodule_index_update_matches_its_path_literally(operation):
with fixture_repo("submodule-special.sh", "glob-path", write=True, subdir="parent") as repo:
submodule = repo.submodules[0]
assert submodule.path == "*"
if operation == "remove":
submodule.remove()
expected = {".gitmodules", "README"}
else:
submodule.move("moved", module=False)
expected = {".gitmodules", "README", "moved"}
assert {entry.path for entry in repo.index.iter_entries()} == expected


@pytest.mark.skipif(sys.platform != "win32", reason="Windows locks files against directory renames")
@pytest.mark.parametrize("use_short_path", [False, True])
def test_submodule_reconnect_with_open_metadata_file(use_short_path):
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/index.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ case "$scenario" in
rm repo/nested/$'first\noutside' 'repo/"quoted"'
printf local >repo/outside
;;
remove-literal)
mkdir repo/src
for name in a.txt b.txt '*.txt' ':(glob)**' src/app.py; do printf %s "$name" >"repo/$name"; done
git -C repo add -A
git -C repo commit -qm 'Add files'
;;
*) printf 'Unknown index fixture: %s\n' "$scenario" >&2; exit 1 ;;
esac
for hook in "$hooks/pre-commit" "$hooks/commit-msg"; do
Expand Down
10 changes: 10 additions & 0 deletions tests/fixtures/submodule-special.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ case "$1" in
git init -q -b master --separate-git-dir="$PWD/metadata" checkout
touch metadata/objects/held-open
;;
glob-path)
git init -q -b master parent
git -C parent commit --allow-empty -qm 'Initial commit'
printf content > parent/README
git -C parent config -f .gitmodules submodule.module.path '*'
git -C parent config -f .gitmodules submodule.module.url https://example.invalid/module.git
git -C parent add README .gitmodules
git -C parent update-index --add --cacheinfo "160000,$(git -C parent rev-parse HEAD),*"
git -C parent commit -qm 'Add submodule'
;;
esac
Loading