■ пространство · прозаический трек
Кто-то ещё вытянул коммит? Да → git revert <sha>. Нет → git reset, любого режима.
--soft хранит правки staged, --mixed (умолчание) unstaged, --hard удаляет их — единственная мясорубка.
Один файл покорёжен? git restore <file> — оставьте ветку в покое.
ДЕМО ОДНОРАЗОВОГО РЕПО
Две минуты в репозитории-однодневке, и разница уже не определение — это лог, который вы видели:
построить репо из двух коммитов — 2 коммита · промах закоммичен, дерево чистое
git init /tmp/revert-vs-reset && cd /tmp/revert-vs-reset
echo one > file.txt && git add . && git commit -m "one"
echo two > file.txt && git add . && git commit -m "two"
revert — история растёт
git revert --no-edit HEAD
git log --oneline
# 1a2b3c4 (HEAD -> main) Revert "two" ← the cancellation
# 5d6e7f8 two ← still in the log
# 9f8e7d6 one
reset — история сжимается
git reset --hard HEAD~1
git log --oneline
# 9f8e7d6 one ← "two" is unreachable
verify: после revert git log --oneline → 3 коммита · после reset → 1 коммит — асимметрия и есть весь ответ
ТРИ РЕЖИМА RESET
Режимы управляют тем, где ваши правки выживут после сдвига указателя — и лишь одна строка этой таблицы удаляет работу:
| Режим | Указатель ветки | Область staging | Рабочее дерево | Ваши правки |
|---|---|---|---|---|
--soft | отступает | остались staged | нетронуты | сохранены целиком |
--mixed (умолчание) | отступает | unstaged | нетронуты | сохранены, unstaged |
--hard | отступает | reset | reset | удалены |
git reset --soft HEAD~1— «Заккомитил слишком рано.» Всё возвращается в staged, готово к перекоммиту.git reset HEAD~1(mixed) — «В staging попали не те файлы.» Правки выживают в рабочем дереве, ничего не staged. Присоедините clean, если заодно хотите убрать потерявшиеся файлы.git reset --hard HEAD~1— «Этот коммит и мои незакоммиченные правки — мусор.» Git переспрашивать не станет; отмены для части рабочего дерева нет, если вы не заложили её в stash и не закоммитили заранее.
verify: git reset --soft HEAD~1 → git status показывает изменение «to be committed» · --mixed → «changes not staged»
revert не имеет режимов, потому что ничего никогда не уничтожает — лишь добавляет компенсирующий коммит.
ЕДИНСТВЕННЫЙ ВОПРОС
Кто-то ещё вытянул этот коммит? Если да — reset под запретом. Сброс ветки, на которой другие строили работу, переписывает общую историю: их следующий pull встречает расходящиеся ветки, и «починка» — это обычно force-push, превращающий вашу ошибку в проблему для всех. Revert добавляет обычный коммит, так что простой git pull аккуратно вливается у всех — потому revert и есть ответ по умолчанию на main и в pull request'ах GitHub, где кнопка «Revert» существует именно потому, что переписывать историю влитой ПРы не вариант.
Reset — для окна до обмена: коммит тридцатисекундной давности, промах со staging, локальная экспериментальная ветка. Если единственная копия работы живёт на вашей машине, вы вольны переставлять историю — в этом весь смысл окна до пуша. Средний случай: вы запушили в свою feature-ветку и на ней никто не строится — force-push после reset там социально приемлем, но revert всё равно дешевле по координации. Приберегите force-push для веток, которыми владеете в одиночку.
Revert ещё и безопаснее конструктивно, не только по обычаю: revert даёт обычный коммит — CI по нему гоняется, diff читается на ревью, и git log объясняет вам-в-будущем, почему изменение исчезло. Reset молча отбрасывает промежуточное состояние; никто на ревью потом не увидит, что и когда было отменено.
ДВА ОСТРЫХ КРАЯ У REVERT
- Отмена merge-коммита требует
git revert -m 1 <sha>— у слияния два родителя, так что «отмени его» неоднозначна;-m 1оставляет линию, в которую вы вливались. Без него git отказывается. Ловушка вдогонку: повторное вливание той же ветки потом выглядит для git уже-применённым, так что заземлить ту работу заново — значит откатить откат. - Revert не машина времени — он отменяет diff одного коммита. Если поздние коммиты трогали те же строки, возможно, придётся разрешить конфликты: git сообщает, что отмена запутана — полезная информация, а не поломка.
КУДА ВСТАЁТ RESTORE
git restore (и git switch, его пара) пришли в git 2.23, чтобы забрать дела, которые reset делал неуклюже из-за перегрузки имён:
| Задача | Старый путь | Ясный путь |
|---|---|---|
| Отбросить правки рабочего дерева в файле | git checkout -- file / git reset --hard | git restore file |
| Снять файл со staging | git reset HEAD file | git restore --staged file |
| Сдвинуть ветку на другой коммит | git reset <sha> | git reset <sha> (замены нет — это остаётся) |
Современное разделение: restore чинит файлы, reset двигает ветки, revert отменяет публичные коммиты. Это одна ментальная модель, разрезанная на три команды. Старые формы git checkout работают везде; новые лишь не дают отправить целую ветку в мясорубку, когда вы хотели снять со staging один файл.
ИТОГ
- Коммит публичный (запушен, другие вытянули) →
git revert <sha>. - Коммит только локальный, хотите переделать →
git reset --softили--mixedи перекоммит. - Только локальный, убрать вместе с незакоммиченным бардаком →
git reset --hard, после честного взгляда на то, что ещё несёт рабочее дерево. - Один файл покорёжен →
git restore <file>и оставьте ветку в покое.
verify: git reflog → HEAD@{n} перечисляет позиции до reset — git reset --hard HEAD@{1} отводит назад
Единственная по-настоящему опасная запись в этом списке --hard — всё остальное оживает через git reflog — он помнит отвязанные коммиты ~90 дней. Необратимых потерь в git мало, и почти все требуют, чтобы вы ввели их явно; привычка, которую стоит построить — пауза в полсекунды перед --hard, а не полный отказ от острых инструментов git. И если всё меню отмены кажется вам церемонией — упрёк справедлив: jujutsu, VCS, совместимый с git, перепевает ровно эту часть.
конец пары отмены · ничего не скачано · всё локально