Я упоминал кое-что из этого в комментарии, но ему нужно много места для реального ответа, чтобы правильно его охватить. Одно из поведений, которое показалось вам странным, было следующее:
- Часто зафиксированные файлы имеют LF-окончания только строки.
- Часто файлы рабочего дерева имеют окончания строк CRLF (как обычно предпочитают пользователи Windows).
- Они могут быть истинными одновременно, и все же,
git status
и git diff
не будут упоминать о каких-либо изменениях в окончаниях строки.
Такое поведение необходимо и уместно. Было бы неправильно запустить:
git checkout master
git diff
и увидите много различий! 1 Но фактическая реализация здесь очень сложна и может привести к некоторой кажущейся странности.
Есть несколько ключевых элементов, которые помогают понять это, а также понять многие другие действия Git. Вы уже упоминали некоторые из них, но давайте углубимся в детали и рассмотрим , как Git управляет окончаниями строк. Вещи, которые нам нужно обсудить:
- способ хранения файлов внутри коммитов: что мне нравится называть сублимированным форматом;
- способ хранения файлов внутри того, что Git вызывает, по-разному: index или область подготовки ;
- способ хранения файлов на вашем компьютере, в вашем рабочем дереве, где вы можете просматривать и работать с ними; и
- как они переходят из одного формата хранения в другой.
Этот последний шаг является ключом к проблемам конца строки, но он запутан с другими элементами.
1 Тем не менее, иногда это происходит по другим причинам. Я тоже коснусь их здесь.
хранит замороженные файлы в хранилище
Каждый коммит хранит полную копию каждого файла - ну, каждого файла, который находится в коммите, но это очевидно тавтологический. Идея этого утверждения заключается в том, что если у вас есть файлы README.md
и main.py
, скажем, и вы делаете новый коммит, в котором вы изменили main.py
, но не README.md
, новый коммит все равно делает еще одну копию README.md
.
Очевидно, что повторная фиксация каждого файла каждый раз будет большой тратой дискового пространства. Git избегает этого с помощью ряда хитрых трюков. Первым очевидным является то, что каждый сохраненный файл сжат (как с gzip
или bzip
или rar
; Git на самом деле использует zlib сжатие). Для большинства файлов их сжатие занимает меньше места. Типичный исходный код сжимается довольно хорошо. Сжатие уже сжатых файлов имеет тенденцию немного иметь неприятные последствия - одна из причин не хранить сжатые файлы в Git! - но не делает их достаточно большими, чтобы быть проблемой, поэтому Git просто запускает zlib deflate для всего.
Тем не менее, более важный трюк заключается в том, что после того, как Git заморозил файл в коммит, этот файл будет полностью, полностью доступен только для чтения. Для этого есть веская техническая причина: Git хранит все - все, что он называет объектами - в простой базе данных значений ключей, где ключами являются хеш-идентификаторов , сформированных хешированием значение, а значение - это байтовая строка, представляющая собой данные файла, с префиксом типа и размера объекта. 2 Поскольку сам ключ зависит от данных, вы буквально не можете изменить данные: если вы попробуете, вы получите вместо этого новый и другой объект с новым и другим хеш-идентификатором. 3 Старый объект все еще находится в базе данных со своим старым ключом и старым сохраненным байт: сжатый и замороженный файл, то есть высушенный сублимацией, файл все еще там.
Что это означает, что Git никогда не должен снова хранить тот же файл.Он может просто повторно использовать файл из предыдущего коммита!То есть, если мы только что сделали новый коммит с новым и другим main.py
, ну, Git должен был написать новый другой main.py
для нового объекта, высушенного вымораживанием, но мы сделали это с тем же старым README.md
, поэтому Git может просто повторно использовать предыдущую сублимированную README.md
. 4
Термин Git для этих сублимированных файлов - объект blob.BLOB-объекты и коммиты - это два из четырех типов объектов Git.Для полноты, оставшимися двумя являются дерево и аннотированный тег , но нам не нужно беспокоиться о них здесь.Нам нужно только взглянуть на объекты BLOB-объектов, и поскольку коммиты - это то, что сохраняет BLOB-объектов (косвенно - через объекты дерева!), Фиксирует объекты (слегка).
2 Префикс гарантирует, что, например, commit <size>\000<commit data>
имеет хеш, отличный от blob <size>\000<copy of the commit's data>
.Git хочет иметь возможность извлекать тип из объекта, поэтому тот факт, что вы можете прочитать существующий коммит, создать файл с этим содержимым и сохранить его как файл, означает, что префикс типа необходим.
Хеш-функция является криптографической, частично, так что вы не можете сознательно возиться с ней, чтобы создать коллизию, но в основном просто для того, чтобы получить действительно хорошие хеш-распределения.Принудительные коллизии хеша теоретически возможны , а может стать проблемой для Git в будущем, поэтому Git переходит на более длинный и более безопасный хеш.См. Как недавно обнаруженное столкновение SHA-1 влияет на Git?
3 Git проверяет, что хеш-код, который он использовал для поиска объектасоответствует хешу данных при извлечении данных из объекта.Это действует как тест на повреждение данных: если хеш данных, полученных ключом, не соответствует исходному ключу, Git знает, что данные на диске являются недействительными, и сообщает вам об этом.
4 Позже, Git сжимает эти объекты хранилища ключей-значений еще больше, беря объекты, которые некоторое время сидели без дела, и упаковывает их в то, что Git называет пакетомфайл .Объекты в файле пакета являются дельта-сжатыми относительно других объектов в этом файле пакета.Чтобы выполнить дельта-кодирование, Git отменяет дефляцию zlib, находит перекрывающиеся последовательности байтов (их, как правило, много в исходном коде) и создает версию с дельта-кодированием, которая говорит, что берет старую копию файла и делает этиизменяется на него: двоичный, байт-кодированный вариант того, что вы видите как git diff
.Эти разделенные объекты пакета затем все помещаются в один файл пакета.Огромное количество усилий затрачивается на то, чтобы решить, что будет отделяться от чего: это не просто «новая версия файла против старой версии файла».
Программное обеспечение Git более высокого уровня просто говорит: дайте мне объектс хэш-идентификатором H .Если объект существует как неупакованный объект, Git получает его, раздувая его заново.В противном случае Git просматривает каждый файл пакета.Если объект есть, Git может собрать его из его разделенных частей, и все это из одного файла пакета.Код на уровень выше никогда не должен знать, был ли файл отдельным объектом или кусками, хранящимися в пакете.Следовательно, точно сказать, что на уровне object Git выполняет только zlib-сжатие, без дельта-сжатия.Дельта-кодирование, если оно вообще происходит, происходит на ниже уровня объекта.
Лиофилизированные файлы повторно сливаются в рабочее дерево
Эта часть довольно проста: есть только одна морщина, которую мы оставим для следующего раздела.Фиксация - это моментальный снимок каждого файла, но все они в этой Git-только-лиофилизированной форме.Они полностью заморожены, что хорошо для архивации, но пока не преобразовано обратно, их даже нельзя использовать;и пока они заморожены, они не годятся для выполнения новой работы.Таким образом, их нужно как-то пересобрать: превратить в обычные файлы, хранящиеся в обычных каталогах / папках, так, как того требует ваша ОС.Результатом регидратации высушенных сублимацией зафиксированных файлов является рабочее дерево.
Индекс / область подготовки
Вот здесь и упоминается морщина, о которой я упоминал. Вместо непосредственного извлечения файлов to вашего рабочего дерева, Git сначала извлекает коммит в то, что Git называет индексом (в некоторых местах) или промежуточной областью (в другой документации).Что такое индекс и что он делает, становится более сложным во время операций слияния, но по большей части его легко описать: это предложенный следующий коммит .
Когда Git собирается сделать новый коммит, Git не использует то, что находится в рабочем дереве 1170 *.Существуют системы контроля версий, подобные Git, в которых do использует рабочее дерево в качестве предлагаемого следующего коммита, и они, как правило, намного проще в использовании, но также и намного медленнее.Используя их, вы говорите системе сделать новый коммит , и он, по сути, переходит и замораживает каждый файл снова в новый коммит.
Git, с другой стороны, говорит: Эй, подожди!Мы уже заморозили большинство ваших файлов.Вместо повторной сушки каждого файла в новом коммите, давайте заставим вас, пользователя, сделать это для определенных файлов, которые вы изменили , заставив вас запустить git add
на них! Итак, Git начинает с извлечения каждого файла в указатель, а затем снова вводит его в рабочее дерево.Команда git add
freeze сушит файл из рабочего дерева и копирует его в индекс, заменяя тот, который уже был там из предыдущего коммита, или, если это новый файл, создаваяновый файл в индексе, которого не было раньше.В любом случае, теперь этот файл готов перейти к следующему коммиту ... как и все файлы, которые вы не git add
.Они все еще там с git checkout
, готовые войти в новый коммит.
Вот откуда взялась вся эта безумие по поводу отслеженных против неотслеживаемых файлов,Отслеживаемый файл - это просто любой файл, который сейчас находится в индексе .Не отслеживаемый файл - это любой файл, который сейчас находится в рабочем дереве, но не в индексе. В любой момент вы можете поместить один файл в индекс прямо сейчас: git add <em>file</em>
.В любой момент вы можете взять один файл из индекса прямо сейчас: git rm <em>file</em>
или git rm --cached <em>file</em>
.Использование git rm
удаляет файл из и индекса и рабочего дерева, в то время как использование git rm --cached
удаляет файл только из индекса, оставляя файл рабочего дереваодин.
Конечно, другие вещи, которые вы делаете , также изменяют индекс.Наиболее очевидным является то, что git checkout
часто должен заменить индекс или, по крайней мере, его части.Эти детали могут быть очень хитрыми - см. Извлечение другой ветки, когда есть неподтвержденные изменения в текущей ветке - но на самом деле все сводится к помещению файлов в индекс или извлечению их вместе с помещением файловв рабочее дерево, или вынимая их, или (например, git rm --cached
или git reset --mixed
) оставляя рабочее дерево в покое при изменении содержимого индекса.
Независимо от того, как изменяется индекс - илине меняется - главное, что нужно иметь в виду: В каждом случае есть до трех активных копий каждого из ваших файлов:
Одна копия является лиофилизированной в текущем (HEAD
) коммите. Вы можете просмотреть это с помощью git show HEAD:<em>file</em>
. Вы никогда не можете изменить этот файл вообще - все, что вы можете сделать, это изменить коммит, который вызывает имя HEAD
, создав новые коммиты или используя git checkout
для перехода к другому коммиту.
Одна копия - лиофилизированная в вашем индексе. Вы можете просмотреть это с помощью git show :<em>file</em>
или git show :0:<em>file</em>
. 5 Вы можете заменить на новое из вашего рабочего дерева, используя git add
.
Последняя копия - обычная ежедневная копия для чтения / записи в вашем рабочем дереве. Вы можете использовать любую из ваших обычных не-Git команд для этого.
Я говорю до трех здесь, потому что, например, конечно, неотслеживаемый файл отсутствует в вашем индексе (независимо от того, находится он в коммите HEAD
) или совершенно новый файл, который еще не был зафиксирован, может находиться как в индексе, так и в рабочем дереве, но не в HEAD
. В общем, должно быть очевидно, сколько копий в каждой ситуации.
Обратите внимание, что индекс на самом деле содержит идентификатор хэша BLOB-объекта файла, высушенного вымораживанием, который уже сохранен в хранилище объектов Git. Если вы фиксируете файл, хэш блоба становится постоянным, поскольку сам коммит теперь использует его. В противном случае срок действия объекта может истечь (но не тогда, когда его хэш останется в индексе). 6
5 Здесь нулевое число - это промежуточный номер , который имеет отношение к слияниям. Номер по умолчанию равен нулю, и, за исключением конфликтов слияния, все всегда находится в нулевом интервале промежуточного слота, поэтому вы можете использовать :0:
или просто :
для обозначения в индексе .
6 Некоторое время в git worktree add
была очень неприятная ошибка. Сборщик мусора не учитывал ни дополнительный файл индекса, ни ссылки для каждого рабочего дерева, связанные с каждым рабочим деревом. Он никогда не сканировал эти дополнительные индексные файлы и ссылки, и если какой-либо конкретный хеш появился в только таком индексе или ссылке, Git иногда истекал бы такими объектами, даже если добавленное рабочее дерево нуждалось в них! Это было исправлено в Git 2.15.
Концы строк, грязные и чистые фильтры
Теперь, когда вы привыкли к мысли, что Git всегда хранит до трех копий каждого файла, сейчас мы можем видеть, как работают манипуляции с окончанием строки в Git. Кроме того, мы можем увидеть, как вы можете определить smudge и clean filters и как они работают.
Процесс извлечения файла из сублимированной формы в коммите HEAD
и помещения его в индекс действительно прост: Git просто определяет относительный путь к файлу, такой как README.md
или dir1/dir2/file.py
и освобождает место в указателе в соответствующем месте - указатель тщательно упорядочен для быстрого доступа - и заполняет ключевую информацию о лиофилизированной копией. Git также вставляет немного информации о копии рабочего дерева в индексную запись для этого файла, как мы увидим чуть позже.
Поскольку индекс содержит только хэш-идентификатор файла, высушенного вымораживанием, индекс будет точно , что будет в следующем коммите, если вы сделаете это прямо сейчас. Если то, что в индексе, получено из коммита HEAD
, это точно , что в коммите HEAD
.
Как и для всех замороженных объектов с хэш-идентификатором, здесь ничего не может измениться . Вы можете создать новый и другой объект с новым и другим хеш-идентификатором, и, поскольку вы можете записать новые хеш-идентификаторы в индекс, вы можете заменить оптовую копию индекса, но поскольку вы не можете вставить новые хеш-идентификаторы в существующий коммит. Вы не можете изменить коммит. Если вы изменяете индекс, вы меняете его на точно , что вы предлагаете вставить в следующий коммит.
Meмежду тем, что входит в рабочее дерево , это регидратированная копия файла. Переданные и индексные копии замораживаются: они имеют формат Git-only. Копии рабочего дерева обычные. Существует преобразование, которое обязательно должно иметь место, во время извлечения из Git, каждый раз включенного в процесс рабочего дерева. Существует соответствующее преобразование, которое обязательно должно иметь место, во время git add
сублимационной сушки файла и загрузки его в хранилище объектов и индексации каждый раз.
Итак: почему бы и нет, во время этого процесса преобразования также выполнить фильтрацию конца строки? И это именно то, что делает Git:
Копирование файла из индекса в рабочее дерево (в основном git checkout
): если файл рабочего дерева должен иметь окончания строк CRLF, Git может превратить окончания строк LF-only в BLOB-объекте в Концы строк CRLF в рабочем дереве. Фактически, он может вставить любой произвольный «грязный» материал, который вы хотели бы иметь, через ваш фильтр . В общем, мы можем обозначить это как нечеткие файлы .
Копирование файла из рабочего дерева в индекс (в основном git add
): если у зафиксированного файла должны быть окончания строк только для LF, Git может превратить любые CRLF-окончания в окончания только для LF, пока написание объекта BLOB. Фактически, он может «очистить» любую «грязь», добавленную вами в ваш грязный фильтр, через ваш чистый фильтр . Мы можем обозначить это как очистка файлов .
Здесь Git предоставляет три встроенных режима смазывания и очистки в конце строки. Если вы хотите, чтобы другие, вы должны написать свой собственный пятно и чистые фильтры:
Ничего не делать: сохранить соответствие индекса и рабочего дерева. Это подходит для всех двоичных данных. В целом, это также уместно в системах Linux, где строки не должны иметь окончаний CRLF, поэтому, если все в хранилище всегда совпадает со всем в рабочем дереве, и ничто никогда не имеет окончаний CRLF, проблем не возникает.
Делать LF-to-CRLF для дерева записи в работу и CRLF-to-LF для записи в индекс. Это подходит для некоторых текстовых файлов для пользователей Windows. .
Ничего не делать для дерева записи в работу, но делать CRLF-to-LF для записи в индекс. Это режим, который Git вызывает input
. На мой взгляд, это не особо подходит для чего-либо. Это может быть причиной того, что input
в основном является функцией обратной совместимости. Вы можете установить тот же режим с помощью eol=lf
в файле `.gitattributes.
git diff
и git status
против пятен / чистых / etc
То, что git diff
делает - или намеревается делать - в основном:
- сравнить весь коммит с другим коммитом; или
- сравнить любой коммит с предлагаемым следующим коммитом (т. Е. Индексом); или
- сравнить любой коммит с рабочим деревом; или
- сравнить предложенный следующий коммит (индекс) с рабочим деревом.
Некоторые из этих операций работают исключительно с BLOB-объектами - сублимированными файлами в коммитах или в индексе. Это легко, сравнительно говоря: они уже в той форме, в которой они всегда будут. Не нужно ничего путать, чистить или чистить. Но все, что сравнивает коммит или индекс с рабочим деревом, имеет проблему, если фильтр конца строки или пятно изменил того, что находится в рабочем дереве.
Есть два очевидных способа решения этой проблемы. Git может:
- очистить файлы рабочего дерева (добавив их куда-нибудь, например, во временный индекс), затем сравнить очищенные файлы; или
- повторно размазать индекс или зафиксировать копии (извлекая их где-нибудь, например, во временные файлы), а затем сравнить размазанные файлы.
Оба они медленные: они означают повторное копирование каждого файла, который использует эти функции, каждый раз, когда вы сравниваете что-то с рабочим деревом. Git сделает это, когда это необходимо (и, основываясь на источнике, он может сделать любой из них - я не уверен, что именно происходит, когда). Но Гит пытается быть более умным, чем это.
Если вы только что извлекли файл - просто скопировали его из индекса в рабочее дерево - копия рабочего дерева должна по определению соответствовать копии индекса, независимо от того, как "smudgey" копия рабочего дерева есть. Аналогично, если вы только что git add
отредактировали файл - просто скопировали его из рабочего дерева в индекс - индексная копия должна , по определению, соответствовать копии рабочего дерева, независимо от того, насколько «чистой» является индексная копия. Git сохраняет в индексе кучу информации на уровне операционной системы о копии рабочего дерева файла по сравнению с индексной копией файла. Если эти два совпадения, Git получает при условии , что индекс и копии рабочего дерева совпадают.
Обратите внимание, что Git сохраняет это предположение в ключевых случаях, даже если это не так. В частности, предположим, что у вас есть зафиксированный файл с окончаниями строк только для LF, и вы настроили свой репозиторий с помощью .gitattributes
и / или другие настройки, которые сообщали Git: При копировании этого файла в любом случае, выполняйте перевод LF / CRLF в соответствии с направлением копирования. С тех пор вы изменили .gitattributes
или другие настройки так, что если бы Git повторно извлек файл сейчас , он бы ничего не делал, а если вы git add
файл сейчас , он ничего не сделает - что добавило бы версию файл с окончанием строки CRLF, к индексу.
Git будет настаивать, чтобы индекс и копии рабочего дерева файла совпадали, даже если они больше не соответствуют. Если вы измените настройки back на режим, в котором Git будет выполнять перевод, теперь файлы снова совпадут. Git постоянно настаивает на том, чтобы файлы совпадали, потому что он использует информацию о статусе файла индекса, чтобы обойти тяжелую работу, чтобы действительно проверить.
Команда git status
состоит, в частности, из запуска двух команд git diff
, одной для сравнения HEAD
с индексом и одной для сравнения индекса с рабочим деревом. Первый diff не имеет проблем с окончанием строки, поэтому здесь не о чем беспокоиться, но второй имеет обычные проблемы index-vs-work-tree. На самом деле он использует тот же код, что и git diff
, поэтому он ведет себя так же, думая, что вещи чистые или нет.
git add --renormalize
Команда git add
в некоторых случаях выполняет аналогичные сокращения. Это позволяет вам делать такие вещи, как git add .
, без необходимости повторного сжатия Git и сублимационной сушки каждого файла в вашем рабочем дереве: он только повторно сжимает и замораживает-высушивает файлы, которые на основе временных отметок и такие, похоже, они действительно нуждаются в этом. Это, конечно, работает плохо, если вы изменили настройку очистки, потому что файлы могут нуждаться в реальной очистке, когда Git считает, что они уже чисты.
Операция git add --renormalize
сообщает Git: Поражение кода особого случая. Не верьте, что индекс и рабочее дерево одинаковы в зависимости от отметок времени файла ОС и т. Д .; действительно выполняйте add
, действительно применяя процесс очистки. Так что это один простой способ обойти эту проблему, если и когда она возникнет. (Я видел здесь сообщения о StackOverflow о том, что он не работает, но никогда с репродуктором.)
Это не единственные источники проблем
Обратите внимание, что возможно:
- зафиксировать файл с фактическим окончанием строки CRLF
- позже, сообщите Git, что он должен извлекать и записывать такие файлы с окончанием строки только для LF
- входит в состояние, когда после извлечения файла его не следует считать "чистым"
и иногда, в зависимости от капризов ОС, это действительно происходит, несмотря на попытки Git быть умнее с файловыми отметками времени и тому подобным.
Чаще, однако, вы увидите случай, когда:
$ git clone <repo>
$ cd <the-clone>
$ git status
показывает моразличные файлы, когда вы работаете в системе Windows или MacOS, где у вас есть локальная файловая система без учета регистра и вы только что клонировали репозиторий, который был написан в системе Linux с файловой системой с учетом регистра.
Пользователь Linux может сделать коммит с двумя разными файлами , имена которых отличаются только регистром, например, README.MD
и ReadMe.md
.Когда ваш Git на вашем Mac или Windows-системе идет, чтобы извлечь эти два разных файла в ваше рабочее дерево, он сначала создает один из них - обычно README.MD
-, а затем переходит к созданию другого, ReadMe.md
, нозаканчивается перезапись содержимого README.MD
содержимым из подтвержденного (теперь индексированного) ReadMe.md
.
То, что вы видите, является измененным README.MD
, с неизмененным ReadMe.md
, потому что вашВ рабочем дереве есть только один файл с именем README.MD
с содержимым из зафиксированного ReadMe.md
.
Нет хороших решений этой проблемы, кроме как заставить ваших коллег по Linux прекратить это делать.Git, вероятно, должен иметь какой-то причудливый способ справиться с этим, но это не так. можно пройти через это, не прибегая к загрузке системы Linux, но запуск виртуальной машины Linux - безусловно, самый простой способ справиться с этим.