Git для Windows ошибочно создаёт альтернативные потоки данных

Выяснилось, что 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