Раскрытие тайн команды «Сброс»

В этом разделе вы рассмотрите команды Reset current branch to this Commit... и Checkout Branch, которые содержатся в контекстном меню коммита и ветки в расширении Git Graph.

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

Три дерева

Разобраться с командами Reset current branch to this Commit... и Checkout Branch будет проще, если считать, что Git управляет содержимым трех различных деревьев. Здесь под «деревом» вы будете понимать «набор файлов», а не специальную структуру данных.

В некоторых случаях индекс ведет себя не совсем так, как дерево, но для ваших текущих целей его проще представлять именно таким.

В своих обычных операциях Git управляет тремя деревьями:

Указатель HEAD

HEAD — это указатель на текущую ветку, которая, в свою очередь, является указателем на последний коммит, сделанный в этой ветке. Это значит, что HEAD будет родителем следующего созданного коммита. Как правило, самое простое считать HEAD снимком вашего последнего коммита.

Индекс

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

Git заполняет индекс списком изначального содержимого всех файлов, выгруженных в последний раз в ваш рабочий каталог. Затем вы заменяете некоторые из таких файлов их новыми версиями и команда Фиксация (Commit) преобразует изменения в дерево для нового коммита.

Технически, индекс не является древовидной структурой. На самом деле, он реализован как сжатый список (flattened manifest),?но для ваших целей такого представления будет достаточно.

Рабочий каталог

Наконец, у вас есть рабочий каталог. Два других дерева сохраняют свое содержимое эффективным, но неудобным способом внутри каталога .git. Рабочий каталог распаковывает их в настоящие файлы, что упрощает для вас их редактирование. Считайте рабочий каталог песочницей, где вы можете опробовать изменения перед их коммитом в индекс (область подготовленных изменений) и затем в историю.

Технологический процесс

Основное предназначение Git — это сохранение снимков последовательно улучшающихся состояний вашего проекта, путем управления этими тремя деревьями.

Рассмотрите этот процесс: допустим вы перешли в новый каталог, содержащий один файл. Данная версия этого файла обозначается v1 и изображается голубым цветом. Создайте Git-репозиторий, и его ссылка HEAD станет указывать на еще несуществующую ветку (ветка main пока не существует).

На данном этапе только дерево рабочего каталога содержит данные.

Теперь вы хотите зафиксировать этот файл, поэтому вы нажимаете (Хранить все промежуточные изменения / Stage All Changes) для копирования содержимого рабочего каталога в индекс.

Затем вы нажимаете Фиксация (Commit) и Visual Studio Code сохраняет содержимое индекса как неизменяемый снимок, создает объект коммита, который указывает на этот снимок, и обновляет ветку main так, чтобы она тоже указывала на этот коммит.

Сейчас в проекте нет никаких изменений, так как все три дерева одинаковые.

Теперь вы хотите внести изменения в файл и зафиксировать его. Вы пройдете через все ту же процедуру. Сначала вы отредактируете файл в нашем рабочем каталоге. Эта версия файла обозначается v2 и изображается красным цветом.

Если сейчас вы посмотрите в раздел Система управления версиями (Source Control) то увидите, что файл находится в разделе Изменения (Changes), так как его представления в индексе и в рабочем каталоге различны.

Теперь вы нажмете (Хранить все промежуточные изменения / Stage All Changes), чтобы поместить его в индекс.

Если сейчас вы посмотрите в панель Система управления версиями (Source Control) то увидите, что файл находится в разделе Промежуточные изменения (Staged Changes), так как индекс и HEAD различны. То есть ваш следующий намеченный коммит сейчас отличается от вашего последнего коммита.

Наконец, вы нажимаете Фиксация (Commit), чтобы окончательно совершить коммит.

Сейчас в проекте нет никаких изменений, так как снова все три дерева одинаковые.

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

Назначение команды «Reset current branch to this Commit...»

Команда Reset current branch to this Commit... становится более понятной, если рассмотреть ее с учетом вышеизложенного.

Предположим, что вы снова изменили файл file.txt и зафиксировали его в третий раз. Так что ваша история теперь выглядит так.

Теперь внимательно проследите, что именно происходит при нажатии Reset current branch to this Commit..., если вы выполняете эту команду на коммите на 9e5e6a4. Эта команда простым и предсказуемым способом управляет тремя деревьями, существующими в Git. Она выполняет три основных операции.

Шаг 1: Перемещение указателя HEAD (Soft)

Первое, что сделает Reset current branch to this Commit... — переместит то, на что указывает HEAD. Обратите внимание, изменяется не сам HEAD, что происходит при выполнении команды Checkout Branch. Команда Reset current branch to this Commit... перемещает ветку, на которую указывает HEAD. Таким образом, если HEAD указывает на ветку main (то есть вы сейчас работаете с веткой main), выполнение команды Reset current branch to this Commit... > Soft — Keep all changes, but reset head сделает так, что main будет указывать на коммит 9e5e6a4.

Теперь взгляните на диаграмму и постарайтесь разобраться, что случилось: фактически была отменена последняя команда Фиксация (Commit). Когда вы выполняете Фиксация (Commit), Git создает новый коммит и перемещает на него ветку, на которую указывает HEAD. Если вы выполняете Reset current branch to this Commit... на предыдущий коммит, то вы перемещаете ветку туда, где она была раньше, не изменяя при этом ни индекс, ни рабочий каталог.

Вы можете обновить индекс и снова выполнить Фиксация (Commit), таким образом добиваясь того же, что делает команда Фиксация (Исправление) (Commit (Amend)) (смотрите Изменить последний коммит).

Шаг 2: Обновление индекса (Mixed)

Заметьте, если сейчас вы посмотрите в раздел Система управления версиями (Source Control) то увидите, что файл находится в разделе Промежуточные изменения (Staged Changes), так как индекс и новый HEAD различны.

Следующим, что сделает Reset current branch to this Commit..., будет обновление индекса содержимым того снимка, на который указывает HEAD.

Если вы выбрали Mixed — Keep working tree, but reset index, выполнение Reset current branch to this Commit... остановится на этом шаге.

Снова взгляните на диаграмму и постарайтесь разобраться, что произошло: отменен не только ваш последний коммит, но также и добавление в индекс всех файлов. Вы откатились назад до момента выполнения команд (Хранить все промежуточные изменения / Stage All Changes) и Фиксация (Commit).

Шаг 3: Обновление рабочего каталога (Hard)

Третье, что сделает Reset current branch to this Commit... — это приведение вашего рабочего каталога к тому же виду, что и индекс. Если вы нажали Reset current branch to this Commit... > Hard — Discard all changes, то выполнение команды будет продолжено до этого шага.

Посмотрите, что сейчас случилось. Вы отменили ваш последний коммит, результаты выполнения команд (Хранить все промежуточные изменения / Stage All Changes) и Фиксация (Commit), а также все изменения, которые вы сделали в рабочем каталоге.

Важно отметить, что только выбор Hard — Discard all changes делает команду Reset current branch to this Commit... опасной, это один из немногих случаев, когда Git действительно удаляет данные. Все остальные вызовы Reset current branch to this Commit... легко отменить, но при выборе режима Hard — Discard all changes команда принудительно перезаписывает файлы в рабочем каталоге.

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

Резюме
Команда Reset current branch to this Commit... в заранее определенном порядке перезаписывает три дерева Git, останавливаясь тогда, когда вы ей скажете:
  1. Перемещает ветку, на которую указывает HEAD (останавливается на этом, если выбран вариант Soft — Keep all changes, but reset head);
  2. Делает индекс таким же как и HEAD (останавливается на этом, если выбран вариант Mixed — Keep working tree, but reset index);
  3. Делает рабочий каталог таким же как и индекс, если выбран вариант Hard — Discard all changes.

Слияние коммитов

Посмотрите, как, используя вышеизложенное, сделать кое-что интересное — слияние коммитов.

Допустим, у вас есть последовательность коммитов с сообщениями вида «упс», «В работе» и «позабыл этот файл». Вы можете использовать Reset current branch to this Commit... для того, чтобы просто и быстро слить их в один. В разделе Объединить коммиты представлен другой способ сделать то же самое, но в данном примере проще воспользоваться командой Reset current branch to this Commit....

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

Вы можете выполнить Reset current branch to this Commit... > Soft — Keep all changes, but reset head на первый коммит, который вы хотите оставить — eb43bf8.

Затем просто снова выполните Фиксация (Commit):

Теперь вы можете видеть, что ваша «достижимая» история (история, которую вы впоследствии отправите на сервер), сейчас выглядит так — у вас есть первый коммит с файлом file-a.txt версии v1, и второй, который изменяет файл file-a.txt до версии v3 и добавляет file-b.txt. Коммита, который содержал файл версии v2 не осталось в истории.

Сравнение с командой «Checkout Branch»

Команда Checkout Branch очень похожа на Reset current branch to this Commit... > Hard — Discard all changes. В процессе их выполнения все три дерева изменяются так, чтобы выглядеть как тот коммит, на котором они выполняются. Но между этими командами есть два важных отличия.

Во-первых, в отличие от Reset current branch to this Commit... > Hard — Discard all changes, команда Checkout Branch бережно относится к рабочему каталогу, и проверяет, что она не трогает файлы, в которых есть изменения. В действительности, эта команда поступает немного умнее — она пытается выполнить в рабочем каталоге простые слияния так, чтобы все файлы, которые вы не изменяли, были обновлены. С другой стороны, команда Reset current branch to this Commit... > Hard — Discard all changes просто заменяет все целиком, не выполняя проверок.

Второе важное отличие заключается в том, как эти команды обновляют HEAD. В то время как Reset current branch to this Commit... перемещает ветку, на которую указывает HEAD, команда Checkout Branch перемещает сам HEAD так, чтобы он указывал на другую ветку.

Например, пусть у вас есть ветки main и develop, которые указывают на разные коммиты и вы сейчас находитесь на ветке develop, то есть HEAD указывает на нее.

Если вы выполните Reset current branch to this Commit... на ветке main, сама ветка develop станет ссылаться на тот же коммит, что и main.

Если вы выполните Checkout Branch на ветке main, то develop не изменится, но изменится HEAD. Он станет указывать на main.

Итак, в обоих случаях вы перемещаете HEAD на коммит A, но важное отличие состоит в том, как вы это делаете. Команда Reset current branch to this Commit... переместит также и ветку, на которую указывает HEAD, а Checkout Branch перемещает только сам HEAD.

Заключение

Наверняка вы разобрались с командой Reset current branch to this Commit... и можете ее спокойно использовать. Но, возможно, вы все еще немного путаетесь, чем именно она отличается от Checkout Branch, и не запомнили всех правил, используемых в различных вариантах вызова.

Ниже приведена памятка того, как эти команды воздействуют на каждое из деревьев. В столбце HEAD указывается REF если эта команда перемещает ссылку (ветку), на которую HEAD указывает, и HEAD если перемещается только сам HEAD. Обратите особое внимание на столбец Сохранность рабочего каталога?. Если в нем указано НЕТ, то хорошенько подумайте прежде чем выполнить эту команду.

По материалам книги Pro Git (авторы Scott Chacon и Ben Straub, издательство Apress). Книга распространяется по лицензии Creative Commons Attribution Non Commercial Share Alike 3.0 license.