
Многие интересные процессы в Git, такие как слияние, перебазирование или даже комит, базируются на отличиях и заплатках.
Разработчики постоянно работают с отличиями, независимо от того, используют Git напрямую или полагаются на просмотр отличий IDE. В этой публикации вы узнаете, что такое отличия и заплаты Git, их структуру и как применять заплаты.
В предыдущей публикации вы узнали об объектах Git. В частности, мы обсуждали, что коммит – это снимок рабочего дерева в определенный момент времени, в дополнение к некоторым метаданным.
Тем не менее, очень трудно понять отдельные комиты, глядя на все рабочее дерево. Скорее, полезнее посмотреть, насколько коммит отличается от своего родительского комита, т.е. диф между этими комитами.
Итак, что я имею в виду, когда говорю diff
? Начнём с истории.
Git’s diff
базируется на утилите diff в системах UNIX. diff
был разработан в начале 1970-х годов для операционной системы Unix. Первая выпущенная версия пришла вместе с 5-м изданием Unix в 1974 году.
git diff
это команда, которая принимает два входных данных и вычисляет разницу между ними. Входными данными могут быть комиты, а также файлы и даже файлы, никогда не вводившиеся в репозиторий.

Это важно – git diff
вычисляет разницу между двумя строками, которые в основном состоят из кода, но не обязательно.
При чтении этой публикации вам предлагается выполнить команды самостоятельно.
Рассмотрим этот очень короткий текстовый файл под названием file.txt
на моей машине, состоящей из 6 строк:

file.txt
состоит из 6 строк (Источник: Кратко)Теперь измените этот файл. Удалите вторую строку и вставьте новую строку как четвертую. Добавить !
до конца последней строки, поэтому вы получите следующий результат:

file.txt
мы получаем разные 6 строк (Источник: Краткое описание)Сохраните этот файл под новым названием, например new_file.txt
.
Теперь мы можем бежать git diff
чтобы вычислить разницу между файлами так:
git diff -–no-index file.txt new_file.txt
(Я объясню --no-index
измените эту команду позже.)

git diff
(Источник: Кратко)Итак, выход из git diff
показывает достаточно много вещей.
Теперь сосредоточьтесь на части, начинающейся с This is a simple line
. Вы видите, что добавлена строка (// new test
) предшествует a +
знак. Удаленной строке предшествует a -
знак.
Интересно, что Git рассматривает измененную строку как последовательность двух изменений — стирание строки и добавление новой строки вместо него. Следовательно, патч включает удаление последней строки и добавление новой строки, равной этой строке, с добавлением !
.

+
удаление строк за -
а строки модификации – это последовательности удалений и добавления (Источник: Краткое описание)Сроки patch
и diff
часто используются как взаимозаменяемые, хотя есть различие, по крайней мере, исторически.
А diff
показывает отличия между двумя файлами или снимками, и при этом может быть достаточно минимальным. А patch
является расширением a diff
, дополненный дополнительной информацией, такой как строки контекста и имена файлов, позволяющие использовать его шире. Это текстовый документ, описывающий, как изменить существующий файл или кодовую базу.
В эти дни программа Unix diff и git diff
может производить нашивки различных видов.
А patch
является компактным представлением отличий между двумя файлами. Здесь описано, как превратить один файл в другой.
То есть, если вы применяете «инструкции», созданы git diff
на file.txt
– то есть убрать вторую строчку, вставить // new text
как четвертая строка и добавьте еще одну !
до последней строки – вы получите содержимое new_file.txt
.
Другая важная вещь, которую следует отметить, что заплата является асимметричной: заплата с file.txt
к new_file.txt
не то же, что патч для другого направления.
Следовательно, в этом примере генерация a patch
между new_file.txt
и file.txt
в этом порядке, означало бы прямо противоположные инструкции, чем раньше – добавить вторую строчку вместо ее удаления и т.д.

patch
состоит из асимметричных инструкций для перехода от одного файла к другому (Источник: Краткое описание)Попробуй:git diff -–no-index new_file.txt file.txt

git diff
в обратном направлении дает обратные инструкции — добавить линию вместо ее удаления и т.д. (Источник: Краткое описание)The patch
Формат использует контекст, а также номера строк, чтобы найти разные регионы файла. Это позволяет a patch
быть применен к несколько более ранней или поздней версии первого файла, чем тот, из которого он был получен, если применяющая программа все еще может найти контекст изменения.
Структура Diff ????
Итак, пора погрузиться глубже ????.
Создать разницу с file.txt
к new_file.txt
еще раз и внимательнее рассмотрите результат:
git diff -–no-index file.txt new_file.txt
Первая строка представляет собой сравниваемые файлы. Git всегда дает название одному файлу a
а другое имя b
. Да и в этом случае file.txt
называется a
тогда как new_file.txt
называется b
.

diff
Вывод представляет сравниваемые файлы (Источник: Кратко)Затем вторую строчку, начиная с index
, включает в себя SHA-файлы BLOB этих файлов. Таким образом, хотя в нашем случае они даже не хранятся в хранилище Git, Git показывает соответствующие значения SHA-1.
Если вам нужно упоминание о блобах в частности и объектах Git в целом, просмотрите эту публикацию.
Третье значение в этой строке, 100644
являются «битами режима», указывающими на то, что это «обычный» файл: не выполняемый и не символическая ссылка.
Использование двух точек (..
) здесь между блобами SHA есть только как разделитель (в отличие от других случаев, когда он используется в Git).
Другие строки заголовка могут указывать старый и новый бит режима, если они изменены, старые и новые имена файлов, если файл переименовался, и так далее.

diff
Исходные данные включают блоб-схемы SHA сравниваемых файлов, а также биты режима (Источник: Кратко)SHA blob (также называемые «идентификаторы blob») полезны, если этот патч позже применен Git к тому же проекту, и при его применении возникают конфликты.
После идентификаторов blob у нас есть две строки: одна начинается с -
знаки, а другие начиная с +
знаки. Это традиционный заголовок «унифицированной разницы», который снова показывает сравниваемые файлы и направление изменений: -
знаки показывают линии в версии A, но отсутствуют в версии B и +
знаки линии отсутствуют в версии A, но присутствуют в версии B.
Если бы патч добавлял или удалял этот файл полностью, тогда было бы одно из них /dev/null
сигнализировать об этом.

-
символы показывают линии в версии A, но отсутствуют в версии B; и +
знаки, линии отсутствуют в версии A, но есть в версии B (Источник: краткое описание)Рассмотрим случай, когда мы удаляем файл:rm file.txt
А потом используем git diff
:

diff
Вывод для удаленного файла (Источник: Кратко)Версия A, представляющая состояние индекса, в настоящее время имеется file.txt
по сравнению с рабочим каталогом, где этого файла не существует, так оно и есть /dev/null
. Всем строкам предшествует -
знаки, поскольку они существуют только в версии A.
Возвращаясь к предыдущей разнице:

diff
Результаты включают разделы изменений, которые называются «звеньями» или «фрагментами» (Источник: Краткое описание)После этого унифицированного заголовка разницы мы переходим к основной части разницы, состоящей из «разделов различий», которые также называются «звеньями» или «фрагментами» в Git.
Обратите внимание, что эти термины используются как взаимозаменяемые, и вы можете встретить любое из них в документации и руководствах Git, а также в исходном коде Git.
Каждый кусок начинается с одной строки, начиная с двух @
знаки. Эти знаки сопровождаются максимум четырьмя числами, а затем заголовком фрагмента — это образованное предположение Git, которое иногда хорошо работает.
Обычно он будет включать начало функции или класса, если это возможно. В этом примере он ничего не содержит, поскольку это текстовый файл, и рассмотрим на мгновение другой пример:
git diff -–no-index example.py example_changed.py

На изображении над заголовком куска содержится начало функции, включающей измененные строки — def example_function(x)
.
Тогда вернемся к нашему предыдущему примеру:

diff
(Источник: Кратко)После двух @
знаки можно найти четыре числа.
Первым цифрам предшествует a -
подпись, как они ссылаются file A
. Первое число представляет номер строки, соответствующий первой строке в file A
этот кусок относится к. В примере выше это да 1
то есть линия This is a simple file
соответствует номеру строки 1
в версии file A
.
После этого числа ставится запятая (,
), а затем количество строк, из которых состоит этот фрагмент file A
. Это число включает в себя все контекстные строки (строки, перед которыми ставится пробел в diff), или строки, обозначенные -
знак, поскольку они являются частью file A
но не линии, обозначенные a +
знак, поскольку их нет в file A
.
В примере выше это число 6
учитывая строчку контекста This is a simple file
, -
линия It has a nice poem:
затем три строчки контекста и наконец Are belong to you
.
Как видите, строки, начинающиеся с пробела, являются строками контекста, то есть они выглядят так, как показано у обоих file A
и file B
.
Тогда мы имеем a +
знаком, чтобы обозначить два числа, которые касаются file B
. Во-первых, номер строки, соответствующий первой строке в file B
а затем количество строк, из которых состоит этот фрагмент — in file B
.
Этот номер включает все контекстные строки, а также строки, отмеченные символом +
знак, поскольку они являются частью file B
но не линии, обозначенные a -
знак.
После заголовка фрагмента мы получаем фактические строки – или контекст, -
или +
линии.
Обычно и по умолчанию кусок начинается и заканчивается тремя строками контекста, если обычно есть три строки до и после измененных строк в исходном файле.

git diff
(Источник: Кратко)Как создавать отличия ⌨️
Приведенный выше пример точно показывает разницу между двумя файлами. Один файл заплаты может содержать отличия для любого количества файлов и git diff
создает отличия для всех измененных файлов в хранилище в одном патче.
Часто вы увидите результат git diff
показывает две версии то же файл и разница между ними.
Чтобы продемонстрировать, рассмотрите это другое хранилище:
cd ~/brief-example
В текущем состоянии активный каталог является репозиторием Git с чистым статусом:
git status

Возьмите существующий файл, например:

my_file.py
(Источник: Кратко)И смените одну из его строк. Для примера рассмотрим вторую строчку:

my_file.py
после смены второй строки (Источник: Кратко)И беги git diff
:

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

HEAD
. (Источник: Кратко)Чтобы увидеть разницу между рабочим каталогом и промежуточной областью, используйте git diff
без каких-либо дополнительных флагов.

git diff
показывает разницу между площадкой (Источник: коротко)Как вы можете видеть, git diff
перечисляет здесь оба file A
и file B
указывая на my_file.py
. Да file A
здесь речь идет о версии my_file.py
в зоне сцены, тогда как file B
ссылается на свою версию в рабочем каталоге.
Обратите внимание, что если вы измените my_file.py
в текстовом редакторе и не храните файл git diff
не будет знать о внесенных вами изменениях, поскольку они не были сохранены в рабочем каталоге.
Есть несколько коммутаторов, которые мы можем предоставить git diff
чтобы получить разницу между рабочим каталогом и определенным комитом, или между промежуточной областью и последним комитом, или между двумя комитами и т.п.
Сначала создайте новый файл, new_file.txt
, и сохраните его. Пока файл находится в рабочем каталоге, и он фактически не отслеживается в Git.

new_file.txt
(Источник: Кратко)Теперь создайте и зафиксируйте этот файл:git add new_file.txt
git commit -m "new file!"
Теперь, состояние HEAD
совпадает с состоянием промежуточной области, а также рабочего дерева:

HEAD
является таким же, как индекс и рабочий каталог (Источник: краткий)Далее редактируйте new_file.txt
добавив новую строку в начале и еще одну новую строку в конце:

new_file.txt
добавив строку в начале и еще одну в конце (Источник: Коротко)В результате состояние такое:

HEAD
(Источник: Кратко)Хорошим трюком было бы воспользоваться git add -p
что позволяет разделить изменения даже в файле и рассмотреть, какие из них вы хотите внести.
Поэтому в этом случае добавьте первую строчку к индексу, но не последнюю. Для этого вы можете разделить кусок с помощью s
а затем примите постановку первого куска (с помощью y
), а не вторую часть (используя n
).
Если вы не уверены, что означает каждая буква, вы всегда можете использовать a ?
и Git расскажет вам.

git add -p
вы можете внести только первое изменение (Источник: Кратко)Так что теперь государство в HEAD
без одной из этих новых строк. В рабочей области мы имеем первую строчку, но не последнюю, а в рабочем каталоге мы обе новые строки.

Если вы используете git diff
Что произойдет?

git diff
показывает разницу между индексом и рабочим каталогом (Источник: краткий)Что же, как уже было сказано, вы получаете разницу между промежуточной областью и рабочим деревом.
Что произойдет, если вы захотите получить разницу между HEAD
а место проведения? Для этого вы можете использовать git diff –cached
:

git diff --cached
показывает разницу между HEAD
и индекс (Источник: Коротко)А что, если нам нужна разница между HEAD
а рабочее дерево? Для этого мы можем бежать git diff HEAD
:

git diff HEAD
показывает разницу между HEAD
и рабочий каталог (Источник: Кратко)Чтобы подытожить различные переключатели для git diff
просмотрите эту диаграмму, к которой вы можете вернуться как справочник, когда это потребуется:

git diff
(Источник: Кратко)Напомним, что в начале этой публикации вы использовали git diff -–no-index
. С --no-index
вы можете сравнить два файла, не являющиеся частью хранилища или какой-либо промежуточной области.
Теперь зафиксируйте изменения, внесенные в промежуточную область:
git commit -m "added a first line"
Чтобы наблюдать разницу между этим комитом и его родительским комитом, вы можете выполнить следующую команду:
git diff HEAD~1 HEAD

git diff HEAD~1 HEAD
(Источник: Кратко)Кстати, мы можем опустить 1
выше и пишите HEAD~
, и получить тот же результат. Использование 1
это очевидный способ указать, что вы имеете в виду первого отца комита.
Обратите внимание, что написание родительского комита здесь, HEAD~1
, сначала приводит к разнице, которая показывает, как перейти от родительского комита к текущему. Конечно, я мог бы также создать обратную разницу, написав:
git diff HEAD HEAD~1

git diff HEAD HEAD~1
генерирует обратный патч (Источник: Кратко)
git diff
(Источник: Кратко)Краткий способ просмотреть разницу между комитом и его родительским, это с помощью git show
к примеру:
git show HEAD
Это то же, что писать:
git diff HEAD~ HEAD
Теперь мы можем обновить нашу диаграмму:

new_file.txt
после использования git reset --hard HEAD~1
(Источник: Кратко)Напоминаем, что комиты Git – это моментальные снимки всего рабочего каталога репозитория в определенный момент времени. Тем не менее, иногда не очень полезно рассматривать комит как целый снимок, а скорее изменения, внесенные этим конкретным комитом. Другими словами, с помощью диф между родительским комитом к следующему комиту.
Важно помнить, что Git сохраняет целые снимки, а diff динамически генерируется из данных снимков путем сравнения корневых деревьев комита и его родительского.
Конечно, Git может сравнивать любые два снимка во времени, а не только смежные комиты, а также генерировать разность файлов, не включенных в хранилище.
Как применять патчи ????????
С помощью git diff
вы можете увидеть патч, а затем применить его с помощью git apply
.
Историческая справка ????
В самом деле, обмен патчами был основным способом обмена кодом в начале открытого кода. Но теперь практически все проекты перешли на общий доступ к Git-комитам непосредственно из-за запросов на получение (на некоторых платформах они называются «запросами на слияние»).
Наибольшая проблема с использованием заплат состоит в том, что трудно применить заплатку, если ваш рабочий каталог не соответствует предыдущему комиту отправителя.
Утрата истории комитов затрудняет разрешение конфликтов. Вы лучше поймете это, когда глубже погрузитесь в процесс git apply
.
Просто заявка
Что значит наклеить заплатку? Пора попробовать!
Возьмите выход из git diff
:
git diff HEAD~1 HEAD
И сохраните его в файле:
git diff HEAD~1 HEAD > my_patch.patch
И reset
чтобы отменить последнюю фиксацию:
git reset –hard HEAD~1
Если вам не совсем удобно git reset
, посмотрите предварительную публикацию, которая подробно это освещала. Короче говоря, это позволяет нам «сбросить» состояние где HEAD
на указывающий, а также состояние индекса и рабочего каталога.
В приведенном выше примере все они установлены в состояние HEAD~1
или Commit 3
на диаграмме.
Итак, после выполнения команды reset содержимое файла выглядит следующим образом:
nano new_file.txt

git diff
(Источник: Кратко)И мы применим этот патч:
nano my_patch.patch
Этот патч сообщает git найти строки:
This is a new file
With new content!
Когда-то это были линии 1
и 2
и добавьте строку START
прямо над ними.
Выполните эту команду, чтобы применить патч:
git apply my_patch.patch
И в результате вы получите эту версию своего файла, так же, как коммит, который вы создали раньше:
nano new_file.txt

new_file.txt
после применения пластыря (Источник: Кратко)Понимание строк контекста ????????????
Чтобы понять значимость строк контекста, рассмотрим более сложный сценарий. Что произойдет, если номера строк изменились с момента создания файла исправления? ????
Для проверки создайте другой файл:

another_file.txt
(Источник: Кратко)Создайте и зафиксируйте этот файл:
git add another_file.txt
git commit -m "another file"
Теперь измените этот файл, добавив новую строку, а также удалив строку перед последней:

another_file.txt
(Источник: Кратко)Обратите внимание на разницу между оригинальной версией файла и версией, содержащей ваши изменения:
git diff -- another_file.txt

git diff -- another_file.txt
(Источник: Кратко)(Используя -- another_file.txt
сообщает Git выполнить команду diff
принимая во внимание только another_file.txt
поэтому вы не получите разницу для других файлов.)
Сохраните эту разницу в файле исправления:
git diff -- another_file.txt > new_patch.patch
Теперь сбросьте свое состояние перед введением изменений:git reset --hard
Если бы вы подали заявку new_patch.patch
теперь это просто сработает. Рассмотрим более любопытный случай.
Изменить another_file.txt
снова, добавив новую строку в начале:

another_file.txt
(Источник: Кратко)В результате номера строк отличаются от исходной версии, где был создан патч. Рассмотрите патч, который вы создали раньше:

new_patch.patch
(Источник: Кратко)Предполагается, что линия So this is a file
является первой строкой в another_file.txt
, что уже не так. Следовательно… будет git apply
работать?

git apply
не применяет патч (Источник: Кратко)Ну уж нет. Патч не применяется. Но почему? Это действительно из-за смены номеров строк?
Чтобы лучше понять процесс, выполняемый Git, вы можете добавить --verbose
флаг к git apply
вот так:
git apply --verbose new_patch.patch

git apply --verbose
показывает процесс, который выполняет Git для применения патча (Источник: кратко)Кажется, что Git искал все содержимое файла, в частности, включая строку So we are writing an example
, больше не существует в файле. Поскольку Git не может найти эту строчку, он не может применить патч.
Почему Git ищет весь файл? По умолчанию Git ищет 3
строки контекста до и после каждого изменения, внесенного в исправление. Если вы возьмете три строки до и после добавленной строки и три строки до и после удаленной строки (на самом деле только одна строка после, поскольку других строк не существует), вы попадете ко всему файлу.
Вы можете попросить Git полагаться на меньше строк контекста, используя -C
аргумент. Например, попросить Git искать 1
строку окружающего контекста, выполните следующую команду:
git apply -C1 new_patch.patch
Пластырь наклеивается чисто! ????
Почему так? Рассмотрите патч еще раз:

new_patch.patch
(Источник: Кратко)При применении пластыря из -C1
Git ищет строки:
It has some really nice lines
Like this one
чтобы добавить линию !!!This is the new line I am adding!!!
между этими двумя линиями. Эти строки существуют (и, что важно, они появляются одна за другой). Git может успешно добавить строку между ними, даже если номера строк изменились.
Так же Git будет искать строки:
And we are now learning about Git
So we are writing an example
Git is lovely!
Поскольку Git может найти эти строчки, Git может стереть средний.
Если бы мы изменили одну из этих строк, скажем, изменили And we are now learning about Git
к And we are now learning about patches in Git
тогда Git не сможет найти приведенную выше строчку, и, следовательно, патч не будет применяться.
В этом сообщении вы узнали, что такое diff, а также разницу между diff и patch. Вы научились создавать разные патчи с помощью различных переключателей для git diff
.
Вы также узнали, что такое выход git diff
как выглядит и как он построен. В конце концов вы узнали, как применяются патчи, и, в частности, важность контекста.
Понимание отличий является важной вехой для понимания многих других процессов в Git, например слияния или перебазирования.
В будущих руководствах вы будете использовать свои знания по этой публикации, чтобы погрузиться в эти другие области Git.
Омер Розенбаум является основным техническим директором Swimm. Он является автором YouTube-канала Brief. Он также является экспертом по киберобучению и основателем Checkpoint Security Academy. Он автор книги «Компьютерные сети» (ивритом). Вы можете найти его в Twitter.