Маленькие pull request: почему их быстрее ревьюят и как резать задачу
Большой pull request кажется эффективным: одна задача, один обзор. На практике он тормозит ревью и прячет баги. Разбираем, как резать крупную задачу на маленькие проверяемые куски.

Разговор о размере pull request часто упирается в возражение: задача просто большая, её нельзя сделать маленькой без искусственного дробления. На практике почти любую задачу можно разрезать на части, которые по отдельности проверяемы и имеют смысл, если подойти к делению заранее, а не пытаться резать уже написанный большой diff после того, как код готов целиком.
Большой pull request на тысячу или две тысячи строк выглядит как эффективная работа: задача закрыта целиком, одним куском, ревьюеру не нужно переключаться между контекстами несколько раз. На практике эффект обратный. Чем больше diff, тем ниже реальное качество ревью: ревьюер физически не может удерживать в голове все изменения сразу, внимание падает к середине файла, а комментарии становятся формальными. Маленькие pull request решают эту проблему не потому что это модная практика, а потому что они соответствуют реальным возможностям человеческого внимания.
Что на самом деле происходит с ревью большого diff
Исследования читаемости кода (в самом общем, не выдуманном смысле: это многократно описанное наблюдение, а не редкий случай) сходятся в одном: качество ревью падает нелинейно с размером изменения. Небольшой diff на тридцать-пятьдесят строк ревьюер разбирает построчно, держит в голове логику целиком и задаёт точные вопросы. Diff на пятьсот строк уже требует несколько проходов, а diff на пару тысяч строк в большинстве случаев получает поверхностное одобрение, потому что детальный разбор занял бы весь рабочий день одного человека.
Вторая проблема больших pull request: они дольше ждут своей очереди. Ревьюер откладывает разбор крупного изменения на свободное время, которого в плотном графике часто не находится, и pull request висит открытым несколько дней. За это время автор уже переключился на следующую задачу, а при необходимости что-то поправить приходится восстанавливать контекст заново, что тоже стоит времени.
Какой размер считать разумным
Универсального числа не существует, потому что сложность не сводится к количеству строк: пятьдесят строк хитрой параллельной логики требуют больше внимания, чем пятьсот строк однообразного переноса данных из одного формата в другой. Но как практический ориентир, pull request, который ревьюер может разобрать за один присест без потери концентрации, обычно укладывается в несколько сотен строк значимых изменений, не считая автоматически сгенерированных файлов.
| Размер diff | Типичное время ревью | Качество обратной связи |
|---|---|---|
| до 100 строк | 10-15 минут | детальное, построчные комментарии |
| 100-400 строк | 30-60 минут | хорошее, при наличии ясной структуры |
| 400-1000 строк | несколько часов или несколько заходов | частичное, риск пропустить проблему |
| более 1000 строк | растягивается на дни | формальное одобрение без глубокого разбора |
Как резать большую задачу: по архитектурным слоям
Первый способ деления подходит для задач, которые естественно проходят через несколько слоёв системы: базу данных, бизнес-логику, API, интерфейс. Вместо одного pull request, который меняет все слои сразу, задача режется по слоям, и каждый слой проходит ревью и мержится отдельно, в порядке зависимости.
- Миграция схемы базы данных и модели, без логики поверх, с обратной совместимостью со старым кодом
- Бизнес-логика и сервисный слой, который использует новую модель, но ещё не подключён к внешнему интерфейсу
- API-эндпоинт или контроллер, который вызывает новую бизнес-логику
- Интерфейс или клиентский код, который наконец показывает результат пользователю
Такое деление работает, если каждый промежуточный шаг не ломает существующую функциональность: новая колонка в базе с значением по умолчанию не мешает старому коду, новый сервисный метод, который ещё никто не вызывает, не влияет на поведение системы. Это требует немного больше планирования на старте, но каждый отдельный pull request становится маленьким и однозначным по смыслу.
Как резать по фича-флагам
Второй способ особенно удобен для изменений в уже работающей функциональности, когда промежуточные состояния нельзя показывать пользователю напрямую. Новая логика разрабатывается и мержится в основную ветку частями, но скрыта за флагом, который выключен, пока вся цепочка изменений не готова целиком. Подробнее про сами флаги и дисциплину вокруг них стоит отдельного разговора, здесь важно, что этот приём напрямую снижает размер каждого pull request: автор может смело мержить промежуточный, неполный код, потому что он не виден пользователю.
// pull request 1: новая функция написана и покрыта тестами,
// но ещё не подключена к реальному потоку пользователя
export function calculateNewPricing(cart) {
// ...
}
// pull request 2, отдельный и маленький: подключение за флагом
if (flags.newPricing) {
total = calculateNewPricing(cart);
} else {
total = calculateLegacyPricing(cart);
}Как резать по шагам миграции
Третий способ подходит для крупных механических изменений: переход на новую версию библиотеки, переименование widely используемой сущности, смена формата конфигурации. Здесь разумно резать не по архитектурным слоям, а по модулям или папкам кодовой базы, перенося их по очереди, с отдельным pull request на каждую группу файлов.
Признак того, что группа файлов выбрана разумно: внутри одного pull request изменение выглядит однотипно, и ревьюер, увидев первые два-три файла, уже понимает логику остальных, а не читает каждый файл заново как новую задачу. Если это условие не выполняется, границы группы, скорее всего, выбраны случайно, просто по алфавиту или по размеру папки, а не по смыслу.
Соблазн сделать такую миграцию одним большим скриптом и одним pull request понятен: механическая правка кажется безопасной, ведь её можно автоматизировать. Но даже автоматическая правка на три тысячи файлов создаёт diff, который невозможно вычитать построчно, и единственная реальная проверка в таком pull request это зелёные тесты, а не внимание ревьюера. Разбивка по модулям позволяет мержить миграцию постепенно, проверяя по пути, что автоматическая правка не задела что-то неожиданное в конкретной части кода.
Как писать описание к маленькому pull request
Размер diff не единственное, что влияет на скорость ревью: описание pull request определяет, сколько времени ревьюер потратит на то, чтобы понять контекст, прежде чем читать сам код. Маленький pull request без описания всё равно заставляет ревьюера гадать, зачем сделано изменение и какую более крупную задачу оно закрывает, особенно если деление на части, описанное выше, растянуто на несколько дней.
- Короткое объяснение, какую часть общей задачи закрывает именно этот pull request и ссылка на остальные части или задачу целиком
- Явное указание, что в этом изменении осознанно не сделано и будет в следующей части, чтобы ревьюер не тратил время на вопрос, почему что-то отсутствует
- Если изменение требует ручной проверки, короткая инструкция, что и как проверить, вместо того чтобы ревьюер придумывал сценарий сам
Маленькие pull request не бюрократическое требование процесса, а способ получить реальную, а не формальную проверку кода. Деление по архитектурным слоям подходит для новой функциональности, деление по фича-флагам для изменений в уже рабочей логике, а деление по модулям для крупных механических миграций. Во всех трёх случаях цель одна: сделать так, чтобы ревьюер мог прочитать весь diff внимательно и за разумное время, а не одобрить его из усталости.
Частые вопросы
Что делать, если задачу физически невозможно разбить на маленькие шаги?
Такое случается реже, чем кажется на старте: обычно разбить мешает не природа задачи, а привычка решать её одним махом. Если после попытки применить деление по слоям, флагам и модулям разбить всё равно не получается, стоит хотя бы заранее договориться с ревьюером о совместном разборе, а не присылать diff без предупреждения.
Не увеличивает ли деление на несколько pull request общее время до готовности фичи?
Суммарное время разработки может немного вырасти за счёт накладных расходов на оформление нескольких pull request, но оно почти всегда компенсируется более быстрым и качественным ревью каждой части и меньшим числом багов, найденных уже после релиза.
Как объяснить команде, которая привыкла к большим pull request, что стоит перейти на маленькие?
Проще показать на практике: взять одну реальную задачу, разбить её одним из трёх способов и сравнить время до мержа с похожей задачей, сделанной одним большим pull request. Разница в скорости ревью обычно убеждает быстрее, чем любой аргумент в обсуждении.



