Любой проект в команде живёт в Git, а любой код однажды ломается. Сегодня вы научитесь сохранять историю работы, предлагать изменения через pull request, разрешать конфликты и спокойно искать баги — вместо слов «у меня не работает».
Делать коммиты в понятной истории и писать сообщения по Conventional Commits
Работать с ветками и проходить цикл «ветка → push → pull request → ревью → merge»
Разрешать конфликт слияния и настраивать .gitignore
Читать стек вызовов, ставить breakpoint в DevTools и смотреть вкладку Network
Описывать баг так, чтобы ИИ (и человек) смог помочь: минимальный воспроизводимый пример
Как работать с этой страницей
Блоки «Терминал» — это команды для вашего компьютера, в браузере они не запускаются: набирайте их в терминале VS Code. Код на JavaScript с кнопкой ▶ Запустить — живой: его можно менять и проверять прямо здесь. Git мы моделируем обычными массивами и объектами, так что логику add, commit, веток и конфликтов можно потрогать без установки. Подсказки к задачам спрятаны: сначала попробуйте сами.
01 Git: история проекта
Представьте, что вы делаете сайт для чойхоны. Сегодня всё работает, завтра вы «немного поправили» меню, и сайт сломался. Как вернуться? Копировать папку с названиями site-final, site-final-2, site-final-REAL? Так делают новички. Профессионалы используют Git — систему контроля версий. Она записывает каждое сохранённое состояние проекта, показывает, что и кто изменил, и позволяет вернуться к любому моменту.
Аналогия: семейный альбом
Вы выбираете из телефона снимки со свадьбы и кладёте их в конверт — это git add. Потом вклеиваете конверт в альбом и подписываете «Никоҳ, 12 июня» — это git commit. Альбом хранит все страницы по порядку. Захотите — откроете любую страницу и увидите, как всё выглядело тогда.
Три зоны и четыре места
Файл в Git проходит три зоны на вашем компьютере. Четвёртое место — сервер GitHub, куда вы отправляете копию.
Рабочая папкафайлы, которые вы правите в редакторе
→
Индекс (staging)«конверт»: что войдёт в следующий коммитgit add
→
Репозиторий«альбом»: сохранённая история на вашем компьютереgit commit
→
GitHub (remote)копия в интернете, общая для командыgit push
mkdir chaykhona-site
cd chaykhona-site
git init -b main
echo "# Чойхона" > README.md
git status
git add README.md
git commit -m "docs: add readme"
git log --oneline
Что произошло по шагам:
git init создал скрытую папку .git — в ней живёт вся история. Удалите её, и проект перестанет быть репозиторием.
git status сказал: файл README.md «untracked» — Git его ещё не отслеживает.
git add положил файл в индекс («конверт»).
git commit -m "…" записал снимок с подписью.
git log --oneline показал историю: короткий код коммита и подпись.
Команды, которые вы будете использовать каждый день
Команда
Что делает
git status
Показывает, какие файлы изменены, какие в индексе, какие не отслеживаются
git add файл / git add .
Кладёт файл (или все изменения) в индекс
git commit -m "текст"
Сохраняет индекс как коммит с подписью
git log --oneline
История коротко, по одной строке на коммит
git diff
Что изменено, но ещё не в индексе
git diff --staged
Что лежит в индексе и войдёт в коммит
git restore файл
Отменяет несохранённые правки файла (вернёт его к последнему коммиту)
git restore --staged файл
Достаёт файл из индекса, правки остаются в папке
Частые ошибки
1.git add . не глядя: в коммит попадают лишние файлы (пароли, node_modules). Сначала git status, потом git add.
2.git commit без -m открывает текстовый редактор Vim. Если застряли: нажмите Esc, введите :q! и Enter. Либо всегда пишите -m.
3. Один гигантский коммит «всё за неделю». Коммит — это одна законченная мысль: «добавил фильтр», «починил итоговую сумму».
Чтобы почувствовать, как устроены зоны, смоделируем их кодом. Это первая задача на странице.
Практика: закрепляем
Мы моделируем Git обычным JavaScript — так вы увидите, что внутри нет магии.
ЗАДАЧА 1.1
Три зоны Git в коде
лёгкая
Мы смоделируем три зоны Git обычными массивами. Допишите add (git add: файл переезжает из work в staged) и commit (git commit: всё из staged уходит в новый коммит, а staged очищается). История выводится как в git log — новые коммиты сверху.
Можно редактировать · Ctrl + Enter
const repo = {
work: ["index.html", "style.css", "app.js"], // изменённые файлы
staged: [], // подготовлены: git add
commits: [], // история: git commit
};
functionadd(file) {
// убрать файл из work и положить в staged
}
functioncommit(message) {
// сохранить файлы из staged в новый коммит { message, files }// и очистить staged
}
add("index.html");
add("style.css");
commit("add page skeleton");
add("app.js");
commit("add order counter");
for (let i = repo.commits.length - 1; i >= 0; i--) {
const c = repo.commits[i];
console.log(`${i + 1}. ${c.message} (${c.files.join(", ")})`);
}
console.log(repo.work.length, repo.staged.length);
В add нужны две вещи: убрать файл из work (например, через filter) и сделать push в staged.
Подсказка 2
В commit сохраните копию списка: files: [...repo.staged]. Потом repo.staged = []. Привычка копировать пригодится в разделе 8 — там мутация ломает программы.
ЗАДАЧА 1.2
Что покажет git status
лёгкая
Функция statusLine описывает файл так, как это делает git status. Но один случай она не знает: файл уже в индексе (staged), а после git add его ещё раз изменили (changedAfterAdd: true). Для него нужна строка staged, but changed again: имя. Допишите условие.
В репозитории есть главная ветка — обычно main (иногда её называют master). В ней лежит версия, которая «точно работает». Новую функцию делают в отдельной ветке: можно пробовать, ошибаться и коммитить сколько угодно — основная ветка не пострадает. Когда функция готова и проверена, ветку вливают обратно.
Аналогия: пробный плов
В большом казане варится плов для гостей — это main. Вы хотите проверить новый рецепт с айвой. Вы не лезете в общий казан, а берёте маленькую кастрюлю — это ветка. Получилось вкусно — добавили айву в общий казан (merge). Не получилось — выбросили кастрюлю, и гости ничего не заметили.
Зелёная линия — main, синяя — ветка feature/filters. Пока вы работаете в ветке, main живёт своей жизнью, потом ветки сливаются коммитом merge
Ветка — это не копия папки. Это всего лишь подвижная «закладка» на один из коммитов. Новый коммит двигает закладку вперёд. Благодаря этому ветки в Git создаются мгновенно и стоят почти ничего. Особая закладка HEAD показывает, на какой ветке вы сейчас стоите.
Терминал
git branch # список веток, звёздочка у текущей
git switch -c feature/filters # создать ветку и перейти на неё
# ...правим файлы, git add, git commit...
git log --oneline --graph --all # история в виде дерева
git switch main # вернуться на main
git merge feature/filters # влить ветку в main
git branch -d feature/filters # удалить ветку, она больше не нужна
Как называть ветки
В командах ветку называют по шаблону тип/короткое-описание: feature/order-filters, fix/wrong-total, docs/readme-tajik. Английские слова, строчные буквы, без пробелов, слова через дефис. По имени сразу видно, что в ветке и зачем.
Частые ошибки
Работать прямо в main. Тогда PR сделать не из чего, а в случае проблемы нечем откатиться. Привычка: сначала git switch -c, потом код.
Переключать ветку с несохранёнными правками. Git либо перенесёт их, либо откажется. Лучше сначала сделать коммит.
Ветки — это граф коммитов, и мы можем обойти его кодом. Начнём с простого: пройдём цепочку «родитель → родитель», как это делает git log.
Практика: закрепляем
ЗАДАЧА 2.1
История ветки
лёгкая
Каждый коммит помнит своего «родителя». Ветка — это просто указатель на последний коммит. Допишите history(branch): пройдите от конца ветки по цепочке parent и верните массив сообщений от новых к старым — так работает git log.
Можно редактировать · Ctrl + Enter
const commits = [
{ id: "c1", parent: null, message: "init project" },
{ id: "c2", parent: "c1", message: "add header" },
{ id: "c3", parent: "c2", message: "add footer" }, // конец main
{ id: "f1", parent: "c2", message: "add filters" }, // ветка feature отошла от c2
{ id: "f2", parent: "f1", message: "add sort" }, // конец feature
];
const branches = { main: "c3", feature: "f2" };
const byId = Object.fromEntries(commits.map((c) => [c.id, c]));
functionhistory(branch) {
// начните с branches[branch] и идите по parent, пока он не станет null
}
console.log(history("main").join(" <- "));
console.log(history("feature").join(" <- "));
Заведите let id = branches[branch] и цикл while (id !== null). В теле: достаньте коммит byId[id], сохраните его message, перейдите к parent.
Подсказка 2
Не забудьте менять id в каждом витке, иначе цикл станет бесконечным.
ЗАДАЧА 2.2
Что есть только в моей ветке
средняя
Перед pull request полезно знать: какие коммиты есть в вашей ветке, но ещё не попали в основную? В Git это записывают как main..feature. Функция history уже готова. Допишите onlyIn(branch, base): сообщения коммитов из branch, которых нет в base.
Git живёт на вашем компьютере. GitHub — это сайт, где хранится копия репозитория (её называют origin — «удалённый репозиторий»). Именно через GitHub команда обменивается кодом, а работодатель смотрит ваше портфолио.
Терминал
git remote add origin https://github.com/ВАШ_ЛОГИН/chaykhona-site.git
git push -u origin main # первая отправка, -u запоминает связь веток
git push # дальше достаточно просто push
git clone https://github.com/ВАШ_ЛОГИН/chaykhona-site.git # скачать чужой репозиторий
git pull # забрать новые коммиты с GitHub
git fetch # только скачать информацию, ничего не менять
git pull — это fetch (скачать) + merge (влить в вашу ветку). Если на GitHub появились новые коммиты коллег, а у вас нет, перед push сначала делают pull.
Pull request — главная идея командной работы
Pull request (PR) — это просьба: «посмотрите мои изменения в ветке и, если всё хорошо, влейте их в main». PR — не команда Git, а возможность GitHub. Вы пушите ветку, на сайте нажимаете «Compare & pull request», пишете описание, а коллега читает изменения построчно, оставляет комментарии, одобряет или просит правки.
Аналогия: чек перед оплатой на бозоре
Продавец записал сумму на бумажке, но перед оплатой вы показываете бумажку соседу: «Проверь, правильно ли сложил?». Сосед нашёл ошибку в одну сомони — вы исправили, и только потом отдали деньги. PR — тот же «взгляд со стороны» перед тем, как код попадёт в основную ветку.
Рабочий цикл в паре
Именно так вы будете работать сегодня на занятии и дальше — в проектах и на работе:
Обновите main: git switch main, git pull.
Создайте ветку под одну задачу: git switch -c feature/order-filters.
Работайте и коммитьте небольшими осмысленными шагами.
Отправьте ветку: git push -u origin feature/order-filters. GitHub напечатает ссылку для создания PR.
Откройте pull request: заголовок, что сделано, зачем, как проверить. Назначьте напарника ревьюером.
Ревью: напарник читает diff, пишет комментарии. Вы отвечаете и правите — новые коммиты в ту же ветку попадут в PR сами.
Merge: после одобрения (approve) и успешных проверок PR вливают в main кнопкой Merge pull request на GitHub.
Работает ли код (запустите ветку)? Понятны ли имена? Нет ли лишних файлов и console.log? Описаны ли граничные случаи (пустой список, ноль, ошибка сети)? Комментарий пишем про код, а не про человека: «Здесь при пустом списке будет ошибка» вместо «Ты забыл».
Три способа влить PR
Кнопка
Что получится в истории
Create a merge commit
Все коммиты ветки сохраняются, плюс коммит слияния
Squash and merge
Все коммиты PR склеиваются в один — история main короче
Rebase and merge
Коммиты ветки переносятся в main по одному, без коммита слияния
Для начала подойдёт первый вариант. Остальные вы выберете, когда в команде договорятся о стиле истории.
Правила безопасности
Не пушьте в main напрямую и не сливайте свой PR без ревью. Во многих командах main закрыт: только через PR.
Не используйте git push --force, пока не понимаете, что делаете: команда стирает чужие коммиты на сервере.
Никогда не коммитьте пароли и ключи API. Разберём в разделе про .gitignore.
Практика: закрепляем
ЗАДАЧА 3.1
Описание pull request
средняя
Хорошее описание PR экономит ревьюеру время. Допишите buildPr(pr): функция возвращает одну строку в формате из «Ожидаемого результата»: заголовок «Что сделано» с маркированным списком, пустая строка, «Зачем», пустая строка, «Как проверить» с нумерованным списком.
Можно редактировать · Ctrl + Enter
functionbuildPr(pr) {
// верните строку; соберите массив строк и вызовите join("\n")
}
const pr = {
what: ["добавлен фильтр заказов по статусу", "фильтр запоминается в localStorage"],
why: "Менеджер хочет быстро находить неоплаченные заказы.",
check: ["Откройте страницу заказов", "Выберите статус «Не оплачен»", "Обновите страницу: фильтр остался"],
};
console.log(buildPr(pr));
Ожидаемый результат
## Что сделано
- добавлен фильтр заказов по статусу
- фильтр запоминается в localStorage
## Зачем
Менеджер хочет быстро находить неоплаченные заказы.
## Как проверить
1. Откройте страницу заказов
2. Выберите статус «Не оплачен»
3. Обновите страницу: фильтр остался
Подсказка 1
Начните с массива const lines = ["## Что сделано"]. Пункты добавляйте через forEach: lines.push(`- ${w}`). Пустая строка в массиве — это просто "".
Подсказка 2
Нумерация: pr.check.forEach((c, i) => lines.push(`${i + 1}. ${c}`)). В конце — lines.join("\n").
ЗАДАЧА 3.2
Можно ли делать merge
сложная
На GitHub кнопка Merge «серая», пока не выполнены условия. Допишите mergeProblems(pr): функция возвращает массив причин (в таком порядке): «нет одобрения» (approvals < 1), «проверки не прошли» (checks не равно "passed"), «есть конфликт» (conflicts), «это черновик» (draft). Если причин нет — пустой массив.
Можно редактировать · Ctrl + Enter
functionmergeProblems(pr) {
const problems = [];
// проверьте четыре условия и добавляйте причины в массивreturn problems;
}
const prs = [
{ title: "add order filters", approvals: 1, checks: "passed", conflicts: false, draft: false },
{ title: "fix cart total", approvals: 0, checks: "passed", conflicts: false, draft: false },
{ title: "wip: new header", approvals: 2, checks: "failed", conflicts: true, draft: true },
];
for (const pr of prs) {
const p = mergeProblems(pr);
console.log(`${pr.title}: ${p.length ? p.join("; ") : "можно мержить"}`);
}
Ожидаемый результат
add order filters: можно мержить
fix cart total: нет одобрения
wip: new header: проверки не прошли; есть конфликт; это черновик
Подсказка 1
Четыре независимых if, каждый делает problems.push("…"). else здесь не нужен: причин может быть несколько сразу.
Подсказка 2
Для проверок используйте pr.approvals < 1 и pr.checks !== "passed".
Git умеет сливать изменения автоматически, если вы и напарник правили разные места. Но если вы оба изменили одну и ту же строку по-разному, Git не знает, чья версия верна, и просит человека решить. Это и есть конфликт. Он не поломка, а обычная часть работы: в любой команде конфликты случаются каждую неделю.
Допустим, вы с напарником правили файл rate.js. Напарник уже влил свою ветку в main, а вы сделали git pull origin main в своей ветке:
Терминал
git switch feature/rate
git pull origin main
# Auto-merging rate.js
# CONFLICT (content): Merge conflict in rate.js
# Automatic merge failed; fix conflicts and then commit the result.
Git остановился и записал в файл маркеры конфликта:
rate.js после неудачного merge
// курс обмена
<<<<<<< HEAD
const convertUsd = (usd, rate = 10.9) => Math.round(usd * rate);
=======
const toSomoni = (usd) => usd * 11;
>>>>>>> origin/main
<<<<<<< HEAD
Начало конфликта. Ниже — ваша версия (из ветки, на которой вы стоите).
=======
Разделитель. Ниже — версия из другой ветки (здесь origin/main).
>>>>>>> origin/main
Конец конфликта и имя ветки, откуда пришла вторая версия.
Как разрешить конфликт
Откройте файл со словом CONFLICT. В VS Code над блоком появятся кнопки: Accept Current (моё), Accept Incoming (чужое), Accept Both (оба) и Compare.
Решите по смыслу, что должно остаться. Иногда нужно взять кусочки из обеих версий — и написать итоговый код руками. Договоритесь с автором второй правки.
Удалите все три строки маркеров. В файле должен остаться только нормальный код.
Запустите проект и убедитесь, что всё работает.
Завершите слияние:
Терминал
git add rate.js
git commit -m "merge main into feature/rate"
git push
Если запутались
Команда git merge --abort (или git rebase --abort) отменяет слияние и возвращает всё, как было до него. Конфликт решают в своей ветке до merge: тогда в main попадёт уже чистый результат.
Опасная ошибка
Закоммитить файл, в котором остались строки <<<<<<<. Такой код не запустится. Перед git add поищите в проекте <<<<<<< (Ctrl+Shift+F в VS Code).
Маркеры — это обычный текст, и разобрать их можно кодом. Первую задачу вы решите руками, как в настоящем конфликте. Во второй напишете «автоматическое разрешение», как это делают редакторы.
Практика: закрепляем
ЗАДАЧА 4.1
Решаем конфликт руками
средняя
После неудачного merge файл выглядит как в переменной conflicted. Команда решила: имя функции оставляем из main (toSomoni), а формулу берём из ветки feature/rate: параметр rate со значением по умолчанию 10.9 и округление. Впишите в resolved итоговый текст файла: без маркеров и с одной строкой кода. Четыре проверки ниже должны выдать false / true / true / true.
Можно редактировать · Ctrl + Enter
const conflicted = `// курс обмена
<<<<<<< HEAD
const toSomoni = (usd) => usd * 11;
=======
const convertUsd = (usd, rate = 10.9) => Math.round(usd * rate);
>>>>>>> feature/rate
`;
const resolved = `// курс обмена
`; // допишите итоговую строку кода
console.log("маркеры остались:", /<<<<<<<|=======|>>>>>>>/.test(resolved));
console.log("имя из main:", resolved.includes("const toSomoni ="));
console.log("формула из feature:", resolved.includes("rate = 10.9") && resolved.includes("Math.round(usd * rate)"));
console.log("лишнее убрано:", !resolved.includes("convertUsd") && !resolved.includes("usd * 11"));
Ожидаемый результат
маркеры остались: false
имя из main: true
формула из feature: true
лишнее убрано: true
Подсказка 1
Итог — одна строка: const toSomoni = (usd, rate = 10.9) => Math.round(usd * rate);. Строки с <<<<<<<, ======= и >>>>>>> в файле остаться не должны.
Подсказка 2
Проверка ищет в тексте точные куски: const toSomoni =, rate = 10.9 и Math.round(usd * rate). Следите за пробелами и опечатками.
ЗАДАЧА 4.2
Автоматическое разрешение
сложная
Напишите resolveAll(text, side), которая убирает маркеры конфликта. side = "ours" оставляет версию из HEAD (до =======), "theirs" — версию из чужой ветки (после =======), "both" — обе подряд. Строки вне конфликта сохраняются как есть. Идите по строкам и помните, в какой части блока сейчас находитесь.
Можно редактировать · Ctrl + Enter
const conflicted = [
"<h1>MakeFirstStep</h1>",
"<<<<<<< HEAD",
"<p>Курсы программирования</p>",
"=======",
"<p>Курсы Frontend и ИИ</p>",
">>>>>>> feature/text",
"<footer>Душанбе</footer>",
].join("\n");
functionresolveAll(text, side) {
const out = [];
let part = "none"; // none | ours | theirsfor (const line of text.split("\n")) {
// ваш код
}
return out.join("\n");
}
for (const side of ["ours", "theirs", "both"]) {
console.log(side + ": " + resolveAll(conflicted, side).split("\n").join(" / "));
}
Три «переключателя»: строка, начинающаяся с <<<<<<< , включает режим "ours"; строка ======= — "theirs"; строка с >>>>>>> возвращает "none". Сами маркеры в результат не попадают.
Подсказка 2
Строку вне конфликта (part === "none") добавляем всегда; строку внутри — только если part === side или side === "both".
В папке проекта бывают файлы, которые нельзя или не нужно хранить в Git. Их перечисляют в файле .gitignore в корне проекта: Git не будет их отслеживать и не покажет в git status.
.gitignore
# зависимости: восстанавливаются командой npm install
node_modules/
# готовая сборка: создаётся командой npm run build
dist/
# секреты: пароли, ключи API
.env
# логи и системный мусор
*.log
.DS_Store
имя/
Папка с этим именем, где бы она ни лежала: dist/, node_modules/.
*.расширение
Все файлы с таким окончанием: *.log.
имя
Файл с точно таким именем: .env. Файл .env.example под это правило не попадёт.
!имя
Исключение: вернуть файл под контроль Git. Стоит после общего правила.
# текст
Комментарий.
Настоящий .gitignore поддерживает ещё много синтаксиса (**, пути со слэшем), но этого хватит почти всегда. В задаче ниже вы пользуетесь именно таким упрощённым «движком».
Самая дорогая ошибка
Закоммитили .env с ключом, а потом добавили его в .gitignore. Не помогает: файл уже в истории. Любой, кто откроет репозиторий, найдёт ключ в старом коммите. Правильно: сразу отозвать (сменить) ключ у сервиса и только потом чистить репозиторий. Если файл уже отслеживается, его можно убрать из Git командой git rm --cached .env — на диске он останется.
Сообщения коммитов: Conventional Commits
Через полгода вы не вспомните, что значило «update» или «fix». Поэтому в командах договариваются о формате: тип(область): что сделано. Область необязательна.
Тип
Когда
Пример
feat
новая возможность
feat: add order filters
fix
исправление бага
fix(cart): count first item in total
docs
документация, README
docs: add install steps
style
форматирование без смысла
style: format with prettier
refactor
переписали, поведение то же
refactor: extract calcTotal
test
тесты
test: cover empty cart
chore
служебное: зависимости, настройки
chore: bump deps
Плохо
update, fix, changes, asdf, final, Fixed bug. Непонятно что, где и зачем; нельзя найти нужный коммит.
Хорошо
fix: wrong total in cart, feat(ui): add dark theme. Глагол в повелительном наклонении («add», «fix»), строчные буквы, до 50 символов, без точки, одна мысль на коммит.
Чтобы закрепить, посмотрим, как классификация сообщений выглядит в коде. Запустите пример и поменяйте сообщения:
Тип коммита по сообщению · Ctrl + Enter
const messages = ["feat: add filters", "fix(cart): sum", "update", "docs: readme"];
for (const m of messages) {
const match = m.match(/^([a-z]+)(\([^)]*\))?: /);
console.log(m.padEnd(20), "->", match ? match[1] : "нет типа");
}
Практика: закрепляем
ЗАДАЧА 5.1
Пишем .gitignore
средняя
Ниже — упрощённый «движок» .gitignore (он понимает имя/, *.расширение, точное имя и !отмену). Впишите в массив rules такие правила, чтобы игнорировались папки node_modules, dist, coverage, файл .env (но не .env.example), системный .DS_Store и все *.log, кромеkeep.log. Остальное должно остаться под контролем Git.
Можно редактировать · Ctrl + Enter
functionmatchRule(rule, path) {
const parts = path.split("/");
const name = parts[parts.length - 1];
if (rule.endsWith("/")) { // «папка/»: любой каталог с таким именемreturn parts.slice(0, -1).includes(rule.slice(0, -1));
}
if (rule.startsWith("*.")) return name.endsWith(rule.slice(1)); // «*.log»: по расширениюreturn name === rule; // точное имя файла
}
functionisIgnored(path, rules) {
let ignored = false;
for (const rule of rules) { // правила читаются сверху внизif (rule.startsWith("!")) { // «!имя» возвращает файл обратноif (matchRule(rule.slice(1), path)) ignored = false;
} elseif (matchRule(rule, path)) {
ignored = true;
}
}
return ignored;
}
const paths = [
"node_modules/react/index.js", "dist/app.js", "src/app.js", ".env", ".env.example",
"debug.log", "logs/server.log", "keep.log", "README.md", ".DS_Store", "src/.DS_Store", "coverage/index.html",
];
const rules = [
// впишите правила строками
];
console.log("игнорируются:", paths.filter((p) =>isIgnored(p, rules)).join(", "));
console.log("остаются:", paths.filter((p) => !isIgnored(p, rules)).join(", "));
Папки пишутся со слэшем на конце: "dist/". Расширение — со звёздочкой: "*.log". Файл — точным именем: ".env".
Подсказка 2
Исключение обязано стоять после общего правила: "*.log", "!keep.log". Побеждает последнее подходящее правило.
ЗАДАЧА 5.2
Статистика коммитов
лёгкая
В формате Conventional Commits сообщение начинается с типа: feat: …, fix(cart): …. Допишите commitType(msg): она возвращает тип (слово до двоеточия или до скобки) либо "other", если сообщение не по формату. Подсчёт и вывод уже готовы.
Можно редактировать · Ctrl + Enter
const messages = [
"feat: add order filters",
"fix: wrong total in cart",
"docs: update readme",
"fix(cart): off-by-one in loop",
"update",
"feat(ui): dark theme",
"fix: crash on empty order",
"Changes",
];
functioncommitType(msg) {
// регулярка: слово в начале, необязательная (область), двоеточие и пробелreturn"other";
}
const counts = {};
for (const m of messages) {
const type = commitType(m);
counts[type] = (counts[type] ||0) + 1;
}
console.log(JSON.stringify(counts));
Ожидаемый результат
{"feat":2,"fix":3,"docs":1,"other":2}
Подсказка 1
Регулярка: /^([a-z]+)(\([^)]*\))?: /. Первая группа — тип, вторая (необязательная) — область в скобках.
Подсказка 2
const m = msg.match(regex); return m ? m[1] : "other";
ЗАДАЧА 5.3
Линтер коммитов
сложная
Напишите lintCommit(msg) — массив проблем. Правила: (1) сообщение должно начинаться с feat|fix|docs|style|refactor|test|chore, необязательной (области) и : ; иначе одна проблема «нет типа вида feat: …» — дальше можно не проверять; (2) «тема длиннее 50 символов» (темой считаем текст после : ); (3) «тема с заглавной буквы»; (4) «точка в конце». Пустой массив значит «всё хорошо».
Можно редактировать · Ctrl + Enter
functionlintCommit(msg) {
const problems = [];
return problems;
}
const list = [
"feat: add order filters",
"Fixed bug.",
"fix(cart): Wrong total.",
"docs: describe in the readme how to install dependencies and run the project",
];
for (const m of list) {
const p = lintCommit(m);
console.log(`${m.slice(0, 24)} -> ${p.length ? p.join("; ") : "ok"}`);
}
Ожидаемый результат
feat: add order filters -> ok
Fixed bug. -> нет типа вида feat: …
fix(cart): Wrong total. -> тема с заглавной буквы; точка в конце
docs: describe in the re -> тема длиннее 50 символов
Подсказка 1
Одна регулярка с тремя группами: /^(feat|fix|docs|style|refactor|test|chore)(\([^)]*\))?: (.+)$/. Тема — это m[3].
Подсказка 2
Если match вернул null — сразу return ["нет типа вида feat: …"]. Остальные проверки — три независимых if с push.
Ошибки случаются у всех: пользователь ввёл пустую строку, сервер не ответил, из localStorage пришёл мусор. Если ничего не делать, программа остановится на первой же строке с ошибкой. Задача разработчика — заранее предусмотреть места, где код может упасть, и сделать так, чтобы пользователь увидел понятное сообщение, а не белый экран.
try / catch / finally
Аналогия: страховка в маршрутке
try — «попробуем проехать по этой дороге». catch — запасной план, если дорога перекрыта: свернуть и поехать в объезд. finally — то, что произойдёт в любом случае: выйти из маршрутки и заплатить водителю.
Ошибка внутри try немедленно прерывает его, и управление уходит в catch. Оставшиеся строки try пропускаются. В catch приходит объект ошибки (его обычно называют error или e).
Что внутри объекта ошибки
name
Тип ошибки: TypeError, ReferenceError, SyntaxError, RangeError…
message
Человеческое описание: «Cannot read properties of null».
stack
Стек вызовов — цепочка функций, которые привели к ошибке. Разберём в разделе 7.
Основные типы ошибок
Тип
Когда возникает
Пример
TypeError
Действие невозможно для этого типа значения: читаем свойство у null / undefined, вызываем не функцию
null.name
ReferenceError
Переменная не существует (опечатка, не объявлена, вне области видимости)
console.log(pirce)
SyntaxError
Код написан не по правилам языка; также ошибки JSON.parse
Иногда ошибку нужно создать самому. Например, функция получила от пользователя отрицательное количество. throw прерывает текущую функцию и «бросает» ошибку вверх — до ближайшего catch. Бросайте объект ошибки (new Error("текст")), а не просто строку: у объекта есть стек.
throw new Error · Ctrl + Enter
functionsetQuantity(qty) {
if (!Number.isInteger(qty)) thrownewTypeError("Количество должно быть целым числом");
if (qty < 1) thrownewRangeError("Количество не может быть меньше 1");
return qty;
}
for (const value of [3, 0, 2.5]) {
try {
console.log("ок:", setQuantity(value));
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
}
Частая ошибка: пустой catch
Пустой catch (e) {} «проглатывает» ошибку: программа молчит и ведёт себя странно, а причину не найти. Минимум — выведите ошибку в консоль или покажите пользователю сообщение. И ловите только те ошибки, которые умеете обработать: остальные пробрасывайте дальше через throw e (задача 6.5).
Этот код выдаст ошибку · Ctrl + Enter
functionrisky() {
thrownewError("что-то пошло не так");
}
risky(); // ошибка не поймана: выполнение остановилось
console.log("до этой строки мы уже не дойдём");
Ошибки в асинхронном коде ловят тем же try / catch вокруг await (вы видели это на уроке 13, а на следующих уроках будете так обрабатывать запросы fetch). Отдельный try вокруг setTimeout не сработает: колбэк выполнится позже, когда блок уже завершился.
Практика: закрепляем
ЗАДАЧА 6.1
Предскажи: порядок с finally
лёгкая
Сначала без запуска решите, в каком порядке появятся строки. Блок finally выполняется «всегда» — даже если в try уже был return. Впишите прогноз в prediction (строки по порядку, ровно как их показал бы console.log) и запустите. Менять остальной код не нужно.
Функция заканчивается на return, но значение «отдаётся» наружу только после того, как выполнится finally.
Подсказка 2
Строка «после» не появится: функция уже вернула значение. «catch» не появится: ошибки не было.
ЗАДАЧА 6.2
Имя ошибки
лёгкая
Допишите errorName(fn): вызовите функцию внутри try, а если она бросила ошибку — верните её name (например, "TypeError"). Если ошибки нет — верните "ok".
Свойство name у объекта ошибки — это «тип»: TypeError, ReferenceError, SyntaxError, RangeError.
ЗАДАЧА 6.3
Безопасное чтение JSON
средняя
Данные из localStorage или с сервера бывают повреждены. Допишите safeParse(text, fallback): возвращает результат JSON.parse, а при ошибке выводит ошибка чтения: ИмяОшибки и возвращает fallback.
В try — return JSON.parse(text). В catch — console.log("ошибка чтения:", e.name) и return fallback.
Подсказка 2
Пустая строка тоже невалидный JSON, поэтому для третьего вызова catch тоже сработает и вернёт пустой массив.
ЗАДАЧА 6.4
Свои ошибки через throw
средняя
Допишите withdraw(balance, amount) — снятие сомони. Если amount не больше нуля, бросьте new RangeError("Сумма должна быть больше нуля"). Если amount больше баланса — new Error("Недостаточно сомони"). Иначе верните остаток. Порядок проверок — как в условии.
Можно редактировать · Ctrl + Enter
functionwithdraw(balance, amount) {
// две проверки с throw, затем return
}
for (const amount of [200, 0, 5000, -10]) {
try {
console.log("остаток:", withdraw(1000, amount));
} catch (e) {
console.log(`${e.name}: ${e.message}`);
} finally {
console.log("— операция завершена —");
}
}
Ожидаемый результат
остаток: 800
— операция завершена —
RangeError: Сумма должна быть больше нуля
— операция завершена —
Error: Недостаточно сомони
— операция завершена —
RangeError: Сумма должна быть больше нуля
— операция завершена —
Подсказка 1
throw сразу прерывает функцию, поэтому после него else не нужен: if (amount <= 0) throw new RangeError("…");.
Подсказка 2
Блок finally в цикле выполнится после каждой операции — и успешной, и неудачной.
ЗАДАЧА 6.5
Ловим только свои ошибки
сложная
Хороший catch обрабатывает только те ошибки, которые ожидает, а чужие пробрасывает дальше: throw e. Допишите validate(order): если у заказа нет name, вызовите fail("name", "укажите имя"); если qty меньше 1 — fail("qty", "минимум 1 штука"). Вспомогательная функция fail уже есть. Вызов process(null) должен упасть настоящим TypeError, который process не глотает.
Когда код падает, браузер печатает красную строку и стек вызовов: список функций от места ошибки до самого начала. Читайте его сверху вниз: первая строка — где упало, следующие — кто вызвал эту функцию.
Ошибка в консоли браузера
Uncaught TypeError: Cannot read properties ofundefined (reading 'price')
at calcTotal (cart.js:12:23)
at renderCart (ui.js:48:15)
at HTMLButtonElement.<anonymous> (main.js:7:3)
Тип и сообщение: TypeError — читаем свойство price у undefined. Значит, где-то вместо товара пришёл undefined.
Верхняя строка стека: calcTotal (cart.js:12:23) — функция calcTotal, файл cart.js, строка 12, символ 23. Здесь упало.
Вторая строка: renderCart вызвала calcTotal. Часто баг не в верхней функции, а в том, что ей передали.
Последняя строка: всё началось с клика по кнопке в main.js.
Клик по cart.js:12 в консоли DevTools откроет этот файл на нужной строке.
Аналогия: цепочка передачи на курсе
Студент спрашивает куратора, куратор — преподавателя, преподаватель — методиста, и в конце выясняется, что в расписании ошибка. Стек — это запись, кто кого спросил. Чтобы найти, откуда ошибка, идём по цепочке снизу вверх, но смотрим на верхний вызов: именно он «не смог».
Вместо десятка console.log можно остановить программу на нужной строке и осмотреться. Откройте вкладку Sources, выберите файл и кликните по номеру строки слева: появится синяя метка — breakpoint. Теперь выполните действие на странице (например, нажмите кнопку): выполнение замрёт перед этой строкой.
Осмотритесь. Справа в панели Scope видны значения всех переменных на этот момент. В Watch можно добавить любое выражение, например items.length. В Call Stack — тот самый стек вызовов.
Идите по шагам.F10 (Step over) — выполнить строку и перейти к следующей. F11 (Step into) — зайти внутрь вызываемой функции. Shift+F11 (Step out) — выйти из функции. F8 — продолжить до следующего breakpoint.
Сравните ожидание с реальностью. На какой строке значение стало не таким, как вы думали? Это и есть место бага.
Удобные вариации: условный breakpoint (правый клик по номеру строки → Add conditional breakpoint, например i === 3) и Pause on exceptions (кнопка с паузой): DevTools сам остановится в момент, когда код бросит ошибку.
Оператор debugger
Если не хочется искать нужную строку в DevTools, поставьте в коде оператор debugger;. Когда DevTools открыты, выполнение остановится именно здесь, как на breakpoint. Если DevTools закрыты, оператор ничего не делает.
debugger в коде
functioncalcTotal(items) {
let sum = 0;
for (const item of items) {
debugger; // здесь программа остановится, если открыт DevTools
sum += item.price;
}
return sum;
}
Не оставляйте debugger и лишние console.log в коде
Перед коммитом и pull request удалите их. Ревьюер увидит такой мусор в diff — и вернёт PR на доработку.
Network — сеть
Вкладка Network показывает каждый запрос страницы: HTML, картинки, fetch к API. Включите её до действия на странице (или обновите страницу). Для запросов fetch нажмите фильтр Fetch/XHR.
Status: 200 — успех, 404 — адрес не найден, 500 — сломался сервер. Красная строка — запрос не удался (например, нет интернета).
Типичный вопрос «почему на странице пусто?»: смотрим в Network — пришёл ли ответ, какой статус, нет ли ошибки в теле ответа.
Метод отладки: шесть шагов
Воспроизведите баг: что именно нажимать и вводить, чтобы он повторился каждый раз.
Сузьте: уберите всё лишнее, пока не останется минимальный код с багом.
Прочитайте сообщение об ошибке и стек. Часто ответ уже написан там.
Выдвиньте гипотезу: «значение стало undefined здесь, потому что…».
Проверьте гипотезу одной проверкой: console.log или breakpoint. Одной, а не пятью сразу.
Исправьте и перепроверьте: баг ушёл, остальное не сломалось? Напишите в коммите, что было причиной.
Практика: закрепляем
ЗАДАЧА 7.1
Читаем стек вызовов
средняя
Перед вами текст стека ошибки (как в красной строке консоли). Допишите parseFrame(line): из строки вида at calcTotal (cart.js:12:23) достаньте имя функции, файл и номер строки. Вернуть нужно объект { fn, file, line }. Остальное уже готово: код отбирает строки, начинающиеся с at, и печатает итог.
Можно редактировать · Ctrl + Enter
const stack = `TypeError: Cannot read properties of undefined (reading 'price')
at calcTotal (cart.js:12:23)
at renderCart (ui.js:48:15)
at HTMLButtonElement.<anonymous> (main.js:7:3)`;
functionparseFrame(line) {
// регулярка: at (.+?) \((.+?):(\d+):(\d+)\)return { fn: "?", file: "?", line: 0 };
}
const frames = stack.split("\n").filter((l) => l.trim().startsWith("at ")).map(parseFrame);
console.log("ошибка в:", frames[0].fn, frames[0].file, frames[0].line);
console.log("глубина стека:", frames.length);
console.log("всё началось в файле:", frames[frames.length - 1].file);
Ожидаемый результат
ошибка в: calcTotal cart.js 12
глубина стека: 3
всё началось в файле: main.js
Подсказка 1
Регулярка в скобках-группах: /at (.+?) \((.+?):(\d+):(\d+)\)/. Группы 1, 2, 3 — функция, файл, строка. Скобки вокруг места в стеке нужно экранировать: \( и \).
Без DevTools «посмотреть внутрь» цикла можно через console.log. Добавьте в цикл одну строку лога так, чтобы после обработки каждого числа выводилось n=…, sum=… (значения на этот момент). Итоговое значение функция печатает сама.
Можно редактировать · Ctrl + Enter
functionsumPositive(numbers) {
let sum = 0;
for (const n of numbers) {
if (n > 0) sum += n;
// сюда добавьте console.log
}
return sum;
}
console.log(sumPositive([5, -3, 10, 0, 7]));
Большая часть багов — это небольшой набор «классических» ошибок. Если вы знаете, как они выглядят, искать станет в разы быстрее. Вот карта:
Тип бага
Как выглядит
Где искать
Off-by-one (ошибка на единицу)
NaN или undefined в конце списка; пропущен первый или последний элемент
Границы циклов: < против <=, старт с 0 или 1, slice
Неверное сравнение
Условие срабатывает «не на тех» значениях; сортировка в странном порядке
= вместо ===, строки вместо чисел, > вместо >=, sort() без функции
Порядок async
В выводе «Promise», сообщения идут не в том порядке, данные «ещё не пришли»
Пропущенный await, forEach с async, ожидание результата вне then
Мутация
Данные меняются «сами по себе» в другом месте программы
sort, push, splice на исходном массиве; копия, которая на самом деле — ссылка; fill([])
Область видимости
В колбэках цикла всегда одно и то же значение
var вместо let
Правило для задач ниже
Сначала запустите код как есть и посмотрите, что он выводит — сравните с ожидаемым результатом. Прочитайте подсказку «симптома» в условии. Выдвиньте гипотезу, исправьте одно место и запустите снова. Подсказки открывайте только после своей попытки.
Пример разбора
Вот баг и его отладка «на словах». Запустите:
Что выведется? · Ctrl + Enter
const scores = [70, 85, 90];
functionaverage(list) {
let sum = 0;
for (let i = 0; i < list.length; i++) {
sum += list[i];
}
return sum / list.length + 1;
}
console.log(average(scores));
Ожидали около 81.67, получили около 82.67. Гипотеза: что-то прибавляется лишнее. Проверка: console.log(sum / list.length) перед return — это 81.67. Значит, виновата последняя строка: + 1 лишняя. Видите: мы не «правили наугад», а сузили поиск проверкой.
Off-by-one и неверные сравнения
Практика: закрепляем
ЗАДАЧА 8.1
Баг: лишний шаг цикла
лёгкая
Корзина должна вернуть сумму 445, а выводится что-то странное. Найдите причину (подсказка в консоли: у какого элемента нет цены?) и исправьте одну строку.
Можно редактировать · Ctrl + Enter
const prices = [120, 80, 45, 200];
functiontotal(list) {
let sum = 0;
for (let i = 0; i <= list.length; i++) {
sum += list[i];
}
return sum;
}
console.log(total(prices));
Ожидаемый результат
445
Подсказка 1
В массиве из 4 элементов индексы идут от 0 до 3. На последнем витке цикла i равен 4, а list[4] — это undefined. Сложение числа с undefined даёт NaN.
Подсказка 2
Замените <= на <. Правило: «до длины, не включая её».
ЗАДАЧА 8.2
Баг: страницы не с той позиции
средняя
Список студентов курса показывается по 3 человека на странице. Первая страница должна начинаться с «Алишер», но выводится не то. Исправьте getPage, чтобы страницы 1, 2, 3 выводили нужные группы.
Люди считают страницы с 1, а массив — с 0. Первая страница начинается с индекса 0, вторая — с индекса 3.
Подсказка 2
Начало страницы: (page - 1) * size.
ЗАДАЧА 8.3
Баг: топ-3 по баллам
лёгкая
Функция должна вернуть три лучших балла по убыванию: 100, 85, 42. Но сортировка выдаёт странный порядок. Почему sort() без аргументов ошибается с числами? Исправьте.
Без функции сравнения sort превращает числа в строки и сортирует по буквам: «100» идёт раньше «42», а «9» — позже «85».
Подсказка 2
Передайте (a, b) => b - a — так получится порядок по убыванию, и reverse() уже не нужен.
ЗАДАЧА 8.4
Баг: строки вместо чисел
средняя
Значения из полей формы всегда приходят строками. cheaper("9", "100") должна вернуть 9, а cheaper("45", "5") — 5. В label тоже ошибка: цена 0 должна печататься как «бесплатно». Найдите два бага.
Можно редактировать · Ctrl + Enter
functioncheaper(a, b) {
return a < b ? a : b;
}
functionlabel(price) {
if (price = 0) return"бесплатно";
return price + " сомони";
}
console.log(cheaper("9", "100"));
console.log(cheaper("45", "5"));
console.log(label(35));
console.log(label(0));
Ожидаемый результат
9
5
35 сомони
бесплатно
Подсказка 1
Две строки сравниваются «по алфавиту»: "9" < "100" ложно, потому что «9» идёт после «1». Преобразуйте значения: Number(a) < Number(b).
Подсказка 2
В label один знак = — это присваивание: цена становится 0 при каждом вызове. Нужно сравнение ===.
Порядок асинхронного кода
ЗАДАЧА 8.5
Баг: forEach и await
средняя
Три блюда готовятся по очереди, и «Все блюда готовы!» должно появиться после трёх строк «… готов». Сейчас итоговая строка выводится первой. Исправьте код: forEach не умеет ждать async-колбэки. Замените его цикл на тот, который умеет.
плов готов
самбуса готов
лагман готов
Все блюда готовы!
Подсказка 1
forEach запускает колбэки и не ждёт их результата, поэтому main сразу идёт дальше. Цикл for…of внутри async-функции ждёт каждый await.
Подсказка 2
for (const d of dishes) { console.log(await cook(d)); }
ЗАДАЧА 8.6
Баг: забыли await
средняя
Должно вывестись Итого: 80 (30 + 50). Вместо этого получается текст с «Promise». Функция getPrice асинхронная. Исправьте showTotal.
Можно редактировать · Ctrl + Enter
const getPrice = (id) =>newPromise((resolve) =>setTimeout(() =>resolve(id * 10), 20));
asyncfunctionshowTotal() {
const a = getPrice(3);
const b = getPrice(5);
console.log("Итого:", a + b);
}
showTotal();
Ожидаемый результат
Итого: 80
Подсказка 1
Без await в переменной лежит сам промис, а не число. Сложение двух промисов превращает их в строки.
Подсказка 2
Поставьте await перед каждым getPrice(...). Или используйте Promise.all, если хотите получить обе цены одновременно.
ЗАДАЧА 8.7
Баг: таймеры в цикле
лёгкая
Три таймера должны напечатать таймер 1, таймер 2, таймер 3. Но они печатают одно и то же число. Исправьте объявление переменной цикла — менять нужно одно слово.
Можно редактировать · Ctrl + Enter
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("таймер", i), i * 10);
}
Ожидаемый результат
таймер 1
таймер 2
таймер 3
Подсказка 1
Переменная var одна на весь цикл, и к моменту срабатывания таймеров цикл уже закончился (i стал 4).
Подсказка 2
С let на каждом витке создаётся своя переменная i.
Мутация и общие ссылки
ЗАДАЧА 8.8
Баг: sort портит корзину
средняя
Функция cheapest должна только найти самый дешёвый товар, а порядок в корзине менять нельзя: после вызова в корзине по-прежнему Чай, Нон, Плов. Сейчас порядок меняется. Исправьте одну строку.
sort меняет сам массив (мутирует), а не возвращает новый. В items лежит ссылка на тот же массив, что и cart.
Подсказка 2
Отсортируйте копию: [...items].sort(…).
ЗАДАЧА 8.9
Баг: общая ссылка
сложная
Два бага с одной причиной — несколько «переменных» смотрят на один и тот же массив. (1) Сетка из трёх строк: после добавления «A1» в первую и «C1» в третью длины должны быть 1,0,1. (2) Копия состояния copy не должна влиять на state: теги в нём должны остаться js. Исправьте обе части.
new Array(3).fill([]) кладёт в три ячейки один и тот же массив. Нужно создать три разных: Array.from({ length: 3 }, () => []) — функция вызывается для каждой ячейки заново.
Подсказка 2
Spread { ...state } копирует только верхний уровень, а tags остаётся общим. Скопируйте и его: { ...state, tags: [...state.tags] }.
ИИ хорошо делает две вещи: формулирует коммит по готовому diff и подсказывает гипотезы про баг. Но ответственность на вас: diff вы прочитали сами, а гипотезы проверяете запуском, а не верите на слово. Правила курса: сначала 15 минут пробуйте сами; не вставляйте код, который не можете объяснить построчно.
1. Коммит и описание PR по diff
Скопируйте вывод git diff --staged и попросите ИИ сформулировать сообщение. Без контекста он напишет «update files»:
придумай коммит
Вот diff: [вставьте вывод git diff --staged]. Напиши коммит-сообщение в стиле Conventional Commits (тип: тема, до 50 символов, английский, повелительное наклонение, без точки) и краткое описание PR: что сделано и зачем. Формат ответа: сначала одна строка коммита, потом PR из трёх разделов «Что сделано», «Зачем», «Как проверить». Не придумывай то, чего нет в diff.
Что проверить в ответе:
Тип соответствует правде: feat или fix, а не «на всякий случай».
В тексте нет того, чего нет в diff (ИИ любит добавлять «улучшил производительность», когда вы её не трогали).
Сообщение конкретное: пропустите его через проверки из задач 5.3 и 9.2.
Если в diff две разные мысли, лучше разбить на два коммита, а не просить ИИ «объединить красиво».
2. Баг: вместо «не работает» — минимальный воспроизводимый пример
у меня не работает корзина, исправь
Баг: ожидаю [что], получаю [что]. Минимальный код: [код]. Ошибка в консоли: [текст]. Дай 3 гипотезы причины по вероятности.
Минимальный воспроизводимый пример (minimal reproducible example, MRE) — самый короткий код, в котором баг всё ещё виден. Как его получить:
Скопируйте проблемное место в отдельный файл или в консоль на этой странице.
Удаляйте всё лишнее по кусочку: стили, соседние функции, лишние поля. После каждого шага проверяйте, что баг на месте.
Подставьте простые данные вместо запросов к серверу: массив из 2–3 элементов.
Запишите ожидаемый и фактический результат, а также текст ошибки дословно.
Бонус: пока вы упрощали код, вы часто сами находите баг. Это нормально и очень ценно. Метод называется «утёнок»: объяснить проблему вслух настолько ясно, что ответ находится сам.
Как не надо работать с гипотезами
ИИ дал три гипотезы. Проверяйте по очереди, от самой вероятной, и после каждой — запуск. Если вставить сразу три «исправления», вы не узнаете, какое сработало, и не научитесь замечать этот баг в следующий раз. Также ИИ иногда уверенно советует несуществующие методы: незнакомое название проверяйте в MDN.
В шпаргалку промптов добавьте две строки этого урока — «Коммиты и pull request» и «Баг вместо „не работает“». Потренируем оба навыка кодом.
Практика: закрепляем
ЗАДАЧА 9.1
Промпт с минимальным примером
средняя
Допишите bugPrompt(bug): она собирает из описания бага готовый промпт для ИИ по формату курса — четыре строки (код идёт со второй строки). Если ошибки в консоли нет (bug.error не задана), в строке ошибки пишем нет.
Можно редактировать · Ctrl + Enter
functionbugPrompt(bug) {
// верните строку из строк:// Баг: ожидаю «…», получаю «…».// Минимальный код:// <код>// Ошибка в консоли: …// Дай 3 гипотезы причины по вероятности.
}
console.log(bugPrompt({
expected: "45",
actual: "NaN",
code: "const prices = [20, 25];\nlet sum = 0;\nfor (let i = 0; i <= prices.length; i++) sum += prices[i];\nconsole.log(sum);",
}));
console.log("---");
console.log(bugPrompt({ expected: "Привет, Али", actual: "Привет, undefined", code: "greet({ nam: 'Али' })", error: "ошибки нет" }));
Ожидаемый результат
Баг: ожидаю «45», получаю «NaN».
Минимальный код:
const prices = [20, 25];
let sum = 0;
for (let i = 0; i <= prices.length; i++) sum += prices[i];
console.log(sum);
Ошибка в консоли: нет
Дай 3 гипотезы причины по вероятности.
---
Баг: ожидаю «Привет, Али», получаю «Привет, undefined».
Минимальный код:
greet({ nam: 'Али' })
Ошибка в консоли: ошибки нет
Дай 3 гипотезы причины по вероятности.
Подсказка 1
Соберите массив из пяти строк (вторая — «Минимальный код:», третья — сам код) и объедините через join("\n").
Подсказка 2
Для необязательного поля подойдёт оператор ??: bug.error ?? "нет" вернёт «нет», если ошибка не задана.
ЗАДАЧА 9.2
Расплывчатые сообщения от ИИ
лёгкая
ИИ иногда предлагает коммиты вроде «update» или «fix: bug». Допишите isVague(msg): сообщение расплывчато, если после типа и двоеточия (если они есть) в теме меньше двух слов или есть слово из списка generic.
Цель: пройти полный цикл командной работы — ветка → PR → ревью → merge — и найти три подсаженных бага в проекте напарника. Работаем в парах: студент А и студент Б. Ориентир по времени на 90 минут: подготовка 10, цикл с PR 35, конфликт 10, охота за багами 25, разбор 10.
Часть 1. Репозиторий и роли (10 минут)
Студент А создаёт на GitHub репозиторий pair-shop (с README) и в настройках Settings → Collaborators приглашает Б. Б принимает приглашение в почте.
Оба клонируют репозиторий:
Терминал
git clone https://github.com/ЛОГИН_А/pair-shop.git
cd pair-shop
Договоритесь, что вы делаете: А — функцию calcTotal и discount, Б — отображение корзины. Каждый работает в своём файле.
Часть 2. Ветка, коммиты, PR (35 минут)
Каждый создаёт свою ветку: А — feature/calc-total, Б — feature/cart-view.
Сделайте минимум три коммита в ветке, каждый — одна мысль, сообщения по Conventional Commits:
Отправьте ветку и откройте pull request на GitHub. В описании — три раздела: «Что сделано», «Зачем», «Как проверить». Ревьюер — напарник.
Терминал
git push -u origin feature/calc-total
Ревью: прочитайте PR напарника по чек-листу из раздела 3. Оставьте минимум два комментария к конкретным строкам: один с вопросом, один с предложением. Запустите код ветки.
Автор отвечает на комментарии и правит код новыми коммитами в ту же ветку. Ревьюер нажимает Approve.
Автор нажимает Merge pull request. Затем оба обновляют main:
Терминал
git switch main
git pull
git branch -d feature/calc-total
Часть 3. Конфликт нарочно (10 минут)
Оба создают новые ветки от свежего main и меняют одну и ту же строку в README (например, заголовок) — по-разному.
А вливает свой PR первым. Б делает git pull origin main в своей ветке — появляется конфликт.
Б решает конфликт вместе с А (договариваются об итоговом тексте), делает git add, git commit, git push. PR Б обновился и больше не конфликтует.
Часть 4. Три подсаженных бага (25 минут)
Каждый тайком вносит в свой проект три бага разных типов (из карты в разделе 8) и пушит в новую ветку bugs/названия. Напарник чинит их, пользуясь методом из раздела 7: воспроизвести, прочитать ошибку, поставить breakpoint или лог, сделать исправление отдельным коммитом fix: … с причиной. Тренировочная версия такого проекта — задача ниже: прочитайте её как ревью чужого кода.
Практика: закрепляем
ЗАДАЧА 10.1
Три бага в чужом проекте
сложная
Напарник написал корзину магазина MakeFirstStep и просит о ревью. Скидка 10% действует от 500 сомони включительно, доставка стоит 15 сомони и добавляется только к копии корзины. В коде спрятаны три бага разных типов. Найдите их, исправьте и в комментарии над каждой правкой напишите причину одной строкой. Ожидаемые числа ниже — это правильные ответы.
Три бага: (1) цикл начинается с i = 1 и пропускает первый товар; (2) в priceWithDiscount стоит > вместо >= — ровно 500 не получает скидку; (3) push меняет исходную корзину.
Подсказка 2
Проверьте себя числами: сумма корзины 550, со скидкой 495, корзина на ровно 500 — 450, с доставкой 565, после этого в исходной корзине по-прежнему 3 товара.
Чек-лист пары на выходе
В репозитории: три и более коммита с осмысленными сообщениями, один влитый PR с описанием и ревью-комментариями, один разрешённый конфликт, три fix:-коммита с описанием причины. Если чего-то нет, доделайте дома: это входит в домашнее задание.
11 Домашнее задание
Оформите репозиторий так, чтобы его не стыдно было показать работодателю, и найдите три бага с разбором причины. Можно взять проект одного из прошлых уроков (например, мини-проект №1) или проект из пары.
Часть 1. Репозиторий
Репозиторий на GitHub публичный, у него осмысленное имя и описание.
В истории минимум 10 осмысленных коммитов по Conventional Commits: каждый — одна мысль, ни одного «update», «fix», «final». Проверьте историю командой git log --oneline.
Есть минимум одна ветка кроме main и влитый pull request с описанием (что сделано, зачем, как проверить). Если работаете в одиночку, откройте PR из своей ветки в main и сами посмотрите diff как ревьюер.
В корне есть .gitignore (минимум: node_modules/, .env, .DS_Store, *.log) и в репозитории нет мусорных файлов.
В корне есть README.md по шаблону ниже.
README.md — шаблон
# Название проекта
Одно предложение: что это и для кого.
## Что умеет
- функция 1
- функция 2
## Как запустить
1. Склонируйте репозиторий
2. Откройте index.html в браузере (или npm install && npm run dev)
## Скриншот

## Что я использовал
HTML, CSS, JavaScript, Git/GitHub
## Автор
Имя, группа курса
Часть 2. Три бага
Найдите в своём или в чужом проекте три реальных бага разных типов: например, off-by-one, неверное сравнение, мутация, ошибка порядка асинхронного кода. Если не нашлось — внесите их в копию проекта сами и попросите знакомого найти.
Для каждого бага сделайте отдельный коммитfix: … с исправлением.
Создайте в репозитории файл BUGS.md и опишите каждый баг по шаблону ниже.
Один из трёх разборов сопроводите промптом к ИИ в формате «минимальный воспроизводимый пример» и вставьте в файл ответ ИИ вместе со своей оценкой: что подтвердилось запуском, а что оказалось неверным.
BUGS.md — шаблон
## Баг 1: итоговая сумма корзины меньше правильной
- **Симптом:** ожидал 550, получил 530.
- **Как воспроизвести:** добавить три товара и посмотреть сумму.
- **Причина:** цикл начинался с i = 1 и пропускал первый товар (off-by-one).
- **Как нашёл:** поставил breakpoint в calcTotal, увидел, что цикл идёт с индекса 1.
- **Исправление:** коммит `a1b2c3d` — `fix: count first item in total`.
- **Как не допустить снова:** проверять границы цикла на массиве из 1 и 0 элементов.
Что сдавать
Ссылку на публичный репозиторий GitHub.
Ссылку на влитый pull request.
В репозитории: README.md, .gitignore, BUGS.md.
Как оцениваем (0–10)
Что
Баллы
10+ осмысленных коммитов по формату, нет «update» и «fix» без пояснения
3
Ветка, pull request с описанием, влит в main
2
.gitignore и README.md оформлены по шаблону, нет мусорных файлов
2
Три бага разных типов с отдельными fix:-коммитами
2
В BUGS.md понятны причина и как нашли; один разбор с ИИ и проверкой запуском
1
Перед сдачей
Откройте репозиторий в приватном окне браузера: всё ли видно без входа? Проверьте git log --oneline: прочитайте сообщения как незнакомец. Если ИИ помогал с кодом или разбором, вы должны уметь объяснить каждую строку на защите.
✓ Шпаргалка урока
Три зоны: рабочая папка → индекс (git add) → репозиторий (git commit); GitHub — копия в сети (git push / git pull). Перед add всегда git status.
Ветка — закладка на коммит. git switch -c feature/имя, работа, push. В main напрямую не пишем.
Цикл в паре: ветка → коммиты → push → pull request → ревью (комментарии, approve) → merge → удалить ветку.
.gitignore:node_modules/, dist/, .env, *.log, .DS_Store. Секрет, попавший в историю, считается утёкшим: меняем ключ.
Коммит:тип(область): что сделано — feat, fix, docs, style, refactor, test, chore. До 50 символов, строчные, без точки, одна мысль.
try / catch / finally:catch получает ошибку, finally выполняется всегда. throw new Error("текст") бросает свою ошибку. Пустой catch — плохо; чужие ошибки пробрасываем дальше.
Типы ошибок:TypeError (не то значение, null.x), ReferenceError (нет переменной), SyntaxError (плохой синтаксис / JSON), RangeError (вне диапазона).
Стек: читаем сверху вниз: где упало, кто вызвал. DevTools: Console (log, table, trace), Sources (breakpoint по номеру строки, F10 / F11 / F8, Watch, Scope), debugger;, Network (статус, ответ, время).
Классические баги: off-by-one (< / <=, индексы с 0), сравнение (===, строки против чисел, sort(fn)), async (await, for…of вместо forEach), мутация ([...arr], Array.from), var в цикле.
Метод отладки: воспроизвести → сузить → прочитать ошибку → гипотеза → одна проверка → исправить и перепроверить.
С ИИ: коммит и PR — по вашему diff, текст проверяем. Баг — минимальный пример + ожидание/факт + дословная ошибка + «3 гипотезы»; проверяем по очереди запуском.