Работайте с агентом как с новым коллегой: каждая задача — в отдельной ветке, каждое изменение вы читаете в git diff до коммита и берёте в коммит только нужные фрагменты через git add -p. Неудачный шаг откатывают по месту ошибки: незакоммиченное — git restore, опубликованный коммит — git revert, локальный неотправленный — git reset с подстраховкой через git reflog. Параллельную задачу отдают второй сессии агента в отдельном git worktree.
После материала вы сможете
- вести работу агента в отдельной ветке и оценивать объём его изменений через
git status --shortиgit diff --statдо коммита - отобрать в коммит только нужные фрагменты через
git add -p, а лишнее убрать командамиgit restoreиgit clean -n/-f - выбрать команду отката по месту ошибки —
restore,revertилиreset— и вернуть отброшенный коммит черезgit reflog - запустить вторую задачу агента в отдельном
git worktreeи найти сломавший тест коммит командойgit bisect run
Что знать заранее
Проверено на
Git 2.54 (Alpine Linux 3.24, Docker)Git 2.55 (Git for Windows, Git Bash)Node.js 24 LTS (24.20)
Сверено с документацией: 12 октября 2026 г.
Содержание
- Зачем агенту Git, если в чате есть откат
- Подготовка: .gitignore для секретов и ветка на задачу
- Читаем дифф до коммита
- Берём в коммит только нужное: add -p, restore, clean
- Откат неудачного шага: revert, reset и reflog
- Вторая задача параллельно: git worktree
- Практика: повторить, изменить, найти виновный коммит
Зачем агенту Git, если в чате есть откат
Агент за один ход меняет несколько файлов, запускает команды, создаёт временные файлы и иногда сам делает коммит. В этом и его польза: он быстро проходит рутину. Но когда правки агента смешались с вашими и с предыдущей попыткой, понять, что именно сломалось и что можно оставить, становится трудно. Git превращает работу агента в цепочку небольших шагов, каждый из которых можно прочитать, проверить и отменить отдельно.
Встроенный откат инструментов эту роль не берёт. В Claude Code есть контрольные точки (checkpoints): команда /rewind возвращает файлы, которые агент правил своими инструментами редактирования. Документация прямо перечисляет ограничения: изменения, сделанные командами оболочки вроде rm, mv и cp, не отслеживаются, правки фоновых субагентов не восстанавливаются, снимки по умолчанию удаляются примерно через 30 дней после последней записи, а сами контрольные точки — «не замена системе контроля версий». Коммит же остаётся после закрытия сессии, виден коллегам и попадает в pull request.
Ниже — сквозной пример: небольшой модуль цен интернет-магазина на JavaScript с тестами на встроенном node:test. Чтобы вывод был повторяемым, правки «агента» в проверке воспроизведены скриптом, а даты коммитов зафиксированы. Все команды Git и их вывод — настоящие, из прогона в контейнере с Git 2.54; тот же сценарий повторён в Git for Windows 2.55, хеши коммитов совпали. Основы — коммиты, ветки, слияние — разобраны в уроке «Git с нуля», здесь только то, что меняется, когда код пишет агент.
Подготовка: .gitignore для секретов и ветка на задачу
Перед первой задачей проверьте две вещи: секреты не попадут в коммит, и агент работает не в основной ветке. Ветку создают до того, как агент начал, а рабочая папка в этот момент должна быть чистой — тогда всё, что появится в диффе, сделал агент.
$ git init -q
$ printf 'node_modules/\n.env\n.env.*\n!.env.example\n.claude/worktrees/\n' > .gitignore
$ printf 'PAYMENT_API_KEY=YOUR_API_KEY\n' > .env
$ printf 'PAYMENT_API_KEY=\n' > .env.example
$ git add .
$ git status --short
A .env.example
A .gitignore
A package.json
A src/price.js
A test/price.test.js
$ git check-ignore -v .env .env.example
.gitignore:2:.env .env
$ git commit -q -m "Pricing module with tests"
$ git switch -c agent/discount-cap
Switched to a new branch 'agent/discount-cap'.env, .env.*
- Зачем в проекте с агентом
- Локальные ключи. Агент нередко сам создаёт
.env.localили.env.test— шаблон.env.*закрывает и их
!.env.example
- Зачем в проекте с агентом
- Шаблон без значений остаётся в репозитории: по нему агент и коллеги знают имена переменных
node_modules/
- Зачем в проекте с агентом
- Зависимости ставятся заново через
npm install, в истории они не нужны
.claude/worktrees/
- Зачем в проекте с агентом
- Здесь Claude Code создаёт рабочие папки при запуске с
--worktree; документация советует добавить путь в.gitignore
Имя ветки с префиксом agent/ показывает, что её вёл агент, и упрощает уборку: git branch --list 'agent/*' выводит все такие ветки. Одна задача — одна ветка: если агенту нужно сделать две несвязанные вещи, это две ветки и два pull request.
Читаем дифф до коммита
Задача агенту: «ограничь скидку 50 %». Агент отчитался, что всё готово. Отчёт описывает намерение, а дифф — результат, поэтому проверку начинают с Git, а не с текста ответа.
$ git status --short
M src/price.js
M test/price.test.js
?? agent-notes.tmp
?? test/discount-cap.test.js
$ git diff --stat
src/price.js | 8 ++++++--
test/price.test.js | 2 +-
2 files changed, 7 insertions(+), 3 deletions(-)
$ git diff src/price.js
diff --git a/src/price.js b/src/price.js
index 3f1a6d4..5231abd 100644
--- a/src/price.js
+++ b/src/price.js
@@ -1,7 +1,10 @@
-// Final price after a percentage discount
+const MAX_DISCOUNT = 50;
+
+// Final price after a percentage discount (capped at MAX_DISCOUNT)
export function finalPrice(price, discountPercent) {
if (price < 0) throw new RangeError("price must be >= 0");
- return price - (price * discountPercent) / 100;
+ const pct = Math.min(discountPercent, MAX_DISCOUNT);
+ return price - (price * pct) / 100;
}
// Sum of line items: [{ price, qty }]
@@ -11,5 +14,6 @@ export function cartTotal(items) {
// Formats an amount: 9000 -> "9000 KZT"
export function formatPrice(amount) {
+ console.log("formatPrice", amount);
return `${amount} KZT`;
}git status --short — карта изменений: M — изменённый файл, ?? — новый, о котором Git ещё не знает. git diff --stat показывает масштаб: если на задачу в одну функцию изменено двадцать файлов, остановитесь и выясните почему. Важная деталь: git diff не показывает новые неотслеживаемые файлы, поэтому test/discount-cap.test.js и agent-notes.tmp видны только в status. Чтобы новый файл попал в обычный дифф, его отмечают командой git add -N — по документации она записывает в индекс только намерение добавить путь.
В диффе src/price.js два фрагмента (hunk — участок изменений с несколькими строками контекста вокруг). Первый реализует задачу. Второй добавляет отладочный console.log в функцию formatPrice, которую никто не просил трогать. А в test/price.test.js изменена одна строка — посмотрим на неё в следующем разделе.
- Изменения за пределами задачи: «заодно» отформатированные файлы, переименования, правки конфигурации
- Отладочный вывод и временные флаги:
console.log,print,debug: true - Отключённые и ослабленные тесты:
test.skip,.only, изменённые ожидаемые значения - Новые зависимости в
package.jsonи lock-файле - Неотслеживаемые файлы: заметки агента, дампы, копии файлов
- Ключи, токены и персональные данные в коде, тестах и сообщении коммита
Это проверка объёма и гигиены. Логику, права доступа и обработку ошибок разбирают отдельно — по чек-листу из урока о ревью кода, написанного ИИ.
Берём в коммит только нужное: add -p, restore, clean
git add -p по очереди показывает фрагменты и спрашивает, добавить ли каждый в индекс (staging area — то, что войдёт в следующий коммит). В терминале вы отвечаете клавишами; в проверке ответы y и n переданы через printf, поэтому после вопросов нет перевода строки.
$ printf 'y\nn\n' | git add -p src/price.js
diff --git a/src/price.js b/src/price.js
index 3f1a6d4..5231abd 100644
--- a/src/price.js
+++ b/src/price.js
@@ -1,7 +1,10 @@
-// Final price after a percentage discount
+const MAX_DISCOUNT = 50;
+
+// Final price after a percentage discount (capped at MAX_DISCOUNT)
export function finalPrice(price, discountPercent) {
if (price < 0) throw new RangeError("price must be >= 0");
- return price - (price * discountPercent) / 100;
+ const pct = Math.min(discountPercent, MAX_DISCOUNT);
+ return price - (price * pct) / 100;
}
// Sum of line items: [{ price, qty }]
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]? @@ -11,5 +14,6 @@ export function cartTotal(items) {
// Formats an amount: 9000 -> "9000 KZT"
export function formatPrice(amount) {
+ console.log("formatPrice", amount);
return `${amount} KZT`;
}
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]?y / n
- Что делает
- Добавить фрагмент в индекс / пропустить
s
- Что делает
- Разбить фрагмент на части, если между изменениями есть неизменённые строки
e
- Что делает
- Отредактировать фрагмент вручную
a / d
- Что делает
- Добавить / пропустить этот и все следующие фрагменты файла
q
- Что делает
- Выйти, не добавляя оставшиеся фрагменты
?
- Что делает
- Справка по всем клавишам
Теперь тест. Агент пометил проверку отрицательной цены как test.skip: с его первой версией она падала, и он её отключил. Отключённый тест — частый побочный эффект «добиться зелёных тестов любой ценой», и брать такую правку нельзя. Возвращаем файл, добавляем полезный новый тест агента и убираем мусор.
$ git diff test/price.test.js
diff --git a/test/price.test.js b/test/price.test.js
index dcef809..ae7c7ac 100644
--- a/test/price.test.js
+++ b/test/price.test.js
@@ -6,7 +6,7 @@ test("applies a percentage discount", () => {
assert.equal(finalPrice(10000, 10), 9000);
});
-test("rejects a negative price", () => {
+test.skip("rejects a negative price", () => {
assert.throws(() => finalPrice(-1, 10), RangeError);
});
$ git restore test/price.test.js
$ git add test/discount-cap.test.js
$ git clean -n
Would remove agent-notes.tmp
$ git clean -f
Removing agent-notes.tmp
$ git restore src/price.js
$ git status --short
M src/price.js
A test/discount-cap.test.js
$ node --test 2>&1 | grep -E '^(✔|✖|ℹ (pass|fail|skipped))'
✔ caps the discount at 50% (3.076503ms)
✔ applies a percentage discount (1.828563ms)
✔ rejects a negative price (0.810228ms)
✔ keeps fractional amounts exact (0.641402ms)
ℹ pass 4
ℹ fail 0
ℹ skipped 0
$ git commit -m "Cap discount at 50%"
[agent/discount-cap a2720be] Cap discount at 50%
2 files changed, 12 insertions(+), 2 deletions(-)
create mode 100644 test/discount-cap.test.jsgit restore test/price.test.jsВозвращает файл к версии из индекса. Отменённые так правки нигде не сохраняются — выполняйте команду после чтения диффа, а не вместо него.
git add test/discount-cap.test.jsНовый тест агента проверяет задачу — добавляем его до уборки, иначе
git cleanудалит и его: для Git он пока такой же неотслеживаемый файл.git clean -n, затемgit clean -fСначала пробный запуск:
-nтолько показывает, что будет удалено. Без-fкоманда по умолчанию ничего не удаляет — так работает настройкаclean.requireForce.git restore src/price.jsУбирает из рабочей папки пропущенный фрагмент с
console.log. Отобранный фрагмент уже в индексе и не теряется.node --testиgit commitТесты прошли на том же коде, который коммитится, — включая возвращённую проверку отрицательной цены.
Откат неудачного шага: revert, reset и reflog
Агенты умеют коммитить сами. Следующий шаг агента округлил итоговую цену и сразу сделал коммит — тесты он не запускал.
$ git log --oneline -3
d8f1269 Round final price to whole tenge
a2720be Cap discount at 50%
ee5d239 Pricing module with tests
$ node --test 2>&1 | grep -E '^(✔|✖|ℹ (pass|fail))'
✔ caps the discount at 50% (0.950324ms)
✔ applies a percentage discount (1.136211ms)
✔ rejects a negative price (0.618073ms)
✖ keeps fractional amounts exact (1.031319ms)
ℹ pass 3
ℹ fail 1
✖ failing tests:
✖ keeps fractional amounts exact (1.031319ms)
$ git show --stat --oneline HEAD
d8f1269 Round final price to whole tenge
src/price.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git revert --no-edit HEAD
[agent/discount-cap 7ebde6d] Revert "Round final price to whole tenge"
Date: Mon Oct 12 10:00:00 2026 +0500
1 file changed, 1 insertion(+), 1 deletion(-)
$ node --test 2>&1 | grep -E '^ℹ (pass|fail)'
ℹ pass 4
ℹ fail 0Если ветка уже отправлена на сервер или по ней открыт pull request, историю не переписывают. git revert записывает новый коммит, обратный ошибочному: в истории видны и ошибка, и её отмена, а у коллег ничего не ломается. По документации revert требует чистую рабочую папку — без незакоммиченных правок.
Другой случай: агент взялся переписать модуль целиком, закоммитил попытку локально, и вы решили, что направление неверное. Коммит никуда не отправлен, поэтому его можно убрать из ветки командой git reset --hard.
$ git log --oneline -2
2aaafa5 WIP: rewrite pricing as a class
7ebde6d Revert "Round final price to whole tenge"
$ git status --short
$ git reset --hard HEAD~1
HEAD is now at 7ebde6d Revert "Round final price to whole tenge"
$ git log --oneline -1
7ebde6d Revert "Round final price to whole tenge"
$ git reflog -3
7ebde6d HEAD@{0}: reset: moving to HEAD~1
2aaafa5 HEAD@{1}: commit: WIP: rewrite pricing as a class
7ebde6d HEAD@{2}: revert: Revert "Round final price to whole tenge"
$ git branch rescue/pricing-class HEAD@{1}
$ git log --oneline -1 rescue/pricing-class
2aaafa5 WIP: rewrite pricing as a classgit reflog записывает, куда указывал HEAD после каждого коммита, сброса и переключения ветки. HEAD@{1} означает «где HEAD был один шаг назад»: по этой записи мы создали ветку rescue/pricing-class и сохранили отброшенную попытку на случай, если из неё что-то пригодится. По умолчанию записи reflog о коммитах, недостижимых из веток, хранятся 30 дней, остальные — 90.
Агент испортил файл, коммита ещё нет
- Команда
git restore файлилиgit restore -p- Что потеряется
- Отменённые правки — их не вернуть
Лишнее попало в индекс
- Команда
git restore --staged файл- Что потеряется
- Ничего: правки остаются в рабочей папке
Ошибочный коммит уже отправлен или в pull request
- Команда
git revert <коммит>- Что потеряется
- Ничего: история не переписывается
Локальный неотправленный коммит не нужен
- Команда
git reset --hard HEAD~1- Что потеряется
- Незакоммиченные правки; сам коммит возвращается через
reflog
Коммит убрать, а изменения оставить для доработки
- Команда
git reset --soft HEAD~1- Что потеряется
- Ничего: изменения остаются в индексе
Вторая задача параллельно: git worktree
Пока одна сессия агента доводит скидку, вторую задачу — формат цены в чеке — удобно отдать другой сессии. Если обе работают в одной папке, их правки смешаются. git worktree создаёт вторую рабочую папку на своей ветке: репозиторий и история общие, а файлы, индекс и HEAD у каждой папки свои.
$ git worktree add ../shop-receipt -b agent/receipt-format
Preparing worktree (new branch 'agent/receipt-format')
HEAD is now at 7ebde6d Revert "Round final price to whole tenge"
$ git worktree list
/lab/shop 7ebde6d [agent/discount-cap]
/lab/shop-receipt 7ebde6d [agent/receipt-format]Во второй папке агент меняет formatPrice. Удалить рабочую папку с незакоммиченной работой Git не даст — по документации git worktree remove удаляет только чистые папки, остальные — лишь с --force.
$ git -C ../shop-receipt status --short
M src/price.js
$ git worktree remove ../shop-receipt
fatal: '../shop-receipt' contains modified or untracked files, use --force to delete it
$ git -C ../shop-receipt commit -q -am "Show prices with two decimals"
$ git worktree remove ../shop-receipt
$ git worktree list
/lab/shop 7ebde6d [agent/discount-cap]
$ git branch --list 'agent/*'
* agent/discount-cap
agent/receipt-format
$ git log --oneline -1 agent/receipt-format
f12f10d Show prices with two decimalsПапка удалена, а ветка agent/receipt-format с коммитом осталась: её можно проверить, слить или открыть по ней pull request. В Claude Code то же делается одним флагом.
claude --worktree имя(или-w) создаёт рабочую папку в.claude/worktrees/<имя>/на новой веткеworktree-<имя>; без имени Claude Code придумает его сам.- Worktree — свежая копия: зависимости в ней ставятся заново, а игнорируемые файлы вроде
.envне копируются. Для копирования есть файл.worktreeincludeс синтаксисом.gitignore. - Одну ветку нельзя открыть в двух рабочих папках одновременно:
git worktree addоткажется. - При выходе из сессии Claude Code проверяет папку: чистую неименованную удаляет вместе с веткой, с изменениями или коммитами — спрашивает, оставить ли её.
Практика: повторить, изменить, найти виновный коммит
Упражнения можно делать на своём проекте или на учебном модуле из урока: его исходники, правки «агента» и сценарий проверки сохранены в архиве проверочных проектов.
Как проверено4 · пройденосверено 12 октября 2026 г.
- запущено, работает
Сквозной сценарий урока: .gitignore и check-ignore, ветка agent/*, status/diff --stat, add -p, restore, clean -n/-f, node --test, revert, reset --hard + reflog, worktree add/remove, bisect run
verify.sh в контейнере node:24-alpine (Node.js 24.20, Git 2.54 из Alpine 3.24, docker run --rm, имя kb-g-git-ai); весь вывод в статье — из out-linux/*.txt. Правки «агента» воспроизведены скриптами agent/step*.sh, даты коммитов зафиксированы (GIT_AUTHOR_DATE/GIT_COMMITTER_DATE), изолированный конфиг Git (GIT_CONFIG_GLOBAL)
- запущено, работает
Тот же сценарий в Git for Windows
bash verify.sh в Git Bash, Git 2.55.0.windows.1, Node.js 24.18 — out/*.txt; хеши коммитов совпали с Linux-прогоном, отличия только в путях worktree list, времени тестов и кавычках в сообщении bisect («first 'bad' commit»)
- запущено, работает
Упражнение «измени»: git restore -p вместо add -p
parts/7-ex-modify.txt в том же контейнере: ответы n, y; git diff --staged --stat — src/price.js, 5 insertions, 2 deletions
- запущено, работает
Упражнение «примени»: git bisect run
parts/6-bisect.txt: четыре коммита agent/step5.sh, bisect run sh -c "node --test > /dev/null 2>&1" нашёл коммит с Math.floor за два запуска
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.