,

Редактирование истории в 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>
  Эту возможность следует использовать если Вы не хотите "честно" редактировать
  историю. Например, коммит надо откатить только на одной из ветвей, либо
  этот коммит был "очень давно", и не хочется перестраивать из-за него всю
  историю целиком.