Редактирование истории в git

Более строго следует говорить не о "редактировании" или "изменении" истории, а о cоздании "альтернативной" истории. Если специально ничего не предпринимать, в репозитории git остаются все объекты "старой" истории, соответствующие предыдущим коммитам и версиям файлов. На эти объекты не будут "ссылаться" ветки, но если Вы вспомните их SHA1-ключи, либо как-то специально позаботитесь их "пометить" (тэгом, или другой веткой), то старая история будет c точки зрения git "ничем не хуже" новой.

Почти во всех командах git можно ссылаться на коммиты любым способом: - с помощью SHA1-ключа, - с помощью имени ветки (если это последний коммит на ветке), - с помощью тэга (если вы его предусмотрительно поставили git tag), - c помощью специальных имён, например HEAD - последний коммит на

данной ветке, HEAD^ - предпоследний (точнее, первый предок
последнего коммита) и т.п. Подробности см. git-rev-parse --help

Ниже в командах, которые допускают любую идентификацию коммита, я буду указывать в качестве аргумента <id>, или <id-…>. Если допускается только имя ветки, указывается <ветка>.

Для начинающих я рекомендую приступая к редактированию истории пометить все ключевые точки тэгами. Их хорошо видно в gitk. Только не забудьте их потом удалить git tag -d

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

Типовые задачи редактирования истории:

1. Отказаться от всех изменений в рабочем каталоге (аналог revert в svn).

Кошерный способ: git checkout -f
Отказаться от части изменений можно с помощью: git checkout <path>
НО: git checkout . не удалит, например, вновь добавленных файлов.
Более жёсткий способ удалить _все_ изменения: git reset --hard HEAD

2. "Сохранить" изменения (состояние) рабочего каталога.

git stash
При этом рабочий каталог "очищается" до HEAD, а сохранённые изменения
можно в последствии "применить" к текущему, либо к любому другому
состоянию рабочего каталога с помощью git stash apply
В частности, это позволяет "переносить" изменения между ветками
(хотя, лучше их оформлять как коммиты, и оперировать потом уже с ними).

3. Отредактировать/дополнить последний коммит:

git commit --amend
Можно применять даже если Вам просто понадобилось переписать commit-log
(например, Вы его "недописали" или он оказался не в той кодировке).
Фактически при выполнении этой операции будет создан _другой_ commit
object, и HEAD ветки будет связан с ним. (Старый объект в репозитории
git тоже сохранится).

4. "Отказаться" от нескольких последних коммитов в истории (в частности,

от последнего)
Создать новую ветку new в нужной нам точке истории и переставить на
неё существующую:
git checkout <id> -b new
git branch -M <нужная нам ветка>
Например, отказаться от последнего коммита на ветке master (если мы
на нём находимся), можно так:
git checkout HEAD^ -b new_master
git branch -M master
После первой команды мы находимся "на один коммит назад" и создали там
новую ветку с именем new_master (текущей веткой является new_master).
После второй команды мы "переименовали" new_master в master, -M позволяет
проигнорировать, что master уже есть.
Тоже самое можно сделать одной командой:
git reset --hard <id>
Но это менее безопасно (см. ниже).

5. "Переставить" метки веток.

git reset [--ключ] <id>
Позволяет "передвинуть" текущий HEAD (и метку ветки) на заданный коммит.
Есть три варианта, задаваемых ключами:
--hard - "выкидывает" всё текущее состояние рабочий копии, вы оказываетесь
на коммите <id>, как будто после него ничего не было;
Т.е. это просто "перестановка ветки".
--soft - "сохраняет" изменения в рабочей копии (и в "индексе" git) и добавляет
к ним изменения из "истории" от <id> до точки, из которой мы переходим.
Более подробно см. п. "Слияние нескольких коммитов в один".
--mixed - (по умолчанию) - ведёт себя как --soft, но не изменяет состояние
"индекса" git (оно будет соответствовать коммиту <id>, на который мы
перешли) - новые и изменённые файлы не считаются "добавленными" в индекс,
т.е. в отличии от --soft для них требуется явно делать git add,
git rm, .etc
Поскольку git reset (особенно --hard), позволяет "потерять" последнее
положение ветки (т.е. оставить HEAD "непомеченным"), следует использовать
эту команду с осторожностью.

6. Слияние нескольких коммитов в один.

Если это "последние" коммиты в истории этой ветки:
git reset --soft <id>
git commit -a -s [--amend]
Первая команда позволяет "отскочить" HEAD на несколько коммитов назад, при
этом сохранив все "изменения" этих коммитов в рабочем каталоге.
Например, git reset --soft HEAD^^ позволит "объединить" изменения последнего
и предпоследнего коммитов.
Если мы хотим "добавить" к этим изменениям, изменения из коммитов с другой
ветки, нам поможет git cherry-pick --no-commit <id>
Эта команда "добавляет" изменения коммита в рабочий каталог и в индекс, но не
выполняет операцию commit.

7. Удаление нескольких коммитов "внутри истории". git-rebase magic

Например, у Вас есть история ветки:
...-(N-5)-(N-4)-(N-3)-(N-2)-(N-1)-(N) - ветка
и вам захотелось удалить коммиты (N-4)-(N-2) включительно.
Это можно сделать с помощью команды git-rebase:
git-rebase --onto <ветка>~5 <ветка>~2 <ветка>
Например, git-rebase --onto master~5 master~2 master
Нотация <id>~<n> означает n-ый коммит назад, т.е. в данном случае:
- master - (N)
- master~2 - (N-2)
- master~5 - (N-5)
Смысл операции git-rebase --onto <id-newbase> <id-upstream> <id-head>:
1) Переключиться на коммит <id-head> (== git checkout <ветка>, если
<id-head> - это HEAD ветки)
2) Начать новую ветку от точки <id-newbase>
3) "Поместить" на новую ветку коммиты от <id-upstream> до <id-head>,
не включая <id-upstream>
4) Если <id-head> - это HEAD ветки, переставить <ветку> на то, что получилось
В данном случае:
От коммита (N-5) мы начинаем "применять" коммиты (N-1) и (N), и переставляем
метку ветки, в результате чего получается "новая история":
(N-1)'-(N)' - ветка
/
...-(N-5)-(N-4)-(N-3)-(N-2)-(N-1)-(N)

8. Объединение коммита с "внутренним" коммитом в истории.

Например, в коммите <id-src> Вы исправили ошибку в "старом исправлении" <id-dst>,
которое было несколько коммитов назад.
Последовательность действий:
1) Создать новую ветку new_branch от коммита <id-dst>, который надо
поменять (дополнить).
git checkout <id-dst> -b new_branch
2) Сделать cherry-pick коммита <id-src>, который вы хотите "приплюсовать" к
внутреннему.
git cherry-pick --no-commit <id-src>
3) "Дополнить" последний коммит изменениями из рабочего каталога.
git commit --amend
4) Добавить в новую историю последовательность "правильных" коммитов:
git rebase --onto HEAD <id-первый коммит>^ <id-последний коммит>
5) Переставить ветку на новый HEAD
git branch -f <имя ветки>

Пояснения требуют два последних действия:
git rebase в данном случае добавляет нужную последовательность коммитов
"в голову" новой ветки, но если <id-последний коммит> - это не HEAD
старой ветки, то после git rebase новый HEAD не будет соответствовать
ни какой ветке ! (так уж работает git rebase)
Для этого требуется последняя операция, она явно переставляет ветку
на HEAD.

Если наше исправление было бы не закоммичено, можно было воспользоваться
git stash и git stash apply вместо git cherry-pick.

9. Редактирование "внутреннего" коммита.

Действия аналогичны п.8, но проще. Пусть мы находимся на ветке <имя ветки>.
1) Извлечь коммит <id-dst>, подлежащий редактированию; ветку new_branch
создавать при этом не обязательно, но желательно:
git checkout <id-dst> [-b new_branch]
2) Исправить код, "дополнить" последний коммит изменениями из рабочего
каталога.
git commit -a --amend
3) Добавить в новую историю последовательность "правильных" коммитов:
git rebase --onto HEAD <id-dst> <имя ветки>
4) Удалить ветку new_branch, если она была создана на шаге 1)
git branch -D new_branch

Специально переставлять ветку <имя ветки> в данном случае не требуется, т.к.
в команде git rebase в п. 3) в качестве последнего аргумента было имя ветки,
а не просто SHA1-id. В такой ситуации эта команда "автоматически" переставит
ref ветки.

10. rebase ветки с помощью git rebase.

git rebase <upstream-branch>
Эта операция подробно рассмотрена в разъяснениях Никиты по идеологии и
сценариям использования git.
Не следует относится к git rebase "формально": например, если Вы считаете,
что некоторые коммиты с ветки разумнее было бы переместить на master, можно
"продублировать" их на master с помощью git cherry-pick, после чего сделать
git rebase. После этого, с веки эти коммиты волшебным образом исчезнут.

11. "Откат" отдельного коммита.

Строго говоря, это не редактирование истории: просто автоматически добавляется
коммит (либо, изменение в рабочей копии), "отменяющее" заданный коммит.
git revert [--no-commit] <id>
Эту возможность следует использовать если Вы не хотите "честно" редактировать
историю. Например, коммит надо откатить только на одной из ветвей, либо
этот коммит был "очень давно", и не хочется перестраивать из-за него всю
историю целиком.