Для кого эта статья
Хочется поделиться инструментами и практиками для работы с 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)

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

ivalexander Автор
28.05.2026 13:52Привет!
Согласен, это один из главных минусов rebase. Я постарался в статье как раз разобрать этот минус. В общих ветках использовать rebase лучше очень осторожно или вообще не использовать, а предпочитать merge.Но во feature-ветках, когда ты работаешь в ней один - риски использовать rebase минимальны. Гит предоставляет инструменты git reflog и git reset, которые позволят тебе откатиться к любому состоянию в прошлом, даже до момента rebase.
Для меня rebase это не замена merge, а инструмент, который позволяет держать историю в чистоте до слияния с main, но нужно учитывать риски обязательно.

unreal_undead2
28.05.2026 13:52Непонятно, почему фокус именно на общих ветках.
Гит предоставляет инструменты git reflog и git reset, которые позволят тебе откатиться к любому состоянию в прошлом
Возможно, не до конца их понимаю. В вашем примере с rebase я после него получаю в истории своего бранча коммиты D' и E' (при этом они могут сильно отличаться от D/E, если в мастере перепахали используемые в фиче интерфейсы и структуры и rebase потребовал заметной ручной работы). Как мне после этого быстро получить код своего бранча в состоянии D?

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
unreal_undead2
28.05.2026 13:52Вот так понятнее. Как то в статье вопрос кофликтов (которые при долгой работе над мажорной фичей есть всегда) обошли стороной.

ivalexander Автор
28.05.2026 13:52Да, спасибо большое за замечание, действительно нужно было вставить что-то про решение конфиликтов
Sazonov
Очередной выборочный хэндмэйд на основе https://git-scm.com/book/en/v2
Ничего нового.