Что значит Линус Торвальдс, когда говорит, что Git «никогда не отслеживает файл»?

284

Цитируя Линуса Торвальдса, когда его спросили, сколько файлов может обработать Git во время его Tech Talk в Google в 2007 году (43:09):

... Git отслеживает ваш контент. Он никогда не отслеживает ни одного файла. Вы не можете отслеживать файл в Git. Что вы можете сделать, так это то, что вы можете отслеживать проект с одним файлом, но если у вашего проекта есть один файл, обязательно сделайте это, и вы можете это сделать, но если вы отслеживаете 10 000 файлов, Git никогда не увидит их как отдельные файлы. Git думает все как полный контент. Вся история в Git основана на истории всего проекта ...

(стенограммы здесь .)

Тем не менее, когда вы погружаетесь в книгу Git , первое, что вам говорят, это то, что файл в Git может быть отслежен или не отслежен . Более того, мне кажется, что весь опыт работы с Git направлен на управление версиями файлов. При использовании git diffилиgit status вывод представлен отдельно для каждого файла. При использовании git addвы также можете выбрать для каждого файла. Вы даже можете просмотреть историю на файловой основе и молниеносно.

Как следует толковать это утверждение? С точки зрения отслеживания файлов, чем Git отличается от других систем контроля версий, таких как CVS?

Симон Рамирес Амая
источник
20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - «Я подозреваю, что для вас сейчас важнее то, что есть разница между тем, как Git представляет файлы пользователям и как он обрабатывает их внутри . Как представляется пользователю, моментальный снимок содержит полные файлы, а не просто diffs. Но внутри Git использует diff для генерации пакетов, которые эффективно хранят ревизии ". (Это резко контрастирует, например, с Subversion.)
user2864740
5
Git не отслеживает файлы, он отслеживает наборы изменений . Большинство систем контроля версий отслеживают файлы. В качестве примера того, как / почему это может иметь значение, попробуйте зайти в пустой каталог для git (spolier: вы не можете, потому что это «пустой» набор изменений).
Эллиотт Фриш
12
@ElliottFrisch Это звучит неправильно. Ваше описание ближе к тому, что делает, например, Даркс . Git хранит снимки, а не наборы изменений.
Мельпомена
4
Я думаю, он имеет в виду, что Git не отслеживает файл напрямую. Файл включает в себя его имя и содержание. Git отслеживает содержимое в виде капель. Имея только BLOB-объект, вы не можете сказать, какое у него соответствующее имя файла. Это может быть содержимое нескольких файлов с разными именами по разным путям. Связи между именем пути и большим двоичным объектом описаны в объекте дерева.
ElpieKay
3
В связи с этим: Рэндал Шварц « Продолжение выступления Линуса» (также в Google Tech) - «... что такое Git на самом деле ... Линус сказал, что Git НЕ».
Питер Мортенсен

Ответы:

316

В CVS история отслеживалась отдельно для каждого файла. Ветвь может состоять из различных файлов со своими собственными различными ревизиями, каждый со своим собственным номером версии. CVS был основан на RCS ( Revision Control System ), которая аналогичным образом отслеживала отдельные файлы.

С другой стороны, Git делает снимки состояния всего проекта. Файлы не отслеживаются и не имеют версий независимо; ревизия в хранилище относится к состоянию всего проекта, а не к одному файлу.

Когда Git ссылается на отслеживание файла, это просто означает, что он должен быть включен в историю проекта. В речи Линуса речь шла не об отслеживании файлов в контексте Git, а о том, как сравнивать модели CVS и RCS с моделью на основе снимков, используемой в Git.

bk2204
источник
4
Вы можете добавить, что именно поэтому в CVS и Subversion вы можете использовать теги, как $Id$в файле. То же самое не работает в Git, потому что дизайн отличается.
Gerrit
58
И контент не привязан к файлу, как вы ожидаете. Попробуйте переместить 80% кода одного файла в другой. Git автоматически обнаруживает перемещение файла + изменение на 20%, даже если вы только что переместили код в существующие файлы.
алло
13
@allo Как побочный эффект этого, git может сделать то, что другие не могут: когда два файла объединены, и вы используете «git blame -C», git может просмотреть обе истории. При отслеживании файлов вы должны выбрать, какой из исходных файлов является настоящим оригиналом, а все остальные строки выглядят совершенно новыми.
Изката
1
@allo, Izkata - и это запрашивающая сущность, которая обрабатывает все это путем анализа содержимого репо во время запроса (фиксации истории и различий между ссылочными деревьями и BLOB-объектами), вместо того, чтобы требовать, чтобы коммитирующая сущность и ее пользователь-пользователь правильно указывали или синтезировали эта информация во время фиксации - ни разработчик инструмента репо не может спроектировать и реализовать эту возможность и соответствующую схему метаданных до того, как инструмент будет развернут. Торвальдс утверждал, что такой анализ со временем только улучшится, и вся история каждого git-репо с первого дня принесет пользу.
Джереми
1
@allo Да, и чтобы понять тот факт, что git не работает на уровне файлов, вам даже не нужно фиксировать все изменения в файле сразу; Вы можете фиксировать произвольные диапазоны строк, оставляя другие изменения в файле вне фиксации. Конечно, пользовательский интерфейс для этого не так прост, поэтому большинство этого не делают, но он редко используется.
Элвин Томпсон
103

Я согласен с Брайаном М. Ответ Карлсона : Линус действительно проводит различие, по крайней мере частично, между файлово -ориентированными и коммит-ориентированными системами контроля версий. Но я думаю, что это еще не все.

В моей книге , которая застопорилась и, возможно, никогда не закончится, я попытался придумать таксономию для систем контроля версий. В моей таксономии термин для того, что нас интересует, это атомность системы контроля версий. Посмотрите, что в данный момент находится на странице 22. Когда VCS имеет атомарность на уровне файлов, фактически существует история для каждого файла. VCS должен помнить имя файла и что происходило с ним в каждой точке.

Git этого не делает. Git имеет только историю коммитов - коммит является его единицей атомарности, а история - это набор коммитов в хранилище. Коммит запоминает данные - целое дерево, полное имен файлов и содержимого каждого из этих файлов, а также некоторые метаданные: например, кто сделал коммит, когда и почему, и внутренний хэш-идентификатор Git. коммит'S родителя коммита. (Именно этот родитель, и ориентированный ациклический граф, сформированный чтением всех коммитов и их родителей, является историей в хранилище.)

Обратите внимание, что VCS может быть ориентированным на принятие, но все же хранить данные файл за файлом. Это деталь реализации, хотя иногда и важная, и Git этого тоже не делает. Вместо этого каждый коммит записывает дерево с именами файлов, кодирующими объекты дерева , режимы (т. Е. Исполняемый файл или нет?) И указатель на фактическое содержимое файла . Сам контент хранится независимо, в объекте BLOB-объекта . Подобно объекту фиксации, BLOB-объект получает хеш-идентификатор, уникальный для его содержимого, но в отличие от коммита, который может появляться только один раз, BLOB-объект может появляться во многих коммитах. Таким образом, основное содержимое файла в Git сохраняется непосредственно в виде большого двоичного объекта, а затем косвенно в объекте дерева, чей идентификатор хеша (прямо или косвенно) записан в объекте коммита.

Когда вы просите Git показать вам историю файла, используя:

git log [--follow] [starting-point] [--] path/to/file

Git на самом деле просматривает историю коммитов , которая является единственной историей Git, но не показывает вам ни одного из этих коммитов, если только:

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

(но некоторые из этих условий могут быть изменены с помощью дополнительных git logопций, и очень сложно описать побочный эффект, называемый упрощением истории, который заставляет Git полностью пропустить некоторые коммиты из истории). История файлов, которую вы видите здесь, в определенном смысле не существует точно в хранилище: это всего лишь синтетическое подмножество реальной истории. Вы получите другую «историю файлов», если будете использовать разные git logопции!

Торек
источник
Еще одна вещь, которую нужно добавить, - это позволяет Git делать такие вещи, как мелкие клоны. Ему просто нужно получить главный коммит и все объекты, на которые он ссылается. Не нужно заново создавать файлы, применяя наборы изменений.
Уэс
@WesToleman: это определенно делает это проще. Mercurial хранит дельты, время от времени сбрасывая их, и хотя люди Mercurial намереваются добавить туда мелкие клоны (что возможно из-за идеи «перезагрузки»), на самом деле они еще этого не сделали (потому что это скорее техническая задача).
Торек
@torek У меня есть сомнения относительно вашего описания того, как Git отвечает на запрос истории файлов, но я думаю, что он заслуживает своего собственного правильного вопроса: stackoverflow.com/questions/55616349/…
Симон Рамирес Амая,
@torek Спасибо за ссылку на вашу книгу, больше ничего подобного не видел.
gnarledRoot
17

Запутанный бит здесь:

Git никогда не видит их как отдельные файлы. Git думает все как полный контент.

Git часто использует 160-битные хэши вместо объектов в своем репо. Дерево файлов - это в основном список имен и хэшей, связанных с содержимым каждого (плюс некоторые метаданные).

Но 160-битный хеш однозначно идентифицирует контент (в юниверсе базы данных git). Таким образом, дерево с хешами в качестве содержимого включает содержимое в своем состоянии.

Если вы измените состояние содержимого файла, его хеш-код изменится. Но если его хеш изменяется, хеш, связанный с содержимым имени файла, также изменяется. Что, в свою очередь, меняет хэш "дерева каталогов".

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

Он организован в виде древовидной структуры с (неизменяемыми, многократно используемыми) указателями на BLOB-объекты или другие деревья, но логически это единый снимок всего содержимого всего дерева. Представление в базе данных мерзавца не плоское содержание данных, но логически это все его данные и ничего другого.

Если вы сериализовали дерево в файловую систему, удалили все папки .git и сказали git добавить дерево обратно в его базу данных, вы ничего не добавите в базу данных - элемент уже будет там.

Это может помочь думать о хешах git как о подсчитанном указателе на неизменяемые данные.

Если вы построили приложение вокруг этого, документ представляет собой набор страниц, которые имеют слои, которые имеют группы, которые имеют объекты.

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

Каждый раз, когда вы изменяете один объект, он порождает новый документ. Старый документ продолжает существовать. Новый и старый документ разделяют большую часть их содержимого - они имеют одинаковые страницы (кроме 1). Эта страница имеет одинаковые слои (кроме 1). Этот слой имеет те же группы (кроме 1). Эта группа имеет те же объекты (кроме 1).

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

Git РЕПО очень похоже на это.

Это означает, что данный набор изменений git содержит свое сообщение коммита (в виде хеш-кода), содержит свое рабочее дерево и содержит родительские изменения.

Эти родительские изменения содержат свои родительские изменения, все назад.

Часть git-репо, которая содержит историю, является той цепочкой изменений. Эта цепочка изменений на уровне выше дерева «каталогов» - из дерева «каталогов» вы не можете однозначно получить набор изменений и цепочку изменений.

Чтобы узнать, что происходит с файлом, вы начинаете с этого файла в наборе изменений. У этого набора изменений есть история. Часто в этой истории существует один и тот же именованный файл, иногда с одинаковым содержимым. Если содержимое совпадает, файл не изменился. Если это не так, то есть изменения, и нужно сделать работу, чтобы понять, что именно.

Иногда файл исчезает; но в дереве «каталогов» может быть другой файл с тем же содержимым (с таким же хеш-кодом), поэтому мы можем отслеживать его таким образом (примечание; именно поэтому вы хотите зафиксировать перемещение файла отдельно от фиксации для -редактировать). Или то же имя файла, и после проверки файл достаточно похож.

Так что git может совместить "историю файлов".

Но эта история файлов происходит из-за эффективного анализа «всего набора изменений», а не из ссылки на одну версию файла на другую.

Якк - Адам Невраумонт
источник
12

«мерзавец не отслеживает файлы» в основном означает , что коммиты Git и состоят из дерева файлов снимки , соединяющая путь в дереве к «сгустку» и совершающий график отслеживания истории коммитов . Все остальное восстанавливается на лету такими командами, как «git log» и «git blame». Эта реконструкция может быть объяснена с помощью различных опций, насколько сложно искать изменения на основе файлов. Эвристика по умолчанию может определять, когда большой двоичный объект изменяется в дереве файлов без изменений, или когда файл связан с другим большим двоичным объектом, чем раньше. Механизмы сжатия, используемые Git, не слишком заботятся о границах BLOB / файлов. Если содержимое уже где-то находится, это позволит сохранить небольшой размер хранилища, не связывая различные BLOB-объекты.

Теперь это хранилище. У Git также есть рабочее дерево, и в этом рабочем дереве есть отслеживаемые и неотслеживаемые файлы. Только индексированные файлы записываются в индекс (область подготовки «кэш»), и только то, что там отслеживается, попадает в хранилище.

Индекс ориентирован на файл, и есть некоторые ориентированные на файл команды для управления им. Но то, что заканчивается в репозитории, это просто коммиты в виде снимков дерева файлов и связанных с ними данных BLOB-объектов и предков коммитов.

Так как Git не отслеживает историю файлов и переименовывает, и ее эффективность не зависит от них, иногда вам приходится несколько раз пробовать разные варианты, пока Git не создаст интересующую вас историю / diffs / blames для нетривиальных историй.

Это отличается от таких систем, как Subversion, которые записывают, а не реконструируют истории. Если это не записано, вы не услышите об этом.

Я на самом деле создал разностный установщик, который сравнивал деревья релизов, проверяя их в Git, а затем создавал сценарий, дублирующий их эффект. Поскольку иногда целые деревья перемещались, это приводило к гораздо меньшим разностным установщикам, чем перезапись / удаление всего, что могло бы произойти.


источник
7

Git не отслеживает файл напрямую, но отслеживает снимки репозитория, и эти снимки состоят из файлов.

Вот способ посмотреть на это.

В других системах контроля версий (SVN, Rational ClearCase) вы можете щелкнуть правой кнопкой мыши файл и получить его историю изменений .

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

Толстый толстяк Double Vision
источник
5
Я думаю, что я понимаю, что вы пытаетесь сказать, но «В Git нет прямой команды, которая делает это» прямо противоречит ответам на вопрос, с которым вы связаны. Хотя версионирование происходит на уровне всего хранилища, в Git обычно есть множество способов достичь чего-либо , поэтому наличие нескольких команд для отображения истории файла не является доказательством многого.
Джо Ли-Мойет
Я просмотрел первые несколько ответов на вопрос, который вы связали, и все они используют git logили какую-то программу, построенную поверх этого (или какой-то псевдоним, который делает то же самое). Но даже если бы было много разных способов, как говорит Джо, это также верно для отображения истории ветвей. (также git log -p <file>встроен и делает именно это)
Voo
Вы уверены, что SVN хранит изменения внутри каждого файла? Я не использовал его уже некоторое время, но я смутно помню, что файлы назывались как идентификаторы версий, а не как отражение файловой структуры проекта.
Артур Бесадовский
3

Кстати, отслеживание «контента» привело к тому, что пустые каталоги не отслеживались.
Вот почему, если вы нажмете последний файл папки, сама папка будет удалена .

Это не всегда так, и только Git 1.4 (май 2006 г.) применил эту политику «отслеживания контента» с коммитом 443f833 :

git status: пропустите пустые каталоги и добавьте -u, чтобы показать все неотслеживаемые файлы

По умолчанию мы используем, --others --directoryчтобы показывать неинтересные каталоги (чтобы привлечь внимание пользователя) без их содержимого (чтобы не перегружать вывод).
Показывать пустые каталоги не имеет смысла, поэтому проходите, --no-empty-directoryкогда мы это делаем.

Предоставление -u(или --untracked) отключает этот беспорядок, чтобы позволить пользователю получить все неотслеживаемые файлы.

Это было отражено годами позже в январе 2011 года с коммитом 8fe533 , Git v1.7.4:

Это соответствует общей философии пользовательского интерфейса: git отслеживает содержимое, а не пустые каталоги.

Тем временем, с Git 1.4.3 (сентябрь 2006 г.), Git начинает ограничивать неотслеживаемый контент непустыми папками с коммитом 2074cb0 :

он не должен перечислять содержимое полностью неотслеживаемых каталогов, а только имя этого каталога (плюс завершающий символ ' /').

Отслеживание контента - это то, что позволило git обвинить в самом начале (Git 1.4.4, октябрь 2006, commit cee7f24 ) быть более производительным:

Что еще более важно, его внутренняя структура предназначена для более легкой поддержки перемещения контента (иначе говоря, вырезания и вставки), позволяя использовать несколько путей из одного и того же коммита.

Это (отслеживание содержимого) - это то, что добавило git add в Git API с Git 1.5.0 (декабрь 2006, commit 366bfcb )

сделать 'git add' первоклассным удобным интерфейсом для индекса

Это раскрывает всю мощь индекса, используя правильную ментальную модель, вообще не говоря об индексе.
Посмотрите, например, как все технические обсуждения были удалены из справочной страницы git-add.

Любой контент, который будет зафиксирован, должен быть добавлен вместе.
Приходит ли этот контент из новых файлов или измененных файлов, не имеет значения.
Вам просто нужно «добавить» его, либо с помощью git-add, либо предоставив git-commit с -a(конечно, только для уже известных файлов).

Это то, что стало git add --interactiveвозможным, с тем же Git 1.5.0 ( commit 5cde71d )

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

Вот почему, чтобы рекурсивно удалить все содержимое из каталога, вам нужно передать -rпараметр, а не просто имя каталога как <path>(все еще Git 1.5.0, commit 9f95069 ).

Просмотр содержимого файла вместо самого файла позволяет сценарию слияния, подобному сценарию, описанному в коммите 1de70db (Git v2.18.0-rc0, апрель 2018 г.)

Рассмотрим следующее слияние с конфликтом переименования / добавления:

  • сторона A: изменить foo, добавить не связанныйbar
  • сторона B: переименовать foo->bar(но не изменять режим или содержимое)

В этом случае трехходовой слияние оригинального Foo, Foo, и Б barприведет к желаемому имени пути barс тем же режимом / содержанием , что А имел для foo.
Таким образом, у A был правильный режим и содержимое для файла, и у него было правильное имя пути (а именно, bar).

Коммит 37b65ce , Git v2.21.0-rc0, декабрь 2018 года, недавно улучшил разрешение конфликтов.
И фиксация bbafc9c еще раз иллюстрирует важность рассмотрения содержимого файла , улучшая обработку конфликтов переименования / переименования (2to1):

  • Вместо того, чтобы хранить файлы в collide_path~HEADи collide_path~MERGE, файлы объединяются и записываются в collide_path.
  • Вместо записи версии переименованного файла, которая существовала на переименованной стороне в индексе (игнорируя, таким образом, любые изменения, внесенные в файл на стороне истории без переименования), мы выполняем трехстороннее объединение контента с переименованным путь, затем сохраните его на стадии 2 или 3.
  • Обратите внимание, что, поскольку слияние содержимого для каждого переименования может иметь конфликты, и тогда мы должны объединить два переименованных файла, мы можем получить вложенные маркеры конфликта.
VonC
источник