📁⚙️ Полное руководство по основам Git
Из-за большого количества команд новичкам бывает сложно освоить Git. В этом руководстве мы расскажем обо всем, что вам нужно знать, чтобы приступить к работе с Git, начиная с создания первого репозитория и заканчивая слиянием веток. Помимо архитектуры Git рассмотрим принципы работы таких команд, как add, checkout, reset, commit, merge, rebase, cherry-pick, pull, push и tag.
В этой статье помимо архитектуры Git будут рассмотрены принципы работы таких команд, как add, checkout, reset, commit, merge, rebase, cherry-pick, pull, push и tag.
💡 Обо всем по порядку
Вы должны практиковаться параллельно с чтением поста.
Давайте сначала создадим новый проект с именем git-101, а затем инициализируем репозиторий git
с помощью команды git init
:
Git CLI предоставляет два типа команд:
- Plumbing – состоит из низкоуровневых команд, используемых Git за кулисами, когда пользователи вводят высокоуровневые команды.
- Porcelain – которые являются высокоуровневыми командами, обычно используемыми пользователями Git.
В этом руководстве мы увидим, как команды plumbing связаны с командами porcelain, которые мы используем изо дня в день.
⚙️ Архитектура Git
Внутри проекта, содержащего репозиторий Git, ознакомимся с компонентами Git:
Мы остановимся на основных:
- .git/objects/
- .git/refs
- HEAD
Разберем подробно каждый компонент.
💾 База данных объектов
Используя find
, инструмент UNIX, мы можем ознакомиться со структурой папки .git/objects
:
В Git все хранится в структуре .git/objects
, которая представляет собой Git Object Database.
Что мы можем сохранить в Git? Все.
🤔 Подождите!
Как это возможно?
С помощью хэш-функций.
🔵 Спасаемся хэшированием
Хэш-функция преобразует данные произвольного динамического размера в значения фиксированного размера. Делая это, мы можем хранить/сохранять что угодно, потому что конечное значение всегда будет иметь один и тот же размер.
Плохая реализация хэш-функций может легко привести к коллизиям, когда два разных данных динамического размера могут отображаться в один и тот же окончательный хэш фиксированного размера.
SHA-1 – известная реализация хэш-функции, которая в целом безопасна и почти не имеет коллизий.
Возьмем, к примеру, хэширование строки my precious
:
Примечание. Если вы работаете в Linux, вы можете использовать команду sha1sum вместо OpenSSL.
🔵 Сравнение различий в содержании
Хорошее хэширование – это безопасная практика, когда мы не можем знать необработанное значение, т. е. реверс-инжиниринг.
В случае если мы хотим знать, изменилось ли значение, мы просто помещаем значение в хэш-функцию и вуаля – мы можем сравнить разницу:
Если хэши разные, то можно считать, что значение изменилось.
Можете ли вы найти здесь возможность? Как насчет использования SHA-1 для хранения данных и просто отслеживания всего путем сравнения хэшей? Это именно то, что Git делает внутри 🤯.
🔵 Git и SHA-1
Git использует SHA-1 для генерации хэширования всего и сохраняет его в .git/objectsпапке. Просто так!
hash-object
, команда plumbing
:
Сравним с OpenSSL версией:
Упс ... это совсем другое. Это потому, что Git добавляет определенное слово, за которым следует размер содержимого и разделитель \0
. Это слово Git называет типом объекта.
Да, у объектов Git есть типы. Первый объект, который мы рассмотрим, – это объект blob.
🔵 blob-объект
Когда мы отправляем, например, строку my precious
в команду hash-object
, Git добавляет паттерн {object_type} {content_size}\0
к функции SHA-1, так что:
Затем:
Ура! 🎉
🔵 Хранение blob в базе данных
Но сама команда hash-object не сохраняется в папке .git/objects
. Мы должны добавить -w
и объект будет сохранен:
Данное изображение и все последующие взяты отсюда.
🔵 Чтение необработанного содержимого блоба
Мы уже знаем, что по криптографическим соображениям невозможно прочитать необработанное содержимое из его хэшированной версии.
🤔 Хорошо, но подождите.
Как Git узнает исходное значение?
Он использует хэш в качестве ключа, указывающего на значение, которое является оригинальным содержимым, используя алгоритм сжатия под названием Zlib, который сжимает содержимое и сохраняет его в базе данных объектов, тем самым экономя место для хранения.
cat-file
, команда plumbing
, при наличии ключа распаковывает сжатые данные, таким образом, получая исходное содержимое:
Таким образом, Git – это база данных с ключом и значением!
🔵 Как поделиться blob
Используя Git, мы хотим работать над содержимым и делиться им с другими людьми
Как правило, после работы над различными файлами/блобами мы готовы поделиться ими и подписать свои имена.
Другими словами, нам нужно сгруппировать, продвигать и добавлять метаданные в наши блобы. Этот процесс работает следующим образом:
- Добавьте большой двоичный объект в промежуточную область
- Сгруппируйте все blob-объекты в рабочей области в древовидную структуру
- Добавьте метаданные в древовидную структуру (имя автора, дата, смысловое сообщение)
Давайте рассмотрим описанные выше шаги подробнее.
🔵 Stage area и index
update-index
, команда plumbing
, позволяет добавить blob
в stage area
и дать ему имя:
--add
: добавляет blob в stage, также называемый индексом.--cacheinfo
: используется для регистрации файла, которого еще нет в рабочем каталоге- хэш blob
index.txt
: имя большого двоичного объекта в индексе.
Где Git хранит индекс?
Недоступен для чтения человеком и сжат с использованием Zlib.
Мы можем добавить в индекс столько больших двоичных объектов, сколько захотим, например:
После добавления blob-объектов в индекс мы можем сгруппировать их в древовидную структуру, чтобы мы могли поделиться ими.
🔵 Объект дерева
Команда write-tree
(plumbing) позволяет Git группировать все blob, которые были добавлены в индекс, и создает в папке еще один объект: .git/objects
Проверяя папку .git/objects
, обратите внимание, что был создан новый объект:
Давайте извлечем исходное значение с помощью cat-file
для лучшего понимания:
Это интересный вывод, он сильно отличается от BLOB-объекта, который вернул исходное содержимое.
В дереве объектов Git возвращает все объекты, которые были добавлены в индекс.
100644
: кэш-информацияblob
: тип объекта- хэш blob
- имя blob
После завершения работы добавим некоторые метаданные в дерево, чтобы мы могли присвоить имя автора, дату и так далее.
🔵 Объект коммита
commit-tree, команда plumbing, получает дерево, сообщение коммита и создает еще один объект в папке .git/objects:
Что это за объект?
А как насчет его стоимости?
- tree
3725c
: объект дерева ссылок - автор/коммиттер
- сообщение коммита my precious commit
🤯 ОМГ! Я вижу здесь закономерность?
Кроме того, коммиты могут ссылаться на другие коммиты:
-p
позволяет ссылаться на родительский коммит:
Мы видим, что, благодаря коммиту с родителем, мы можем рекурсивно пройти все коммиты по всем их деревьям, пока не доберемся до финальных blob-объектов .
Возможное решение:
И так далее. Ну вы попали в точку.
🔵 Логирование для восстановления
git log
, команда porcelain
, решает эту проблему, просматривая все коммиты, их родителей и деревья, давая нам представление о временной хронологии нашей работы.
🤯 ОМГ!
Git – это гигантская, но легкая база данных графа ключ-значение!
🔵 Граф Git
В Git мы можем манипулировать указателями на граф.
- Blob – это моментальные снимки данных/файлов.
- Деревья представляют собой набор блобов или другое дерево.
- Коммиты ссылаются на деревья и/или другие коммиты, добавляя метаданные
Это очень мило и все такое, но использование sha1 в команде git log
может быть громоздким.
Как насчет присвоения имен хэшам? Используйте ссылки.
Ссылки на Git
Ссылки находятся в папке .git/refs
:
🔵 Дадим имена коммитам
Мы можем связать любой хэш коммита с произвольным именем, расположенным в .git/refs/heads
, например:
Теперь давайте выполним git log
, используя новую ссылку:
Что еще лучше, Git предоставляет update-ref
, команду plumbing
, и мы можем использовать ее для обновления связи коммита со ссылкой:
Звучит знакомо, да? Да, речь идет о ветках.
🔵 Ветки
Ветки – это ссылки, указывающие на конкретный коммит.
Поскольку ветки представляют команду update-ref, хэш коммита может измениться в любое время, то есть ссылка на ветку является изменяемой.
На мгновение давайте подумаем о том, как git log
работает без аргументов:
🤔 Хм...
Как Git узнает, что моя текущая ветка является «основной»?
🔵 HEAD
Ссылка на HEAD находится в .git/HEAD
. Это один файл, который указывает на главную ссылку (ветвь):
Точно так же, используя команду porcelain
:
Используя symbolic-ref
, команду plumbing
, мы можем управлять тем, на какую ветку указывает HEAD
:
Как и update-ref
в ветках, мы можем обновить HEAD
, используя symbolic-ref
в любое время.
На картинке ниже мы изменим HEAD
с ветки main
на ветку fix
:
Без аргументов команда git log
обходит корневой коммит, на который ссылается текущая ветвь (HEAD
):
До сих пор мы изучали архитектуру и основные компоненты в Git, а также вспомогательные команды, которые являются более низкоуровневыми командами.
Пришло время связать все эти знания с porcelain командами, которые мы используем ежедневно.
🍽️ Porcelain команды
Git предоставляет большое количество команд высокого уровня, которые мы можем использовать без необходимости напрямую манипулировать объектами и ссылками.
Эти команды называются porcelain командами.
🔵 git add
Команда git add
принимает файлы в рабочем каталоге в качестве аргументов, сохраняет их как blob-объекты в базе данных и добавляет их в индекс.
Короче говоря, git add
:
- запускает
hash-object
для каждого аргумента файла - запускает
update-index
для каждого аргумента файла
🔵 git commit
git commit
принимает в качестве аргумента сообщение, группирует все ранее добавленные в индекс файлы и создает объект коммита.
Сначала выполняется write-tree
:
Затем выполняется commit-tree
:
🕸️ Управление указателями в Git
Широко используются следующие команды porcelain, которые манипулируют ссылками Git под капотом.
Предполагая, что мы только что клонировали проект, в котором HEAD
указывает на main ветку, которая указывает на коммит C1
:
Как мы можем создать еще одну новую ветку из текущей HEAD
и переместить HEAD
в эту новую ветку?
🔵 git checkout
Используя git checkout
с параметром -b
, Git создаст новую ветку из текущей (HEAD
) и переместит HEAD
в эту новую ветку.
Какая plumbing-команда отвечает за перемещение HEAD
? Точно, symbolic-ref
.
После этого мы делаем новую работу в ветке fix
, а затем выполняем git commit
, который добавит новый коммит под названием C3
:
Запустив git checkout
, мы можем продолжать переключать HEAD
между разными ветвями:
Иногда нам может понадобиться переместить коммит, на который указывает ветка.
Мы уже знаем, что это делает команда plumbing update-ref
:
🔵 git reset
Команда git reset
(porcelain) запускает update-ref
внутри, поэтому нам просто нужно выполнить:
Но как Git узнает, какую ветку нужно переместить? Что ж, git reset
перемещает ветку, на которую указывает HEAD
.
Что делать, если есть различия между ревизиями? Используя reset, Git перемещает указатель, но оставляет все различия в рабочей области (индексе).
Проверка с помощью git status
:
Коммит ревизии был изменен в ветке fix
и все отличия перенесены в index
.
Тем не менее, что нам делать, если мы хотим сбросить и отбросить все различия? Просто использовать параметр --hard
:
При использовании git reset --hard
любые различия между ревизиями будут отброшены, и они не будут отображаться в индексе .
💡 Золотой совет о перемещении ветки
Если мы хотим выполнить подключение update-ref
к другой ветке, нет необходимости проверять ветку, как это необходимо в git reset
.
Вместо этого мы можем выполнить porcelain-команду git branch -f source target
:
Под капотом он выполняет git reset --hard
в исходной ветке. Давайте проверим, на какой коммит указывает основная ветка:
Также мы подтверждаем, что ветка fix
по-прежнему указывает на коммит 369cd
:
Мы сделали git reset
без перемещения HEAD
!
Нередко вместо перемещения указателя ветки мы хотим применить конкретный коммит к текущей ветке.
🔵 git cherry-pick
С помощью porcelain-команды git cherry-pick
мы можем применить произвольную фиксацию к текущей ветке.
Возьмем следующий сценарий:
- main-пункты к C3 – C2 – C1
- fix для точек на C5 – C4 – C2 – C1
- HEAD указывает на fix
В ветке исправления отсутствует фиксация C3
, на которую ссылается основная ветка.
Для этого запустим git cherry-pick C3
:
Обратите внимание, что:
- коммит
C3
будет клонирован в новый коммит с именемC3
- этот новый коммит будет ссылаться на коммит
C5
- fix переместит указатель на C3'
HEAD
продолжает указывать на исправление
После применения изменений график будет представлен следующим образом:
Однако есть еще один способ переместить указатель ветки. Он заключается в использовании произвольного коммита другой ветки, и при необходимости выполняется слияние различий.
Вы не ошиблись, здесь мы говорим о git merge
.
🔵 git merge
Опишем следующий сценарий:
- Main-пункты к C3 – C2 – C1
- Fix точек на C4 – C3 – C2 – C1
HEAD
указывает наmain
Мы хотим применить исправленную ветку к текущей (основной) ветке, т. е. выполнить git merge fix
.
Обратите внимание, что ветка fix
содержит все коммиты, принадлежащие основной ветке (C3 – C2 – C1
), имея только один коммит перед основным (C4
).
В этом случае основная ветвь будет «переадресована», указывая на тот же коммит, что и ветка исправления.
Такое слияние называется fast-forward
, как показано на изображении ниже:
Когда fast-forward невозможен
Иногда текущее состояние нашей древовидной структуры не позволяет выполнять ускоренную перемотку вперед. Рассмотрим сценарий ниже:
Когда в ветке слияния – ветке исправления в приведенном выше примере – отсутствует одна или несколько коммитов из текущей ветки (основной): коммит C3
.
Таким образом, fast-forward
невозможен.
Однако, чтобы слияние прошло успешно, Git выполняет технику, называемую Snapshotting
, состоящую из следующих шагов.
Во-первых, Git ищет следующего общего родителя двух ветвей, в этом примере коммит C2
.
Во-вторых, Git делает снимок целевой ветки фиксации C3
:
В-третьих, Git делает снимок исходной ветки фиксации C5
:
Наконец, Git автоматически создает слияние фиксации (C6
) и указывает на двух родителей соответственно: C3
(цель) и C5
(источник):
Вы когда-нибудь задумывались, почему в дереве Git отображаются некоторые коммиты, созданные автоматически?
Не заблуждайтесь, этот процесс слияния называется трехсторонним слиянием!
Далее давайте изучим другой метод слияния, при котором fast-forward
невозможен, но вместо моментального снимка и автоматического слияния коммита Git применяет различия поверх исходной ветки.
Да, это git rebase
.
🔵 git rebase
Рассмотрим следующее изображение:
- Main-пункты к
C3 – C2 – C1
- fix точек на
C5 – C4 – C2 – C1
HEAD
указывает наfix
Мы хотим перебазировать основную ветку в ветку исправления, посредством git rebase main
. Но как работает git rebase
?
👉git reset
Сначала Git выполняет git reset main, при этом ветка fix будет указывать на тот же указатель основной ветки: C3 – C2 – C1
.
На данный момент у коммитов C5-C4
нет ссылок.
👉git cherry-pick
Во-вторых, Git выполняет git cherry-pick C5
в текущую ветку:
Обратите внимание, что выбранные коммиты клонируются, поэтому окончательный хэш изменится: C5 – C4 станет C5' – C4'.
После у нас может быть следующий сценарий:
👉git reset еще раз
Наконец, Git выполнит git reset C5'
, поэтому указатель ветки fix переместится с C3
на C5'
.
Процесс rebase
завершен.
До сих пор мы работали с локальными ветками, т.е. хранящимися на нашем устройстве. Пришло время научиться работать с удаленными ветками, которые синхронизированы с удаленными репозиториями в интернете.
🌐 Удаленные ветки
Чтобы работать с удаленными ветками, нам нужно добавить удаленную ветку в наш локальный репозиторий с помощью команды porcelain – git remote
.
Удаленные репозитории находятся в папке .git/refs/remotes
:
🔵 Скачать с удаленного репозитория
Как нам синхронизировать удаленную ветку с нашей локальной веткой?
Git предлагает два шага:
👉git fetch
С помощью git fetch origin main
Git загрузит удаленную ветку и синхронизирует ее с новой локальной веткой с именем origin/main
, также известной как upstream branch
(восходящая ветка).
👉git merge
После извлечения и синхронизации вышестоящей ветки мы можем выполнить git merge
origin/main
и, поскольку восходящая ветка опережает нашу локальную ветку, Git безопасно применит ускоренное слияние.
Однако комбинация fetch
+ merge
может повториться, так как мы будем синхронизировать локальные/удаленные ветки несколько раз в день.
Но сегодня наш счастливый день, и Git предоставляет команду git pullchina
, которая выполняет fetch
+ merge
от нашего имени.
👉git pull
git pull
выполнит выборку (синхронизирует удаленную ветвь с вышестоящей ветвью), а затем объединит восходящую ветвь с локальной ветвью.
Итак, мы увидели, как получать/загружать изменения с репозитория. С другой стороны, как насчет отправки локальных изменений на удаленные?
🔵 Загрузить на удаленный репозиторий
Git предоставляет porcelain команду под названием git push
:
👉git push
Выполнение git push origin main
приведет к загрузке изменения на удаленный репозиторий:
Затем Git объединит восходящую ветвь origin/main
с локальной main-веткой:
В конце мы получим следующее изображение:
Где:
- Удаленный репозиторий обновлен (локальные изменения отправлены)
- main к C4
- origin/main к C4
- HEAD указывает на main
🔵 Предоставление неизменяемых имен коммитам
Мы знаем, что ветки – это просто изменяемые ссылки на коммиты, поэтому мы можем переместить указатель ветки.
Однако Git также предлагает способ предоставления неизменяемых ссылок, указатели которых не могут быть изменены (если только вы не удалите их и не создадите снова).
Неизменяемые ссылки полезны, например, когда мы хотим пометить/отметить коммиты, которые готовы для какого-то производственного выпуска.
Да, мы говорим о тегах.
👉git tag
Используя команду (porcelain) git tag
, мы можем давать имена коммитам, но мы не можем выполнить сброс или любую другую команду, которая изменила бы указатель.
Это очень полезно для управления версиями. Теги находятся в папке .git/refs/tags
:
Если мы хотим изменить указатель тега, мы должны удалить его и создать еще один с тем же именем.
💡 Git reflog
И последнее, но не менее важное – вызываемая команда git reflog, которая сохраняет все изменения, которые мы сделали, в нашем локальном репозитории.
Это очень полезно, если мы хотим перемещаться вперед и назад по временной шкале Git. Наряду с reset
, cherry-pick
и подобными, это мощный инструмент, если мы хотим освоить Git.
Подведение итогов
Какое долгое путешествие!
Эта статья была слишком длинной, но я смог изложить основные темы, которые, по моему мнению, важны для понимания Git.
Я надеюсь, что после прочтения этой статьи вы будете более уверенно использовать Git, разрешая ежедневные конфликты и сложные случаи во время процесса слияния/перебазирования.
Материалы по теме
- Git за полчаса: руководство для начинающих
- 📁 Настраиваем Git для правильной работы с опенсорс-проектами
- Основы Git: контроль версий для самых маленьких
- 💽 Git для Data Science: контроль версий моделей и датасетов с помощью DVC
- 👍 Как правильно писать сообщения коммитов в GIT, чтобы всем было хорошо