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

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

(cc) Mike Chistolinov

15 thoughts on “Редактирование истории в git”

  1. спасибо за документ
    хорошо бы еще поменять все угловые скобки на html-entities – некоторые куски не видны

  2. ого!!
    первый раз вижу нечто по гиту, настолько разжеванное =)) здорово!

    1. Это копия материала из стендового трака. Там еше есть, но, кажется, совсем специфичное.

  3. Господин владелец сайта,
    А задавались ли вы вопросом (и можете ли мне объяснить), почему ширина вашего сайта жёстко лимитирована и составляет всего-то 780px? Вы считаете, что без жёсткого лимита браузер не справится с разметкой? Или, может, жёсткий лимит повышает удобство использования?
    Уже давно не редкость большие мониторы. А с другой стороны, кто-то может и на КПК сайт открыть. В первом случае будут неиспользуемые полосы слева и справа, во втором – горизонтальная прокрутка. В обоих случаях – вам только минус.
    К чему использование тега pre в основном тексте статьи? Неужели вы считаете, что так лучше? Открою вам секрет – ваш pre

    не влазит

    в ваши 780px.
    Статья отличная, но внешний вид подачи материала просто удручает.

    1. почему ширина вашего сайта жёстко лимитирована и составляет всего-то 780px

      Потому что на экране с dpi порядка 100-120 это комфортная ширина, которая не заставляет при чтении вертеть головой.
      Поэтому граница сверху вполне оправдана, хоть по хорошему должна быть не в пикселях, а в типографских величинах.
      Граница снизу – это артефакт появившийся в новой версии K2, который надо бы поправить.

      К чему использование тега pre в основном тексте статьи?

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

      Открою вам секрет – ваш pre
      не влазит
      в ваши 780px.

      Зависит от DPI. На высоких – да не влазит.

      1. Комфортная кому? Похоже, вы за всех всё решили. Про границы (точнее поля) – я говорил о тех, что слева и справа: они пустые и потому бесполезные. А на счёт pre – он нужен далеко не для всего текста. Сказали бы сразу “лениво” – я бы понял.
        Вот, посмотрите: http://imagebin.ca/view/3F4tjBzd.html
        Из имеющихся по ширине 1600px для сайта реально используется около 460px, то есть – около 29%. Вы считаете это нормально? Лично я считаю, что такие сайты наплевательски относятся к своим посетителям.
        Всё ещё не теряю надежду на понимание и дружественные к пользователю изменения.

        1. Комфортная кому?

          Конкретно мне.

          Похоже, вы за всех всё решили.

          Нет, исключительно за себя.

          А на счёт pre – он нужен далеко не для всего текста.

          Текст есть. Он набран в расчете на отображение моноширинным шрифтом. Какие еще вопросы? Есть желание его переформатировать с заменой списков на html, добавить параграфы, мягкие переносы – пожалуйста, я не запрещаю, лицензия позворяет.

          Вот, посмотрите: http://imagebin.ca/view/3F4tjBzd.html

          Что смотреть? DPI какой у монитора? У меня при 90 и при 100 всё замечательно. При 120 – да уже неудобно, подсказать адрес багтрекера K2? Там можно попросить перевести пиксели в миллиметры.

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

  4. Присоединяюсь к замечанию cub.uanic – у меня на мониторе текст по ширине не полностью виден.

  5. > Эта операция подробно рассмотрена в разъяснениях Никиты по идеологии и сценариям использования git.
    Вот здесь непонятно: где эти разъяснения?

Leave a Reply to GQ Cancel reply

Your email address will not be published. Required fields are marked *