Git. Урок 4.
Перемещение курсора и отмена изменений. Команды git restore, git rm, git reset, git checkout, git commit, git revert.

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

Git. Урок 4.
Перемещение курсора и отмена изменений. Команды git restore, git rm, git reset, git checkout, git commit, git revert.

В этом уроке мы с вами узнаем, как перемещать указатель HEAD. Это знание откроет перед нами много возможностей. Например, мы сможем откатиться к предыдущему коммиту, добавить в созданный ранее коммит файлы или исправить ошибку в уже сделанном коммите.
Smartiqa Git cover
  • Урок: 4
  • Команды: git restore, git rm, git reset, git checkout, git commit, git revert.

Оглавление

1
Теоретический блок
1. Удаляем и восстанавливаем файлы правильно. Команды git rm и git restore.
2. Просмотр старых коммитов и перемещение указателя HEAD

  1. История выводится не полностью
  2. Как переключиться обратно
  3. Зачем может понадобиться переключать указатель на старые коммиты, и важные особенности состояния "detached head".
3. Откат коммитов. Команда git revert.
4. Удаление и объединение коммитов. Команда git reset.

  1. Редактирование или отмена последнего коммита
  2. Объединение нескольких коммитов в один.
  3. Удаление последних нескольких коммитов.
  4. Отмена изменений команды git reset.
5. Различие команд git reset и git checkout и git revert.


Перейти
2
Практический блок
1. Задание
2. Решение
Перейти

ТЕОРЕТИЧЕСКИЙ БЛОК

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

Удаляем и восстанавливаем файлы правильно. Команды git rm и git restore.

Начнем с удаления файлов. У читателя может возникнуть вопрос: почему нельзя просто взять и переместить файл из репозитория в корзину, как мы привыкли это делать с обычными файлами? Давайте разбираться.

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

1. Отслеживаемый.
  1. Измененный (неподготовленный к коммиту).
  2. Подготовленный к коммиту.
2. Неотслеживаемый.

Когда мы делаем файл отслеживаемым или подготавливаем его к коммиту, информация об этом файле записывается в индекс (файл .git/index). Эта информация никуда не денется, даже если мы удалили файл из рабочей копии. Рассмотрим пример. Допустим, мы выполним следующие действия:

1. Создадим файл sample.txt и внесем в него какие-то изменения;
2. Сделаем его отслеживаемым;
3. Удалим файл sample.txt;
4. Сделаем коммит;

В таком случае, просматривая содержимое нового коммита, мы обнаружим, что файл sample.txt в нем присутствует, несмотря на то, что мы вроде бы его удалили. Это произошло из-за того, что хоть мы и удалили файл из рабочей копии, но не удалили информацию о нем из индекса.

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

Команда git rm

Формат
git rm <ключ> <имя файла>
Ключи
-f, --forced
Форсированное удаление. Файл будет удален, несмотря на все предупреждения. Используйте этот ключ осторожно.

--cached
С этим ключом команда очистит только информацию о файле в индексе. Сам файл в рабочей копии останется нетронутым, независимо от того, изменен он или нет. При этом файл из области "подготовленный к коммиту" (англ. staged) перейдет в область "неотслеживаемый" (англ. untracked).
Что делает
Удаляет файл из рабочей копии и индекса / только из индекса. Данная команда не может удалить файл только из рабочей копии.
Пример
# Удалим файл sample.txt из рабочей копии и индекса
$ git rm sample.txt

# Удалим файл sample.txt из индекса и перемеcтим его в категорию Untracked
$ git rm --cached sample.txt
Внимательный читатель спросит: но почему нельзя удалить файл из репозитория обычным образом, а затем добавить изменения в индекс командой git add и сделать коммит? На самом деле можно. Команда git rm является сокращением вышеописанных действий. Ее применение считается более правильным, поскольку она короче и красивее. Давайте рассмотрим небольшой пример:
Git Bash

# Удалим файл обычным образом
$ rm sample.txt
$ git status
On branch main
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    sample.txt
$ git add -A

# И вся последовательность команд выше эквивалентна всего одной:
$ git rm sample.txt
rm 'sample.txt'

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        deleted:    sample.txt

# Осталось только сделать коммит, чтобы сохранить изменения:
$ git commit -m "L-04: Deleted sample.txt"
[main f34705a] L-04: Deleted sample.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 sample.txt
Обычное удаление файла, который отслеживается Git
Обычное удаление файла, который отслеживается Git
”Правильное” удаление файла. Команда git rm
"Правильное" удаление файла. Команда git rm.
Итак, мы разобрались с удалением файлов. Но что, если мы захотим восстановить файл после удаления или изменения в рабочей копии или индексе? Для этого существует команда git restore. Давайте разберем ее подробнее.

Команда git restore

Формат
git restore <ключ> <имя файла>
Ключи
-s, --source=<tree>
Этот ключ нужен, чтобы передать команде путь к коммиту (ветке, пользовательскому указателю), откуда мы будем восстанавливать файл. По умолчанию файл берется из области индекса.

--worktree (англ. рабочая копия)
--staged (англ. область индекса)

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

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

Если же вы хотите восстановить файл и в рабочей копии, и в области индекса, вам нужно передать оба ключа.
Что делает
Восстанавливает указанный файл из переданного источника. По умолчанию источником является индекс. Если файла нет в указанном источнике, файл будет удален.
Пример
# Если вы случайно удалили файл sample.txt обычным способом, то можно восстановить его из индекса
$ git restore sample.txt

# Вернем файл sample.txt к определенному коммиту с хэшем 09c2240. При этом мы изменим только файл в рабочей копии, файл в области индекса не поменяется.
$ git restore --source 09c2240 sample.txt

# Вернем файл sample.txt в индексе к состоянию последнего коммита (отменим все внесенные изменения или удалим файл, если в предыдущем коммите его не было), при этом изменения коснутся только индекса файла, рабочая копия не поменяется.
$ git restore --staged sample.txt

# Сделаем то же, что и в предыдущем примере, но теперь изменения затронут и файл в рабочей копии.
$ git restore --staged --worktree sample.txt
С помощью команды git restore можно сделать неотслеживаемыми файлы, которые вы случайно добавили командой git add. Приведем пример:
Git Bash

# Допустим у нас есть некоторый репозиторий, просмотрим статус файлов:
$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
       file_to_commit.txt
       another_file_to_commit.txt
       file_not_for_commit.txt

# Для простоты добавим все файлы в индекс разом
$ git add -A

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   file_to_commit.txt
        new file:   another_file_to_commit.txt
        new file:   file_not_for_commit.txt

# А теперь восстановим файл “file_not_for_commit.txt” в области индекса. Тогда источником станет последний коммит, а в нем такого файла нет (файл же новый). Поэтому файл будет удален из области индекса. Кстати, можно заметить, что даже Git подсказывает нам: “use "git restore --staged <file>..." to unstage”. 

$ git restore --staged file_not_for_commit.txt

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   another_file_to_commit.txt
        new file:   file_to_commit.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file_not_for_commit.txt

# Для справки: в данном случае эквивалентной командой будет:
$ git rm --cached file_not_for_commit.txt
rm 'file_not_for_commit.txt'

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   another_file_to_commit.txt
        new file:   file_to_commit.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file_not_for_commit.txt
Подведем итог

Итак, мы теперь умеем удалять и восстанавливать файлы из индекса и рабочей копии.
1. Чтобы удалить файл правильно, воспользуйтесь командой git rm. Она удалит файл из индекса и из рабочей копии.
  1. Вариант git rm --cached удалит файл из индекса и переместит его в категорию Untracked.
  2. По своей сути git rm <filename> представляет сокращение двух команд: rm <filename> и git add <filename>.

2.Чтобы восстановить файл в рабочей копии и/или индексе, воспользуйтесь командой git restore.
  1. Ключ --source=<tree> позволит вам указать место, откуда брать файл для восстановления. По умолчанию этим местом является область индекса.
  2. По умолчанию файл восстанавливается в рабочей копии, но вы можете восстановить файл в области индекса с ключом --staged или одновременно в области индекса и в рабочей копии, передав ключи --staged --worktree. В этом случае по умолчанию (если вы не передали ключ --source) файл будет взят из коммита, на который указывает HEAD.
2

Просмотр старых коммитов и перемещение указателя HEAD

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

1. Весь репозиторий – это древовидный граф, ноды которого – наши коммиты, а ребра – родительские отношения между коммитами.
2. HEAD – это указатель (то есть ссылка на один из коммитов), главное назначение которого - определять, в каком состоянии находится рабочая копия. На какой коммит указывает HEAD – в таком состоянии файлы и находятся в рабочей области.
3. Обычно HEAD указывает не на определенный коммит, а на указатель ветки, который в свою очередь указывает на конкретный коммит.
4. HEAD можно перемещать: при перемещении указателя файлы в рабочей копии изменятся так, чтобы соответствовать коммиту, на который указывает HEAD.
5. Указывая путь до чего бы то ни было, вы можете использовать как абсолютные указатели, например, хэш коммита или имя ветки, так и относительные. Вспомним, как пользоваться относительными указателями:
  1. Знак ^ означает "предыдущий". Например путь HEAD^ означает "предыдущий коммит перед тем, на который указывает HEAD"
  2. Знак ~ позволяет вам указать число коммитов. Например, запись HEAD~7 означает "7 коммитов назад от коммита, на который указывает HEAD".

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

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

Команда git checkout

Формат
git checkout <путь>
Что делает
Переводит курсор HEAD на указанный коммит или другой указатель и копирует.
Пример
# Передвинем HEAD на два коммита назад
$ git checkout HEAD~2
HEAD is now at 7194f7e L-04: Fixing gradient bug

Можно заметить, что синтаксис совершенно такой же, как и в переключении ветки из предыдущего урока. Все верно, ведь переключение ветки – ни что иное, как передвижение указателя HEAD с указателя одной ветки на указатель другой. Чтобы подробно разобраться, что происходит при передвижении указателя, давайте рассмотрим пример. Изначально у нас есть такой репозиторий.
Исходный репозиторий
Исходный репозиторий
Серыми овалами обозначены коммиты, текст на них – часть хэша соответствующего коммита. Коричневым прямоугольником обозначен указатель ветки: она у нас одна и называется main. Белым прямоугольником обозначен указатель HEAD, иногда его называют курсором. Его-то мы и будем двигать. Давайте попробуем сдвинуть HEAD на два коммита назад (то есть на коммит с хэшем 90ab…) и посмотрим, что будет.
Git Bash

$ git checkout HEAD~2
Note: switching to 'HEAD~2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 90abf7e L-04: Fixing gradient bug
Предупреждение Git мы разберем чуть ниже, а сейчас давайте посмотрим, в каком состоянии мы оказались.
Граф Git после перевода указателя HEAD на два коммита назад
Граф Git после перевода указателя HEAD на два коммита назад
Как видно из рисунка, указатель HEAD сейчас действительно передвинут на два коммита назад. Но указатель ветки main остался на месте и все еще указывает на последний коммит. Такое состояние, когда HEAD указывает не на указатель ветки, а непосредственно на сам коммит, называется detached head.
Detached head (англ. дословно – отрубленная голова, имеется в виду указатель HEAD, отключенный от графа) – состояние, при котором указатель HEAD не указывает ни на одну из веток репозитория, а ссылается непосредственно на сам коммит.
Если прочесть предупреждение Git выше, можно заметить, что он сообщает нам, что мы оказались в состоянии detached head. Давайте рассмотрим несколько особенностей этого состояния:

2.1. История выводится не полностью

Действительно, если мы из текущего состояния выполним команду git log:
Git Bash

$ git log --pretty=oneline
90abf7ef211229adfa4cb75e0f35a0561dd15467 (HEAD)  L-04: Fixing gradient bug
3a3bb706651a19013822c09e5c70c9fc425a66dc L-04: Adding gradient function
d469222a7a760daa3cd56747e216e3de2a3343ee L-04: Initial commit
Видно, что вывелись только те коммиты, которые были сделаны позже коммита, на котором сейчас стоит HEAD. Если же мы хотим просмотреть всю историю, нужно воспользоваться ключом --all:
Git Bash

$ git log --all --pretty=oneline
62aa1ffe0587c7ffa3d865a3233da04d65818030 (main) L-04: Adding autograd
33ff7207fed9cbb34c9f3334249ef0707477f278 L-04: Adding neuron class
90abf7ef211229adfa4cb75e0f35a0561dd15467 (HEAD)  L-04: Fixing gradient bug
3a3bb706651a19013822c09e5c70c9fc425a66dc L-04: Adding gradient function
d469222a7a760daa3cd56747e216e3de2a3343ee L-04: Initial commit
Тогда нам видна полная история репозитория. Если присмотреться, то можно увидеть, что напротив первого сверху коммита в скобках написано main. Так Git сообщает нам, что на данный коммит указывает ветка main. Аналогично с третьим сверху коммитом: напротив него написано HEAD. Это означает, что HEAD указывает на этот коммит.

2.2. Как переключиться обратно

Вам вовсе необязательно смотреть полную историю каждый раз, когда вы хотите вернуться обратно на ветку main. В предыдущем уроке мы рассматривали команду git checkout - и говорили, что с ее помощью можно вернуться на ветку, с которой мы переключились на текущую. Так вот, эта команда работает не только для веток, но и для любых перемещений HEAD. Если мы хотим перенести HEAD обратно, достаточно выполнить:
Git Bash

$ git checkout -
Previous HEAD position was 90abf7e L-04: Fixing gradient bug
Switched to branch 'main'
Как видно по выводу Git, мы успешно вернулись обратно на ветку main и вышли из состояния detached head.

У данного способа есть один минус: если вы "прыгали" по коммитам репозитория несколько раз, то git checkout - вернет вас на последний коммит, на котором вы были, а не на ветку main. В данном случае поможет простое и гениальное:
Git Bash

$ git checkout main
Previous HEAD position was 90abf7e L-04: Fixing gradient bug
Switched to branch 'main'
То есть мы напрямую указываем Git, что хотим переключиться на ветку main (или любую другую), таким образом выходя из состояния detached head.

2.3. Зачем может понадобиться переключать указатель на старые коммиты, и важные особенности состояния "detached head".

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

Пример 1
Самый банальный пример – просмотр файлов в определенном коммите. Допустим, вы хотите просмотреть содержимое файла main.py в коммите с определенным хэшем.
Просто выполните:
Git Bash

# Команда cat выводит в консоль содержимое файла.
$ git checkout 5df3f7e
$ cat main.py
print(“Hello world”)
Таким образом, вы можете просмотреть содержимое любого, когда либо закоммиченного файла, что бывает довольно удобно.


Пример 2
Кроме того, когда вы работаете над большим проектом, может быть нужно вернуться на несколько коммитов назад и создать свою ветку оттуда: например, чтобы протестировать экспериментальную функцию в том состоянии проекта. Сделать это можно так:
Git Bash

# Переключим указатель HEAD на определенный коммит:
$ git checkout 9a4e88b

# Теперь создадим новую ветку
$ git checkout -b feature
Switched to a new branch 'feature'

# Добавим файл и сделаем коммит
$ echo "just docs we forgot to put" > docs.md
$ cat docs.md
just docs we forgot to put

$ git add -A
$ git commit -m "L-04: Adding docs"
[feature d5e3273] L-04: Adding docs
 1 file changed, 1 insertion(+)
 create mode 100644 docs.md
Теперь у нас есть целая ветка feature, которая берет свое начало с коммита 9a4e88b. На ней мы можем проводит эксперименты с тем состоянием репозитория, которое было несколько коммитов назад, не боясь навредить остальной части репозитория.

Заметьте, что когда мы создали и переключились на ветку, мы вышли из состояния detached head. Если бы мы сделали коммит, не создавая для этого ветки, этот коммит оказался бы "оторванным" от истории: на него не ссылалась бы ни одна ветка, а в выводе истории (даже полной) его не было бы видно. Иначе говоря, если бы мы сделали коммит без ветки, мы бы смогли получить к нему доступ только запомнив его хэш наизусть.

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

На самом деле, если вы сделали несколько коммитов из состояния detached head и забыли создать ветку, ничего страшного: ветку можно создать прямо сейчас. Рассмотрим пример с тем же репозиторием:
Git Bash

# Переключимся в состояние “detached head” на ветке main.
$ git checkout d5e3273

# Теперь мы создадим несколько коммитов, исключительно для примера:
$ echo 'print('hello world')' > hello_world.py
$ echo 'print('Hi')' > say_hi.py
$ echo 'Some text file' > about.txt
$ git add hello_world.py
$ git commit -m "L-04: Adding hello_world.py"
[detached HEAD d013c75] L-04: Adding hello_world.py
 1 file changed, 1 insertion(+)
 create mode 100644 hello_world.py

$ git add say_hi.py
$ git commit -m "L-04: Adding say_hi.py"
[detached HEAD 58f8c22] L-04: Adding say_hi.py
 1 file changed, 1 insertion(+)
 create mode 100644 say_hi.py

$ git add about.txt
$ git commit -m "adding about.txt"
[detached HEAD 7c10724] L-04: Adding about.txt
 1 file changed, 1 insertion(+)
 create mode 100644 about.txt

# А сейчас мы вспомнили, что сделали несколько коммитов в состоянии “detached head” и испугались, что потеряем их. Но не стоит бояться, можно просто создать ветку прямо сейчас:
$ git checkout -b feature
Switched to a new branch 'feature'

# И чтобы убедиться, что мы не потеряли ни одного коммита, давайте взглянем на историю ветки feature
$ git log --pretty=oneline main..feature
7c10724b12dba69ed1acf4c6fef804c251f7c290 (HEAD -> feature) L-04: Adding about.txt
58f8c226952d500df7a9c2f798011ab20165a286 L-04: Adding say_hi.py
d013c75418ac71ca8f4578583aa23d7567dab332 L-04: Adding hello_world.py
d5e327368d12d6e5ef5b04e16af1d96319069805 L-04: Adding docs
Даже в состоянии detached head коммиты сохраняют родительские отношения, то есть каждый коммит знает, кто его предшественник. Поэтому мы в любой момент можем восстановить их последовательность, если найдем крайний коммит в данной цепочке. То есть для нас главное не забыть создать ветку, находясь в состоянии detached head. А вот создать ее можно в любой момент: до того, как мы сделаем первый коммит из этого состояния, или в любой другой момент – не важно, главное сделать это до переключения на другую ветку или коммит. Тогда у нас будет ссылка на крайний коммит ветки, а по его родителям мы сможем определить всю последовательность.
Подведем итог

  1. Указатель HEAD можно перемещать на разные коммиты, точно так же, как мы перемещали его между ветками. Для этого нужно использовать команду git checkout.
  2. Когда мы перемещаем HEAD с указателя ветки на коммит, мы попадаем в состояние detached head.
  3. В состоянии detached head стоит быть осторожным: если планируете делать коммиты, лучше сразу создайте ветку командой git checkout -b <имя ветки>.
  4. Если вы сделали несколько коммитов из состояния detached head, забыв создать ветку – ничего страшного, ветку можно создать в любой момент.
  5. Обычно detached head используется редко, но вы можете использовать его, чтобы просматривать старые версии файлов или экспериментировать с предыдущими версиями проекта.
    3

    Откат коммитов. Команда git revert.

    Пожалуй одна из самых важных частей в изучении Git – научиться откатываться к предыдущим коммитам. Смысл отката мы обсуждали в предыдущих уроках: ваш проект может перестать работать по непонятным вам причинам после внесения некоторых изменений в код, в таком случае важно быстро вернуть все к рабочему состоянию и только потом заниматься поиском ошибки. В этом-то случае нам и поможет откат коммитов и команда git revert.

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

    Команда git revert

    Формат
    git revert <ключи> <адрес коммита>
    Ключи
    -n
    Не делает коммит. С данным ключом изменения коснутся только рабочей копии.

    --abort

    Данный ключ используется только при разрешении конфликтов.
    Прерывает выполнение команды после разрешения конфликтов.

    --continue
    Данный ключ используется только при разрешении конфликтов.
    Продолжает выполнение команды после разрешения конфликтов.
    Что делает
    Отменяет изменения, внесенные в переданном коммите.
    Пример
    # Отменим изменения, внесенные 2 коммита назад
    $ git revert HEAD~2

    # Отменим все изменения в коммитах, начиная с пятого с конца и заканчивая вторым с конца.
    $ git revert HEAD~5..HEAD~2
    Команда git revert очень важна, поэтому давайте разберем, как с ней работать. Рассмотрим самый простой пример, в котором у нас не возникнет файловых конфликтов. Допустим, мы хотим отменить изменения предпоследнего коммита. Главное, что здесь нужно запомнить – это что у нас не должно быть незакоммиченых изменений в рабочей директории, ведь мы все-таки делаем реверт-коммит. Если у вас есть таковые, лучшим решением станет закоммитить (или удалить) все изменения, и только потом делать реверт.
    Git Bash
    
    # Выполним реверт предпоследнего коммита. Кстати, такой реверт никогда не вызовет конфликтов.
    
    $ git revert HEAD~1
    
    # Как только мы выполним команду, будет открыт консольный редактор, чтобы вы могли отредактировать сообщение нового коммита. Можно что-то дописать, можно сразу закрыть редактор сочетанием Ctrl+X (здесь приведено сочетание для редактора “nano”). Решать вам. В нашем случае окно редактора будет выглядеть так.
    
    Revert "L-04: Addit docs.txt"
    
    This reverts commit aadfbc3a6756289727e56ac3de59004e66e40033.
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    #
    # On branch master
    # Your branch is up to date with 'origin/master'.
    #
    # Changes to be committed:
    #       modified:   docs.txt
    #
    
    
    
    ^G Get Help  ^O Write Out ^W Where Is  ^K Cut Text  ^J Justify   ^C Cur Pos
    ^X Exit      ^R Read File ^\ Replace   ^U Paste Text^T To Spell  ^_ Go To Line
    
    # После закрытия редактора, в терминал будет выведена информация об успешном реверте коммита
    
    [main e933971] Revert "addit docs.txt"
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    Итак, это был самый простой пример. В основном сложности git revert связаны с разрешением файловых конфликтов. Разрешать мы их научимся в следующем уроке, а пока давайте обсудим, из-за чего может возникнуть конфликт. Может, обладая этими знаниями и столкнувшись с конфликтом во время выполнения git revert,вы поймете, что вам совсем не нужен откат.

    Иногда при отмене изменений может возникнуть ситуация, когда в файл в рабочей копии были внесены изменения с момента коммита находится не в том же состоянии, как в коммите, который мы отменяем. Звучит сложно, поэтому давайте приведем пример. Допустим, у нас есть следующий репозиторий.
    Репозиторий: 4 коммита - 4 изменения в файле docs.md
    Репозиторий: 4 коммита - 4 изменения в файле docs.md
    В каждом из вышеуказанных коммитов всего один файл: docs.md. Над хэшем каждого коммита на рисунке указано содержимое docs.md, которое было внесено в данный коммит.

    Теперь, когда перед глазами у вас есть диаграмма, давайте разберемся, в каком случае при откате возникнет конфликт файлов. На самом деле, конфликт возникнет при откате любого, кроме самого последнего коммита. Как мы уже говорили, при реверте коммита, его изменения отменяются, то есть файлы возвращаются к состоянию предыдущего коммита. Например, если мы попробуем откатить коммит 33ff, то содержимое файла docs.md изменится с
    Содержимое docs.md, коммит 33ff
    
    Some docs here
    and here
    
    
    на
    Содержимое docs.md, коммит 90ab
    
    Some docs here
    
    
    Но ведь в последнем коммите, т.е. в рабочей копии находится файл, содержимое которого
    Содержимое docs.md, коммит c732f69
    
    Some docs here
    and here
    also here
    
    
    что совсем не соответствует содержимому коммита, который мы откатываем. То есть с момента того коммита мы уже успели изменить файл docs.md, и теперь Git не очень понимает, как именно делать откат:
    1. оставить файл в рабочей копии нетронутым или,
    2. наоборот, заменить файл в рабочей копии на файл после отката.

    Поэтому и возникает конфликт. Git напрямую спрашивает у нас: "Какой файл мне оставить?". На практике это выглядит вот так:
    Git Bash
    
    # Пробуем откатить коммит .
    $ git revert 33ff381
    Auto-merging docs.md
    CONFLICT (content): Merge conflict in docs.md
    error: could not revert 33ff381... L-04: Adding info to docs
    hint: after resolving the conflicts, mark the corrected paths
    hint: with 'git add <paths>' or 'git rm <paths>'
    hint: and commit the result with 'git commit'
    
    В следующем уроке, когда мы будем говорить о слиянии веток, мы научимся разрешать файловые конфликты.

    Кстати, если вы попали в ситуацию, когда Git сообщил вам о конфликте файлов, а вы такого не ожидали, или вы просто передумали делать откат из-за конфликта, можете в любой момент выполнить git revert --abort. Это остановит откат, и вы сможете спокойно разобраться, откуда возникает конфликт.
    Подведем итог

    1. git revert – команда, отменяющая изменения переданного коммита. Она заменяет файлы в рабочей копии на файлы предка переданного коммита, а затем делает коммит, чтобы сохранить изменения.
    2. Иногда во время отката возникают конфликты. Их не стоит бояться, но нужно быть внимательным и разобраться, откуда возникает конфликт.
    3. Если вы столкнулись с конфликтом во время отката и передумали продолжать git revert, выполните команду git revert --abort. Она вернет все как было и отменит откат.
    4. В сообщении реверт-коммита следует указывать полезную информацию: зачем вы сделали откат, каким образом вы сливали файлы, если были конфликты, и т.д.
    4

    Удаление и объединение коммитов. Команда git reset.

    Помните, мы говорили про команду git checkout и перемещение указателя HEAD? Тогда мы перемещали только сам указатель HEAD и попадали в состояние detached head. Команда git reset позволяет нам перемещать указатель ветки вместе с указателем HEAD. Давайте разберем эту команду подробнее.

      Команда git reset

      Формат
      git reset <ключи> <адрес коммита>
      Ключи
      --soft
      С этим ключом, команда не отменяет изменения ни в индексе, ни в рабочей копии. Все ваши файлы останутся в том же состоянии, в котором были, но указатель ветки будет передвинут.

      --hard
      С этим ключом команда удалит все изменения так, чтобы состояние индекса и рабочей копии полностью соответствовали коммиту, к которому мы сделали reset
      Что делает
      Переносит указатель ветки на переданный коммит.
      Пример
      # Сделаем reset последнего коммита, который мы сделали по ошибке. При этом оставим файлы в том же состоянии.
      $ git reset --soft HEAD^

      # Отменим последние три коммита и удалим все изменения в файлах.
      $ git reset --hard HEAD~3
      HEAD is now at 2f96b73 L-04: Create main.py

      Чтобы лучше понять смысл этой команды приведем граф репозитория. Изначально он выглядел так.
      Репозиторий до ресета
      Репозиторий до ресета
      Теперь выполним "мягкий" reset последних двух коммитов.
      Git Bash
      
      $ git reset --soft HEAD~2
      
      
      Теперь наш граф выглядит так.
      Репозиторий после выполнения команды git reset
      Репозиторий после выполнения команды git reset
      Причем файлы в рабочей копии остались в том же состоянии, что и были в коммите 62aa.

      Как видно из рисунка, Git создал новый указатель – ORIG_HEAD. Этот указатель создается при выполнении команды git reset и ссылается на тот коммит, от которого мы делали reset. Как мы увидим ниже, это очень полезный в работе с git reset указатель.

      Кстати, если мы сейчас сделаем коммит:
      Git Bash
      
      $ git add -A
      $ git commit -m “L-04: Reset commit”
      [main 03953f8] L-04: Reset commit
       1 file changed, 3 insertions(+), 1 deletion(-)
      
      То получим такую картину:
      Новый коммит после ресета
      Новый коммит после ресета
      То есть наш коммит пойдет по ответвлению от исходной ветки main. Поэтому будьте внимательны, создавая новые коммиты.

      Итак, теперь поговорим, как на практике используется git reset. В основном, все использование сводится к трем пунктам:
      1. Редактирование/отмена последнего коммита.
      2. Объединение нескольких коммитов в один.
      3. Удаление коммитов.
      4. Отмена изменений команды git reset.

      Давайте подробно разберем каждый пункт.

      4.1. Редактирование или отмена последнего коммита

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

      Команда git commit

      Формат
      git commit <ключи> <адрес переданного коммита>
      Ключи

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


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

      --amend
      С этим ключом коммит будет объединен с последним коммитом на текущей ветке. Этот ключ используется для редактирования содержимого и сообщения последнего коммита.
      Что делает
      Создает новый коммит.
      Пример
      # Сделаем новый коммит с таким же сообщением и информацией об авторе, как у коммита, на который указывает ветка feature.
      $ git commit -C feature

      # Сделаем то же самое, но отредактируем сообщение коммита.
      $ git commit -с feature
      Теперь, когда мы познакомились с новыми ключами, можем перейти к редактированию последнего коммита. Последовательность действий в данной ситуации такая:
      1. Откатиться к предпоследнему коммиту командой git reset --soft HEAD^
      2. Добавить в коммит новые файлы, если вам это нужно, использовав команду git add.
      3. Выполнить git commit -c ORIG_HEAD, если вы хотите отредактировать сообщение, или git commit -C ORIG_HEAD, если вы хотите оставить сообщение коммита без изменений.
      Полезно знать
      Кстати, данная последовательность команд идентична git commit --amend. Чтобы отредактировать последний коммит, следуйте инструкции:
      1. Если вы хотите добавить в коммит файлы, то для начала добавьте их в индекс командой git add <имя файла>. Если не хотите, то проверьте, что проиндексированных изменений нет, иначе они будут добавлены в последний коммит.
      2. Выполните
      1. git commit --amend. Тогда откроется консольный редактор, где вы сможете отредактировать сообщение последнего коммита.
      2. git commit --amend -m "<новое сообщение>". Тогда сообщение последнего коммита будет заменено на <новое сообщение>.
      В случае, если же вы хотите насовсем удалить последний коммит, просто выполните
      Git Bash
      
      $ git reset --hard HEAD^
      HEAD is now at c732f69 L-04: Adding more info to docs
      
      Таким образом, последний коммит будет удален и не будет отображаться в логе. Тем не менее, вернуться к нему вы сможете по указателю ORIG_HEAD, который оставит Git.

      4.2. Объединение нескольких коммитов в один.

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

      1. Выполните git reset --soft HEAD~n, где n это число коммитов, которые вы хотите объединить. Эта команда вернет указатель ветки на n коммитов назад, оставив изменения в индексе и рабочей копии нетронутыми. То есть после выполнения этой команды вы откатитесь на n коммитов назад, но все изменения внесенные этими коммитами останутся у вас в рабочей копии и индексе.
      2. Выполните git commit -c ORIG_HEAD, а затем отредактируйте сообщение коммита должным образом. Эта команда сделает коммит всех изменений в индексе. То есть она сделает коммит, который по своему содержимому представляет объединение последних n коммитов.

      Собственно, это все. То есть на самом деле мы просто откатили последние несколько коммитов, а затем создали новый коммит, который собирает в себе все их изменения. При этом изначальные коммиты никуда не делись. Их не будет видно в истории, но если вы помните их хэш, то в любой момент сможете переключиться на любой из них с помощью команды git checkout.

      4.3. Удаление последних нескольких коммитов.

      Пожалуй, самая простая ситуация. Чтобы удалить последние n коммитов, выполните
      Git Bash
      
      $ git reset --hard HEAD~n
      
      
      Где n – это, конечно, какое-то конкретное число. После этого удаленные коммиты не будут выводиться в истории (совсем, даже с флагом --all).

      4.4. Отмена изменений команды git reset.

      Если вы каким-то образом случайно выполнили git reset и решили все вернуть, просто переместите указатель ветки обратно, использовав команду
      Git Bash
      
      $ git reset ORIG_HEAD
      
      
      Эта команда вернет указатель ветки на коммит, с которого вы делали git reset, и вы вернете все изменения, даже если использовали ключ --hard.
      Подведем итог
      Итак, мы выучили новую команду git reset. Она используется, чтобы перемещать указатель ветки по графу Git. На практике ее в основном применяют для
      1. Редактирования или удаления последнего сделанного коммита.
      2. Объединения последних нескольких коммитов в один.
      3. Удаления последних нескольких коммитов.
      После использования git reset, Git создаёт указатель ORIG_HEAD, который ссылается на коммит, с которого мы сделали reset.

      Не стоит забывать, что даже если вы использовали git reset --hard, вы всегда можете вернуть все к изначальному состоянию, выполнив git reset ORIG_HEAD. Это вернет указатель ветки на коммит, с которого вы делали reset.
      5

      Различие команд git reset и git checkout и git revert

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

      Изученные команды

      Команда git revert
      Данная команда создает новый коммит, который отменяет действие одного из предыдущих коммитов. То есть новый коммит появится в истории, а предыдущие коммиты не изменятся.
      Команда git reset
      Работа этой команды зависит от вызова. Если вызвать git reset <ссылка>, то команда переместит ветку, на которую указывает HEAD, на переданную ссылку. Затем, в зависимости от опций --soft/--hard/--mixed, команда:
      1. либо останавливается,
      2. либо обновляет индекс и рабочую копию так, чтобы они соответствовали текущему коммиту,
      3. либо обновляет только индекс соответственно.

      То есть она изменит историю, исключив из нее все коммиты, оставшиеся после HEAD. Говоря проще, команда действует так:
      1. Перемещает ветку, на которую указывает HEAD, или только HEAD (если он находится в состоянии detached HEAD). Останавливается на этом шаге, если передан ключ --soft.
      2. Делает индекс таким же, как в коммите, на который указывает HEAD. Останавливается на этом шаге, если не передан ключ --hard.
      3. Делает рабочую копию такой же, как коммит, на который указывает HEAD

      Вы также можете вызвать основную команду (без ключей --hard и --soft), передав ей путь к файлу: git reset <ссылка> <имя файла>. В этом случае команда будет действовать также, но эффект будет отличаться, поэтому этот случай стоит рассмотреть отдельно.

      В случае, если вы не передали ссылку, вместо нее будет подставлен HEAD. То есть она обновит индекс так, чтобы он соответствовал коммиту, на который указывает HEAD. Иначе говоря, скопирует файл из HEAD в индекс. То есть ее действие противоположно git add <имя файла>: она отменит добавление файла в индекс.

      Если же вы указали ссылку, то в вашем индексе окажется файл из одного из предыдущих коммитов. То есть в рабочей копии файл не изменится, но если вы сразу же сделаете коммит, то в новом коммите будет файл из одного из предыдущих коммитов.
      Команда git checkout
      Действие этой команды тоже зависит от вызова.
      1. Если вызвать git checkout <ссылка>, то команда либо перемещает указатель HEAD на переданную ссылку (т.е. на другую ветку или коммит). Историю данная команда не меняет, если только не забыть использовать ключ --all: git log --all.
      2. Если выполнить git checkout <ссылка> <имя файла>, то команда скопирует содержимое файла из переданного коммита в рабочую копию и индекс.
      Команда git restore

      Копирует файл из переданной ссылки в рабочую копию, индекс или сразу и туда, и туда. Изначально эта команда появилась, как аналог git checkout <ссылка> <имя файла>, но ее функционал немного шире. Данная команда также не меняет историю.
      Между git checkout, git reset и git restore есть несколько различий:

      1. Команда git checkout <ссылка> похожа на git reset --hard <ссылка>: в обоих случаях перемещается HEAD и меняется рабочая копия. Однако есть и разница:.
      1. Первое отличие состоит в том, что git checkout перемещает только HEAD, в то время как git reset перемещает HEAD и ветку, на которую указывает HEAD.
      2. Еще одно отличие заключается в том, что git checkout проверяет, что у вас в рабочей копии нет измененных файлов, в то время как git reset просто заменяет все файлы без разбора.

      В случае, когда мы находимся в состоянии detached head и у нас нет измененных файлов, эти две команды идентичны.

      2. Команда git checkout <ссылка> <имя файла> также имеет общие черты с git reset <ссылка> <имя файла>. Отличие состоит в следующем:

      git checkout <ссылка> <имя файла> изменяет и рабочую копию, и индекс, в то время, как git reset <ссылка> <имя файла> меняет только индекс. Кстати, идентичной командой для git checkout <ссылка> <имя файла> была бы команда git reset --hard <ссылка> <имя файла>, если бы ее можно было так использовать: это очень небезопасно для рабочей копии.

      3. Команда git restore задумывалась как аналог git checkout <ссылка> <имя файла>. Тем не менее, между ними есть следующие отличия:
      1. Используя git checkout <ссылка> <имя файла>, в качестве ссылки вы можете передать только определенный коммит, в то время как git restore может скопировать файл в рабочую копию прямо из индекса
      2. Команда git checkout всегда копирует файлы одновременно и в рабочую копию, и в область индекса, что не всегда удобно. В свою очередь, git restore может принять ключи --staged и --worktree, определяющие, скопировать файл только в область индекса, только в рабочую копию, или сразу в оба места.

      Для закрепления, давайте приведем таблицу сравнения команд git revert, git reset, git checkout и git reset по их взаимодействию с указателем HEAD, историей, индексом и рабочей копией.

      ПРАКТИЧЕСКИЙ БЛОК

      1

      Задание. Библиотека Geometric Lib.


      Сегодня было много теории, пора закрепить ее на практике. Мы снова воспользуемся командой git clone (как и в прошлом уроке). Напомним, что она копирует удаленный репозиторий к вам на компьютер.

      Также напомним, что наш удаленный репозиторий представляет собой библиотеку на Python, которая позволяет высчитывать площади и периметры некоторых геометрических фигур.
      Ссылка на библиотеку Geometric Lib на GitHub: https://github.com/smartiqaorg/geometric_lib

      В этот раз в нашем репозитории появились еще две ветки, теперь его структура выглядит следующим образом:
      Структура библиотеки geometric_lib
      
      # Ветка main (Основная стабильная ветка)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
      
      # Ветка develop (Ветка разработки)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
          └── calculate.py
      
      # Ветка feature (Ветка для новых функций)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
          └── rectangle.py
      
      К файлам добавилось три новых: triangle.py, rectangle.py и calculate.py. Первый содержит функции для вычисления периметра и площади треугольника, второй – то же для квадрата. В свою очередь, calculate.py объединяет функционал этих файлов: он предназначен для расчета площади и периметра переданной пользователем фигуры.
      2

      Задание. Условие.

      1. Клонирование репозитория и знакомство с его структурой
      1.1. Выполните git clone https://github.com/smartiqaorg/geometric_lib. Эта команда создаст директорию geometric_lib/ на вашем компьютере и скопирует наш удаленный репозиторий. Не забудьте перейти в эту директорию командой cd geometric_lib, когда клонирование будет завершено.

      Кстати, когда вы склонируете к себе наш репозиторий, у вас будет только одна локальная ветка: main. Чтобы создать остальные, нужно выполнить git checkout <имя ветки>. Эта команда переключит вас на коммит, на который указывает удаленная ветка и создаст там локальную ветку с таким же именем. Эту команду нужно запустить для каждой ветки отдельно. То есть у вас получится два запуска: для ветки feature и ветки develop.

      1.2. Постройте полный граф истории, чтобы познакомиться со структурой комитов.
      2. Работа с веткой feature
      В последнем коммите ветки feature допущена ошибка. Откатите этот неудачный коммит.
      3. Работа с веткой develop
      Теперь заметьте, что у нас есть два коммита в ветке develop одной и той же тематики: "L-04: Add calculate.py", "L-04: Update docs for calculate.py". Объедините их в один коммит и напишите к нему пояснение.
      4. Эксперименты. Работа с файлами calculate.py и rectangle.py в ветке experiments
      Ветку develop мы привели в порядок. Теперь давайте представим, что мы хотим протестировать совместную работу файлов calculate.py и rectangle.py. Чтобы не мешать работе других файлов, создадим отдельную ветку experiment, которая будет брать начало в конце ветки main. Новая ветка будет хранить коммиты с результатами наших экспериментов. Задания:

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

      4.2. Мы хотим провести эксперименты с файлом calculate.py, но текущая документация (файл docs/README.md) устарела. Добавьте в нашу рабочую копию документацию, которая содержит информацию о файле calculate.py. Такая есть, например, в последнем коммите ветки develop. Для этого скопируйте файл docs/README.md из последнего коммита ветки develop в рабочую копию. Подсказка: указатель develop находится на последнем коммите ветки develop.

      4.3. Добавьте в индекс и рабочую копию файл calculate.py из последнего коммита ветки develop.

      4.4. Добавьте все нужные файлы в индекс и сделайте коммит.

      4.5. Мы поняли, что файлы circle.py и square.py могут помешать чистоте наших экспериментов. Удалите их и сделайте коммит.
      3

      Задание. Решение.

      Решение - Git Bash
      
      # п 1. Клонируем репозиторий и активируем локальные ветки
      $ git clone https://github.com/smartiqaorg/geometric_lib
      $ cd geometric_lib/
      $ git branch --all
      $ git checkout develop
      $ git checkout feature
      $ git branch --all
      
      # п 2. Работаем с веткой feature: откатываем коммит
      $ git checkout feature
      $ git log
      $ git revert HEAD
      $ git log
      
      # п 3. Работа с веткой develop: Объединение коммитов
      $ git checkout develop
      $ git log
      $ git reset --soft HEAD~2
      $ git log
      $ git status
      $ git commit -c ORIG_HEAD
      $ git log
      
      # п 4. Работа с веткой experiment: Перенос и удаление файлов
      $ git checkout main
      $ git checkout -b experiment
      $ git restore --worktree docs/README.md --source develop
      $ git status
      $ git restore --worktree --staged calculate.py --source develop
      $ git status
      $ git add docs/README.md
      $ git commit -m "L-04: Experimental commit"
      $ git rm circle.py square.py 
      $ git status
      $ git commit -m "L-04: Deleted circle and square"
      

      Более подробный разбор задания

      Также на странице Задания и Ответы по курсу Git мы даем более подробный разбор текущего задания.
      Приводим в нем не только команды Git, но и их консольный вывод, а также даем комментарии к каждой команде.
      Как вам материал?

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