Git: это не просто "сохранить файл"
Git изобрёл Линус Торвальдс в 2005 году за две недели — потому что существующие системы контроля версий его раздражали. Результат стал стандартом де-факто для всей современной разработки.
Но большинство разработчиков используют только 10% возможностей Git: git add, git commit, git push. И потом удивляются, почему в команде хаос, история проекта нечитаема, а деплой — это страшный ритуал.
Правильное использование Git — это не набор команд, это культура разработки. Сегодня разберём, как устроена эта культура в реальных командах.
Анатомия правильного коммита
Коммит — это единица изменений. Плохой коммит: "исправил баги и добавил фичи". Хороший коммит: одно логическое изменение, понятное описание.
Conventional Commits — стандарт сообщений
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Типы:
feat— новая функциональностьfix— исправление багаdocs— только документацияstyle— форматирование, точки с запятой (нет изменений логики)refactor— рефакторинг (нет новой функциональности, нет фикса)perf— оптимизация производительностиtest— добавление тестовchore— обслуживание: обновление зависимостей, конфигурации CIci— изменения CI/CD конфигурацииrevert— откат предыдущего коммита
Примеры:
# Плохо:
git commit -m "fix"
git commit -m "wip"
git commit -m "changes"
git commit -m "поправил немного"
# Хорошо:
git commit -m "fix(auth): исправлена утечка токена при logout"
git commit -m "feat(modbus): добавлена поддержка FC15 (write multiple coils)"
git commit -m "perf(historian): оптимизирован batch-insert, +340% throughput"
git commit -m "docs(api): добавлены примеры для /api/v1/devices endpoint"
# С телом для сложных изменений:
git commit -m "fix(plc): исправлено переполнение счётчика при rollover
Счётчик типа UINT использовался для значений >65535.
Изменён на DINT (32-бит). Затронутые устройства: все узлы с FC03.
Closes #247"
Почему это важно?
Автоматический CHANGELOG — инструменты как
conventional-changelogгенерируют его автоматическиСемантическое версионирование —
feat→ minor,fix→ patch,feat!илиBREAKING CHANGE→ majorЧитаемая история — через год понятно что и зачем было сделано
Быстрый поиск —
git log --grep="fix(modbus)"найдёт все фиксы Modbus
Стратегии ветвления
Git Flow — классика для релизного цикла
main ────────────────────────────────────── (production-ready, теги версий)
\ /
release/1.2.0 ─────────────────────────── (только bagfixes перед релизом)
\ /
develop ──────────────────────────────── (интеграция фич)
\ \ /
feat/A feat/B feat/C
Ветки:
main— всегда стабильный, деплоится в прод, только через merge изrelease/*develop— основная ветка разработки, всегда должна собиратьсяfeature/*— новые фичи, создаются изdevelop, мержатся вdeveloprelease/*— подготовка релиза (версия, changelog), только bugfixhotfix/*— срочные фиксы прод, мержатся вmainИdevelop
# Создать фичу:
git checkout develop
git checkout -b feature/modbus-fc15-support
# Завершить фичу:
git checkout develop
git merge --no-ff feature/modbus-fc15-support # --no-ff сохраняет историю
git branch -d feature/modbus-fc15-support
# Подготовить релиз:
git checkout develop
git checkout -b release/1.2.0
# Обновить версию, CHANGELOG...
git commit -m "chore(release): version 1.2.0"
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
Trunk-Based Development — для быстрых команд
Все разработчики работают в одной ветке (main), фичи прячутся за feature-флагами. Деплой несколько раз в день. Подходит для опытных команд с хорошим покрытием тестами.
# Только короткоживущие ветки (1-2 дня максимум)
git checkout -b task/PLC-247-fix-counter-overflow
# ... работа ...
git push origin task/PLC-247-fix-counter-overflow
# Pull Request → Review → Merge в main
# Деплой автоматически
GitHub Flow — для непрерывного деплоя
Упрощённый Git Flow без develop:
main= то, что в продеFeature branches — от main, в main через PR
Деплой = merge в main
Code Review: как делать правильно
Code review — не поиск ошибок, это обмен знаниями и повышение качества. Хороший review делает команду сильнее.
Для автора PR:
## Описание
Добавлена поддержка записи нескольких coils (FC15) в Modbus slave.
## Мотивация
Клиент запросил управление 16 выходными реле через один Modbus-запрос
вместо 16 отдельных FC05. Уменьшает нагрузку на шину в 16 раз.
## Изменения
- `ModbusSlave::handle_fc15()` — новый обработчик функционального кода 15
- Обновлён маппинг coils на GPIO пины
- Добавлены unit-тесты: 8 тест-кейсов
## Тестирование
- [x] Unit-тесты: все зелёные
- [x] Интеграционный тест с реальным Modbus-мастером (Python pymodbus)
- [x] Проверен на железе: Raspberry Pi + MCP2551
## Breaking Changes
Нет. FC05 продолжает работать.
## Связанные Issues
Closes #247
Для ревьюера:
Проверяйте:
Логику — правильно ли реализовано то, что задумано?
Граничные случаи — что при пустых данных? При переполнении? При сетевой ошибке?
Безопасность — нет ли SQL-инъекций, XSS, незащищённых данных?
Производительность — нет ли N+1 запросов, бесконечных циклов?
Тесты — покрывают ли они описанную функциональность?
Документацию — понятно ли из кода и комментариев что происходит?
Не проверяйте:
Стиль форматирования (для этого есть линтеры и форматтеры)
Личные предпочтения (если оба подхода корректны)
Тон комментариев:
❌ Это неправильно, так делать нельзя
✅ Здесь возможно переполнение при dlc > 8, как насчёт проверки?
❌ Почему ты использовал цикл вместо map()?
✅ Можно ли тут использовать list comprehension для читаемости?
❌ Нет, переделай.
✅ Мне кажется, паттерн Strategy тут подошёл бы лучше — как думаешь?
GitHub Actions: автоматизация всего
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
# ===== ЛИНТИНГ И СТАТИЧЕСКИЙ АНАЛИЗ =====
lint:
name: Lint & Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install flake8 pylint mypy black isort
pip install -r requirements.txt
- name: Check formatting (black)
run: black --check --diff .
- name: Check imports (isort)
run: isort --check-only --diff .
- name: Lint (flake8)
run: flake8 . --max-line-length=100 --exclude=.venv,migrations
- name: Type check (mypy)
run: mypy src/ --strict --ignore-missing-imports
# ===== ТЕСТИРОВАНИЕ =====
test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
needs: lint
services:
# Поднимаем сервисы для интеграционных тестов
redis:
image: redis:7
ports: ['6379:6379']
mosquitto:
image: eclipse-mosquitto:2
ports: ['1883:1883']
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12'] # Тестируем на всех версиях
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt -r requirements-dev.txt
- name: Run tests with coverage
run: |
pytest tests/ \
--cov=src \
--cov-report=xml \
--cov-report=term-missing \
--cov-fail-under=80 \
-v
env:
REDIS_URL: redis://localhost:6379
MQTT_HOST: localhost
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
# ===== СБОРКА DOCKER ОБРАЗА =====
build:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64 # Для x86 серверов И Raspberry Pi
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ===== ДЕПЛОЙ =====
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: deploy
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
cd /opt/gateway
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/v')
environment: production # Требует одобрения в GitHub
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: deploy
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /opt/gateway
export IMAGE_TAG=${{ github.ref_name }}
docker compose pull
docker compose up -d --remove-orphans
# Smoke test
sleep 10
curl -f http://localhost:8080/health || (docker compose logs && exit 1)
Git Hooks: автоматизация на уровне репозитория
# .git/hooks/pre-commit (запускается перед каждым коммитом)
#!/bin/bash
set -e
echo "🔍 Pre-commit проверки..." # Форматирование Python if command -v black &> /dev/null; then
black --check . --quiet
if [ $? -ne 0 ]; then
echo "❌ Форматирование не соответствует black. Запустите: black ."
exit 1
fi fi # Быстрые тесты (только изменённые файлы)
CHANGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$') if [ -n "$CHANGED_PY" ]; then pytest tests/unit/ -x -q --tb=short fi
echo "✅ Все проверки прошли"
Лучше использовать pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-merge-conflict
- id: detect-private-key # Не допускаем секреты в коде!
- repo: https://github.com/psf/black
rev: 23.10.1
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.12.0
hooks:
- id: commitizen # Проверяет формат сообщения коммита
# Установка:
pip install pre-commit
pre-commit install # Устанавливает хуки в .git/hooks/
pre-commit run --all-files # Запуск вручную
Семантическое версионирование и автоматический релиз
Semantic Versioning: MAJOR.MINOR.PATCH
PATCH (1.2.3 → 1.2.4): багфиксы, обратно совместимые изменения
MINOR (1.2.4 → 1.3.0): новая функциональность, обратно совместимая
MAJOR (1.3.0 → 2.0.0): несовместимые изменения API
Автоматический выпуск с commitizen:
# pyproject.toml:
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
version_files = [
"src/__init__.py:__version__",
"pyproject.toml:version"
]
update_changelog_on_bump = true
# Команды:
cz bump # Автоматически определяет тип bumpa из коммитов
cz bump --major # Принудительно major
cz changelog # Генерирует CHANGELOG.md
Автоматический CHANGELOG.md из conventional commits:
## v1.3.0 (2024-03-15)
### Features
- **modbus**: добавлена поддержка FC15 (write multiple coils) (#247)
- **historian**: реализован deadband-алгоритм сжатия, экономия 78% места
### Bug Fixes
- **uart**: исправлена потеря байт при высоких нагрузках (#251)
- **pid**: устранено интегральное насыщение при длительной работе
### Performance
- **batch-write**: оптимизирован bulk insert в InfluxDB, +340% throughput
Практические советы
.gitignore — не игнорируйте важное
# Python
__pycache__/
*.pyc
*.pyo
.venv/
.env
venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Сборка
dist/
build/
*.egg-info/
# Тесты
.coverage
htmlcov/
.pytest_cache/
# Секреты (НИКОГДА не коммитить!)
*.key
*.pem
.env.local
config.secret.yaml
# OS
.DS_Store
Thumbs.db
Работа с секретами — никогда в репозиторий!
# Плохо: секреты в коде
MQTT_PASSWORD = "supersecret123"
# Хорошо: из переменных окружения
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD") # В .env локально, в CI — secrets
# Для локальной разработки: .env файл (в .gitignore!)
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
# Для проверки что секрет не утёк:
git log --all --full-history -- "*.env" # Поиск в истории
git grep "supersecret" # Поиск в текущем состоянии
Интерактивный rebase для чистой истории
# Перед мержем PR привести историю в порядок
git rebase -i origin/main
# Редактор покажет:
# pick a1b2c3 WIP fix something
# pick d4e5f6 another fix
# pick g7h8i9 добавил логирование
# Меняем на:
# reword a1b2c3 fix(modbus): исправлен CRC при DLC=0
# squash d4e5f6 # Объединить с предыдущим
# pick g7h8i9 feat(logging): добавлено структурированное логирование
# Результат: чистая, осмысленная история
Заключение
Git — это не инструмент, это язык коммуникации в команде. Правильные коммиты рассказывают историю проекта. Грамотное ветвление изолирует работу. CI/CD устраняет ручной труд и человеческие ошибки при деплое.
Начните с малого: установите .pre-commit-config.yaml с black и detect-private-key. Перейдите на conventional commits. Добавьте один GitHub Actions workflow с тестами. Каждый из этих шагов принесёт немедленную пользу.
Инвестиция в культуру работы с кодом возвращается многократно: меньше времени на дебаггинг, меньше страха перед деплоем, больше времени на реальную разработку.
Create an account or sign in to leave a review
There are no reviews to display.