Базовый цикл: add → commit → push

Базовый цикл: add → commit → push

Ежедневная работа с Git сводится к простому циклу: изменил файлы, выбрал что включить в коммит, зафиксировал с осмысленным сообщением, отправил на remote. Этот цикл повторяется десятки раз в день, и чем лучше ты его понимаешь, тем меньше сюрпризов.

В первом уроке мы говорили о трёх зонах Git (working directory, staging area, repository). Сейчас разберём, как именно данные перемещаются между ними, и какие инструменты помогают контролировать этот процесс.

Три дерева Git: working directory, staging, HEAD и команды-переходы между ними

git status - твой компас

Перед любым действием смотри git status. Он показывает, в каком состоянии файлы:

git status

# On branch main
# Changes not staged for commit:
#   modified:   internal/handler/user.go
#
# Untracked files:
#   internal/handler/auth.go

Файлы бывают в четырёх состояниях:

  • Untracked - Git не знает об этом файле (новый файл, не добавлен через git add)
  • Modified - файл изменён, но изменения не в staging area
  • Staged - файл добавлен в staging area, готов к коммиту
  • Committed - файл зафиксирован в репозитории, совпадает с последним коммитом
# Краткий формат (удобнее для повседневной работы)
git status -s

# M  internal/handler/user.go    # modified, staged
#  M internal/service/auth.go    # modified, not staged
# ?? internal/handler/auth.go    # untracked
# A  internal/model/token.go     # new file, staged

git diff - что именно изменилось

git status показывает какие файлы изменились, а git diff показывает что именно изменилось:

# Разница между working directory и staging area
# (что изменилось, но ещё не добавлено в staging)
git diff

# Разница между staging area и последним коммитом
# (что попадёт в следующий коммит)
git diff --staged

# Разница между working directory и последним коммитом
git diff HEAD

# Разница для конкретного файла
git diff internal/handler/user.go

# Только имена изменённых файлов
git diff --name-only
Привычка делать `git diff --staged` перед `git commit` спасает от случайных изменений в коммите. Ты видишь ровно то, что будет зафиксировано.

git add - выбираем что коммитить

git add перемещает изменения из working directory в staging area:

# Добавить конкретный файл
git add internal/handler/user.go

# Добавить всю директорию
git add internal/handler/

# Добавить все изменённые и новые файлы
git add .

# Добавить все отслеживаемые файлы (без untracked)
git add -u

Частичное добавление с git add -p

Самая мощная опция - интерактивное добавление по частям. Допустим, ты изменил файл в двух местах, но хочешь разбить изменения на два логических коммита:

git add -p internal/handler/user.go

Git покажет каждый изменённый фрагмент (hunk) и спросит, что делать:

@@ -15,6 +15,8 @@ func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
+    // validate token
+    token := r.Header.Get("Authorization")

Stage this hunk [y,n,q,a,d,s,e,?]?
  • y - добавить этот фрагмент в staging
  • n - пропустить
  • s - разделить на более мелкие фрагменты
  • q - выйти

Это позволяет делать атомарные коммиты: один коммит - одно логическое изменение.

git commit - фиксируем снимок

# Коммит с однострочным сообщением
git commit -m "feat: add token validation to GetProfile"

# Открыть редактор для многострочного сообщения
git commit

# Добавить все отслеживаемые файлы и сразу закоммитить (без git add)
git commit -am "fix: handle nil pointer in auth middleware"

Conventional Commits

В командной работе принято придерживаться формата сообщений. Один из самых популярных - Conventional Commits:

<тип>(<область>): <описание>

<тело - опционально>

<футер - опционально>

Типы:

  • feat - новая функциональность
  • fix - исправление бага
  • refactor - рефакторинг без изменения поведения
  • docs - документация
  • test - тесты
  • chore - инфраструктура, зависимости, CI

Единый формат сообщений нужен не только для красоты: из него потом генерируется changelog между тегами релизов.

Примеры:

git commit -m "feat(auth): add JWT refresh token endpoint"
git commit -m "fix(handler): return 404 instead of 500 for missing user"
git commit -m "refactor(service): extract validation into separate method"
git commit -m "test(progress): add unit tests for lesson completion"
Через полгода `git log` покажет историю. Сообщение «fix» ничего не объясняет. Сообщение «fix(auth): handle expired token gracefully instead of panic» - объясняет всё.

git commit --amend

Если ты только что сделал коммит и понял, что забыл добавить файл или допустил опечатку в сообщении:

# Добавить забытый файл в последний коммит
git add internal/handler/auth.go
git commit --amend --no-edit

# Изменить сообщение последнего коммита
git commit --amend -m "feat(auth): add JWT validation middleware"
`--amend` создаёт новый коммит вместо старого - это тот же класс операций, что и [rebase](./05-merge-rebase.md), то есть переписывание истории. Если ты уже запушил коммит, amend потребует `git push --force`. Делай amend только для локальных (не запушенных) коммитов.

git push - отправляем на remote

# Первый push ветки (--set-upstream связывает локальную ветку с remote)
git push -u origin main

# Последующие push-и (upstream уже установлен)
git push

git pull vs git fetch

Обе команды получают изменения с remote, но работают по-разному:

git fetch - скачивает новые коммиты с remote, но не трогает твои локальные файлы. Безопасная операция - ты можешь посмотреть, что изменилось, перед тем как применять.

git fetch origin

# Посмотреть, что пришло
git log main..origin/main --oneline

# Применить, когда готов
git merge origin/main

git pull - это git fetch + git merge (или git rebase, если настроен pull.rebase). Удобно, но может неожиданно создать merge-коммит.

# Обычный pull (fetch + merge)
git pull

# Pull с rebase (линейная история)
git pull --rebase
Если ты работаешь в feature-ветке, `git pull --rebase` даёт более чистую историю. Настрой глобально: `git config --global pull.rebase true`.

.gitignore и .gitkeep

.gitignore

Файл .gitignore в корне репозитория указывает, какие файлы Git должен игнорировать (полный разбор синтаксиса, глобального gitignore и защиты от утечки секретов - в отдельном уроке):

# Скомпилированные бинарники Go
/bin/
*.exe

# Зависимости
/vendor/

# IDE
.idea/
.vscode/

# Переменные окружения
.env
.env.local

# Системные файлы
.DS_Store
Thumbs.db
# Проверить, игнорируется ли файл
git check-ignore -v .env
# .gitignore:12:.env    .env

.gitkeep

Git не отслеживает пустые директории. Если тебе нужна пустая папка в репозитории (например, logs/ или uploads/), положи в неё файл .gitkeep:

mkdir -p uploads
touch uploads/.gitkeep
git add uploads/.gitkeep
git commit -m "chore: add empty uploads directory"

.gitkeep - это конвенция, а не фича Git. Файл может называться как угодно, но .gitkeep - общепринятое название.

Remote tracking branches

Когда ты делаешь git push -u origin main, Git создаёт связь между локальной веткой main и удалённой origin/main. Это remote tracking branch. То же самое работает и для feature-веток, с которыми ты будешь работать каждый день.

# Посмотреть связи
git branch -vv
# * main    3a7f1b2 [origin/main] feat: add auth handler
# develop   1c2d3e4 [origin/develop: ahead 2] refactor: extract service

# «ahead 2» значит: у тебя есть 2 локальных коммита, которых нет на remote
# «behind 3» значит: на remote есть 3 коммита, которых нет у тебя

Мини-задание

  • Создай репозиторий с двумя файлами, измени оба, но закоммить только один с помощью git add <файл>
  • Попробуй git add -p - добавь часть изменений из одного файла
  • Сделай коммит с сообщением в формате Conventional Commits
  • Используй git commit --amend чтобы исправить сообщение последнего коммита
  • Настрой .gitignore для Go-проекта (бинарники, .env, .idea/)

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.