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

Выяснилось, что Git странно себя ведёт, когда речь идёт о файлах, в именах которых содержится двоеточие.

Кроме как в изначальном префиксе диска (например, C:\), Windows не допускает иных двоеточий в именах файлов или путях. У Unix такого ограничения нет. Для проверки был создан тестовый репозиторий, содержащий один файл foo:bar, содержащий запись hello. Клонирование репозитория стандартной версией Git for Windows не выдаёт никаких ошибок или предупреждений:

Вместо файла с именем foo:bar, тем не менее, вы получите пустой файл foo:

Это и так странно, но ещё интереснее то, что Git имеет другой взгляд на это:

Git замечает неотслеживаемый файл foo, но считает, что foo:bar тоже присутствует и содержит исходную строку.

Всё становится ещё более странным, когда вы активируете опцию core.fscache (которая включена по умолчанию в версиях 2.8.2 и более поздних) — после этого foo:bar регистрируется как отсутствующий:

Но почему всё так происходит?

Главной причиной этого является довольно неочевидная фича NTFS, называющаяся «альтернативные потоки данных». Про это также можно почитать здесь и здесь.

Вкратце, файлы в NTFS — не просто данные, а наборы из одного и более потоков данных. То, что мы считаем содержимым файла, на самом деле является содержимым главного, неименованного потока. Также данные можно хранить и в других, именованных потоках. К этим потокам можно прямо обращаться, прибавляя :streamname к обычному пути файла, например, к потоку MyStream в файле qwerty.txt можно получить доступ по пути qwerty.txt:MyStream.

Так, хотя foo:bar и не является допустимым именем файла в Windows, файловые API Windows рады принять его для операций чтения/записи, т.к. это вполне нормальный путь к чему-то в файловой системе, в нашем случае — альтернативный поток bar файла foo.

Что делает Git

Теперь, когда мы знаем про альтернативные потоки данных, всё проясняется.

При клонировании Git перемещает контент по пути foo:bar. Это полностью разрешенный путь, поэтому система не выдаёт ошибок. В результате мы получаем файл foo без содержимого в основном потоке данных (поскольку его длина равна 0), но 6 байтов в альтернативном потоке bar:

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