Подтвердить что ты не робот

Как git обрабатывает перемещение файлов в файловой системе?

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

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

4b9b3361

Ответ 1

Чтобы понять, как справится с этим git, вам нужно знать две вещи, чтобы начать с:

  • Каждый отдельный файл (в любом каталоге, в любом фиксации) сохраняется отдельно.
  • Но он хранится по его объекту-идентификатору, который уникален для любых данных в файле.

Как хранятся вещи

Скажем, у вас есть новое репо с одним огромным файлом в нем:

$ mkdir temp; cd temp; git init
$ echo contents > bigfile; git add bigfile; git commit -m initial
[master (root-commit) d26649e] initial
 1 file changed, 1 insertion(+)
 create mode 100644 bigfile

Теперь у репо есть одно коммит, у которого есть одно дерево (каталог верхнего уровня), у которого есть один файл, который имеет уникальный идентификатор объекта. ( "Большой" файл - ложь, он довольно мал, но он будет работать одинаково, если бы его было много мегабайт.)

Теперь, если вы скопируете файл во вторую версию и выполните следующее:

$ cp bigfile bigcopy; git add bigcopy; git commit -m 'make a copy'
[master 971847d] make copy
 1 file changed, 1 insertion(+)
 create mode 100644 bigcopy

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

$ git cat-file -p HEAD:
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigcopy
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigfile

Этот большой SHA-1 12f00e9... является уникальным идентификатором для содержимого файла. Если файл действительно был огромным, git теперь будет использовать примерно вдвое меньше места репо, чем рабочий каталог, потому что у репо есть только одна копия файла (под именем 12f00e9...), тогда как рабочий каталог имеет два.

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

Динамическое определение переименования

Теперь предположим, что у вас более сложная структура каталогов (репо с более "древовидными" объектами). Если вы перетасовываете файлы вокруг, но содержимое "нового" файла (ов) - независимо от названия (ов) - в новых каталогах совпадает с содержимым, которое раньше было в старых, вот что происходит внутри:

$ mkdir A B; mv bigfile A; mv bigcopy B; git add -A .
$ git commit -m 'move stuff'
[master 82a64fe] move stuff
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename bigfile => A/bigfile (100%)
 rename bigcopy => B/bigcopy (100%)

Git обнаружил (эффективное) переименование. Посмотрим на одно из новых деревьев:

$ git cat-file -p HEAD:A
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigfile

Файл все еще находится под тем же старым идентификатором объекта, поэтому он остается только в репо один раз. Легко для git обнаруживать переименование, потому что идентификатор объекта совпадает, хотя имя пути (как хранится в этих "древовидных" объектах) может и не быть. Сделайте последнее:

$ mv B/bigcopy B/two; git add -A .; git commit -m 'rename again'
[master 78d92d0] rename again
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename B/{bigcopy => two} (100%)

Теперь попросите разницу между HEAD~2 (перед любыми переименованиями) и HEAD (после переименования):

$ git diff HEAD~2 HEAD
diff --git a/bigfile b/A/bigfile
similarity index 100%
rename from bigfile
rename to A/bigfile
diff --git a/bigcopy b/B/two
similarity index 100%
rename from bigcopy
rename to B/two

Даже если это было сделано в два этапа, git может сказать, что перейти от того, что было в HEAD~2 к тому, что сейчас находится в HEAD, вы можете сделать это за один шаг, переименовав bigcopy в B/two.

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

Другими словами, diff всегда выполняется commit-pair-wise: "В прошлом мы имели A. Теперь у нас есть Z. Как мне перейти непосредственно от A к Z?" В то время git проверяет возможность переименования и выводит их на выход diff при необходимости.

Как насчет небольших изменений?

Git будет по-прежнему (иногда) показывать переименования, даже если есть небольшое изменение содержимого файла. В этом случае вы получаете "индекс подобия". В принципе, вы можете сказать git, что "если какой-то файл был удален в rev A, другой файл с иными именами добавлен в rev Z" (при отличных оборотах A и Z), он должен попробовать различать два файла, чтобы увидеть, "достаточно близко". Если они есть, вы получите "файл, переименованный, а затем измененный" diff. Элемент управления для этого - аргумент -M или --find-renames для git diff: git diff -M80 говорит, чтобы показать изменение как переименование и редактирование, если файлы не менее похожи на 80%.

Git также будет искать "скопированный, затем измененный", с флагом -C или --find-copies. (Вы можете добавить --find-copies-harder, чтобы выполнить более дорогостоящий поиск по всем файлам, см. .

Это (косвенно) относится к тому, как git также хранит репозитории в размерах с течением времени.

Дельта-сжатие

Если у вас большой файл (или даже небольшой файл) и внесены в него небольшие изменения, git будет хранить две полные копии файла, используя эти идентификаторы объектов. Вы находите эти вещи в .git/objects; например, этот файл с идентификатором 12f00e90b6ef79117ce6e650416b8cf517099b78 находится в .git/objects/12/f00e90b6ef79117ce6e650416b8cf517099b78. Они сжаты для экономии места, но даже сжаты, большой файл все еще может быть довольно большим. Таким образом, если базовый объект не очень активен и появляется во многих транзакциях с небольшими изменениями время от времени, git имеет способ сжать модификации еще дальше. Он помещает их в "пакетные" файлы.

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

Git делает этот вид сжатия для всех объектов (а не только для файлов), поэтому он может сжимать фиксацию против другой фиксации или дерева друг против друга. Это действительно довольно эффективно, но с одной проблемой.

Проблема двоичного файла

Некоторые (не все) бинарные файлы дельта-сжимают очень плохо друг против друга. В частности, файл, сжатый с помощью чего-то вроде bzip2, gzip или zip, делает одно небольшое изменение в любом месте, как правило, также изменяет остальную часть файла. Изображения (jpg и т.д.) Часто сжимаются и страдают от такого эффекта. (Я не знаю многих несжатых форматов изображений. Файлы PBM полностью несжаты, но это единственное, что я знаю из рук, которые все еще используются.)

Если вы не вносите никаких изменений в двоичные файлы, git сжимает их суперэффективно из-за неизменных идентификаторов объектов низкого уровня. Если вы вносите небольшие изменения, git алгоритмы сжатия могут (не обязательно "будут" ) терпеть неудачу на них, так что вы получите несколько копий двоичных файлов. Я знаю, что большие gzip'ed cpio и tar-архивы делают очень плохо: одно небольшое изменение в таком файле и 2 GB-репо становится репозиторией на 4 ГБ.

Хорошо ли ваши конкретные двоичные файлы сжимаются или нет, вам нужно поэкспериментировать. Если вы просто переименовываете файлы, у вас не должно быть никаких проблем. Если вы часто меняете большие изображения JPG, я ожидаю, что это будет работать плохо (но стоит поэкспериментировать).


1 В "нормальных" файлах пакета объект может быть дельта-сжат для других объектов в том же файле пакета. Это сохраняет файлы пакетов как самостоятельные. "Тонкий" пакет может использовать объекты не в самом пакете; они предназначены для инкрементных обновлений по сетям, например, с помощью git fetch.

Ответ 2

Репозиторий git отличает файлы по контрольной сумме, а не по имени или местоположению. Если вы зафиксируете, а затем переместите файл в другое место и зафиксируете, файл в переднем месте и файл в последовании будут иметь одну и ту же контрольную сумму (потому что они имеют одинаковый контент). Поэтому репозиторий не сохраняет новую "копию" файла; он просто записывает тот факт, что файл с этой контрольной суммой теперь имеет второе местоположение.