Выяснилось, что Git странно себя ведёт, когда речь идёт о файлах, в именах которых содержится двоеточие.
Кроме как в изначальном префиксе диска (например, C:\
), Windows не допускает иных двоеточий в именах файлов или путях. У Unix такого ограничения нет. Для проверки был создан тестовый репозиторий, содержащий один файл foo:bar
, содержащий запись hello
. Клонирование репозитория стандартной версией Git for Windows не выдаёт никаких ошибок или предупреждений:
C:\src
> git clone https://github.com/latkin/filetest.git
Cloning into 'filetest'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 3
Unpacking objects: 100% (3/3), done.
Checking connectivity... done.
Вместо файла с именем foo:bar
, тем не менее, вы получите пустой файл foo
:
C:\src
> cd .\filetest\
C:\src\filetest
> dir -force
Directory: C:\src\filetest
Mode LastWriteTime Length Name
---- ------------- ------ ----
d--h-- 7/17/2016 5:53 PM .git
-a---- 7/17/2016 5:47 PM 0 foo
Это и так странно, но ещё интереснее то, что Git имеет другой взгляд на это:
C:\src\filetest
> git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo
nothing added to commit but untracked files present (use "git add" to track)
Git замечает неотслеживаемый файл foo
, но считает, что foo:bar
тоже присутствует и содержит исходную строку.
Всё становится ещё более странным, когда вы активируете опцию core.fscache
(которая включена по умолчанию в версиях 2.8.2 и более поздних) — после этого foo:bar
регистрируется как отсутствующий:
C:\src\filetest
> git config core.fscache true
C:\src\filetest
> git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: foo:bar
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo
no changes added to commit (use "git add" and/or "git commit -a")
Но почему всё так происходит?
Главной причиной этого является довольно неочевидная фича NTFS, называющаяся «альтернативные потоки данных». Про это также можно почитать здесь и здесь.
Вкратце, файлы в NTFS — не просто данные, а наборы из одного и более потоков данных. То, что мы считаем содержимым файла, на самом деле является содержимым главного, неименованного потока. Также данные можно хранить и в других, именованных потоках. К этим потокам можно прямо обращаться, прибавляя :streamname
к обычному пути файла, например, к потоку MyStream
в файле qwerty.txt
можно получить доступ по пути qwerty.txt:MyStream
.
Так, хотя foo:bar
и не является допустимым именем файла в Windows, файловые API Windows рады принять его для операций чтения/записи, т.к. это вполне нормальный путь к чему-то в файловой системе, в нашем случае — альтернативный поток bar
файла foo
.
Что делает Git
Теперь, когда мы знаем про альтернативные потоки данных, всё проясняется.
При клонировании Git перемещает контент по пути foo:bar
. Это полностью разрешенный путь, поэтому система не выдаёт ошибок. В результате мы получаем файл foo
без содержимого в основном потоке данных (поскольку его длина равна 0), но 6 байтов в альтернативном потоке bar
:
C:\src\filetest
> Get-Item .\foo -Stream * | ft Stream,Length
Stream Length
------ ------
:$DATA 0
bar 6
C:\src\filetest
> cat .\foo:bar
hello
Git использует различные алгоритмы проверки в зависимости от того, включена ли опция core.fscache
.
Если core.fscache
отключена, метаданные файла проверяются по одному, что приводит к вызову GetFileAttributesEx для каждого пути. Git даже не догадывается, что имеет дело с альтернативным потоком, потому что эти API ведут себя так же, как и в случае нормального пути. Существует ли foo:bar
? Да! Совпадает ли последнее время изменения с ожидаемым? Тоже да! А содержимое? Аналогично! Отлично, этот файл менять не нужно.
Если же core.fscache
включена, Git кэширует метаданные файла, затем читает их из кэша, а не вызывает файловые API напрямую. Это приводит к другой ситуации — при нумерации файлов в директории Windows отмечает лишь foo
, поскольку это единственный присутствующий файл. Поэтому кэш, когда у него запрашивают метаданные foo:bar
, считает, что такого файла нет.
Вывод
Всё это достаточно глупо и вообще не должно происходить. Git должен находить неправильное имя файла, выдавать ошибку и не пытаться записывать файл на диск. Именно так уже обрабатываются валидные в Unix, но невалидные в Windows имена файлов (например, файл с именем \Windows\System32\crypt32.dll
будет заблокирован). Такие файлы будут корректно обрабатываться вне зависимости от опции core.fscache
.
Источник: latkin.org