Для кого эта статья

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

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

В чем проблема

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

  • возникают проблемы при автоматической генерации changelog из сообщений коммитов;

  • встречаются случаи боязни использования терминального Git;

  • страдает история коммитов.

В первой части разберём следующие темы и попробуем ответить на вопрос: кто же наш лучший друг и союзник?

Путешествуем по истории GIT

Актуальная тема для тех, кто по каким-то причинам боится, что коммит пропал, код удалился и кажется, что всё пропало. Замечательная новость для вас — в GIT очень сложно потерять что-то сразу и насовсем. Коммиты остаются в репозитории до момента срабатывания GC (garbage collector) GIT, обычно это примерно 30-90 дней.

Для путешествия нам помогут две замечательные команды: 

  • git reflog

  • git reset

git reset — эта команда перемещает указатель HEAD, не удаляет коммиты, помогает нам двигаться по истории. 

Разновидности:

  • git reset -mixed → стандартное поведение, выполнится даже без использования флага. Перемещает указатель HEAD и оставляет изменения в рабочей директории.

  • git reset --soft → перемещает указатель HEAD и оставляет изменения в staged, то есть изменения готовы к созданию нового коммита (это ключевая разница с --mixed).

  • git reset --hard → перемещает указатель HEAD и удаляет изменения из рабочей директории, звучит страшно, но на самом деле не всё так плохо.

  • git reflog → позволяет просмотреть историю всех перемещений HEAD, может отобразить даже «потерянные» коммиты. Можно сказать, что данная команда — это «чёрный ящик» GIT.

Важные ограничения: reflog только локальный, если у вашего коллеги его нет, после срабатывания GC изменения вернуть уже невозможно.

Рассмотрим, что показываем git reflog:

  • commit hash – хэш коммита, на который указывал HEAD в момент определенного действия;

  • action (rebase, reset, commit) – действие, которое происходило;

  • позиция HEAD.

Мы можем вернуть историю в момент любого действия. 

Зачем же нам в этой теме две команды — git reset и git reflog? А всё очень просто — эти команды удобно использовать вместе для поиска нужного действия и отката HEAD. 

Разберём на реальных кейсах

Потерянный коммит

Разберём, как вернуть утерянное в данной ситуации.
Проверяем локальную историю перед всеми изменениями.

git log --oneline

a1b2c3d (HEAD -> feature/login) fix: validation
d4e5f6g feat: add login form
h7i8j9k initial commit

Ломаем историю (откатываем коммит и изменения на один коммит назад HEAD~1)

git reset --hard HEAD~1

В итоге получаем:

git log --oneline

d4e5f6g (HEAD -> feature/login) feat: add login form
h7i8j9k initial commit

Коммит пропал, изменения пропали ?, кажется, что всё потеряно, но тут на помощь приходит магия GIT.

Проверяем git reflog:

git reflog

a1b2c3d HEAD@{1}: commit: fix: validation
d4e5f6g HEAD@{2}: commit: feat: add login form

В выводе видим старый коммит, тот самый который мы удалили. С помощью нехитрых манипуляций переводим указатель HEAD:

git reset --hard a1b2c3d

Проверяем состояние:

git log --oneline

a1b2c3d (HEAD -> feature/login) fix: validation
d4e5f6g feat: add login form
h7i8j9k initial commit

Коммит и код вернулись на своё законное место.

Кривой rebase/merge

Что может быть при кривом rebase/merge:

В вашей ветке могут появится

  • куча нерешенных конфликтов,

  • непонятно откуда взявшийся легаси-код,

  • пропавший код.

Разберем, что с этим можно сделать.

Смотрим локальную историю до внесения изменений:

git log --oneline

abc1111 (HEAD -> feature/cart) fix: price bug
def2222 feat: add cart logic
ghi3333 (origin/main) main update

Делаем git rebase (допустим всё сломалось), вызываем конфликты и хаос, мечтаем вернуть всё как было.

Смотрим reflog

git reflog

xyz9999 HEAD@{0}: rebase finished
abc1111 HEAD@{1}: rebase: fix: price bug
def2222 HEAD@{2}: rebase: feat: add cart logic
abc1111 HEAD@{3}: checkout: moving from main to feature/cart

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

git reset --hard HEAD@{3}

Тем самым мы вернули локальную ветку в состояние до выполнения команды git rebase. Пропадают артефакты и прочие сюрпризы от кривого rebase.

Откат на несколько коммитов назад и создание нового коммита

Часто наблюдал ситуацию из бесконечного числа коммитов с сообщением по типу «fix»,«fix»,«fix»,«fix»,«fix»….что превращает историю в ад, который невозможно читать, понимать и анализировать. Что же можно сделать до вливания вашей feature-ветки в default ветку? Здесь нам может помочь git reset --soft

Дело в том, что мы можем указать аргументом, к какому коммиту по счету необходимо откатиться, а флаг --soft позволит нам откатить коммиты и подготовить изменения для нового коммита.

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

git reset --soft HEAD~{COUNT}

где COUNT это количество коммитов, например:

git reset --soft HEAD~5

вернёт указатель на 5 коммитов назад и добавить изменения в staged, они будут готовы для создания нового коммита.

rebase VS merge — когда и что лучше использовать

Что важнее — сохранить историю чистой или более детальной? Давайте попробуем разобраться в этом поединке века. 

И та, и другая команда позволяет нам объединять ветки (вливать изменения из одной ветки в другую, но делают это совершенно по-разному).

git merge — позволяет объединить ветки, при объединении создает merge commit, что, в свою очередь, позволяет сохранить реальную историю изменений.

Важно:

  • история становится «ветвистой»

  • появляются лишние коммиты, особенно это является проблемой для feature-веток

git rebase — переписывает историю коммитов, позволяет перенести ваши коммиты поверх коммитов целевой ветки (например main) как будто так и было.

Плюсы такого подхода:

  • чистая история,

  • удобно читать git log (никаких merge коммитов),

  • идеален в кейсах, когда необходимо актуализировать main во feature-ветке.

Но важно осознавать и минусы:

  • переписывает commit hash, так как создает новые коммиты;

  • может сломать историю у других людей;

  • требует git push -f для отправки в remote.

Когда используем rebase:

  • удалённая ветка исключительно твоя, и в ней больше никто не работает;

  • перед тем как вливать feature-ветку в main;

  • хочешь чистую историю.

Когда используем merge:

  • общая ветка (в ней работают несколько разработчиков),

  • важно сохранять реальную историю изменений,

  • работаешь с main/develop.

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

  • rebase — чисто, но важно использовать аккуратно,

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

Разберём, как работает merge и rebase на графических примерах (обычно так проще для понимания).

git merge

История коммитов в ветке main

Создаем новую feature-ветку от main и переходим в нее (git checkout -b feature)

Пишем в ней какой-то код и создаем новый коммит с изменениями

Вливаем нашу feature-ветку в main (git merge) и смотрим, что получилось:

 

Видим, что создался новый коммит при вливании ветки в main.

git rebase

История коммитов в ветке main

Создаем новую feature-ветку от main и переходим в нее (git checkout -b feature)

Пишем в ней какой-то код и создаем новый коммит с изменениями:

Пока что все наши действия похожи на предыдущие, но дальше будет интересно!


Пока мы работали в своей feature-ветке, кто-то успел поработать и влить свои изменения в main и теперь наша ветка создана не от актуального main, что может стать причиной конфликтов или каких-то иных ошибок, на самом деле довольно-таки типичная ситуация в командной разработке

Для актуализации main во feature — ветке выполняем команду git rebase origin/main, что произойдёт?

История feature-ветки стала линейной, это выглядит так, будто feature-ветка всегда была создана от последнего коммита main. Важно понимать, что D’ и E’ — это новые коммиты, созданные на базе D и E, так что GIT просто так не даст переписать удаленную историю, придётся воспользоваться git push -f.

Интерактивный rebase — страшно или не очень?

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

В каких ситуациях используем:

  • необходимо почистить историю перед вливанием PR/MR,

  • объединить мелкие коммиты,

  • исправить сообщения,

  • удалить лишнее.

В общем во всех ситуациях, когда хотим сделать историю читаемой.

Для запуска используется команда: 

git rebase -i HEAD~{COUNT}

где COUNT — количество коммитов, на которые необходимо выполнить rebase.

При выполнении команды откроется файл для редактирования в редакторе, который установлен как default в конфиге GIT. Разберем, что такого в этом файле:

pick 0c5196fb # message
pick 0c28b255 # some message
pick 1ecb32ed # wtf

# Rebase 8871809d..1ecb32ed onto 8871809d (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# Если вы удалите строку здесь, то УКАЗАННЫЙ КОММИТ БУДЕТ УТЕРЯН.
#
# Но если вы удалите все, то процесс перемещения будет прерван.
#

Что можем здесь увидеть? 

  • таблица с коммитами (действие, хэш, сообщение),

  • инструкция по возможным действиям для каждого коммита.

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

Список основных действий: 

  • pick(p) — оставить как есть,

  • reword(r) — изменить сообщение,

  • squash(s) — склеить коммиты,

  • fixup(f) — склеить без сообщения,

  • drop(d) — удалить.

Действия можно писать как полностью, так и сокращенный вариант (например drop - p).

Что реально происходит при интерактивном rebase

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

Какие плюсы:

  • чистая история,

  • понятные коммиты,

  • удобное review,

  • ну и основное — выглядит профессионально.

Чтобы пользоваться инструментом важно оценивать минусы и риски:

  • меняет commit hash,

  • требует git push -f,

  • можно сломать историю,

  • опасно выполнять в общей ветке.

Когда точно нельзя использовать:

  • в main,

  • в общей ветке,

  • после того, как другие разработчики подтянули себе изменения.

Может быть очень больно и коллеги спасибо вам не скажут.

Рассмотрим реальные кейсы, где интерактивный rebase это must-have:

Объединяем коммиты.

Просматриваем историю.

git log --oneline

a1b2c3ed fix
a2b4cfda fix
ab94c5ca fix

Видим здесь ряд проблем, которые хотелось бы исправить:

  • сообщения коммитов не описательные,

  • засоряет общую историю если влить такое в main.

Попробуем сделать из всего этого один единственный коммит с хорошим описанием.

Выполняем:

git rebase -i HEAD~3

Смотрим открывшийся файл и ставим действия squash для склеивания коммитов:

pick a1b2c3ed # fix
squash a2b4cfda # fix
squash ab94c5ca # fix

# Rebase 8871809d..ab94c5ca onto 8871809d (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# Если вы удалите строку здесь, то УКАЗАННЫЙ КОММИТ БУДЕТ УТЕРЯН.
#
# Но если вы удалите все, то процесс перемещения будет прерван.

Что мы сделали:

  • начали процесс склеивания коммитов,

  • последний оставляем как есть.

GIT откроет редактор с сообщениями всех склеиваемых коммитов для редактирования. Удаляем лишнее и оставляем только одно сообщение в файле:

feat: added validation for login form

Сохраняем и закрываем.

Проверяем историю снова:

git log --oneline

c6c6c6c6 feat: added validation for login form

Видим, что остался только один единственный коммит. 

Для того, чтобы залить изменения в remote используем  git push -f

  • пропало безумие из односложных сообщений,

  • для вливания в main у нас доступен всего лишь один коммит с качественным сообщением.

Удаляем хлам

Проверяем историю до внесения изменений

git log --oneline

a1b2c3ed add feature
a2b4cfda debug log
ab94c5ca console.log ?
j1k2l3b4 final fix

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

Выполняем

git rebase -i HEAD~4

Выбираем действие для ненужных коммитов

pick a1b2c3ed add feature
drop a2b4cfda debug log
drop ab94c5ca console.log ?
pick j1k2l3b4 final fix

Также можно просто удалить строки с коммитами, которые хотим удалить

Что получаем в итоге

git log --oneline

a1b2c3ed add feature
j1k2l3b4 final fix

Коммитов, которые захламляли историю, больше не существует. Но будьте внимательны изменения в коде тоже удалятся!

Немного выводов про git rebase -i

  • мощнейший инструмент для работы с историей коммитов,

  • позволяет управлять историей,

  • дает делать историю коммитов читаемой,

  • позволяет убирать мусор,

  • позволяет объединять изменения.

В следующей части рассмотрим ещё несколько полезных инструментов для работы с историей Git. Разберём, когда стоит использовать git push --force, в каких случаях поможет git commit --amend, а также научимся исследовать историю изменений с помощью git blame. Поговорим о преимуществах, рисках и типичных сценариях применения каждого инструмента.
Делитесь своим опытом и задавайте вопросы в комментариях!

Комментарии (7)


  1. Sazonov
    28.05.2026 13:52

    Очередной выборочный хэндмэйд на основе https://git-scm.com/book/en/v2

    Ничего нового.


  1. unreal_undead2
    28.05.2026 13:52

    Для меня главный аргумент против rebase - сложно откатиться к состоянию на конкретный момент в прошлом.


    1. ivalexander Автор
      28.05.2026 13:52

      Привет!
      Согласен, это один из главных минусов rebase. Я постарался в статье как раз разобрать этот минус. В общих ветках использовать rebase лучше очень осторожно или вообще не использовать, а предпочитать merge.

      Но во feature-ветках, когда ты работаешь в ней один - риски использовать rebase минимальны. Гит предоставляет инструменты git reflog и git reset, которые позволят тебе откатиться к любому состоянию в прошлом, даже до момента rebase.

      Для меня rebase это не замена merge, а инструмент, который позволяет держать историю в чистоте до слияния с main, но нужно учитывать риски обязательно.


      1. unreal_undead2
        28.05.2026 13:52

        Непонятно, почему фокус именно на общих ветках.

        Гит предоставляет инструменты git reflog и git reset, которые позволят тебе откатиться к любому состоянию в прошлом

        Возможно, не до конца их понимаю. В вашем примере с rebase я после него получаю в истории своего бранча коммиты D' и E' (при этом они могут сильно отличаться от D/E, если в мастере перепахали используемые в фиче интерфейсы и структуры и rebase потребовал заметной ручной работы). Как мне после этого быстро получить код своего бранча в состоянии D?


        1. ivalexander Автор
          28.05.2026 13:52

          Да, если после команды git rebase origin/main получили конфликты, то после их решения новые коммты могут заметно отличаться. Но прелесть rebase в том, что при его проведении гит будет просить (в дефолтном редакторе) поправить конфликты в каждом из коммитов feature-ветки (если они есть). При проведение rebase git переходит в специальное состояние rebase, если ввести команду git status, то в терминале будет что-то вроде "rebase in progress". В этом состоянии нам доступны еще три команды

          git rebase --continue
          git rebase --skip
          git rebase --abort

          То есть после решения конфликта вводим git rebase --continue и git перескакивает к следующему коммиту.

          Если не уверен что rebase был правильной идеей, то можно вернуться на состояние до rebase с помощью git rebase --abort.

          Если выполнил rebase, но есть необходимость вернуться в состояние до него и получить предыдущий код, то пользуемся git reflog, для того, чтобы найти указатель HEAD до rebase. При вводе комманды git reflog вывод будет примерно такой

          abc9999 HEAD@{0}: rebase (finish): returning to refs/heads/feature
          def8888 HEAD@{1}: rebase (pick): feat: add validation
          ghi7777 HEAD@{2}: rebase (pick): feat: add api integration
          jkl6666 HEAD@{3}: rebase (start): checkout main
          
          mno5555 HEAD@{4}: commit: feat: add api integration
          pqr4444 HEAD@{5}: commit: feat: add validation
          stu3333 HEAD@{6}: checkout: moving from main to feature


          Здесь видно, где был HEAD на каждом этапе, мы можем перескочить в любое состояние, например выполняем git reset --hard HEAD@{4} и это будут ровно то состояние ветки, когда были актуальны коммиты D и E


          1. unreal_undead2
            28.05.2026 13:52

            Вот так понятнее. Как то в статье вопрос кофликтов (которые при долгой работе над мажорной фичей есть всегда) обошли стороной.


            1. ivalexander Автор
              28.05.2026 13:52

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