Как делать12 октября 2026 г.9 мин чтения
Вайбкодинг: от прототипа к продуктуШаг 2 из 7

Git в AI-разработке: маленькие шаги, ветки и откат за агентом

Коротко

Работайте с агентом как с новым коллегой: каждая задача — в отдельной ветке, каждое изменение вы читаете в git diff до коммита и берёте в коммит только нужные фрагменты через git add -p. Неудачный шаг откатывают по месту ошибки: незакоммиченное — git restore, опубликованный коммит — git revert, локальный неотправленный — git reset с подстраховкой через git reflog. Параллельную задачу отдают второй сессии агента в отдельном git worktree.

Пошаговый урокУровень: НачальныйВайбкодинг: от прототипа к продуктуПрактика: ~1,5–3 ч

После материала вы сможете

  • вести работу агента в отдельной ветке и оценивать объём его изменений через 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 г.

Содержание
  1. 01Зачем агенту Git, если в чате есть откат
  2. 02Подготовка: .gitignore для секретов и ветка на задачу
  3. 03Читаем дифф до коммита
  4. 04Берём в коммит только нужное: add -p, restore, clean
  5. 05Откат неудачного шага: revert, reset и reflog
  6. 06Вторая задача параллельно: git worktree
  7. 07Практика: повторить, изменить, найти виновный коммит

Зачем агенту 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 для секретов и ветка на задачу

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

terminal
$ 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'
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод). .env.example нет в ответе check-ignore: правило !.env.example возвращает шаблон в репозиторий, а сам .env в индекс не попал.

.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, а не с текста ответа.

terminal
$ 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 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод).

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, поэтому после вопросов нет перевода строки.

terminal
$ 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,?]?
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод). Первый фрагмент добавлен (y), фрагмент с console.log пропущен (n).

y / n

Что делает
Добавить фрагмент в индекс / пропустить

s

Что делает
Разбить фрагмент на части, если между изменениями есть неизменённые строки

e

Что делает
Отредактировать фрагмент вручную

a / d

Что делает
Добавить / пропустить этот и все следующие фрагменты файла

q

Что делает
Выйти, не добавляя оставшиеся фрагменты

?

Что делает
Справка по всем клавишам

Теперь тест. Агент пометил проверку отрицательной цены как test.skip: с его первой версией она падала, и он её отключил. Отключённый тест — частый побочный эффект «добиться зелёных тестов любой ценой», и брать такую правку нельзя. Возвращаем файл, добавляем полезный новый тест агента и убираем мусор.

terminal
$ 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.js
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод). После уборки рабочая папка совпадает с индексом, и тесты проверяют ровно то, что войдёт в коммит.
  1. git restore test/price.test.js

    Возвращает файл к версии из индекса. Отменённые так правки нигде не сохраняются — выполняйте команду после чтения диффа, а не вместо него.

  2. git add test/discount-cap.test.js

    Новый тест агента проверяет задачу — добавляем его до уборки, иначе git clean удалит и его: для Git он пока такой же неотслеживаемый файл.

  3. git clean -n, затем git clean -f

    Сначала пробный запуск: -n только показывает, что будет удалено. Без -f команда по умолчанию ничего не удаляет — так работает настройка clean.requireForce.

  4. git restore src/price.js

    Убирает из рабочей папки пропущенный фрагмент с console.log. Отобранный фрагмент уже в индексе и не теряется.

  5. node --test и git commit

    Тесты прошли на том же коде, который коммитится, — включая возвращённую проверку отрицательной цены.

Откат неудачного шага: revert, reset и reflog

Агенты умеют коммитить сами. Следующий шаг агента округлил итоговую цену и сразу сделал коммит — тесты он не запускал.

terminal
$ 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
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод). grep оставляет из отчёта node --test только строки с результатами.

Если ветка уже отправлена на сервер или по ней открыт pull request, историю не переписывают. git revert записывает новый коммит, обратный ошибочному: в истории видны и ошибка, и её отмена, а у коллег ничего не ломается. По документации revert требует чистую рабочую папку — без незакоммиченных правок.

Другой случай: агент взялся переписать модуль целиком, закоммитил попытку локально, и вы решили, что направление неверное. Коммит никуда не отправлен, поэтому его можно убрать из ветки командой git reset --hard.

terminal
$ 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 class
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод). Перед сбросом git status --short пуст: незакоммиченных правок, которые сотрёт --hard, нет.

git 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 у каждой папки свои.

terminal
$ 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]
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод).

Во второй папке агент меняет formatPrice. Удалить рабочую папку с незакоммиченной работой Git не даст — по документации git worktree remove удаляет только чистые папки, остальные — лишь с --force.

terminal
$ 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
Git 2.54, Node.js 24.20, контейнер node:24-alpine (реальный вывод).

Папка удалена, а ветка 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.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.