Как восстановить потерянные коммиты и ветки в Git

Иногда после неудачного действия в Git кажется, что несколько часов работы исчезли: ветки нет, нужные коммиты не видны, а файлы вернулись к старой версии. Но во многих случаях данные всё ещё лежат в локальном репозитории, Git просто перестал показывать их в обычной истории.

В статье разберём, как найти и вернуть потерянные коммиты, восстановить удалённую ветку или отложенные изменения из stash. Все сценарии пройдём пошагово: от первых безопасных действий до случаев, когда Git уже не сможет вернуть данные.

Помог со статьёй:

Андрей Хомутов

Эксперт-разработчик в Ростелеком ИТ. Тренер-преподаватель и дипломный руководитель по направлениям Python и JavaScript в Нетологии. Эксперт в ИТ-школе Ростелекома (DevOps)

Сначала остановитесь: чего не нужно делать после потери данных

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

Сначала проверьте текущее состояние:

git status

Затем используйте только команды, которые ничего не изменяют:

git reflog --date=localgit show git log --oneline --decorate 

До восстановления не запускайте:

git gcgit prunegit clean

Не запускайте без проверки команды вроде git reset --hard HEAD@{1}. HEAD@{1} — не специальное обозначение состояния до аварии, а предыдущая запись в конкретном reflog. Если после потери данных вы переключались между ветками или запускали другие команды, нужный коммит может находиться под другим номером.

Надёжнее найти его хеш, проверить содержимое и создать временную ветку:

git branch rescue 

Эта команда не переключает ветку и не трогает файлы. Она создаёт новую ссылку на найденный коммит, поэтому тот снова становится достижимым и больше не зависит только от reflog.

Как Git помнит коммиты, которые исчезли из истории

Для восстановления достаточно понимать, как связаны HEAD, ветки и коммиты.

Обычно HEAD указывает на текущую ветку, а ветка — на последний коммит в ней. Когда появляется новый коммит, Git передвигает указатель ветки вперёд. git reset, переключение ветки и rebase тоже двигают ссылки, только иногда не туда, куда ожидал пользователь.

Коммит при этом не обязательно удаляется. Ветка — это имя, которое указывает на определённый коммит. Если удалить ветку или переместить её назад, объект может остаться в каталоге .git/objects.

Историю обновления ссылок хранит reflog — локальный журнал перемещений HEAD и веток. Посмотреть журнал HEAD можно так:

git reflog

Если журнал длинный, записи можно отфильтровать по тексту reflog-сообщения (для коммитов туда попадает тема):

git reflog --grep-reflog="payment validation"

Для поиска по автору коммита используйте:

git log -g --all --author="имя или email" --oneline

Фильтр --author ищет автора коммита, а не пользователя, который переместил ссылку.

Например, после сброса история может выглядеть так:

17c6a41 HEAD@{0}: reset: moving to HEAD~38f29d0e HEAD@{1}: commit: add payment validation6a3107b HEAD@{2}: commit: update error messages

HEAD@{0} — текущее положение, HEAD@{1} — предыдущее, HEAD@{2} — ещё более раннее. Это позиции в журнале, а не постоянные имена: после новых действий нумерация сдвинется.

Поэтому при восстановлении лучше скопировать сам хеш:

git show 8f29d0egit branch rescue-reset 8f29d0e

Reflog существует только в конкретном локальном репозитории. Он не передаётся на сервер при push и не загружается из чужого репозитория при fetch. У вас и у коллег могут быть разные журналы и разные шансы найти один и тот же коммит.

DevOps-инженер PRO — Git и процесс в команде

Журналы у коллег разные, а процесс можно выстроить общий: Git-репозитории, CI/CD и инфраструктура. Соберите его на трёх проектах.

Посмотреть программу →

Ситуация: после git reset —hard коммиты пропали из истории

Допустим, в ветке было три новых коммита, после чего вы выполнили:

git reset --hard HEAD~3

Ветка переместилась на три коммита назад. Вместе с ней Git обновил индекс и рабочую директорию, поэтому новые коммиты исчезли из обычного git log, а файлы вернулись к старому состоянию.

HEAD~3 означает третьего предка текущего коммита по первой родительской линии. Если такого предка нет — например, вся история состоит только из двух коммитов, — Git завершит команду с ошибкой и не переместит ветку.

Как восстановить коммиты

Сначала откройте reflog с локальным временем:

git reflog --date=local

Найдите запись reset: moving to HEAD~3 и состояние непосредственно перед ней. Не подставляйте HEAD@{1} автоматически: проверьте хеш и сообщение коммита.

git show --stat 

Если это нужная версия, сохраните её отдельной веткой:

git branch rescue-reset 

Теперь исходная цепочка коммитов снова достижима. Дальше всё зависит от задачи.

Если нужно вернуть текущую ветку целиком, сначала убедитесь, что после аварии в рабочей директории не появились несохранённые изменения:

git status

Затем переместите ветку на найденный коммит:

git reset --hard 

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

git cherry-pick 

cherry-pick не восстанавливает исходный коммит. Команда переносит его изменения в текущую ветку и создаёт новый коммит с другим хешем. Если нужно вернуть всю ветку, сначала создайте её на прежней вершине, а затем выберите reset, merge или cherry-pick.

Что произошло

git reset --hard выполнил сразу три действия: переместил текущую ветку, привёл индекс к целевому коммиту и перезаписал отслеживаемые файлы в рабочей директории (а в некоторых случаях может затронуть и неотслеживаемые). В отличие от него, git reset --soft перемещает только HEAD и текущую ветку, не меняя индекс и рабочую директорию. Поэтому изменения из отменённых коммитов остаются подготовленными к новому коммиту. Если нужно вернуть прежнюю историю, найдите старую вершину в reflog и создайте на ней ветку:

git branch rescue-soft 

Старые коммиты остались в базе, а прежнее положение ветки сохранилось в reflog. А вот изменения, которые существовали только в рабочей директории, Git мог перезаписать без отдельного объекта для восстановления. К этой границе ещё вернёмся.

Ситуация: после amend понадобилась предыдущая версия коммита

git commit --amend не изменяет существующий коммит, а создаёт новый и перемещает на него текущую ветку. Предыдущий коммит перестаёт быть виден в обычной истории, но некоторое время остаётся доступным через reflog.

Найдите состояние до amend:

git reflog --date=local

В журнале обычно будет запись вида:

51bc208 HEAD@{0}: commit (amend): update payment validation8f29d0e HEAD@{1}: commit: update payment validation

Проверьте предыдущий коммит и закрепите его временной веткой:

git show --stat 8f29d0egit branch rescue-amend 8f29d0e

Теперь можно сравнить обе версии и решить, какую из них оставить.

Ситуация: после rebase изменилась история ветки

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

Способ восстановления зависит от того, закончилась ли операция.

Если rebase ещё продолжается

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

git status

Если Git сообщает, что rebase находится в процессе, его можно отменить:

git rebase --abort

Команда прекращает rebase и возвращает HEAD к состоянию до его начала. Это проще и безопаснее, чем вручную искать старую вершину и запускать reset во время незавершённой операции.

Если rebase уже завершился

Посмотрите журнал перемещений HEAD:

git reflog --date=local

Либо журнал конкретной ветки:

git reflog show 

Найдите последнюю вершину до начала rebase, проверьте её и создайте страховочную ветку:

git show --stat git branch before-rebase 

После этого можно сравнить старую и новую историю:

git log --oneline before-rebasegit range-diff ..before-rebase ..

Если новая история не нужна, текущую ветку можно вернуть на сохранённую вершину. Делать это стоит только после проверки git status и создания резервной ветки:

git reset --hard before-rebase

Почему не всегда помогает ORIG_HEAD

В начале rebase Git обычно записывает прежнюю вершину ветки в ORIG_HEAD. Поэтому сразу после ошибки можно проверить:

git show ORIG_HEAD

Но ORIG_HEAD — один служебный указатель, который обновляют разные команды: rebase, reset, merge и другие операции, которые меняют HEAD. Если во время или после rebase запускался git reset, прежнее значение могло быть перезаписано. Reflog хранит последовательность перемещений, поэтому для расследования он надёжнее.

Ситуация: удалили ветку через git branch -D

Команда git branch -D удаляет ветку, даже если её изменения не были слиты с другой веткой. Из списка веток она исчезает сразу, но коммиты могут остаться в базе.

Но есть нюанс: вместе с веткой Git удаляет и её собственный reflog. Поэтому после удаления команда git reflog show обычно уже не сработает.

Как восстановить ветку

Поищите её последний коммит в остальных локальных журналах:

git reflog --all

Если вы недавно работали в удалённой ветке, её вершина, скорее всего, осталась в reflog HEAD. Проверьте подходящий хеш:

git show --stat 

И пересоздайте ветку:

git branch  git switch 

Удаление локальной ветки не удаляет одноимённую remote-tracking-ссылку origin/. Если ветка раньше загружалась через fetch или отправлялась через push, сначала проверьте эту ссылку:

git branch -r --list "origin/"git show --stat origin/

Если ссылка указывает на нужный коммит, восстановите локальную ветку:

git switch -c  --track origin/

Чтобы проверить прежние состояния remote-tracking-ссылки, используйте:

git reflog show origin/

Ссылка origin/ может отсутствовать, если ветку ни разу не получали с сервера и не отправляли туда, либо указывать на более старый коммит. История remote-tracking-ссылки поможет, только если нужное состояние раньше попадало в локальный репозиторий. Серверный reflog к вам не передаётся.

Что произошло

Ветка в Git — ссылка на коммит. git branch -D удаляет ссылку и её журнал, но сами объекты удаляются не сразу. Если хеш сохранился в reflog HEAD, другом журнале или базе объектов, достаточно создать новую ветку на найденной вершине.

Ситуация: нужного коммита нет в reflog

Если reflog не помог, проверьте базу объектов напрямую.

Для этого используйте git fsck:

git fsck --no-reflogs --unreachable

По умолчанию git fsck начинает проверку от ссылок, индекса и reflog. Флаг --no-reflogs говорит не считать коммиты достижимыми только потому, что они упомянуты в журнале. Это полезно, если нужный объект там есть, но найти его по обычному выводу не удалось.

В результате могут появиться строки:

unreachable commit 8f29d0e...unreachable tree 14c921a...unreachable blob 792f830...

Нас в первую очередь интересуют коммиты. Проверяйте их по очереди:

git show --stat 8f29d0e

Если это вершина потерянной истории, закрепите её:

git branch rescue-fsck 8f29d0egit log --oneline rescue-fsck

Что делает —lost-found

Есть и другой режим:

git fsck --lost-found

Команда записывает dangling-объекты в каталог .git/lost-found/. Dangling-коммиты, обычно вершины недостижимых цепочек, попадают в .git/lost-found/commit/. Другие dangling-объекты сохраняются в .git/lost-found/other/. Для blob-объектов Git записывает в файл их содержимое.

Все коммиты внутри недостижимой цепочки отдельно в lost-found не появляются, но их можно увидеть, двигаясь по истории от найденной вершины:

git log --oneline 

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

Чем dangling отличается от unreachable

Unreachable-объект существует в базе, но до него нельзя дойти от выбранных ссылок и других исходных точек. Dangling-объект — недостижимый объект, на который при этом не ссылается ни один другой объект. Например, dangling commit может оказаться вершиной целой цепочки недостижимых коммитов.

В практических гайдах эти слова часто используют как синонимы, но вывод git fsck различает их не случайно: один термин описывает достижимость от ссылок, второй — прямые связи между объектами.

Ситуация: удалили stash через git stash drop или clear

Stash хранится не в отдельном формате, а в виде специальных коммитов. git stash drop удаляет ссылку на одну запись, а git stash clear очищает список целиком. После этого сами объекты могут некоторое время оставаться в репозитории.

Как найти удалённый stash

Получите список недостижимых коммитов:

git fsck --unreachable | grep commit

Проверяйте найденные хеши:

git show --summary 

У stash-коммита как минимум два родителя, а сообщение часто начинается с WIP on или On . Но пользовательское сообщение может выглядеть иначе, поэтому фильтр только по слову WIP найдёт не все варианты.

Когда нужный объект определён, сначала закрепите его временной веткой:

git branch rescue-stash 

Затем примените найденный stash:

git stash apply 

Если нужно восстановить и состояние индекса:

git stash apply --index 

После успешного применения сохраните изменения обычным коммитом. Не применяйте тот же stash повторно поверх уже восстановленных изменений: это может вызвать конфликт. Ветка rescue-stash закрепляет найденный объект в базе; без ссылки он может быть удалён сборщиком мусора.

Можно ли восстановить незакоммиченные изменения

Всё зависит от того, успел ли Git сохранить потерянное содержимое. Даже без коммита изменения могли попасть в индекс или stash.

Изменения были только в рабочей директории

Если вы отредактировали отслеживаемый файл, но не выполняли git add, а затем перезаписали его через git reset --hard, Git обычно не сможет вернуть эту версию. Reflog хранит историю перемещений ссылок, а не снимок после каждого сохранения файла.

Остаются внешние источники: Local History в IDE, резервные копии, снапшоты файловой системы и облачная синхронизация.

Изменения попадали в индекс

Во время git add Git сохраняет содержимое файла в базе объектов и добавляет ссылку на него в индекс. Даже если коммит не был создан, после reset такой blob иногда остаётся среди недостижимых объектов.

Поищите его:

git fsck --no-reflogs --unreachable

Содержимое найденного blob можно вывести так:

git cat-file -p 

Отдельный blob не хранит ни имени файла, ни его пути в проекте. Если объектов много, содержимое придётся просматривать вручную. Это запасной способ, а не замена коммитам.

Изменения находились в stash

В этом случае ищите stash-коммит через git fsck --unreachable и применяйте его командой git stash apply . Шанс выше, чем у неиндексированных изменений, потому что stash уже создал объекты Git.

Чего Git вернуть не сможет

Фраза «Git почти ничего не удаляет» работает только для данных, которые успели попасть в его базу. Рабочая директория сама по себе — не история версий.

Что потеряно

Вернётся ли

Где искать

Коммиты после reset --hard

Обычно да

git reflog → git branch rescue

Коммиты после завершённого rebase

Обычно да

git reflog show

Удалённая ветка

Обычно да

git reflog --all → git branch

Удалённый stash

Иногда

git fsck --unreachable → git stash apply

Изменения, добавленные в индекс

Иногда

git fsck → git cat-file -p

Изменения только в рабочей директории

Обычно нет

Local History или резервная копия

Файлы после git clean

Средствами Git — нет

История IDE, бэкап или файловая система

Коммиты после чужого force push

Только если объект где-то сохранился

Reflog локальной ссылки на удалённую ветку, клоны коллег или CI

Неиндексированные изменения после reset —hard

Если версия файла существовала только в рабочей директории и была перезаписана, Git обычно её не хранит. Искать нужно в истории IDE, бэкапах и файловой системе.

Неотслеживаемые файлы после git clean

Команда:

git clean -fd

удаляет неотслеживаемые файлы и каталоги. Если они никогда не попадали в индекс, коммит или stash, средствами Git восстановить их нельзя.

Игнорируемые файлы эта команда по умолчанию не затрагивает. Для их удаления нужен дополнительный флаг:

git clean -fdx

Перед реальным удалением полезно посмотреть, какие файлы будут затронуты:

git clean -nd

Команда git clean -nd не покажет файлы, исключённые через .gitignore. Чтобы увидеть, что будет удалено с флагом -x, выполните:

git clean -ndx

Здесь -n, или --dry-run, только показывает будущий результат; -d рекурсивно обходит неотслеживаемые каталоги; -x добавляет игнорируемые файлы. Для реального удаления используется -f, или --force: git clean -fd удаляет неигнорируемые неотслеживаемые файлы и каталоги, а git clean -fdx — ещё и игнорируемые.

Объекты, фактически удалённые из базы

Если недостижимый объект уже удалён сборщиком мусора или командой git prune, git fsck его не найдёт. Внутри репозитория данных больше нет.

Сам запуск git gc ещё не означает, что нужный объект удалён. Git сохраняет данные, на которые продолжают ссылаться ветки, теги, индекс или reflog. Объект пропадает окончательно только после удаления из базы.

Чужой force push без локального объекта

Если коллега переписал удалённую ветку, а вы ни разу не получали её прежние коммиты через fetch, pull или clone, в вашем репозитории нет объектов для восстановления.

Искать стоит у автора force push, в локальных клонах коллег, репозитории CI, ссылках pull request или через поддержку Git-хостинга. Если старое состояние раньше попадало к вам, проверьте git reflog show origin/.

Сколько времени Git хранит потерянные коммиты

По умолчанию обычные записи reflog хранятся 90 дней. Для записей, недостижимых от текущей вершины ветки, действует более короткий срок — 30 дней. Эти значения задают gc.reflogExpire и gc.reflogExpireUnreachable. Записи старше этих сроков удаляются при обслуживании, а не автоматически по таймеру.

Для удаления недостижимых объектов используется отдельная настройка gc.pruneExpire. Её значение по умолчанию — 2 weeks ago. При обслуживании Git не должен удалять более свежие объекты.

Но объект не удаляется автоматически ровно через две недели. Сначала он должен стать недостижимым, затем должно запуститься обслуживание. Кроме того, его не должны защищать ветка, тег, индекс или reflog. Настройки конкретного репозитория могут отличаться.

Чем раньше проверить git reflog и git fsck, тем выше вероятность найти данные.

Как не доводить восстановление до git fsck

Делайте ветку-страховку перед reset или rebase

Перед операцией, которая переписывает историю, создайте дополнительную ссылку на текущую вершину:

git branch backup-before-rebase

После проверки результатов попробуйте удалить резервную ветку через -d. Команда сработает, только если ветка полностью влита в свою upstream-ветку (или в HEAD, если upstream не настроен):

git branch -d backup-before-rebase

Если операция сломалась, не придётся искать старый хеш: он уже закреплён обычной веткой.

Коммитьте небольшими порциями

Локальный коммит не обязан быть готов к публикации. Промежуточные коммиты позже можно объединить или отредактировать через интерактивный rebase. Но пока работа существует только в рабочей директории, возможностей для восстановления значительно меньше.

Добавляйте понятное сообщение к stash

Перед переключением контекста сохраните работу вместе с неотслеживаемыми файлами:

git stash push -u -m "payment form before rebase"

Флаг -u добавляет неотслеживаемые файлы, а сообщение помогает отличить эту запись от других stash.

Сначала проверяйте, что удалит git clean

Сначала посмотрите будущий результат:

git clean -nd

Если собираетесь использовать флаг -x, сначала проверьте игнорируемые файлы:

git clean -ndx

И только после проверки запускайте удаление:

git clean -fd

Используйте —force-with-lease вместо —force

git push --force-with-lease

Обычный --force отключает проверку fast-forward и может затереть коммиты, появившиеся на сервере после вашей последней синхронизации. --force-with-lease проверяет ожидаемое значение удалённой ссылки и отклоняет push при несовпадении.

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

Первая помощь: короткий алгоритм

Если работа пропала, начните с четырёх команд:

git statusgit reflog --date=localgit show --stat git branch rescue 

Если нужного состояния нет в журнале:

git fsck --no-reflogs --unreachable

Если найден обычный коммит, закрепите его веткой. Если найден stash, сначала закрепите его временной веткой, а затем примените через git stash apply . И только после этого решайте, как вернуть изменения в основную историю: reset, cherry-pick, merge или переключение на восстановленную ветку.

Логика восстановления почти всегда одна: найти объект, проверить его содержимое и создать на него ссылку. Так найденные данные будут закреплены до того, как вы начнёте менять историю.

Потерянный коммит чаще не удалён — на него просто больше ничто не ссылается

Reset, rebase и удаление ветки меняют историю, которую Git показывает по обычным ссылкам. Но сами коммиты после этого часто продолжают лежать в локальной базе объектов.

Первое место для поиска — git reflog. Если журнал не помогает, остаётся git fsck. В обоих случаях найденный хеш сначала нужно проверить через git show и закрепить временной веткой. Так результат восстановления не исчезнет после следующего движения HEAD.

Хуже всего Git справляется с тем, чего никогда не видел: неиндексированными изменениями и неотслеживаемыми файлами. Поэтому лучше делать небольшие локальные коммиты, сохранять изменения в stash перед переключением контекста и создавать резервную ветку перед reset или rebase.

Что ещё изучить по теме

  • Документация Git: git reflog — как устроен локальный журнал ссылок и сколько хранятся его записи.

  • Pro Git: Maintenance and Data Recovery — восстановление потерянной истории через reflog и базу объектов.

  • Документация Git: git fsck — различия между unreachable и dangling-объектами, режимы --no-reflogs и --lost-found.

  • Документация Git: git rebase — отмена незавершённого rebase и ограничения ORIG_HEAD.

  • Документация Git: git stash — официальный сценарий поиска ошибочно удалённых stash-записей.

  • Документация Git: git gc — сроки хранения reflog и правила удаления недостижимых объектов.

Где потренироваться и чему научиться

Git остаётся базой, а ИИ уже помогает с кодом, прототипами и агентами. Начните с чего-то небольшого и бесплатного:

  • вводного курса «Вайб-кодинг» — 5 дней, чтобы собрать работающий прототип с ИИ без навыков программирования;

  • вводного курса «ИИ-агенты» — 5 занятий, чтобы собрать первого агента и понять, как автоматизировать задачи;

  • вводного курса «Специалист по искусственному интеллекту» — чтобы попробовать профессию и пройти часть реальной практики;

  • вводного курса «Инженер по тестированию» — 5 дней, чтобы познакомиться с задачами QA-специалистов;

  • вводного курса «Нейросети для бизнеса и управленцев» — чтобы планировать задачи и коммуникацию с нейросетями.

А если хочется не разового знакомства, а системного результата — добрать нехватающие навыки или сменить специальность, стоит смотреть на системное обучение:

  • на курсе «Нейросети для разработчиков» — 7 недель, чтобы подключить ИИ к коду, GitHub Actions и CI/CD;

  • на программе «Вайб-кодинг» — 3 месяца, чтобы собирать прототипы и MVP с ИИ;

  • на курсе «Безопасная разработка программного обеспечения» — 5 месяцев основной программы и дипломы Нетологии и НИУ ВШЭ, чтобы выстроить защиту на всех этапах разработки;

  • на программе «ИИ-архитектор с нуля до профи» — от промптинга до ИИ-агентов, чтобы вырасти в архитектора ИИ-решений;

  • на курсе «Специалист по защите информации» — три специализации, чтобы освоить защиту информации и разобрать требования ФСТЭК.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев