Git: от первого коммита до уверенной работы в команде

Учебный материал по Git: от базовых концепций до уверенной работы с ветками, конфликтами и командным workflow. Без магии, с объяснением что происходит внутри

Git — инструмент, которым пользуются все, но по-настоящему понимают немногие. Большинство разработчиков запоминают десяток команд и работают по инерции, пока не сталкиваются с merge-конфликтом, потерянным коммитом или непонятным detached HEAD. В этот момент начинается паника и git push --force.

Эта статья — не справочник по командам, а учебный материал. Цель — чтобы после прочтения вы понимали, что Git делает на самом деле, и могли спокойно разбираться с любой ситуацией.

Это первая статья серии про Git. Если вы только настроили окружение (как это сделать) — Git логично следующий шаг.

Git как лента снапшотов: коммиты на рельсе времени, ветки-указатели, развилки и страховочная сеть

В статье

Что такое Git и три состояния файла

Git — это система контроля версий: он хранит историю изменений проекта так, что в любой момент можно посмотреть, кто и что менял, и вернуться к любому прошлому состоянию. Это не бэкап (бэкап — слепок «как было») и не облачное хранилище (Dropbox синхронизирует файлы, но не понимает смысл изменений).

Git распределённый: у каждого разработчика на диске лежит полная копия истории, а не только последняя версия. Можно работать и коммитить без интернета; сервер (GitHub, GitLab) — лишь общая точка обмена, а не «главный и единственный» репозиторий.

Ключ к пониманию — три состояния файла:

working directory  →  staging area  →  repository
(рабочая папка)       (индекс)          (история коммитов)
   git add ────────────►   git commit ────────►
  • working directory — файлы, с которыми вы работаете прямо сейчас;
  • staging area (индекс) — что попадёт в следующий коммит;
  • repository — зафиксированная история.

Staging area часто кажется лишней, но в ней весь смысл: вы решаете, что именно войдёт в коммит, а не сваливаете всё подряд. Это позволяет делать осознанные коммиты — один коммит = одно логическое изменение.

Первые шаги

Один раз настройте, кто вы, — иначе первый git commit упрётся в ошибку «please tell me who you are»:

git config --global user.name  "Имя Фамилия"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main   # ветка по умолчанию — main

Дальше — базовый цикл:

git init                 # создать репозиторий в текущей папке
git clone <url>          # склонировать существующий

git status               # что изменено, что в staging
git add file.txt         # добавить в staging (git add . — всё)
git commit -m "message"  # зафиксировать staged-изменения

git log --oneline        # история коммитов
git diff                 # что изменено, но ещё не в staging
git diff --staged        # что в staging, но ещё не закоммичено

git status — самая полезная команда новичка: она почти всегда подсказывает, что делать дальше. А .gitignore описывает, что не должно попадать в репозиторий: артефакты сборки, зависимости (node_modules/, target/), секреты (.env), файлы IDE. Правило простое: в Git хранится исходник, а не то, что из него генерируется.

Раз staging — это осознанный выбор, держите наготове git add -p: он показывает изменения кусками (hunks) и спрашивает по каждому, добавлять ли. Так из перемешанных правок легко собрать один чистый коммит «по теме», а остальное оставить на следующий. Если кусок слишком крупный — клавиша s внутри git add -p разбивает его на более мелкие (можно добавить буквально пару строк).

Как Git устроен внутри

Понимание внутренностей снимает 90% страха. Главное прозрение: коммит — это не diff, а snapshot (полный слепок дерева файлов на момент коммита). Git хранит содержимое как объекты, адресуемые по хешу:

  • blob — содержимое файла;
  • tree — каталог (список blob’ов и вложенных tree);
  • commit — snapshot (ссылка на tree) + автор, сообщение и ссылка на родительский коммит;
  • каждый объект адресуется хешем своего содержимого — поэтому одинаковые файлы не дублируются, а история неизменяема.

И второе прозрение: ветки и теги — это просто указатели на коммиты. Ветка main — это файл с хешем одного коммита; git commit сдвигает указатель вперёд. HEAD — указатель на «где вы сейчас». Создать ветку дёшево, потому что это создать один маленький указатель, а не копировать файлы.

git reflog                   # журнал, куда двигался HEAD — даже «потерянные» коммиты тут
git switch -c rescue <hash>  # спасти найденный коммит в новую ветку (безопасно)
git checkout <hash>          # только ПОСМОТРЕТЬ это состояние — это detached HEAD (см. «Частые проблемы»)

git reflog — главная страховка: даже если вы «потеряли» коммит (сбросили ветку, отменили слияние), он почти всегда находится здесь ещё недели. Зная это, перестаёшь бояться экспериментировать.

Ветки: merge vs rebase

Ветка изолирует работу: вы пилите фичу, не трогая main.

git switch -c feature    # создать ветку и переключиться (старое: git checkout -b)
git switch main          # переключиться обратно
git branch               # список веток

Интегрировать изменения можно двумя способами:

  • merge — создаёт коммит-слияние, объединяющий две ветки. История сохраняется как есть, со всеми ответвлениями. Если main не уходил вперёд — происходит fast-forward: указатель просто сдвигается, без merge-коммита.
  • rebase — «переносит» ваши коммиты поверх свежего main, переписывая их. История становится линейной, как будто вы начали работу от последнего состояния.
исходно:           A───B───C  (main)
                        \
                         D───E  (feature)

merge feature:     A───B───C───M  (main)   M — merge-коммит
                        \     /
                         D───E

rebase main:       A───B───C───D'──E'  (feature)   D',E' — те же изменения,
                                                    новые коммиты поверх main
git merge feature        # влить feature в текущую ветку (merge-коммит)
git rebase main          # перенести коммиты текущей ветки поверх main

Правило безопасности: rebase хорош для локальных, ещё не опубликованных коммитов (чистая линейная история перед PR), но опасен для уже запушенных — он переписывает хеши, и у коллег, забравших старые коммиты, история разойдётся. Не делайте rebase того, что уже разделили с командой.

Конфликты

Конфликт возникает, когда merge/rebase не может автоматически совместить изменения: один и тот же участок файла правили по-разному в двух ветках. Git помечает спорное место:

<<<<<<< HEAD
ваш вариант
=======
вариант из вливаемой ветки
>>>>>>> feature

Подпись «ваш вариант / вариант из вливаемой ветки» верна для обычного merge. При rebase стороны могут восприниматься наоборот (HEAD — это уже переносимая основа, а не «ваша» ветка), поэтому не полагайтесь на подписи слепо — ориентируйтесь на содержимое блоков и подсказки IDE.

Разрешить — значит оставить нужный код (свой, чужой или комбинацию), убрать маркеры, затем git add файла и завершить операцию (git commit для merge или git rebase --continue). Удобнее это делать в IDE/редакторе с тремя панелями, чем вручную в CLI. Лучшая профилактика — мелкие частые коммиты и регулярная синхронизация с main: чем меньше расхождение, тем реже и проще конфликты.

Работа с remote

Remote — это удалённая копия репозитория (на GitHub/GitLab). По умолчанию она называется origin.

git remote -v            # какие remote настроены и куда они ведут
git branch -vv           # с какой remote-веткой связана локальная (tracking)
git fetch                # скачать изменения, НЕ трогая рабочую ветку
git pull                 # fetch + интеграция в текущую ветку
git push                 # отправить свои коммиты
git push -u origin feat  # первый push новой ветки + связать с remote

Полезно сразу видеть, куда вы пушите (git remote -v) и с чем связана ветка (git branch -vv) — это снимает половину «а почему не туда ушло».

Важно различать: fetch только скачивает обновления (можно спокойно посмотреть, что пришло), а pull = fetch + интеграция их в вашу ветку. Сама интеграция — это merge или rebase, в зависимости от настроек. origin — обычно ваш основной remote; upstream принято называть оригинальный репозиторий, если вы работаете с форком.

Отдельный осознанный выбор — git pull --rebase против git pull с merge: rebase даёт линейную историю без «лишних» merge-коммитов от каждой синхронизации. Многие команды ставят его по умолчанию (git config pull.rebase true).

Командный workflow

В команде Git — это процесс, а не только команды:

  • GitHub Flow — короткие feature-ветки от main, Pull Request, ревью, merge. Просто и подходит большинству.
  • Trunk-Based Development — все коммитят близко к main мелкими порциями, ветки живут часы. Требует хорошего CI.
  • Git Flow — ветки develop/release/hotfix; мощно, но тяжеловесно — оправдано при релизах по расписанию.

Поверх этого:

  • Pull / Merge Request — code review до попадания в main: обсуждение, проверки CI, апрув.
  • Conventional Commits — формат сообщений (feat:, fix:, docs:) — делает историю читаемой и позволяет автоматизировать changelog.
  • Защита веток — запрет прямого push в main, обязательный PR и зелёный CI. Базовая гигиена командной работы.

Одно простое правило закрывает большинство командных проблем: в main руками не коммитим — всегда через ветку и Pull Request.

Частые проблемы

Перед любым «опасным» действием (reset, rebase, --force) сначала осмотритесь — это снимает панику и часто показывает, что всё цело:

git status                              # что в рабочей папке и в staging
git log --oneline --graph --decorate   # где ветки и куда указывает HEAD
git branch                              # на какой ветке вы сейчас
git reflog                              # куда двигался HEAD — страховка

Шпаргалка по типичным «ой»:

Ситуация Решение
Закоммитил не то (локально) git reset --soft HEAD~1 — снять последний коммит, изменения остаются в staging
Нужно отменить уже опубликованный коммит git revert <hash> — создаёт обратный коммит, история не переписывается
«Потерял» коммит git refloggit switch -c rescue <hash> (безопасно)
detached HEAD (вы «вне ветки») git switch -c new-branch — закрепить текущее состояние веткой
Взять один коммит из другой ветки git cherry-pick <hash>
Отложить незавершённую работу git stashgit stash pop

⚠️ Осторожно с git reset --hard. Он может безвозвратно удалить незакоммиченные изменения из рабочей папки (не только сдвигает указатель). Иногда их ещё реально достать (local history в IDE, уже добавленное в staging как объекты), но рассчитывать на это не стоит. Применяйте, только если точно уверены, что правки не нужны; сомневаетесь — сначала git stash, он их сохранит.

Про git push --force: он переписывает удалённую историю и может затереть чужие коммиты. На общих ветках (main) — почти всегда нельзя. На своей feature-ветке после rebase — используйте безопасный вариант git push --force-with-lease (он откажется пушить, если на remote появилось чужое).

Минимальный ежедневный цикл

Если свести всё к привычке, рабочий день в Git выглядит так:

git switch main && git pull --ff-only   # основная ветка (может зваться иначе) — обновиться от свежего
git switch -c feature/x   # ветка под задачу (не коммитим в main)
# ... правки ...
git status                # осмотреться
git add -p                # собрать чистый коммит из нужных кусков
git commit -m "feat: x"   # зафиксировать
git push -u origin feature/x   # отправить и связать ветку
# открыть Pull Request → ревью → merge

Этого цикла и понимания «коммит — snapshot, ветка — указатель, reflog — страховка» достаточно, чтобы пользоваться Git уверенно, а не по инерции.

Полезно один раз настроить алиасы и подпись коммитов в ~/.gitconfig, но это уже после того, как базовый цикл стал привычным.

Главное — не бояться: благодаря reflog и неизменяемости коммитов почти любое действие в Git обратимо. Понимание «коммит — это snapshot, ветка — это указатель» превращает магию в предсказуемый инструмент.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии