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

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

Три дерева

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

Указатель HEAD

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

Индекс

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

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

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

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

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

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

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

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

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

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

Затем вы нажимаете Фиксировать и 1C:EDT сохраняет содержимое индекса как неизменяемый снимок, создает объект коммита, который указывает на этот снимок, и обновляет ветку master так, чтобы она тоже указывала на этот коммит.

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

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

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

Теперь вы нажмете (Добавить выбранные файлы в индекс) для этого файла, чтобы поместить его в индекс.

Если сейчас вы посмотрите в панель Индексирование Git то увидите, что файл находится в поле Индексированные изменения, так как индекс и HEAD различны. То есть ваш следующий намеченный коммит сейчас отличается от вашего последнего коммита.

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

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

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

Назначение команды Сброс

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

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

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

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

Первое, что сделает Сброс — переместит то, на что указывает HEAD. Обратите внимание, изменяется не сам HEAD, что происходит при выполнении команды Извлечь. Команда Сброс перемещает ветку, на которую указывает HEAD. Таким образом, если HEAD указывает на ветку master (то есть вы сейчас работаете с веткой master), выполнение команды Сброс > Мягко (только HEAD) сделает так, что master будет указывать на коммит 9e5e6a4.

Не важно с какими опциями вы вызвали команду Сброс, она всегда будет пытаться сперва сделать данный шаг. При вызове Сброс > Мягко (только HEAD) на этом выполнение команды и остановится.

Теперь взгляните на диаграмму и постарайтесь разобраться, что случилось: фактически была отменена последняя команда Фиксировать. Когда вы выполняете Фиксировать, Git создает новый коммит и перемещает на него ветку, на которую указывает HEAD. Если вы выполняете Сброс на предыдущий коммит, то вы перемещаете ветку туда, где она была раньше, не изменяя при этом ни индекс, ни рабочий каталог.

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

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

Заметьте, если сейчас вы посмотрите в панель Индексирование Git то увидите, что файл находится в поле Индексированные изменения, так как индекс и новый HEAD различны.

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

Если вы нажали Сброс > Средне (HEAD и индекс), выполнение Сброс остановится на этом шаге.

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

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

Третье, что сделает Сброс — это приведение вашего рабочего каталога к тому же виду, что и индекс. Если вы нажали Сброс > Жестко (HEAD, индекс и рабочий каталог), то выполнение команды будет продолжено до этого шага.

Посмотрите, что сейчас случилось. Вы отменили ваш последний коммит, результаты выполнения команд (Добавить выбранные файлы в индекс) и Фиксировать, а также все изменения, которые вы сделали в рабочем каталоге.

Важно отметить, что только выбор Жестко (HEAD, индекс и рабочий каталог) делает команду Сброс опасной, это один из немногих случаев, когда Git действительно удаляет данные. Все остальные вызовы Сброс легко отменить, но при указании опции Жестко (HEAD, индекс и рабочий каталог) команда принудительно перезаписывает файлы в рабочем каталоге.

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

Резюме
Команда Сброс в заранее определенном порядке перезаписывает три дерева Git, останавливаясь тогда, когда вы ей скажете:
  1. Перемещает ветку, на которую указывает HEAD (останавливается на этом, если выбран вариант Мягко (только HEAD));
  2. Делает индекс таким же как и HEAD (останавливается на этом, если выбран вариант Средне (HEAD и индекс));
  3. Делает рабочий каталог таким же как и индекс, если выбран вариант Жестко (HEAD, индекс и рабочий каталог).

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

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

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

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

Вы можете выполнить Сброс > Мягко (только HEAD) на первый коммит, который вы хотите оставить — eb43bf8.

Затем просто снова выполните Фиксировать:

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

Сравнение с командой «Извлечь»

Команда Извлечь очень похожа на Сброс > Жестко (HEAD, индекс и рабочий каталог). В процессе их выполнения все три дерева изменяются так, чтобы выглядеть как тот коммит, на котором они выполняются. Но между этими командами есть два важных отличия.

Во-первых, в отличие от Сброс > Жестко (HEAD, индекс и рабочий каталог), команда Извлечь бережно относится к рабочему каталогу, и проверяет, что она не трогает файлы, в которых есть изменения. В действительности, эта команда поступает немного умнее — она пытается выполнить в рабочем каталоге простые слияния так, чтобы все файлы, которые вы не изменяли, были обновлены. С другой стороны, команда Сброс > Жестко (HEAD, индекс и рабочий каталог) просто заменяет все целиком, не выполняя проверок.

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

Например, пусть у вас есть ветки master и develop, которые указывают на разные коммиты и вы сейчас находитесь на ветке develop, то есть HEAD указывает на нее. Если вы выполните Сброс на ветке master, сама ветка develop станет ссылаться на тот же коммит, что и master. Если вы выполните Извлечь на ветке master, то develop не изменится, но изменится HEAD. Он станет указывать на master.

Итак, в обоих случаях вы перемещаете HEAD на коммит A, но важное отличие состоит в том, как вы это делаете. Команда Сброс переместит также и ветку, на которую указывает HEAD, а Извлечь перемещает только сам HEAD.

Заключение

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

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

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