← назад к разделу

Git — система контроля версий: она запоминает историю изменений кода и позволяет нескольким людям работать над одним проектом, не затирая друг друга. Сегодня это не «один из инструментов», а среда обитания: код живёт в Git, задачи закрываются через Git, код-ревью происходит в Git. Ни в одну команду — бэкенд, фронтенд, тестирование — нельзя войти без него.

Обязательно

Проблема, которую он решает

Без контроля версий история проекта выглядит как проект_финал_2_испр_НОВЫЙ.zip. Вопросы без ответов: что изменилось со вчера? кто и зачем поменял эту строку? как откатить неудачную правку, не потеряв удачные? как двум людям править один файл?

Git отвечает на все: каждое сохранённое состояние подписано автором, датой и пояснением; любое можно посмотреть, сравнить и вернуть; параллельная работа — штатный режим, а не катастрофа.

Снимки, а не различияспросят на собеседовании

Ключевая ментальная модель: Git хранит снимки (snapshots) всего проекта. Каждый коммит — это «сфотографированное» состояние всех файлов плюс ссылка на предыдущий снимок. История — цепочка снимков, по которой можно ходить как по ленте времени.

Коммит дёшев и мгновенен: неизменённые файлы не копируются, а переиспользуются, поэтому коммитить часто нормально и правильно.

Внутри это устроено проще, чем кажется. Git хранит четыре вида объектов, и имя каждого это хеш его содержимого (SHA-1, в новых репозиториях SHA-256). Blob это содержимое одного файла без имени: два одинаковых файла в разных папках дают один blob. Tree это каталог: список имён, прав и хешей вложенных blob и tree. Commit это снимок: хеш корневого tree, хеш родительского коммита (у слияния двух), автор, дата и сообщение. Tag с аннотацией это подписанная закладка на коммит. Отсюда следствия, которые объясняют половину поведения Git: неизменённый файл в новом коммите это тот же blob, поэтому снимок не стоит места; поправить старый коммит нельзя, потому что изменится его хеш, а за ним хеши всех потомков, и ровно это делает rebase; а идентификатор коммита вида a1b2c3d это первые семь знаков хеша. Любой объект можно посмотреть командой git cat-file -p <хеш>.

Три места, где живёт файлспросят на собеседовании

Вы поправили десять файлов, а в коммит должны войти три, относящиеся к задаче. Поэтому между редактором и историей у Git стоит промежуточная ступень, и файл живёт в одном из трёх мест. Обычные файлы, которые вы редактируете, это рабочая директория (working directory). То, что явно отобрано в следующий снимок, лежит в индексе (staging area), «предбаннике» коммита. А сохранённые коммиты и есть репозиторий.

ваш компьютер рабочая директория индекс репозиторий pricing.sqlnotes.txt pricing.sqlnotes.txt пусто pricing.sql git add git commit C1 C2 C3 git push сервер (GitHub) git statusдва файла изменены, в снимке пока ничего git add pricing.sqlв следующий снимок пойдёт только он git commit -m "исправил счёт"снимок C3 записан в локальную историю на сервере всё ещё C2коллеги вашего C3 не видят: коммит ≠ push git pushC3 уехал на сервер, теперь он общий git add → git commit → git pushтри шага: в индекс, снимком в историю, затем на сервер

Путь одной правки. git add кладёт в индекс только выбранный файл — notes.txt остаётся в рабочей директории. git commit превращает индекс в снимок C3 и добавляет его в локальную цепочку. На сервере в этот момент всё ещё C2: пока не сделан git push, работу не видит никто, кроме вас.

Есть и нулевая ступень, с которой начинается любой новый файл. Пока вы ни разу не сделали для него git add, Git за ним не следит — такой файл называется неотслеживаемым (untracked), и git status выводит его отдельным блоком «Untracked files». Отсюда частое недоумение новичка: «я же создал файл, почему его нет среди изменённых?» — потому что Git пока не знает о его существовании и сравнивать ему не с чем. Первый git add переводит файл в индекс, и дальше он живёт по общим правилам.

git status              # что изменено и что в индексе
git add file.txt        # положить файл в индекс
git commit -m "пояснение"  # сделать снимок из индекса

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

Точность эта доходит до отдельных строк. Если в одном файле оказались две правки к разным задачам, git add -p показывает изменения кусками и по каждому спрашивает, класть ли его в индекс:

git add -p db/migrations/0042_order_total.sql
# @@ -41,7 +41,7 @@ ... кусок с исправлением суммы
# Stage this hunk [y,n,q,a,d,s,e,?]? y
# @@ -88,3 +88,9 @@ ... кусок с новым индексом
# Stage this hunk [y,n,q,a,d,s,e,?]? n

y кладёт кусок, n пропускает, s режет кусок мельче. После этого git commit заберёт только первый кусок, а второй останется ждать своего коммита в рабочей директории. Так один файл с двумя правками превращается в два чистых коммита, и ради этого индекс и существует.

Локальный и удалённый

Git — распределённая система: полная копия истории лежит у вас на машине. Коммиты, ветки, просмотр истории — всё работает локально, без сети. Удалённый репозиторий (на GitHub, GitLab, Bitbucket) — это точка синхронизации команды: туда вы отправляете свои коммиты (push) и оттуда забираете чужие (pull).

Своя копия заводится одной командой: git init создаёт репозиторий с нуля в папке, где проекта ещё нет, а git clone забирает существующий вместе со всей историей.

git init                                       # завести репозиторий в своей папке
git clone https://github.com/team/project.git  # или забрать себе уже существующий
git pull   # забрать новое с сервера
git push   # отправить свои коммиты на сервер

«Закоммитил» ещё не значит «отправил»: коллеги увидят вашу работу только после push. И у каждого, кто синхронизировался, лежит своя полная копия истории, но только той её части, которая успела уехать на сервер: незапушенные коммиты и незакоммиченные правки существуют в одном экземпляре, на вашем диске. Отсюда привычка пушить рано и часто: это ещё и резервная копия.

Первый репозиторий: init, config и первый коммит

Пока репозиторий всегда «уже есть», кажется, что Git начинается с git clone. Но первый проект в жизни вы создаёте сами, и начинается он с пустой папки.

Сначала Git должен знать, кто вы: имя и почта попадут в каждый коммит, по ним потом читают git blame.

git config --global user.name "Иван Петров"
git config --global user.email "ivan@example.com"
git config --global init.defaultBranch main

--global записывает это в ~/.gitconfig один раз для всех репозиториев. Третья строка называет первую ветку main, иначе старые версии Git назовут её master.

Дальше три команды: превратить папку в репозиторий, положить файлы в индекс, сделать снимок.

mkdir shop && cd shop
git init
echo "# Shop" > README.md
git add README.md
git commit -m "Add README"

После git init в папке появляется скрытый каталог .git: там и будут жить все снимки. Пока его нет, папка для Git не существует; удалили .git, удалили всю историю. git status сразу после init скажет, что коммитов ещё нет и README.md не отслеживается; git add переводит файл в индекс, git commit делает первый снимок. Дальше цикл тот же, что описан выше, а подключить удалённый репозиторий и отправить туда историю поможет статья про удалённые репозитории.

Где это применяется

Git — это не только «сдать код». История — инструмент отладки: «когда сломалось?» решается просмотром недавних коммитов, «кто и зачем написал эту строку» — командой git blame, «воспроизвести баг версии 2.3» — переключением на тег этой версии. Тег это закладка, которую Git никогда не двигает: ветка уезжает вперёд с каждым коммитом, а v2.3.0, поставленный командой git tag -a v2.3.0 -m "Release 2.3.0", всегда указывает на тот самый снимок, из которого собирали релиз; git switch --detach v2.3.0 открывает код ровно таким, каким он был у пользователей. Как теги отправляют на сервер и почему опубликованный тег не переставляют, разобрано в статье про ветки. А вся автоматика доставки — CI/CD-конвейер — запускается именно от событий в Git: push в ветку собирает и тестирует код, слияние в главную — деплоит.

Дополнительно: при первом чтении можно пропустить

Глубже: доступ к серверу: SSH-ключ или токенрасширенное

Первый git push на GitHub или GitLab у новичка упирается не в Git, а в доступ: пароль от аккаунта по HTTPS сервер не принимает (GitHub перестал с августа 2021 года), а откуда взять что-то другое, репозиторий не объясняет.

Способа два. HTTPS-адрес (https://github.com/org/shop.git) требует персональный токен: его выпускают в настройках аккаунта, дают ему права только на репозитории и срок жизни, а при запросе пароля вставляют вместо пароля. Чтобы не вводить его каждый раз, включают менеджер учётных данных: git config --global credential.helper osxkeychain на macOS, manager на Windows, libsecret на Linux.

SSH-адрес (git@github.com:org/shop.git) работает по паре ключей: закрытый лежит у вас в ~/.ssh, открытый вы один раз добавляете в аккаунт.

ssh-keygen -t ed25519 -C "ivan@example.com"   # пара ключей, закрытый в ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub                       # открытый вставить в настройки аккаунта
ssh -T git@github.com                           # проверка: сервер назовёт вас по имени

Для ежедневной работы SSH удобнее: пароль не спрашивается вовсе, ключ не истекает, один ключ подходит ко всем репозиториям, а роботам на серверах и в CI выдают отдельные ключи с правами только на чтение (deploy keys). Токен берут там, где SSH-порт закрыт корпоративной сетью или где нужен доступ через прокси. Уже склонированный по HTTPS репозиторий переключается одной командой: git remote set-url origin git@github.com:org/shop.git.

Глубже: окончания строк и .gitattributesрасширенное

В команде, где часть людей на Windows, а часть на macOS и Linux, однажды случается странный дифф: файл «изменился весь», хотя правили одну строку, а при слиянии конфликт в каждой строке. Причина в окончаниях строк: Windows заканчивает строку парой символов CRLF, остальные системы одним LF. Редактор на Windows пересохранил файл, и для Git изменилась каждая строка.

Настройка core.autocrlf в каждом клоне решает это по-своему и поэтому не решает: у одного включена, у другого нет. Надёжнее договорённость в самом репозитории, файл .gitattributes в корне:

* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary

Первая строка велит Git хранить все текстовые файлы в истории с LF и выдавать их в рабочую папку в родном для системы виде; строки ниже закрепляют исключения, а binary запрещает трогать картинки. Файл коммитят один раз, и настройки участников перестают иметь значение. Если в истории уже лежат файлы с CRLF, после добавления .gitattributes их один раз нормализуют: git add --renormalize . и коммит «Normalize line endings».

Коротко

  • Git хранит историю как цепочку снимков-коммитов; коммит дёшев — коммитьте часто.
  • Три места: рабочая директория → индекс (git add) → репозиторий (git commit); новый файл до первого add Git вообще не видит.
  • Индекс — не лишний шаг, а способ собрать коммит только из файлов одной задачи.
  • Система распределённая: вся история лежит локально, сервер — точка синхронизации.
  • Коммит ≠ push: пока не сделан git push, вашу работу не видит никто.
  • git status — ваш компас: всегда показывает, где вы и что изменено.
  • Новый проект: git config --global user.name/user.email, затем git init, git add, git commit; каталог .git и есть история.
  • На сервер ходят по SSH-ключу или по персональному токену вместо пароля; окончания строк закрепляют в .gitattributes, а не в настройках каждого.
  • git add -p собирает коммит по кускам внутри файла; тег это неподвижная закладка на снимок релиза, по ней воспроизводят код версии.
  • Внутри четыре вида объектов с именем-хешем содержимого: blob (файл), tree (каталог), commit (снимок с родителем), tag; изменить старый коммит значит получить новый хеш, поэтому историю переписывают, а не правят.

Что почитать дальше