Переместиться текущим указателем на указанный коммит при этом не изменяя файлов в рабочей директории

Обновлено: 04.07.2024

К моему удивлению на целом хабрахабре нет ни одного поста где бы было понятно написано про 3 вида git reset . Например, во второй по релевантности статье по запросу «git reset» автор пишет что «данное действие может быть двух видов: мягкого(soft reset) и жесткого(hard reset)». Режим --mixed , используемый по умолчанию, почему-то не удостоился упоминания.

Ничего удивительного, что часто видишь непонимание работы этой команды. Под катом коротко и ясно расскажу о всех трёх режимах git reset , после прочтения топика неясностей остаться не должно.

Сделанные изменения в репозитории по умолчанию имеют статус unstaged. Для того чтобы их закоммитить сначала вы должны добавить изменения в индекс, выполнив git add . Когда вы делаете git commit , в репозиторий будет закоммичено только то, что было в индексе.

git reset --soft

Возьмем для примера ветку:
- A - B - C (master)
HEAD указывает на C и индекс совпадает с C.

После выполнения
HEAD будет указывать на B и изменения из коммита C будут в индексе, как будто вы их добавили командой git add . Если вы сейчас выполните git commit вы получите коммит полностью идентичный C.

git reset --mixed (по умолчанию)

Режим --mixed используется по умолчанию, т.е. git reset --mixed = git reset

Вернемся к тем же начальным условиям:
- A - B - C (master)

Выполнив
или
HEAD опять же будет указывать на B, но на этот раз изменения из С не будут в индексе и если вы запустите здесь git commit ничего не произойдет т.к. ничего нет в индексе. У нас есть все изменения из С, но если запустить git status то вы увидите, что все изменения not staged. Чтобы их закоммитить нужно сначала добавить их в индекс командой git add и только после этого git commit .

git reset --hard

Те же самые начальные условия:
- A - B - C (master)

Последний режим --hard также как и --mixed переместит HEAD на В и очистит индекс, но в отличие от --mixed жесткий reset изменит файлы в вашей рабочей директории. Если выполнить
то изменения из С, равно как и незакоммиченные изменения, будут удалены и файлы в репозитории будут совпадать с B. Учитывая то, что этот режим подразумевает потерю изменений, вы всегда должны проверять git status перед тем как выполнить жесткий reset чтобы убедиться что нет незакоммиченных изменений(или они не нужны).

Правильно ли я представляю себе процесс взаимодействия между рабочей папкой проекта и папкой .git?

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

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

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

Я надеялся получить такой результат используя команду:

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


50.3k 152 152 золотых знака 54 54 серебряных знака 211 211 бронзовых знаков

каталог .git — это и есть само хранилище (другое название — «репозиторий»). там хранится вся история изменений.

ветка (branch) и метка (tag) — это два типа указателей на коммит. ветка — «плавающий» (автоматически переключается после каждой команды commit ), метка — фиксированный.

HEAD — это специальный мета-указатель, подсказывающий вам и программе git, из какого именно указателя в данный момент извлечены файлы/каталоги в рабочем каталоге ( HEAD может указывать и непосредственно на коммит — это особое состояние «detached head»).

программа git использует значение HEAD , например, для вычисления, какие именно файлы были изменены/добавлены/удалены пользователем в дереве рабочего каталога.

переопределять самостоятельно значение HEAD (хранится в файле .git/HEAD ) ни в коем случае не стоит — так вы введёте в заблуждение программу git. она сама изменяет это значение при необходимости — в процессе выполнения некоторых команд (например: checkout , reset , commit ).

Вот у меня есть история коммитов: A->B->C->D->E->F. Я хочу привести свою рабочую папку в то состояние в котором делался коммит E (т.е. все папки и файлы в том виде, в котором они были на тот момент).

просто привести состояние рабочего каталога к определённому коммиту можно командой checkout :

а чтобы после этого переставить на данный коммит указатель типа «ветка» (к примеру — master ), вам сначала надо переключиться на этот указатель (сделать его «текущим»):

Если вы не видите иллюстраций, попробуйте переключиться на версию со стандартными картинками (без SVG).

SVG изображения были отключены. (Включить SVG изображения)

На этой странице представлена краткая наглядная справка для наиболее часто используемых команд git. Если у вас уже есть небольшой опыт работы с git, эта страница поможет вам закрепить ваши знания. Если вам интересно, как был создан этот сайт, загляните на мой репозиторий на GitHub.

Содержание

Основные команды

Следующие четыре команды предназначены для копирования файлов между рабочей директорией, сценой, также известной как «индекс», и историей, представленной в форме коммитов.

  • git add файлы копирует файлы в их текущем состоянии на сцену.
  • git commit сохраняет снимок сцены в виде коммита.
  • git reset -- файлы восстанавливает файлы на сцене, а именно копирует файлы из последнего коммита на сцену. Используйте эту команду для отмены изменений, внесённых командой git add файлы . Вы также можете выполнить git reset , чтобы восстановить все файлы на сцене.
  • git checkout -- файлы копирует файлы со сцены в рабочую директорию. Эту команду удобно использовать, чтобы сбросить нежелательные изменения в рабочей директории.

Вы можете использовать git reset -p , git checkout -p , и git add -p вместо имён файлов или вместе с ними, чтобы в интерактивном режиме выбирать, какие именно изменения будут скопированы.

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

  • git commit -a аналогичен запуску двух команд: git add для всех файлов, которые существовали в предыдущем коммите, и git commit.
  • git commit файлы создаёт новый коммит, в основе которого лежат уже существующие файлы, добавляя изменения только для указанных файлов. Одновременно, указанные файлы будут скопированы на сцену.
  • git checkout HEAD -- файлы копирует файлы из текущего коммита и на сцену, и в рабочую директорию.

Соглашения

Иллюстрации в этой справке выдержаны в единой цветовой схеме.

Коммиты раскрашены зелёным цветом и подписаны 5-ти буквенными идентификаторами. Каждый коммит указывает на своего родителя зелёной стрелочкой. Ветки раскрашены оранжевым цветом; ветки указывают на коммиты. Специальная ссылка HEAD указывает на текущую ветку. На иллюстрации вы можете увидеть последние пять коммитов. Самый последний коммит имеет хеш ed489. main (текущая ветка) указывает на этот коммит, stable (другая ветка) указывает на предка main-ового коммита.

Подробно о командах

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

Commit

Когда вы делаете коммит, git создаёт новый объект коммита, используя файлы со сцены, а текущей коммит становится родителем для нового. После этого указатель текущей ветки перемещается на новый коммит. Вы это видите на картинке, где main — это текущая ветка. До совершения коммита main указывал на коммит ed489. После добавления нового коммита f0cec, родителем которого стал ed489, указатель ветки main был перемещён на новый коммит.

То же самое происходит, если одна ветка является предком другой ветки. Ниже показан пример нового коммита 1800b в ветке stable, которая является предком ветки main. После этого ветка stable уже больше не является предком ветки main. И в случае необходимости объединения работы, проделанной в этих разделённых ветках, вам следует воспользоваться командой merge (что более предпочтительно) или rebase.

Четвертый случай коммита из состояния «detached HEAD» будет рассмотрен далее.

Checkout

Команда checkout используется для копирования файлов из истории или сцены в рабочую директорию. Также она может использоваться для переключения между ветками.

Когда вы указываете имя файла (и/или ключ -p ), git копирует эти файлы из указанного коммита на сцену и в рабочую директорию. Например, git checkout HEAD

foo.c копирует файл foo.c из коммита HEAD

(предка текущего коммита) в рабочую директорию и на сцену. Если имя коммита не указано, то файл будет скопирован со сцены в рабочую директорию. Обратите внимание на то, что при выполнении команды checkout позиция указателя текущей ветки (HEAD) остаётся прежней, указатель никуда не перемещается.

В том случае, если мы не указываем имя файла, но указываем имя локальной ветки, то указатель HEAD будет перемещён на эту ветку, то есть мы переключимся на эту ветку. При этом сцена и рабочая директория будут приведены в соответствие с этим коммитом. Любой файл, который присутствует в новом коммите (a47c3 ниже), будет скопирован из истории; любой файл, который был в старом коммите (ed489), но отсутствует в новом, будет удалён; любой файл, который не записан ни в одном коммите, будет проигнорирован.

В том случае, если мы не указываем имя файла, и не указываем имя локальной ветки, а указываем тег, дистанционную (remote) ветку, SHA-1 хеш коммита или что-то вроде main

3, то мы получаем безымянную ветку, называемую «Detached HEAD» (оторванная голова). Это очень полезная штука, если нам надо осмотреться в истории коммитов. К примеру, вам захочется скомпилировать git версии 1.6.6.1. Вы можете набрать git checkout v1.6.6.1 (это тег, не ветка), скомпилировать, установить, а затем вернуться в другую ветку, скажем git checkout main . Тем не менее, коммиты из состояния «Detached HEAD» происходят по своим особым важным правилам, и мы рассмотрим их ниже.

Коммит из состояния «Detached HEAD»

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

Если после такого коммита вы переключитесь в ветку main, то коммит 2eecb, совершённый из состояния «Detached HEAD», потеряется и попросту будет уничтожен очередной сборкой мусора только потому, что нет ни одного объекта, который бы на него ссылался: ни ветки, ни тега.

В том случае, если вы хотите сохранить этот коммит на будущее, вы можете создать на основе него новую ветку командой git checkout -b new .

Reset

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

Если коммит указан без имён файлов, указатель ветки будет перемещён на этот коммит, а затем сцена приведётся в соответствие с этим коммитом. Если мы используем ключ --soft , то сцена не будет изменена. Если мы используем ключ --hard , то будет обновлена и сцена, и рабочая директория.

Если имя коммита не будет указано, по умолчанию оно будет HEAD. В этом случае указатель ветки не будет перемещён, но сцена (а также и рабочая директория, если был использован ключ --hard ) будет приведена к состоянию последнего коммита.

Если в команде указано имя файла (и/или ключ -p ), то команда работает так же, как checkout с именем файла, за исключением того, что только сцена (но не рабочая директория) будет изменена. Если вы подставите имя коммита на место двойной черты, вы сможете получить состояние файла из этого коммита, тогда как в случае с двойной чертой вы получите состояние файла из коммита, на который указывает HEAD.

Merge

Команда merge (слияние) создает новый коммит на основе текущего коммита, применяя изменения других коммитов. Перед слиянием сцена должна быть приведена в соответствие с текущим коммитом. Самый простой случай слияния — это когда другой коммит является предком текущего коммита: в этом случае ничего не происходит. Другой простой случай слияния — когда текущий коммит является предком другого коммита: в этом случае происходит быстрая перемотка (fast-forward). Ссылка текущей ветки будет просто перемещена на новый коммит, а сцена и рабочая директория будут приведены в соответствие с новым коммитом.

Во всех других случаях выполняется «настоящее» слияние. Вы можете изменить стратегию слияния, но по умолчанию будет выполнено «рекурсивное» слияние, для которого будет взят текущий коммит (ed489 ниже на схеме), другой коммит (33104) и их общий предок (b325c); и для этих трех коммитов будет выполнено трёхстороннее слияние. Результат этого слияния будет записан в рабочую директорию и на сцену, и будет добавлен результирующий коммит со вторым родителем (33104).

Cherry Pick

Rebase

Перебазирование (rebase) — это альтернатива слиянию для задач объединения нескольких веток. Если слияние создаёт новый коммит с двумя родителями, оставляя нелинейную историю, то перебазирование применяет все коммиты один за одним из одной ветки в другую, оставляя за собой линейную историю коммитов. По сути это автоматическое выполнение нескольких команд cherry-pick подряд.

На схеме выше вы видите как команда берёт все коммиты, которые есть в ветке topic, но отсутствуют в ветке main (коммиты 169a6 and 2c33a), и воспроизводит их в ветке main. Затем указатель ветки перемещается на новое место. Следует заметить, что старые коммиты будут уничтожены сборщиком мусора, если на них уже ничего не будет ссылаться.

Используйте ключ --onto чтобы ограничить глубину захвата объединяемой ветки. На следующей схеме вы можете увидеть как в ветку main приходят лишь последние коммиты из текущей ветки, а именно коммиты после (но не включая) 169a6, т. е. 2c33a.

Есть также интерактивный режим перебазирования git rebase --interactive , с помощью которого вы сможете сделать вещи похитрее простого линейного применения коммитов, а именно сбрасывание (dropping), изменение порядка (reordering), правка (modifying) и выдавливание (squashing) коммитов. Нет наглядной схемы, чтобы показать эти возможности; за описанием лучше обратиться к справке по git-rebase(1).

Технические заметки

Содержание файлов не хранится в индексе (.git/index) или в объектах коммитов. Правильнее было бы сказать, что каждый файл хранится в базе данных объектов (.git/objects) в двоичном представлении; найти этот файл можно по его SHA-1 хешу. В файле индекса записаны имена файлов, их хеши и дополнительная информация. В информации о коммитах вы встретите тип данных дерево, для идентификации которого также используется SHA-1 хеш. Деревья описывают директории в рабочей директории, а также содержат информацию о других деревьях и файлах, принадлежащих обозначенному дереву. Каждый коммит хранит идентификатор своего верхнего дерева, которое содержит все файлы и другие деревья, изменённые в этом коммите.

Если вы делаете коммит из состояния «оторванной головы» (detached HEAD), то на этот коммит будет ссылаться ссылка истории HEAD. Но рано или поздно время хранения этой ссылки истечёт, и этот коммит будет уничтожен сборщиком мусора точно так же, как это делается при выполнении команд git commit --amend и git rebase .

Copyright © 2010, Mark Lodato. Russian translation © 2012, Alex Sychev.

Это произведение доступно по лицензии Creative Commons Attribution-NonCommercial-ShareAlike (Атрибуция — Некоммерческое использование — С сохранением условий) 3.0 США.

Перевод статьи «Git Reset Explained – How to Save the Day with the Reset Command».


Photo by Clay Banks on Unsplash

«Помогите! Я закоммитил не в ту ветку!» «Ну вот, опять… Где мой коммит?» Знакомые ситуации, правда?

Я такое слышал неоднократно. Кто-то окликает меня по имени и просит помочь, когда у него что-то пошло не так с git. И такое происходило не только когда я учил студентов, но также и в работе с опытными разработчиками.

Со временем я стал кем-то вроде «того парня, который разбирается в Git».

Мы используем git постоянно, и обычно он помогает нам в работе. Но порой (и куда чаще, чем нам хотелось бы!) что-то идет не так.

Бывает, мы отправляем коммит не в ту ветку. Бывает, теряем часть написанного кода. А можем и добавить в коммит что-то лишнее.

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

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

В прошлом посте я рассказывал о внутреннем устройстве Git. И хотя понимать его полезно, читая теория практически всегда недостаточна. Как применить свои знания внутреннего устройства git и использовать их для решения возникающих проблем?

В этом посте я хотел бы построить мост между теорией и практикой и рассказать о команде git reset . Мы разберем, что делает эта команда, что происходит за кулисами, а также применим эти знания в различных сценариях.

Исходные условия — рабочая директория, индекс и репозиторий

Чтобы разобраться во внутренних механизмах git reset , важно понимать процесс записи изменений внутри git. В частности, я имею в виду записи в рабочей директории, индексе и репозитории.

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

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


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


Да, git не записал (не закоммитил) изменения, сделанные в рабочей директории, напрямую в репозиторий.

Вместо этого изменения сначала регистрируются в индексе (или в стейджинге). Оба эти термина означают одно и то же, и оба часто используются в документации git. В этой статье мы тоже будем пользоваться обоими, так как они полностью взаимозаменяемы.

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


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


Если мы теперь выполним git commit , мы создадим коммит на основе состояния индекса. Таким образом новый коммит (в примере — commit 3) будет включать файл, который мы чуть ранее добавили в стейджинг.


Рабочая директория находится в точно таком же состоянии, как индекс и репозиторий.

При выполнении git commit текущая ветка master начинает указывать на только что созданный объект commit.

Внутренняя работа git reset

Мне нравится представлять git reset как команду, которая поворачивает вспять описанный выше процесс (внесение изменений в рабочей директории, добавление их в индекс, а затем сохранение в репозиторий).

У git reset есть три режима: --soft , --mixed и --hard . Я рассматриваю их как три стадии:

  • Стадия 1. Обновление HEAD — git reset --soft
  • Стадия 2. Обновление индекса — git reset --mixed
  • Стадия 3. Обновление рабочей директории — git reset --hard

Прежде всего, git reset меняет то, на что указывает HEAD. Если мы выполним git reset --hard HEAD

1 , HEAD будет указывать не на master, а на HEAD

1. Если использовать флаг --soft , git reset на этом и остановится.

Если вернуться к нашему примеру, HEAD будет указывать на commit 2, и таким образом new_file.txt не будет частью дерева текущего коммита. Но он будет частью индекса и рабочей директории.


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


Иными словами, мы вернули процесс на стадию, где мы уже применили git add , но еще не применяли git commit .

Если мы используем git reset --mixed HEAD

1 , git не остановится на обновлении того, на что указывает HEAD. Помимо этого обновится еще и индекс (до состояния уже обновленного HEAD).

В нашем примере это значит, что индекс будет в том же виде, что и commit 2:


Таким образом мы вернули процесс на стадию до выполнения команды git add . Новосозданный файл является частью рабочей директории, но не индекса и не репозитория.


Если использовать git reset -- hard HEAD

1 , то после перевода указателя HEAD (на что бы он ни указывал раньше) на HEAD

1, а также обновления индекса до (уже обновленного) HEAD, git пойдет еще дальше и обновит рабочую директорию до состояния индекса.

Применительно к нашему примеру это означает, что рабочая директория будет приведена к состоянию индекса, который уже приведен в состояние commit 2:


Собственно, мы вернули весь процесс на этап до создания файла my_file.txt.

Применяем наши знания в реальных сценариях

Теперь, когда мы разобрались с тем, как работает git reset , давайте применим эти знания, чтобы спасти какую-нибудь ситуацию!

1. Упс! Я закоммитил что-то по ошибке

Рассмотрим следующий сценарий. Мы создали файл со строкой «This is very importnt», отправили его в стейджинг, а после — в коммит.


А затем — ой! — обнаружили, что в предложении у нас опечатка.

1 . Теперь моно отредактировать содержимое файла и сделать коммит еще раз.

Совет. В данном конкретном случае мы также можем использовать git commit --amend , как описано здесь.

2. Упс! Я сделал коммит не в ту ветку, а эти изменения мне нужны в новой ветке

Со всеми нами такое случалось. Сделал что-то, закоммитил…


О нет, мы сделали коммит в ветку master, а надо было создать новую и затем сделать пул-реквест.

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


Собственно, от желаемого состояния нас отделяют три изменения.

  1. Ветка new должна указывать на наш недавно добавленный коммит.
  2. Ветка master должна указывать на предыдущий коммит.
  3. HEAD должен указывать на new.

Мы можем достичь желаемого положения в три шага:

Во-первых, нужно сделать так, чтобы ветка new указывала на недавно добавленный коммит. Достичь этого можно при помощи команды git branch new . Таким образом мы достигаем следующего состояния:


Во-вторых, нужно сделать так, чтобы master указывала на предыдущий коммит (иными словами, на HEAD

1). Достичь этого можно при помощи команды git reset --hard HEAD

1 . Таким образом мы достигаем следующего состояния:


Наконец, мы хотели бы оказаться в ветке new, т. е. сделать так, чтобы HEAD указывал на new . Это легко достижимо путем выполнения команды git checkout new .

  • git branch new
  • git reset --hard HEAD

3. Упс! Я отправил коммит не в ту ветку, а он мне нужен в другой (уже существующей) ветке

В этом случае мы проходим те же шаги, что и в предыдущем сценарии. Мы проделали какую-то работу и закоммитили изменения…


Давайте снова изобразим текущее и желаемое положение:


У нас опять же есть три отличия.

Нам нужно, чтобы самый последний коммит оказался в ветке existing. Поскольку в настоящее время на этот коммит указывает master , мы можем попросить git взять последний коммит из ветки master и применить его к ветке existing :

  • git checkout existing — переключение на ветку existing ,
  • git cherry-pick master — применение последнего коммита в ветке master к текущей ветке ( existing ).

Теперь наше положение следующее:


Все, что нам нужно, это сделать так, чтобы master указывала на предыдущий коммит, а не на самый последний. Для этого:

  • git checkout master — смена активной ветки на master ,
  • git reset --hard HEAD

Таким образом мы достигли желаемого положения:


Итоги

В этой статье мы изучили, как работает git reset , а также разобрали три разных режима этой команды: --soft , --mixed и --hard .

Также мы применили свои новые знания для решения жизненных задач.

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

Читайте также: