Git-процесс для 30 разработчиков: опыт с КредитСофт

Проблемы в команде клиента

Финтех-компания «КредитСофт» — 30 разработчиков, 4 backend-команды, 2 frontend-команды, 1 мобильная. Основной продукт — платформа онлайн-кредитования на Java (Spring Boot) и React. Ежемесячный оборот — 2 миллиарда рублей через платформу.

Когда CTO обратился к нам в itfresh.ru, проблемы с Git стоили компании минимум 20 человеко-часов в неделю:

  • Merge-конфликты каждый день. Все 30 человек работали в ветке develop напрямую. Утренний pull превращался в 30-минутное разруливание конфликтов.
  • Сломанный master. За последний квартал master был в нерабочем состоянии 11 раз. Дважды это блокировало hotfix для продакшена.
  • Нет code review. Коммиты попадали в develop без проверки. Качество кода падало, баги доезжали до продакшена.
  • Хаотичные коммиты. Сообщения вроде «fix», «test», «WIP», «asdf». Невозможно понять, что менялось, без чтения каждого diff.
  • Потерянные изменения. Минимум раз в неделю кто-то терял свою работу из-за неправильного git reset или git checkout.

Наша задача: за 3 недели внедрить Git-процесс, который масштабируется на 30+ разработчиков, интегрируется с CI/CD и обеспечивает стабильность master-ветки.

Branching strategy: Trunk-Based Development с Feature Flags

Мы оценили три стратегии: GitFlow, GitHub Flow и Trunk-Based Development (TBD). Для финтеха с ежедневными релизами выбрали модифицированный TBD:

  • main — всегда в состоянии, готовом к деплою. Каждый коммит в main автоматически проходит полный pipeline.
  • feature/* — короткоживущие ветки (1-3 дня максимум). Создаются от main, вливаются через Pull Request.
  • hotfix/* — экстренные исправления. Создаются от main, вливаются напрямую после ускоренного review.
  • release/* — создаются раз в 2 недели для стабилизации перед продакшен-релизом.

Правила, которые мы закрепили в Git hooks и CI:

# Запрет прямого push в main
# .gitlab-ci.yml или GitHub Branch Protection
# Требования для merge в main:
# 1. Минимум 2 approve от ревьюеров
# 2. Все CI-проверки пройдены (тесты, линтер, SAST)
# 3. Нет unresolved discussions
# 4. Branch is up to date with main

Для крупных фич, которые занимают больше 3 дней, мы внедрили Feature Flags через библиотеку Togglz:

// Фича ещё не готова, но код уже в main
if (featureManager.isActive(Features.NEW_SCORING_MODEL)) {
    return newScoringService.calculate(application);
} else {
    return legacyScoringService.calculate(application);
}

// Включение/выключение через конфиг без передеплоя
# application.yml
togglz:
  features:
    NEW_SCORING_MODEL:
      enabled: false
      strategy: gradual-rollout
      param:
        percentage: 0

Это позволило разработчикам вливать незаконченный код в main небольшими порциями, не ломая функциональность.

Code review: процесс и автоматизация

Мы выстроили процесс review с нуля. Каждый Pull Request проходил три уровня проверки:

Уровень 1: Автоматический (CI pipeline). Запускался автоматически при создании PR:

# .gitlab-ci.yml — фрагмент
code_quality:
  stage: test
  script:
    # Компиляция
    - mvn compile -q
    # Юнит-тесты
    - mvn test
    # Линтер и стиль кода
    - mvn checkstyle:check
    # Статический анализ безопасности
    - mvn spotbugs:check
    # Покрытие тестами (минимум 80%)
    - mvn jacoco:report
    - 'awk -F, '"'"'NR>1{s+=$4+$5; m+=$5}END{pct=m/s*100; if(pct<80){print "Coverage: "pct"% < 80%"; exit 1}}'"'"' target/site/jacoco/jacoco.csv'
  rules:
    - if: $CI_MERGE_REQUEST_ID

Уровень 2: Peer review. Каждый PR требовал одобрения от двух инженеров. Мы настроили CODEOWNERS для автоматического назначения ревьюеров по области кода:

# CODEOWNERS
# Критические модули — review от tech lead
src/main/java/com/credsoft/scoring/**  @tech-lead-scoring
src/main/java/com/credsoft/payment/**  @tech-lead-payment

# Инфраструктура — review от DevOps
Dockerfile                              @devops-team
.gitlab-ci.yml                          @devops-team
k8s/**                                  @devops-team

# Миграции БД — review от DBA
src/main/resources/db/migration/**      @dba-team

Уровень 3: Commit message validation. Git hook проверял формат сообщения:

#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG=$(cat "$1")
PATTERN='^(feat|fix|refactor|docs|test|chore|perf)(\(.+\))?: .{10,72}$'

if ! echo "$COMMIT_MSG" | head -1 | grep -qE "$PATTERN"; then
  echo "ERROR: Invalid commit message format."
  echo "Expected: type(scope): description (10-72 chars)"
  echo "Types: feat, fix, refactor, docs, test, chore, perf"
  echo "Example: feat(scoring): add ML-based risk assessment model"
  exit 1
fi

За первый месяц количество багов, дошедших до продакшена, сократилось на 60%. Code review также стал основным инструментом обмена знаниями между командами.

Rebase vs Merge: когда что использовать

Один из главных споров в команде «КредитСофт» — rebase или merge. Мы установили чёткие правила:

Rebase — для обновления feature-ветки из main. Это сохраняет линейную историю:

# Ваша feature-ветка отстала от main
git checkout feature/new-scoring
git fetch origin
git rebase origin/main

# Если есть конфликты — решаем по одному коммиту
# ... редактируем файлы ...
git add resolved_file.java
git rebase --continue

# После rebase нужен force-push (ветка переписана)
git push --force-with-lease origin feature/new-scoring

Критически важно: --force-with-lease вместо --force. Если коллега запушил коммиты в вашу ветку, --force-with-lease откажет, а --force молча затрёт его работу.

Merge — для вливания feature-ветки в main через PR. Merge commit сохраняет информацию о том, что изменения пришли из конкретной ветки:

# В GitLab/GitHub это делается через UI при merge PR
# Результат: merge commit с двумя родителями
# abc1234 Merge branch 'feature/new-scoring' into main

Squash Merge — для мелких фич, где история отдельных коммитов не важна:

# Все коммиты из ветки схлопываются в один
git merge --squash feature/minor-fix
git commit -m "fix(scoring): correct rounding in interest calculation"

Правило, которое мы внедрили: никогда не делайте rebase публичных веток (main, release/*). Rebase переписывает историю, и если кто-то уже основывал работу на этих коммитах — будет хаос.

Для интерактивного rebase перед созданием PR мы рекомендовали приводить историю в порядок:

# Объединить последние 5 коммитов в логические группы
git rebase -i HEAD~5

# В редакторе:
pick abc1234 feat(scoring): add base scoring algorithm
squash def5678 WIP scoring
squash ghi9012 fix typo
pick jkl3456 feat(scoring): add income verification step
squash mno7890 fix tests

Cherry-pick, bisect, stash: продвинутые техники

Три команды, которые спасали команду «КредитСофт» регулярно:

Cherry-pick — перенос конкретного коммита из одной ветки в другую. Незаменим для hotfix:

# Баг найден в main, но исправлен в feature-ветке
# Берём конкретный коммит с фиксом
git checkout main
git cherry-pick a1b2c3d4

# Если нужно перенести без коммита (для ручной правки)
git cherry-pick --no-commit a1b2c3d4

# Перенос нескольких коммитов
git cherry-pick a1b2c3d4 e5f6g7h8

# Перенос диапазона (не включая первый)
git cherry-pick a1b2c3d4..e5f6g7h8

Реальный кейс: в пятницу вечером обнаружили баг в расчёте процентной ставки. Фикс был в ветке feature/refactor-rates, которая ещё не прошла review. Cherry-pick позволил взять только коммит с фиксом, не затягивая весь незаконченный рефакторинг.

Git bisect — бинарный поиск коммита, который сломал функциональность:

# Знаем, что в коммите abc всё работало, а сейчас (HEAD) сломано
git bisect start
git bisect bad                    # текущий коммит — плохой
git bisect good abc1234           # этот коммит — хороший

# Git переключит на средний коммит — проверяем
# Если работает:
git bisect good
# Если сломано:
git bisect bad

# За log2(N) шагов находим виновный коммит
# При 500 коммитах — максимум 9 шагов вместо ручного перебора

# Автоматизация — скрипт проверки
git bisect start HEAD abc1234
git bisect run ./test_interest_rate.sh
# Git сам найдёт коммит за секунды

# Завершение
git bisect reset

Stash — сохранение незакоммиченных изменений:

# Срочно нужно переключиться на другую ветку
git stash push -m "WIP: scoring refactor"

# Список сохранённых изменений
git stash list
# stash@{0}: On feature/scoring: WIP: scoring refactor
# stash@{1}: On main: debug logging

# Восстановление
git stash pop stash@{0}

# Применить без удаления из stash (на случай, если нужно в нескольких ветках)
git stash apply stash@{0}

# Частичный stash — выбрать конкретные файлы
git stash push -m "only models" src/main/java/models/

Git hooks и CI/CD интеграция

Мы настроили набор Git hooks, которые автоматизировали рутинные проверки:

Pre-commit hook — запускался перед каждым коммитом:

#!/bin/bash
# .git/hooks/pre-commit

# 1. Проверка на секреты
if git diff --cached --name-only | xargs grep -l 'password\|secret\|api_key\|token' 2>/dev/null | \
   grep -v '.env.example\|test\|mock'; then
  echo "ERROR: Possible secrets detected in staged files!"
  echo "Use git diff --cached to review."
  exit 1
fi

# 2. Проверка размера файлов (не более 5MB)
for file in $(git diff --cached --name-only); do
  if [ -f "$file" ]; then
    size=$(wc -c < "$file")
    if [ "$size" -gt 5242880 ]; then
      echo "ERROR: $file is $(( size / 1048576 ))MB. Max allowed: 5MB"
      echo "Use Git LFS for large files."
      exit 1
    fi
  fi
done

# 3. Запуск быстрых тестов для изменённых модулей
CHANGED_MODULES=$(git diff --cached --name-only | grep 'src/main' | \
  sed 's|src/main/java/com/credsoft/||' | cut -d'/' -f1 | sort -u)

for module in $CHANGED_MODULES; do
  echo "Running tests for module: $module"
  mvn test -pl "$module" -q 2>/dev/null
  if [ $? -ne 0 ]; then
    echo "ERROR: Tests failed for module $module"
    exit 1
  fi
done

Pre-push hook — дополнительные проверки перед отправкой на сервер:

#!/bin/bash
# .git/hooks/pre-push

# Запрет push в main напрямую
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" = "main" ]; then
  echo "ERROR: Direct push to main is prohibited."
  echo "Create a feature branch and open a Pull Request."
  exit 1
fi

# Проверка, что ветка актуальна относительно main
git fetch origin main --quiet
BEHIND=$(git rev-list --count HEAD..origin/main)
if [ "$BEHIND" -gt 20 ]; then
  echo "WARNING: Your branch is $BEHIND commits behind main."
  echo "Run: git rebase origin/main"
fi

Для распространения hooks мы использовали core.hooksPath:

# В корне репозитория создали директорию .githooks/
# и настроили Git на использование этих hooks:
git config core.hooksPath .githooks

Это позволяло хранить hooks в репозитории и автоматически применять их ко всем разработчикам.

Результаты и метрики

Через 3 недели внедрения и 2 месяца работы по новому процессу результаты были следующими:

МетрикаДоПосле
Merge-конфликты в день12-151-2
Сломанный main (за квартал)11 раз0
Баги в продакшене (за месяц)239
Среднее время code review2.5 часа
Частота деплоев1 раз в 2 неделиЕжедневно
Время на разруливание Git-проблем20 ч/неделю2 ч/неделю
Потерянные изменения1-2 раза в неделю0 за 2 месяца

Главные уроки из проекта:

  • Короткоживущие ветки — залог отсутствия конфликтов. Если ветка живёт дольше 3 дней — что-то пошло не так с декомпозицией задачи.
  • Автоматизация через hooks экономит больше времени, чем любые регламенты. Людям свойственно забывать правила, а hook не забывает.
  • Code review — это не bottleneck, а инвестиция. 2.5 часа на review экономят 20 часов на дебаг бага в продакшене.
  • Обучение команды — ключевой фактор. Мы провели 4 воркшопа по 2 часа: основы Git, branching/merging, rebase и продвинутые техники, CI/CD интеграция.

Если ваша команда тратит время на Git-конфликты и нестабильный main — специалисты itfresh.ru помогут выстроить процесс, который масштабируется.

Часто задаваемые вопросы

GitFlow подходит для проектов с редкими релизами (раз в месяц) и длинными циклами тестирования. Trunk-Based Development — для команд, которые деплоят ежедневно. Для большинства современных проектов TBD эффективнее, но требует хорошего CI/CD и feature flags.
Три правила: короткоживущие ветки (1-3 дня), ежедневный rebase на main, и разделение ответственности по модулям (CODEOWNERS). Если два разработчика не редактируют один файл — конфликтов не будет.
Зависит от размера PR. Для мелких фич (1-5 коммитов) squash удобен — один чистый коммит в main. Для крупных фич лучше обычный merge — сохраняется подробная история. Мы рекомендуем оставлять выбор за автором PR.
Git хранит все объекты минимум 30 дней. Используйте git reflog для поиска последнего коммита удалённой ветки, затем git checkout -b branch_name commit_hash для восстановления. Для веток на remote — проверьте GitLab/GitHub, там удалённые ветки из merged PR хранятся в архиве.
Храните hooks в директории .githooks/ в репозитории и настройте git config core.hooksPath .githooks. Это применится ко всем, кто клонирует репозиторий. Для принудительного применения — добавьте проверку в CI/CD, дублируя логику hooks на серверной стороне.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#git#git workflow#branching strategy#code review#CI/CD#git hooks#rebase#cherry-pick
Комментарии 0

Оставить комментарий

загрузка...