Webrium
Подписаться

Git-приёмы, которые перестают быть страшными

Как на самом деле git хранит историю, почему опасные команды почти ничего не удаляют, когда работа теряется безвозвратно и как вернуть её через reflog

Цепочка коммитов уходит за границу указателя ветки: последний коммит недостижим, но физически не удалёнуказатель веткикоммит не удалён

Строка git reset --hard набрана, палец завис над Enter, и в голове крутится одно: а вдруг это сожжёт несделанную работу. Знакомо? Страх перед «опасными» командами почти всегда держится на одном: непонятно, что именно команда удаляет. А удаляет она, как правило, ничего. Ниже — устройство git, из которого это следует, и приёмы по шагам: reflog, reset, интерактивный rebase, bisect — с условиями, при которых каждый уместен.

Почему git почти ничего не стирает

Всё, что git хранит, — это объекты: файлы внутри .git/objects, адресованные по хешу содержимого. Коммит — объект со снимком файлов и ссылкой на родителя; дерево ссылается на поддеревья и блобы. Ветка — не «папка с кодом», а крошечный файл в .git/refs/heads, внутри которого один хеш. HEAD — указатель на указатель: файл, говорящий, на какой ветке вы стоите.

Отсюда следует главное. Команды вроде reset, rebase, commit --amend коммиты не удаляют — они перемещают указатели. Старые коммиты становятся «недостижимыми»: ни одна ветка на них не смотрит, но физически они лежат в хранилище. Вычистить их может только сборка мусора, а до неё путь назад держит reflog — журнал в .git/logs, куда пишется, где HEAD побывал на каждом шаге. Срок жизни записей настраивается параметрами gc.reflogExpire и gc.reflogExpireUnreachable: посмотрите эти значения в своей конфигурации — это и есть ваш запас прочности после катастрофы.

Само спасение на практике выглядит так:

git reflog                 # журнал: где HEAD был на каждом шаге
git branch rescue <хеш>    # ветка на состояние до катастрофы

Находите в журнале строку перед неудачной операцией, копируете хеш, ставите на него ветку. Всё: коммиты снова достижимы, работа найдена. Единственное условие — чтобы сборка мусора не успела их вычистить, а у свежей катастрофы с этим всегда хорошо.

Кстати, то самое пугающее сообщение про detached HEAD означает ровно одно: HEAD смотрит не на ветку, а прямо на коммит. Ничего не сломано. Хотите оставить то, что наработали в этом состоянии, — поставьте ветку (git branch <имя>), а вернуться назад можно через git switch -. Страшно сообщение, а не состояние.

Тупик: «вернуть всё в нормальное состояние»

Когда посреди работы всё поехало, первое, что напрашивается, — откатить репозиторий «в норму»: сбросить рабочее дерево до последнего коммита. git checkout ., git restore ., а если нервы уже на пределе — git reset --hard поверх грязного дерева. Напрашивается — и именно это по-настоящему разрушительный ход.

Причина в асимметрии, которая видна из устройства системы. Всё из предыдущего раздела касается закоммиченного. А правки, которые вы не закоммитили, в хранилище объектов не попадали: они живут только в файлах рабочего дерева. Reflog их не видит, потому что видеть нечего — таких объектов просто нет. Поэтому git restore . по грязному дереву стирает работу насмерть, без журналов и запасных выходов (untracked-файлы эти команды не трогают, но страховкой это считать не стоит).

И неудачная часть: то «нормальное состояние», к которому вы пытаетесь вернуться, — чаще всего и есть ваши незакоммиченные правки. Сброс его не проявляет. Он его удаляет.

Правильное первое движение — противоположное: заморозить бардак.

git add -A
git commit -m "wip: разберёмся позже"

Теперь бардак — коммит, а коммиты почти бессмертны: они видны в reflog, с них можно снять diff, их можно восстановить. Выбросить вы их всегда успеете; вернуть выброшенное — никогда. Картина страха переворачивается: страшные на вид команды по коммитам щадящие, а безобидные на вид — по незакоммиченному дереву — смертельные. git stash тоже спасает: он внутри создаёт коммиты, просто не оставляет на них обычных веток.

Reset в трёх режимах: шаги и условия

Чтобы не путаться в аргументах, держите в голове три дерева: HEAD (куда смотрит ветка), индекс (что попадёт в следующий коммит) и рабочую директорию (файлы на диске). Режимы reset различаются тем, сколько из этого они трогают.

git reset --soft <коммит> перемещает только указатель ветки. Индекс и файлы не тронуты, поэтому вся разница между старой и новой позицией уже лежит staged и готова к новому коммиту. Нужен, когда хотите перекроить последний коммит: другое сообщение, другое разбиение, забытый файл в комплекте. Условие: ветку ещё никто не видел.

git reset --mixed <коммит> — это дефолт, его можно не писать. Перемещает указатель и сбрасывает индекс, файлы на диске не трогает: изменения остаются, просто unstaged. Нужен, когда собираете коммиты заново из тех же кусков — скажем, разбиваете один большой на несколько осмысленных.

git reset --hard <коммит> сбрасывает всё перечисленное. Нужен, когда изменения с этого момента действительно не нужны. И трюк, снимающий половину страха:

git branch backup
git reset --hard <коммит>

Ветка — указатель, он ничего не стоит. После этой пары команд жёсткий сброс обратим: всё сброшенное достижимо через backup.

Условие применимости для reset вообще: ветка должна быть вашей. Если её уже видели другие — коллеги, сервер, CI, — двигать общий указатель нельзя: история у всех разъедется, и понадобится force-push, а вот это уже честно опасная территория, о чём ниже. Для общих веток есть git revert <коммит>: он не отматывает историю, а добавляет новый коммит, отменяющий старый. История растёт вперёд, никто не ломается. Короткое правило: своя ветка — reset, общая — revert.

Интерактивный rebase: порядок до того, как ветку увидят другие

Вторая классическая фобия, и тоже снимается шагами.

git rebase -i <база>

Открывается список коммитов от базы до HEAD, у каждого — действие: pick оставить, reword поменять сообщение, squash влить в предыдущий, edit остановиться и поправить, drop выбросить. Пугает обычно сам список, но править его руками каждый раз не обязательно. Рабочая связка:

git commit --fixup <хеш-коммита>
git rebase -i --autosquash <база>

--fixup создаёт коммит-заплатку с пометкой «про вот этот коммит», а --autosquash сам расставляет заплатки по местам и помечает их на вливание. История в итоге читается так, будто вы сразу писали аккуратно.

Условия те же, что у reset: только свои, ещё не отправленные коммиты. Всплыли конфликты — разрешаете, git add, git rebase --continue. Паника — git rebase --abort: всё вернётся как было. Само знание, что отмена доступна на любом шаге, убирает большую часть страха.

Бонус в одну строку:

git config rerere.enabled true

После этого git запоминает, как вы разрешали конфликт, и когда тот же конфликт вернётся — например, при повторном rebase, — подставит решение сам.

Bisect: git сам найдёт виновника

Когда что-то сломалось, а подозреваемых много, перебирать коммиты руками — занятие на весь вечер. Bisect превращает перебор в деление диапазона:

git bisect start
git bisect bad                  # сейчас сломано
git bisect good <хеш-или-тег>   # здесь точно работало

Git ставит вас на коммит посередине подозрительного отрезка. Вы проверяете — запускаете тест, воспроизводите баг — и говорите git bisect good или git bisect bad. Каждый ответ выбрасывает половину отрезка, поэтому число проверок равно тому, сколько раз ваша история делится пополам, пока не останется один коммит. Прикиньте на своей ветке: даже длинная история проверяется за горстку шагов. Когда отрезок схлопнется, git назовёт первый плохой коммит. Вернуться: git bisect reset.

Позиция редакции

Мы считаем: бояться нужно не команд, а состояний. Страшна не команда reset --hard, а ситуация, в которой вы не можете сказать, что она сделает с HEAD, индексом и рабочим деревом. Как только можете — команда перестаёт быть страшной, какой бы жёсткой ни звучала. Отсюда наши рабочие рефлексы: перед любой операцией с историей — беглый git status и взгляд в reflog; при сомнении — ветка backup перед сбросом; и главное — коммитить рано и мелко, а порядок наводить rebase потом. Страховочная сетка git держит только закоммиченное, так что пусть работа попадает в неё как можно раньше.

Наша позиция неверна, когда последствия операции выходят за границу вашего локального репозитория. Понимание того, что команда сделает с вашими тремя деревьями, спасает только вас — а при force-push в общую ветку ломается не у вас: у коллег, чьи указатели разъехались с вашими, чей reflog вам не помощник и чьи три дерева вы не видите. Там страх — не сигнал непонимания, а трезвая оценка цены, и никакое знание механики его не отменяет; безопасной такую операцию делает только договорённость с теми, кто на эту ветку опирается. То же с переписыванием истории ради вычистки секретов: локальное хранилище вы почистите, но чужие клоны и серверные копии — нет, и опасность живёт ровно там, куда ваше понимание не дотягивается. В таких случаях наш совет «коммить и раскладывай потом» тоже не работает: мусор в общую историю лучше не пускать вообще.

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Шесть столбцов потерь времени в терминале: видимые потери — нажатия, опечатки, повторяемые пути — низкие, а невидимые — вспомнить команду, найти каталог, восстаЧасы — не в наборе
Предыстория

Терминал, который экономит часы, а не минуты

Git-приёмы живут в терминале, и его стоит настроить заранее. Почему быстрые эмуляторы и алиасы не экономят время — и три шага: поиск по истории, сессии, которые не умирают, конфиг как код.

Полная стоимость self-hosted как вложенные друг в друга слои: инфраструктура, труд, риски, миграции. В самой глубине выделена цена сервера — лишь одна строка изСервер — лишь доля
Усиление

Self-hosted вместо подписок: честный расчёт

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

Сетка часов рабочего дня: закрашена лишь малая часть ячеек — набор кода, который ускорил ассистент; соседний счётчик показывает, что общий срок задачи не изменисрок задачи тот же
Усиление

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

С Git-приёмами разобрались, а вот ИИ-ассистент хитрее: ускоряет набор кода, но не сроки задачи. Как посчитать свой выигрыш по закону Амдала и когда вычитка подсказок его съедает.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть