libgit2 disagrees with git-for-windows on symlink target with a trailing slash
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 68/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- c
- Área
- devtools, operating-systems
Línea de trabajo
Start with git_win32_path_readlink_w and follow its call into git_win32_path_remove_namespace and git_win32_path_trim_end in src/util/win32/path_w32.c; compare this with git_fs_path_mkposix in p_readlink. Reproduce the Windows symlink case described in the issue, then verify that index blobs, status, and diff preserve targetdir/ and agree with Git for Windows.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
On Windows, when a directory symlink’s target ends with a separator (targetdir\), Git for Windows stores targetdir/, while libgit2 stores targetdir — the trailing separator is dropped.
A repo written by libgit2/pygit2 then looks dirty under git diff / git status, and the reverse is also dirty.
Versions
- OS: Windows 10, with NTFS symlink permission enabled (by enabling Windows Developer Mode)
- libgit2 1.9.7 (via pygit2 1.20.1)
- Git for Windows 2.55.0.windows.5
core.symlinks=true
Reproduction (pygit2)
Requires Windows Developer Mode (or equivalent) so real symlinks can be created.
> python.exe
Python 3.14.7 (tags/v3.14.7:823f032, Aug 5 2026, 10:51:32) [MSC v.1944 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import pygit2
>>> pygit2.__version__
'1.20.1'
>>> pygit2.LIBGIT2_VERSION
'1.9.7'
>>> import os
>>> import subprocess
>>> import tempfile
>>> from pathlib import Path
>>>
>>> def setup(repo_dir):
... Path(repo_dir, "targetdir").mkdir()
... Path(repo_dir, "targetdir", "x.txt").write_bytes(b"x")
... Path(repo_dir, "dirlink").symlink_to("targetdir" + os.sep, target_is_directory=True)
...
>>> td = tempfile.TemporaryDirectory()
>>> td.name
'C:\\Users\\X\\AppData\\Local\\Temp\\tmp4m27u5uk'
>>>
>>>
>>> libgit2_dir = Path(td.name) / "libgit2"
>>> libgit2_dir.mkdir()
>>> libgit2_repo = pygit2.init_repository(libgit2_dir)
>>> setup(libgit2_dir)
>>> os.readlink(libgit2_dir / "dirlink")
'targetdir\\'
>>> libgit2_repo.index.add_all()
>>> libgit2_repo.index.write()
>>> libgit2_repo[libgit2_repo.index["dirlink"].id].data
b'targetdir'
>>> list(libgit2_repo.diff())
[]
>>> subprocess.check_call(["git", "diff"], cwd=libgit2_dir)
diff --git a/dirlink b/dirlink
index 9cf79c6..4a6c2d5 120000
--- a/dirlink
+++ b/dirlink
@@ -1 +1 @@
-targetdir
\ No newline at end of file
+targetdir/
\ No newline at end of file
0
>>>
>>>
>>> git_dir = Path(td.name) / "git"
>>> git_dir.mkdir()
>>> subprocess.check_call(["git", "init"], cwd=git_dir)
Initialized empty Git repository in C:/Users/X/AppData/Local/Temp/tmp4m27u5uk/git/.git/
0
>>> setup(git_dir)
>>> os.readlink(git_dir / "dirlink")
'targetdir\\'
>>> subprocess.check_call(["git", "add", "--all"], cwd=git_dir)
0
>>> subprocess.check_output(["git", "cat-file", "-p", ":dirlink"], cwd=git_dir)
b'targetdir/'
>>> subprocess.check_call(["git", "diff"], cwd=git_dir)
0
>>> git_repo = pygit2.Repository(git_dir)
>>> git_repo[git_repo.index["dirlink"].id].data
b'targetdir/'
>>> for patch in git_repo.diff():
... print(patch.text)
...
diff --git a/dirlink b/dirlink
index 4a6c2d5..9cf79c6 120000
--- a/dirlink
+++ b/dirlink
@@ -1 +1 @@
-targetdir/
\ No newline at end of file
+targetdir
\ No newline at end of file
>>>
Expected
Match Git for Windows: normalize \ to /, keep a trailing separator. Blob / status / diff should all see targetdir/.
Actual
libgit2 drops the trailing separator. git_index_add_* writes a different blob than git add. git_status / git_diff then disagree with git status / git diff on the same worktree.
Likely cause (This section is generated by AI and not verified by me)
Windows git_win32_path_readlink_w reads SubstituteName, then always runs it through git_win32_path_remove_namespace, which ends with git_win32_path_trim_end (src/util/win32/path_w32.c). That trim is correct for opening \\?\ paths (Win32 APIs that fail on a trailing \). It should not rewrite symlink target bytes that Git stores as a blob.
Git for Windows compat/mingw.c normalize_ntpath() strips NT prefixes and converts \ → /, and does not trim a trailing separator.
git_fs_path_mkposix in p_readlink only does the slash conversion; the missing / is from git_win32_path_trim_end.
When is the trailing slash useful?
On Windows, when extracting a tarball, the built-in tar (bsdtar 3.8.4) treats a trailing slash on the symlink target as the directory-link hint (even without the LIBARCHIVE.symlinktype= pax extension): extracting dirlink -> targetdir/ creates a <SYMLINKD> even if targetdir is missing, while dirlink -> targetdir creates a file <SYMLINK> even if targetdir already exists as a directory. A file symlink to a directory does not work on Windows. So a trailing slash helps tar on Windows create the right kind of symlink.
- Lenguaje dominante
- C
- Estrellas
- 10.6k
- Forks
- 2.7k
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de libgit2/libgit2
-
hashsig: the similarity heap evicts but never inserts, so the retained set is not the extremes Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
Todos los issues de libgit2/libgit2
Issues similares
-
internal.h中,漏掉了1个定义。 Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 95/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
Broadcast Documentation Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
kovidgoyal/kitty#10516 ·
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 80/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
zephyrproject-rtos/zephyr#120011 ·