Отмена ошибки
Ситуация
Три неприятности за утро. Вы случайно стёрли строку в файле. Вы отправили в общий репозиторий коммит, который закрыл запись на занятия, — а Борис уже забрал его к себе. Вы на середине правки, а нужно срочно забрать изменения коллеги. Для каждой есть своя команда — и есть «быстрые» команды, которые решат задачу ценой чужой или вашей работы.
Главная мысль
Сначала ответьте, где ошибка (карта из пяти мест — в карте курса):
| Где ошибка | Команда | Что теряется |
|---|---|---|
| Правка в рабочей папке, не нужна | git restore файл | эта незакоммиченная правка |
| Лишнее в индексе | git restore --staged файл | ничего: правка остаётся в папке |
| Коммит уже отправлен | git revert коммит | ничего: новый коммит отменяет старый |
| Недоделанная работа мешает | git stash push -m "…", потом git stash pop | ничего, если не удалять запись |
Чем дальше изменение ушло по пути «папка → индекс → коммит → общий репозиторий», тем важнее отменять его новым действием, а не стиранием истории.
Новые слова
git restore— вернуть файл в папке к версии из индекса (а с--staged— убрать файл из индекса).git revert— создать новый коммит, который делает обратное указанному коммиту.git stash— временно убрать незакоммиченные правки в отдельное хранилище и вернуть их позже.
Разбор
Исходное состояние — конец главы 6: клоны anna-site и boris-site совпадают с общим репозиторием,
git status в обоих чистый. Если начинаете здесь — setup.md. Всё ниже — в папке Анны,
если не сказано иное. Хеши — пример.
1. Испорченная правка в папке: git restore
cd ~/git-course/anna-site
sed -i '/<li>Четверг/d' schedule.html
git diff
<li>Вторник, 19:00 — Git: основы</li>
-<li>Четверг, 19:00 — практика</li>
<li>Суббота, 11:00 — разбор вопросов</li>
Строка удалена случайно. Вернём файл к последней сохранённой версии:
git restore schedule.html
git status --short
grep -c Четверг schedule.html
1
git status --short пуст, строка на месте. Осторожно: git restore без --staged выбрасывает
незакоммиченную правку файла — Git её нигде не хранил, вернуть нельзя. Перед ним всегда смотрите git diff
и убеждайтесь, что выбрасываете именно ошибку.
2. Лишнее в индексе: git restore --staged
Анна дописала README и заодно сохранила черновик заметок, а потом отметила всё сразу:
printf 'черновик: идеи для страницы контактов\n' > notes.txt
printf 'Сайт обновляется по вторникам.\n' >> README.md
git add .
git status --short
M README.md
A notes.txt
Черновику в коммите не место. Убираем его из индекса — файл остаётся в папке:
git restore --staged notes.txt
git status --short
M README.md
?? notes.txt
git commit -m "Указать день обновления в README"
3. Отправленный коммит: git revert
Анна по ошибке закрыла запись и успела отправить коммит:
sed -i 's|<p>Занятия: вторник и четверг, 19:00.</p>|&\n<p>Запись на занятия закрыта.</p>|' index.html
git add index.html
git commit -m "Закрыть запись на занятия"
git push
git log --oneline -3
04a1de6 Закрыть запись на занятия
f14e3ce Указать день обновления в README
540bad7 Merge remote-tracking branch 'origin/main'
Коммит уже в общем репозитории, Борис мог его забрать. Стирать его из истории нельзя: у Бориса он останется,
и истории разойдутся. Отменяем новым коммитом. Отменяемый коммит — последний, поэтому вместо хеша (у вас он
другой) можно написать HEAD:
git revert --no-edit HEAD
[main 37279b7] Revert "Закрыть запись на занятия"
Date: Fri Oct 16 14:38:20 2026 +0300
1 file changed, 1 deletion(-)
git show --stat HEAD
…
Revert "Закрыть запись на занятия"
This reverts commit 04a1de617dbc7cb9b939544fe5b8d98aa5770242.
index.html | 1 -
revert отменяет весь коммит. Если в нём были и нужные правки (как в главе 3), лучше исправить одну строку
новым коммитом. Отправляем отмену:
git push
У Бориса (cd ~/git-course/boris-site && git pull --ff-only) — простая перемотка: в истории и ошибка, и отмена,
а файл как до ошибки.
4. Недоделанная работа мешает: git stash
Борис отправил правку расписания (это делает скрипт setup.md). Анна в это время начала добавлять пятницу в тот же файл и не готова коммитить:
cd ~/git-course/anna-site
sed -i 's|</ul>|<li>Пятница, 18:00 — консультация</li>\n&|' schedule.html
git pull --ff-only
error: Your local changes to the following files would be overwritten by merge:
schedule.html
Please commit your changes or stash them before you merge.
Aborting
Git отказался, чтобы не затереть правку. Уберём её на время, с подписью:
git stash push -m "Черновик: консультация в пятницу"
git stash list
Saved working directory and index state On main: Черновик: консультация в пятницу
stash@{0}: On main: Черновик: консультация в пятницу
Папка чистая (кроме неотслеживаемого notes.txt — stash по умолчанию его не трогает). Забираем и возвращаем
черновик:
git pull --ff-only
git stash pop
Auto-merging schedule.html
…
modified: schedule.html
…
Dropped refs/stash@{0} (6e0900d…)
Правки Бориса и черновик Анны соединились, запись в хранилище удалена — она больше не нужна. Если бы pop
наткнулся на конфликт, Git оставил бы запись (The stash entry is kept in case you need it again.) — тогда
разрешите конфликт как в главе 5 и только потом удалите запись.
Быстрые команды, которые уничтожают работу
Эти команды встречаются в советах из интернета. Здесь — что они делают с теми же ситуациями.
git reset --hard— возвращает ветку, индекс и папку к коммиту. Все незакоммиченные правки отслеживаемых файлов исчезают без возврата; если указать старый коммит, более новые коммиты ветки пропадают из её истории. В ситуации 1 хватилоgit restoreодного файла; в ситуации 3resetубрал бы коммит только у Анны, а у Бориса он остался бы.git clean -fd— удаляет все неотслеживаемые файлы и папки. Черновикnotes.txtисчез бы навсегда: Git его никогда не хранил. Нужно убрать один файл — удалите именно его, посмотревgit status.git push --force— перезаписывает ветку в общем репозитории вашей версией. Коммиты коллег, которых у вас нет, пропадают из общей истории. В ситуации 3 правильно —revertи обычныйpush.git stash drop— удаляет запись из хранилища. Если правки не были возвращены (popс конфликтом, забытая запись), они практически потеряны. Удаляйте запись, только убедившись, что её содержимое в папке или в коммите:git stash show -p stash@{0}.
Практика: три ошибки сразу
Скрипт setup.md создаёт ситуацию в клоне Анны:
- Анна отправила коммит «Перейти на порт 9000» — ошибка, сайт должен остаться на 8082;
- после этого Борис отправил правку расписания — Анна её ещё не забрала;
- в папке Анны две незакоммиченные правки: нужный черновик в
schedule.html(понедельник — выходной) и случайно удалённая строка с почтой вabout.html.
Задача (в папке ~/git-course/anna-site): вернуть строку с почтой, сохранить черновик, забрать правку Бориса,
отменить порт 9000 для всех участников.
Признаки успеха:
- в
anna-siteиboris-sitegrep PORT site.conf→PORT=8082, последний коммит —Revert "Перейти на порт 9000"; - коммит Бориса «Указать аудиторию для четверга» есть в истории обоих клонов;
- в
anna-siteчерновик про понедельник — в папке (M schedule.html),git stash listпуст; grep -c 'Почта для вопросов' about.html→1.
Подсказка 1
Разберите каждую правку по таблице «где ошибка». Порядок важен: что мешает забрать правку Бориса?
Подсказка 2
about.html — git restore; черновик — git stash push -m …; затем git pull --ff-only и git stash pop.
Подсказка 3
Хеш ошибочного коммита найдёт git log -S 9000 --oneline. Дальше git revert --no-edit <хеш>, git push,
и git pull --ff-only у Бориса.
Решение — в answers/07-undo.md.
Если результат другой
git revertпишетYour local changes to the following files would be overwritten by mergeиfatal: revert failed. Незакоммиченные правки в том же файле, что меняет отменяемый коммит. Уберите их вstash, выполнитеrevert, вернитеpop.- После
stash popконфликт. Запись осталась вgit stash list. Разрешите конфликт в файле,git add файл, проверьте результат и только потомgit stash drop. - Открылся редактор с сообщением
Revert "…". Вы не указали--no-edit. Сохраните и закройте редактор.
Проверка понимания
- Коммит с ошибкой есть только у вас, вы его ещё не отправляли. Подходит ли
git revert? Есть ли риск у других способов? - Почему перед
git restore файлнужно смотретьgit diff, а передgit restore --staged файл— не обязательно?
Ответы — в answers/07-undo.md.
Дальше: 8. Итоговая работа и проверка.
Подготовка исходного состояния (setup.md)
Блоки работают только в отдельной учебной папке ~/git-course. При неожиданном состоянии они отказывают до записи; ошибка после начала подготовки может оставить часть действий выполненной. Сначала прочитайте сообщение, не удаляйте существующие файлы. Для продолжения после предыдущей главы выбирайте только нужный блок.
Перед главой 7
Конец главы 6: оба клона совпадают с общим репозиторием. Выполняется после блока «Перед главой 6». Если в клонах
нет своих неотправленных коммитов, блок сам заберёт в них последние изменения (git pull --ff-only).
bash <<'SETUP'
rp_before7() {
# --- Проверки: ничего не меняют ---
fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
course_repo() {
[ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
|| { fail "$1 — не учебный репозиторий курса"; return 1; }
}
quiet() { # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
local x f
[ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
done
for x in rebase-merge rebase-apply; do
[ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
done
[ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
for f in $(git -C "$1" ls-files --others --exclude-standard); do
case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
done
}
last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
clone_ok() { # клон курса: связан с общим репозиторием, все его коммиты уже там
course_repo "$1" || return 1
[ "$(git -C "$1" remote get-url origin 2>/dev/null)" = "$HOME/git-course/remote/site.git" ] \
|| { fail "$1 не связан с ~/git-course/remote/site.git"; return 1; }
git -C ~/git-course/remote/site.git merge-base --is-ancestor "$(git -C "$1" rev-parse main)" main 2>/dev/null \
|| { fail "в $1 есть коммиты, которых нет в общем репозитории — отправьте или разберите их"; return 1; }
}
command -v git >/dev/null || { fail "не найден git"; return 1; }
local c
for c in anna-site boris-site; do # сначала проверить оба клона
clone_ok ~/git-course/$c || return 1
quiet ~/git-course/$c main "" || return 1
done
# --- Подготовка: после первой записи при ошибке скрипт останавливается ---
(
set -e
cd ~/git-course/anna-site
git pull -q --ff-only
cd ~/git-course/boris-site
git pull -q --ff-only
)
if [ $? -ne 0 ]; then
echo "Ошибка после начала подготовки: часть шагов могла выполниться."
echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
return 1
fi
echo "anna-site: $(git -C ~/git-course/anna-site log -1 --format=%h), boris-site: $(git -C ~/git-course/boris-site log -1 --format=%h)"
echo "Готово: оба клона совпадают с общим репозиторием."
}
rp_before7 </dev/null
SETUP
Оба хеша в итоговой строке должны совпасть.
Стеш в разборе главы 7
Борис отправляет правку расписания — нужно для пункта 4 разбора главы 7.
bash <<'SETUP'
rp_stash7() {
# --- Проверки: ничего не меняют ---
fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
course_repo() {
[ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
|| { fail "$1 — не учебный репозиторий курса"; return 1; }
}
quiet() { # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
local x f
[ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
done
for x in rebase-merge rebase-apply; do
[ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
done
[ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
for f in $(git -C "$1" ls-files --others --exclude-standard); do
case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
done
}
last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
clone_ok() { # клон курса: связан с общим репозиторием, все его коммиты уже там
course_repo "$1" || return 1
[ "$(git -C "$1" remote get-url origin 2>/dev/null)" = "$HOME/git-course/remote/site.git" ] \
|| { fail "$1 не связан с ~/git-course/remote/site.git"; return 1; }
git -C ~/git-course/remote/site.git merge-base --is-ancestor "$(git -C "$1" rev-parse main)" main 2>/dev/null \
|| { fail "в $1 есть коммиты, которых нет в общем репозитории — отправьте или разберите их"; return 1; }
}
command -v git >/dev/null || { fail "не найден git"; return 1; }
clone_ok ~/git-course/boris-site || return 1
quiet ~/git-course/boris-site main || return 1
# --- Подготовка: после первой записи при ошибке скрипт останавливается ---
(
set -e
cd ~/git-course/boris-site
git pull -q --ff-only
sed -i 's|<li>Вторник, 19:00 — Git: основы</li>|<li>Вторник, 19:00 — Git: основы (ауд. 12)</li>|' schedule.html
git commit -q -am "Указать аудиторию для вторника"
git push -q
)
if [ $? -ne 0 ]; then
echo "Ошибка после начала подготовки: часть шагов могла выполниться."
echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
return 1
fi
git -C ~/git-course/boris-site log --oneline -1
echo "Готово: Борис отправил правку расписания."
}
rp_stash7 </dev/null
SETUP
Практика главы 7
Если в папке Анны остался черновик про пятницу из разбора, сначала сохраните его:
cd ~/git-course/anna-site && git add schedule.html && git commit -m "Добавить консультацию в пятницу" && git push.
Неотслеживаемый notes.txt в клоне Анны можно оставить — это единственный разрешённый лишний файл.
bash <<'SETUP'
rp_practice7() {
# --- Проверки: ничего не меняют ---
fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
course_repo() {
[ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
|| { fail "$1 — не учебный репозиторий курса"; return 1; }
}
quiet() { # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
local x f
[ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
done
for x in rebase-merge rebase-apply; do
[ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
done
[ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
for f in $(git -C "$1" ls-files --others --exclude-standard); do
case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
done
}
last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
clone_ok() { # клон курса: связан с общим репозиторием, все его коммиты уже там
course_repo "$1" || return 1
[ "$(git -C "$1" remote get-url origin 2>/dev/null)" = "$HOME/git-course/remote/site.git" ] \
|| { fail "$1 не связан с ~/git-course/remote/site.git"; return 1; }
git -C ~/git-course/remote/site.git merge-base --is-ancestor "$(git -C "$1" rev-parse main)" main 2>/dev/null \
|| { fail "в $1 есть коммиты, которых нет в общем репозитории — отправьте или разберите их"; return 1; }
}
command -v git >/dev/null || { fail "не найден git"; return 1; }
clone_ok ~/git-course/anna-site || return 1
quiet ~/git-course/anna-site main "notes.txt" || return 1
clone_ok ~/git-course/boris-site || return 1
quiet ~/git-course/boris-site main || return 1
# --- Подготовка: после первой записи при ошибке скрипт останавливается ---
(
set -e
cd ~/git-course/anna-site && git pull -q --ff-only
cd ~/git-course/boris-site && git pull -q --ff-only
cd ~/git-course/anna-site
sed -i 's/^PORT=8082/PORT=9000/' site.conf
git commit -q -am "Перейти на порт 9000"
git push -q
cd ~/git-course/boris-site
git pull -q --ff-only
sed -i 's|<li>Четверг, 19:00 — практика</li>|<li>Четверг, 19:00 — практика (ауд. 14)</li>|' schedule.html
git commit -q -am "Указать аудиторию для четверга"
git push -q
cd ~/git-course/anna-site
sed -i 's|<ul>|&\n<li>Понедельник — выходной</li>|' schedule.html
sed -i '/Почта для вопросов/d' about.html
git status --short
)
if [ $? -ne 0 ]; then
echo "Ошибка после начала подготовки: часть шагов могла выполниться."
echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
return 1
fi
echo "Готово: ситуация практики главы 7 создана."
}
rp_practice7 </dev/null
SETUP
В конце блок покажет M about.html, M schedule.html (и ?? notes.txt, если он остался).
Разбор и проверка понимания
Лист ученика: 07-undo.md. Хеши — из проверочного запуска.
Практика
Папка ~/git-course/anna-site. Сначала разберём, где что:
git status --short
git diff about.html
M about.html
M schedule.html
…
-<p>Почта для вопросов: course@example.invalid.</p>
1. Случайная правка в папке — вернуть файл:
git restore about.html
2. Нужный черновик мешает забрать изменения — убрать на время:
git stash push -m "Черновик: понедельник"
git pull --ff-only
git stash pop
Updating 7550449..7c40f94
Fast-forward
schedule.html | 2 +-
…
Dropped refs/stash@{0} (…)
Правка Бориса (аудитория 14) и черновик Анны — в разных строках, pop соединил их без конфликта.
3. Отправленный ошибочный коммит — отменить новым коммитом:
git log -S 9000 --oneline
7550449 Перейти на порт 9000
git revert --no-edit 7550449 # подставьте свой хеш из git log -S
git push
grep PORT site.conf
[main aac7e0a] Revert "Перейти на порт 9000"
…
To /root/git-course/remote/site.git
7c40f94..aac7e0a main -> main
PORT=8082
revert прошёл, хотя в папке есть черновик: отменяемый коммит меняет site.conf, а черновик — schedule.html.
4. У Бориса:
cd ~/git-course/boris-site
git pull --ff-only
grep PORT site.conf
PORT=8082
Итог в anna-site: git status --short → M schedule.html (черновик), git stash list пуст.
Допустимо: закоммитить черновик вместо stash, если он готов, — но тогда это осознанный коммит
с сообщением, а не «временный».
Недопустимо: git reset --hard (уничтожит черновик), git push --force (затрёт коммит Бориса),
git checkout -- ./git restore . без разбора (выбросит и черновик вместе с ошибкой).
Воспроизведено: Git 2.43.0, Ubuntu 24.04.
Проверка понимания
- Подходит:
revertбезопасен всегда, просто в истории останутся ошибка и её отмена. Пока коммит не отправлен, можно и «переделать» его (git commit --amend,git resetбез--hard) — это вне курса, и риск у них есть: неверный ключresetвыбросит правки. Главное правило — переписывать можно только то, что никто, кроме вас, ещё не получил. git restore файлвыбрасывает незакоммиченную правку безвозвратно — нужно убедиться, что выбрасываете ошибку.git restore --staged файлтолько убирает файл из индекса; правка остаётся в папке, ничего не теряется.
Практика без терминала
Разобрать два варианта в браузере. Выберите наблюдения, проверьте ремонт и сохраните прогресс в аккаунте.
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.