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), «предбаннике» коммита. А сохранённые коммиты и есть репозиторий.
Путь одной правки. 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); новый файл до первогоaddGit вообще не видит. - Индекс — не лишний шаг, а способ собрать коммит только из файлов одной задачи.
- Система распределённая: вся история лежит локально, сервер — точка синхронизации.
- Коммит ≠ 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; изменить старый коммит значит получить новый хеш, поэтому историю переписывают, а не правят.
Что почитать дальше
- Коммиты и история — как делать хорошие коммиты и читать историю.
- Ветки и слияние — главный механизм параллельной работы.
- Push, pull, fetch — как локальный репозиторий обменивается коммитами с сервером.
- Команды Linux-терминала — если терминал сам по себе ещё непривычен.