Использование Git. Параметр AutoCRLF

Немного рекламы

Эта статья сначала задумывалась как продолжение статьи Использование Git, но когда составил план того, о чем хотелось бы в ней написать, то оказалось, что тем слишком много, причем все они разные и слабо между собой перекликаются, поэтому решил разделить ее на более мелкие части, которые буду выкладывать по отдельности.

Настройка AutoCRLF

Для начала хотелось бы рассмотреть одну настройку для Git, устанавливаемую с помощью все того же git config. Называется она autocrlf и располагается в разделе core.

Как известно, в разных операционных системах приняты разные символы для перевода строк, в Windows - это два символа \r\n (или CR и LF, код 0D0A в 16-ричной системе счисления), в *nix - просто \n (LF, код 0A), а в Mac - \r (CR, код 0D). Настройка core.autocrlf предназначена для того, чтобы в главном репозитории все переводы строк текстовых файлах были одинаковы.

Работает эта настройка так. Пусть для репозитория настройка core.autocrlf установлена в значение true следующей командой

git config core.autocrlf true

Тогда при коммитах (или, если более строго, при чтении файлов из файловой системы) все переводы строк при хранении будут приведены к виду \n, как в *nix, а при записи в файлы будут преобразованы к виду \r\n, как в Windows.

Давайте проведем несколько экспериментов в этим параметром.

Так как по крайней мере в Windows значение core.autocrlf по умолчанию установлено в true, то для чистоты эксперимента установим значение этого параметра явно в конфиге пользователя:

git config --global core.autocrlf true

Эксперимент первый

Создадим два файла с одинаковым с виду содержимым:

first line
second line
third line

Первый файл с именем crlf.txt будет содержать этот текст с переводами строк \r\n:

А второй, с именем lf.txt будет содержать переводы строк \n:

Теперь создадим репозиторий, который будет считаться основным, для этого в папке с только что созданными файлами выполним команду:

git init

После этого добавим файлы к коммиту:

git add .

При этой операции мы получим следующее предупреждение:

warning: LF will be replaced by CRLF in lf.txt

Git предупреждает о том, что переводы строк в файле lf.txt будут заменены на \r\n. Нам отступать некуда, поэтому все-таки выполняем коммит:

git commit -m "commit comment"

Git еще раз выведет то же самое предупреждение, но коммит сделает:

[master (root-commit) 55f65b0] commit comment
warning: LF will be replaced by CRLF in lf.txt
 2 files changed, 6 insertions(+), 0 deletions(-)
 create mode 100644 crlf.txt
 create mode 100644 lf.txt

Если теперь мы посмотрим на наши исходные файлы, то они не изменились, что логично. А теперь сделаем клон этого репозитория.

git clone <путь до исходного репозитория>

Если теперь посмотрим на файлы клона в 16-ричном редакторе, то увидим, что оба файла crlf.txt и lf.txt имеют переводы строк \r\n. Получилось, что файлы lf.txt в исходном репозитории и в клоне стали разные, хотя git их считает идентичными. Это произошло из-за того, что в самом начале мы установили значение core.autocrlf в true в конфиге пользователя, поэтому такое значение и будет применяться для только что созданного репозитория по умолчанию, то есть это значение установлено в true для обоих репозиториев.

Давайте теперь изменим файл lf.txt в клоне, добавив еще одну строку, причем переводы строк оставим как \r\n:

first line
second line
third line
fourth line

Сделаем коммит и отправим изменения обратно в главный репозиторий:

git commit -a -m "change lf.txt"
git push

Напомню, что параметр -a у команды git commit указывает, что к коммиту нужно добавить все измененные файлы, которые уже есть в репозитории (но не новые).

Теперь в главном репозитории выполняем следующую команду, чтобы исходники рабочей копии соответствовали содержимому репозитория:

git reset --hard

Смотрим что стало с содержимым файла lf.txt в шестнадцатеричном редакторе и видим:

В нем переводы строк тоже поменялись на \r\n, теперь в файле lf.txt в обоих репозиториях находятся файлы с одинаковыми переводами строк.

Эксперимент второй

До сих пор у нас для обоих репозиториев значения параметра core.autocrlf были установлены в true, давайте теперь создадим новый репозиторий, но в котором значение этого параметра будет установлено в false.

Возьмем все тот же исходный файл lf.txt с переводом строк \n:

first line
second line
third line

Создадим репозиторий в папке с этими файлами:

git init

Установим значение параметра core.autocrlf в false:

git config core.autocrlf false

Глобальное значение этого параметра по-прежнему остается true, но для данного репозитория это уже не играет роли.

Теперь добавим файл к коммиту:

git add .

На этот раз предупреждения не будет, так как в репозиторий файлы будут добавлены с теми переводами строк, что и были созданы. Выполняем коммит:

git commit -m "Commit comment"

В результате ничего экстраординарного не произойдет, коммит будет произведен удачно:

[master (root-commit) 7355a5b] Comment commit
 1 files changed, 3 insertions(+), 0 deletions(-)
 create mode 100644 lf.txt

А теперь создадим клон этого репозитория:

git clone <путь до главного репозитория>

Дело в том, что для этого нового репозитория-клона файл конфига не будет содержать параметра core.autocrlf, поэтому по умолчанию будет использоваться то значение, которое мы установили в самом начале, то есть true. А раз этот параметр установлен в true, то при создании файлов переводы строк будут преобразованы в \r\n, что мы и увидим в файле lf.txt:

Эксперимент третий

Для начала немного предыстории. Git я использую под Windows (сборку msysgit) для переноса исходников из дома на работу. Но иногда получалась ситуация, когда приношу измененные исходники в главном репозитории, делаю git pull, чтобы обновить исходники в рабочей копии, команда без проблем выполняется, но затем, если даже ничего не трогать, то некоторые файлы помечаются как измененные.

Я долго не мог понять в чем дело, просмотр изменений по сравнению с предыдущей версией показывал, что якобы изменились все строки в файлах, причем сами на себя. Команда git reset --hard ничего не меняла, даже откат изменений с помощью git checkout -- <измененный файл> ничего не давал, файлы оставались помечены как измененные.

Сразу закралась мысль о переводах строк, но повторить ситуацию в лабораторных условиях, чтобы точно определить когда такое происходит, удалось только недавно, также случайно нашлось решение этой проблемы, но вопрос почему это происходит до сих пор открыт.

Итак, для того, чтобы воссоздать проблему (хочется назвать ее багом, но не уверен), нужно проделать следующее (я не буду писать команды Git, чтобы было более наглядно):

0. Глобальное значение параметра core.autocrlf должно быть true.

1. Создаем пустой репозиторий.

2. Устанавливаем для него параметр core.autocrlf в false.

3. Создаем файл с переводами строк \r\n.

4. Добавляем файл к коммиту.

5. Выполняем коммит.

6. Создаем клон этого репозитория

Все, клон создается, но файл в клоне Git считает изменившимся, причем побитово файл в клоне полностью совпадает с файлов в исходном репозитории.

Я приведу лог, по которому видно, что сбросить состояние этого файла в состояние без изменений не удается:

>git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   test.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

>git reset --hard
HEAD is now at ba37f2d 111

>git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   test.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

>git checkout -- test.txt

>git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   test.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

Таким образом, такая ситуация возникает с файлами, у которых окончания строк \r\n (для файлов с окончаниями строк \n такая последовательность действий проблем не воссоздает), если у клона и главного репозитория разные значения параметра core.autocrlf.

Решение проблемы пришло неожиданно во время экспериментов. Оказывается, что достаточно после возникновения такой ситуации установить значение параметра core.autocrlf в false. Даже если затем значение этого параметра снова установить в true, то файл будет считаться неизмененным. Все это показано на следующем логе работы:

>git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   test.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

>git config core.autocrlf false

>git status
# On branch master
nothing to commit (working directory clean)

>git config core.autocrlf true

>git status
# On branch master
nothing to commit (working directory clean)

Заключение

На мой взгляд применение параметра core.autocrlf - довольно спорное решение из-за того, что фактически файлы в разных репозиториях оказываются разные, хотя задумка интересная. Теоретически применение этого параметра должно облегчить написание кода пользователями в разных операционных системах, не задумываясь о том какие символы для перевода строк они используют.

Но по крайней мере в Windows проблемы с этой настройкой есть, кроме того возможны ситуации, когда Git примет бинарные файлы за текстовые и примется в них исправлять переводы строк. В этом случае мы получим порчу файлов. Если честно, немного странно видеть эту возможность в Git, когда Линус Торвальдс на одном из докладов сказал такую фразу, что когда он начал делать Git, то хотел бы, чтобы в ней файлы не портились и извлекались из репозитория в том виде, в каком они туда попали. Хотя эта возможность отключается.

Мы не рассмотрели еще одно значение этого параметра, а именно inline, когда Git преобразует переводы строк в \n только при чтении файлов, и при записи создает файлы с таким же окончанием строк. Но по аналогии с нашими предыдущими экспериментами можно самостоятельно разобраться как этот режим работает.

Также хотелось бы обратить ваше внимание на параметр core.safecrlf, который тесно связан с core.autocrlf, но который мы также не рассматривали.

На этом пока все, статья оказалась даже больше, чем я ожидал, но надеюсь, что она окажется полезной.

Немного рекламы

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 4.5/5. Всего 63 голос(а, ов)



y0prst 21.03.2012 - 16:32

Значение параметра

> Мы не рассмотрели еще одно значение этого параметра, а именно inline, когда Git преобразует переводы строк в n только при чтении файлов, и при записи создает файлы с таким же окончанием строк.

Во-первых, не inline, а input. Во-вторых, при чтении откуда? при записи куда?
Если core.autocrlf=input, то файлы чекаутятся as is, а при коммите происходит конвертация CRLF->LF для текстовых файлов.

Joshuan 05.09.2012 - 16:56

Ох как сложно вы написали о довольно простой вещи.

Evghenii 27.12.2013 - 19:18

Отлично

Отлично написанно, спасибо


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 787