Як працює Git

В цьому есе описана схема роботи Git. Передбачається, що ви знайомі з Git достатньо, щоб використовувати його для контролю версій своїх проектів.

Есе концентрується на структурі графа, на якій заснований Git, і на те, як властивості цього графа визначають поведінку Git. Вивчаючи основи, ви будуєте своє подання на достовірній інформації, а не на гіпотезах, отриманих з експериментів з API. Правильна модель дозволить вам краще зрозуміти, що зробив Git, що він робить і що він збирається зробити.

Текст розбитий на серії команд, що працюють з єдиним проектом. Іноді зустрічаються спостереження з приводу структури даних графа, що лежить в основі Git. Спостереження ілюструють властивість графа і поведінка, засноване на ньому.

Після прочитання для ще глибшого занурення можна звернутися до рясно коментованого вихідного коду моїй реалізації Git на JavaScript.

Створення проекту
~ $ mkdir alpha
 
~ $ cd alpha


Користувач створює директорію alpha

~/alpha $ mkdir data
 
~/alpha $ printf 'a' > data/letter.txt


Він переміщається в цю директорію і створює директорію data. Всередині він створює файл letter.txt з вмістом «а». Директорія alpha виглядає так:

alpha
 
└── data
 
└── letter.txt


Ініціалізуємо репозиторій
~/alpha $ git init
 
Initialized empty Git repository


git init вносить поточну директорію в Git-репозиторій. Для цього він створює директорію .git і створює в ній кілька файлів. Вони визначають всю конфігурацію Git і історію проекту. Це звичайні файли – ніякої магії. Користувач може їх читати і редагувати. Тобто користувач може читати і редагувати історію проекту так само просто, як файли самого проекту.

Тепер директорія alpha виглядає так:

alpha
 
├── data
 
| └── letter.txt
 
└── .git
 
├── objects
 
etc...


Директорія .git з її вмістом ставиться з Git. Всі інші файли називаються робочою копією і належать користувачеві.

Додаємо файли
~/alpha $ git add data/letter.txt


Користувач запускає git add на data/letter.txt. Відбуваються дві речі.

По-перше, створюється новий блоб-файл в директорії .git/objects/. Він містить стислий вміст data/letter.txt. Його ім'я – згенерований хеш на основі вмісту. Приміром, Git робить хеш від а і отримує 2e65efe2a145dda7ee51d1741299f848e5bf752e. Перші два символи хеша використовуються для імені директорії в базі об'єктів: .git/objects/2e/. Залишок хеш – це ім'я блоб-файлу, що містить нутрощі доданого файлу: .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

Зауважте, що просте додавання файлу в Git призводить до збереження його вмісту в директорії objects. Воно буде зберігатися там, якщо користувач видалить data/letter.txt з робочої копії.

По-друге, git add додає файл в індекс. Індекс – це список, що містить всі файли, за якими Git було наказано стежити. Він зберігається .git/index. Кожна рядок дає у відповідність отслеживаемому хеш файлу його вміст і час додавання. Ось такий виходить вміст індексу після команди git add:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e


Користувач створює файл data/number.txt містить 1234.

~/alpha $ printf '1234' > data/number.txt


Робоча копія виглядає так:

alpha
 
└── data
 
└── letter.txt
 
└── number.txt


Користувач додає в файл в Git.

~/alpha $ git add data


Команда git add створює блоб-файл, в якому зберігається вміст data/number.txt. Він додає індекс запис data/number.txt, що вказує на блоб. Ось вміст індексу після повторного запуску git add:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
 
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3


Зауважте, що в індексі перераховані тільки файли з директорії data, хоча користувач давав команду git add data. Сама директорія data окремо не вказується.

~/alpha $ printf '1' > data/number.txt
 
~/alpha $ git add data


Коли користувач створив data/number.txt він хотів написати 1, а не 1234. Він вносить зміни і знову додає файл до індексу. Ця команда створює новий блоб з новим вмістом. Вона оновлює запис в індексі для data/number.txt із зазначенням на новий блоб.

Робимо комміт
~/alpha $ git commit -m 'a1'
 
[master (root-commit) 774b54a] a1


Користувач робить комміт a1. Git виводить дані про нього. Незабаром ми пояснимо їх.

У команди commit є три кроки. Вона створює граф, який представляє вміст версії проекту, яку коммитят. Вона створює об'єкт коміта. Вона спрямовує поточну гілку на новий об'єкт коміта.

Створення графа
Git запам'ятовує поточний стан проекту, створюючи деревоподібний граф з індексу. Цей граф записує розташування і вміст кожного файлу в проекті.

Граф складається з двох типів об'єктів: блобы і дерева.

Блобы зберігаються через git add. Вони представляють вміст файлів.

Дерева зберігаються при коммите. Дерево являє директорію в робочій копії.

Нижче наведено об'єкт дерева, записав вміст директорії data при новому коммите.

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
 
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt


Перший рядок записує все необхідне для відтворення data/letter.txt. Перша частина зберігає права доступу до файлу. Друга – що вміст файлу зберігається в блобе, а не в дереві. Третя – хеш блоба. Четверта – ім'я файлу.

Друга строчка тим же чином відноситься до data/number.txt.

Нижче зазначений об'єкт-дерево для alpha, кореневої директорії проекту:

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data


Єдиний рядок вказує на дерево data.



У наведеному графі дерево root вказує на дерево data. Дерево data вказує на блобы для data/letter.txt і data/number.txt.

Створення об'єкта коміта
git commit створює об'єкт коміта після створення графа. Об'єкт коміта – це ще один текстовий файл .git/objects/:

tree ffe298c3ce8bb07326f888907996eaa48d266db4
 
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
 
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
 

 
a1


Перший рядок вказує на дерево графа. Хеш – об'єкта дерева, що представляє корінь робочої копії. Тобто, директорії alpha. Останній рядок – коментар коміта.



Направити поточну гілку на новий комміт
Нарешті, команда коміта направляє поточну гілку на новий об'єкт коміта.

Яка у нас поточна гілка? Git йде в файл HEAD .git/HEAD і бачить:

ref: refs/heads/master


Це означає, що HEAD вказує на master. master – поточна гілка.

HEAD і master – це посилання. Посилання – це мітка, використовувана Git або користувачем, для ідентифікації визначеного коміта.

Файлу, що посилання master, не існує, оскільки це перший комміт в репозиторії. Git створює файл .git/refs/heads/master і задає його вміст хеш – об'єкта комміт:

74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd


(Якщо ви паралельно читання вбиваєте в Git команди, ваш хеш a1 буде відрізнятися від мого. Хеш об'єктів вмісту блобов і дерев – завжди виходять тими ж. А комітів – ні, тому що вони враховують дати та імена творців).

Додамо до граф Git HEAD і master:



HEAD вказує на master, як і до коміта. Але тепер master існує і вказує на новий об'єкт коміта.

Робимо не перший комміт
Нижче наведено граф після коміта a1. Включені робоча копія та індекс.



Зауважте, що у робочої копії, індексу і коміта однаковий вміст data/letter.txt і data/number.txt. Індекс та HEAD використовують хеши для вказівки на блобы, але вміст робочої копії зберігається у вигляді тексту в іншому місці.

~/alpha $ printf '2' > data/number.txt


Користувач змінює вміст data/number.txt на 2. Це оновлює робочу копію, але залишає індекс і HEAD без змін.



~/alpha $ git add data/number.txt


Користувач додає в файл в Git. Це додає блоб, що містить 2, в директорію objects. Покажчик запису індексу для data/number.txt вказує на новий блоб.



~/alpha $ git commit -m 'a2'
 
[master f0af7e6] a2


Користувач робить комміт. Кроки у нього такі ж, як і раніше.

По-перше, створюється новий деревоподібний граф для представлення вмісту індексу.

Запис в індексі для data/number.txt змінилася. Старе дерево data вже не відображає проиндексированное стан директорії data. Потрібно створити новий об'єкт-дерево data.

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
 
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt


Хеш у нового об'єкта відрізняється від старого дерева data. Потрібно створити нове дерево root для запису цього хеша.

040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data


По-друге, створюється новий об'єкт коміта.

tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
 
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
 
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
 
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
 

 
a2


Перший рядок об'єкта коміта вказує на новий об'єкт root. Другий рядок вказує на a1: батьківський комміт. Для пошуку батьківського коміта Git йде в HEAD, проходить до master і знаходить хеш коміта a1.

У третіх, вміст файлу-гілки master змінюється на хеш нового коміта.


Отримати a2


Граф Git без робочої копії і індексу

Властивості графа:
• Вміст зберігається у вигляді дерева об'єктів. Це означає, що у базі зберігаються тільки зміни. Погляньте на граф зверху. Комміт а2 повторно використовує блоб, зроблений до коміта а1. Точно так само, якщо вся директорія від коміта до коміта не змінюється, неї дерево і всі блобы і нижчерозташовані дерева можна використовувати повторно. Зазвичай зміни між коммитами невеликі. Це означає, що Git може зберігати великі історії комітів, займаючи трохи місця.
• У кожного коміта є предок. Це означає, що в репозиторії можна зберігати історію проекту.
• Посилання – вхідні точки для тієї чи іншої історії коміта. Це означає, що коммитам можна давати осмислені імена. Користувач організовує роботу у вигляді родоводу, осмисленої для свого проекту, з посиланнями типу fix-for-bug-376. Git використовує символічні посилання начебто HEAD, MERGE_HEAD і FETCH_HEAD для підтримки команди редагування історії комітів.
• Вузли в директорії objects/ незмінні. Це означає, що вміст редагується, але не видаляється. Кожен шматочок вмісту, доданий коли-небудь, кожен зроблений комміт зберігається десь в директорії objects.
• Посилання змінні. Отже, сенс посилання може змінитися. Комміт, на який вказує master, може бути найкращою версією проекту на поточний момент, але незабаром його може змінити новий комміт.
• Робоча копія і коміти, на які є посилання, доступні відразу. Інші коміти – ні. Це означає, що недавню історію легше викликати, але і змінюється вона частіше. Можна сказати, що пам'ять Git постійно зникає, і її треба стимулювати все більш жорсткими стусанами.

Робочу копію найлегше викликати з історії, оскільки вона знаходиться в корені репозиторію. Її виклик навіть не вимагає команди Git. Але також це і сама непостійна точка в історії. Користувач може зробити десяток версій файлу, але Git не запише ні одну з них, поки вони не додані.

Комміт, на який вказує HEAD, дуже легко викликати. Він знаходиться на кінці підтвердженої гілки. Щоб переглянути його вміст, користувач може просто зберегти і потім вивчити робочу копію. І в той же час, HEAD – найбільш часто змінюється посилання.

Комміт, на який вказує конкретне посилання, легко викликати. Користувач просто підтвердить цю гілку. Кінець гілки змінюється рідше, ніж HEAD, але достатньо для того, щоб сенс назви гілки можна було міняти.

Складно викликати комміт, на який не вказують посилання. Чим далі користувач йде від посилання, тим важче йому відтворити сенс коміта. Але чим далі в минуле він іде, тим менше ймовірність, що хтось змінив історію з моменту їх попереднього перегляду.

Підтвердження (check out) коміта
~/alpha $ git checkout 37888c2
 
You are in 'detached HEAD' state...


Користувач підтверджує комміт а2, використовуючи його хеш. (Якщо ви запускаєте ці команди, то дана команда у вас не спрацює. Використовуйте git log, щоб з'ясувати хеш вашого коміта а2).

Підтвердження складається з чотирьох кроків.

По-перше, Git отримує комміт а2 і граф, на який він вказує.

По-друге, він робить записи про файли у графі робочої копії. В результаті змін не відбувається. У робочій копії вже є вміст графа, оскільки HEAD вже вказував на комміт а2 через master.

По-третє, Git робить записи про файли в граф в індексі. Тут теж змін не відбувається. В індексі вже міститься комміт а2.

По-четверте, вмісту HEAD присвоюється хеш коміта а2:

f0af7e62679e144bb28c627ee3e8f7bdb235eee9


Запис хеша в HEAD призводить репозиторій в стані з відокремленим HEAD. Зверніть увагу на графі нижче, що HEAD вказує безпосередньо на комміт а2, а не на master.



~/alpha $ printf '3' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m 'a3'
 
[detached HEAD 3645a0e] a3


Користувач записує вміст data/number.txt значення 3 коммитит зміна. Git йде до HEAD для отримання батьків коміта а3. Замість того, щоб знайти і слідувати за посиланням на гілку, він знаходить і повертає хеш коміта а2.

Git оновлює HEAD, щоб він вказував на хеш нового коміта а3. Репозиторій, як і раніше, знаходиться в стані з відокремленим HEAD. Він не на гілки, оскільки жоден комміт не вказує ні на а3, ні на будь-який з його нащадків. Його легко втратити.

Далі ми будемо опускати дерева і блобы з діаграм графів.



Створити відгалуження (branch)

~/alpha $ git branch deputy


Користувач створює нову гілку під назвою deputy. В результаті з'являється новий файл .git/refs/heads/deputy, що містить геш, на який вказує HEAD: хеш коміта а3.

Гілки – це тільки посилання, а посилання – це лише файли. Це означає, що гілки Git важать дуже небагато.

Створення гілки deputy розміщує новий комміт а3 на відгалуженні. HEAD все ще відділений, оскільки все ще показує безпосередньо на комміт.



Підтвердити відгалуження
~/alpha $ git checkout master
 
Switched to branch 'master'


Користувач підтверджує відгалуження master.

По-перше, Git отримує комміт а2, на який вказує master, і отримує граф, на який вказує комміт.

По-друге, Git вносить файлові записи в граф файли робочої копії. Це змінює вміст data/number.txt 2.

По-третє, Git вносить файлові записи в граф індексу. Це оновлює входження для data/number.txt на хеш блоба 2.

По-четверте, Git направляє HEAD на master, змінюючи його вміст з хеша на

ref: refs/heads/master




Підтвердити гілку, несумісну з робочою копією

~/alpha $ printf '789' > data/number.txt
 
~/alpha $ git checkout deputy
 
Your changes to these files would be overwritten
 
by checkout:
 
data/number.txt
 
Commit your changes or stash them before you
 
switch branches.


Користувач випадково присвоює вмісту data/number.txt значення 789. Він намагається підтвердити deputy. Git перешкоджає підтвердження.

HEAD вказує на master, який вказує на a2, де в data/number.txt записано 2. deputy вказує на a3, де в data/number.txt записано 3. У версії для робочої копії data/number.txt записано 789. Всі ці версії різні, і різницю треба якось усунути.

Git може замінити версію робочої копії версією підтверджується коміта. Але він усіма силами уникає втрати даних.

Git може об'єднати версію робочої копії з підтверджується версією. Але це складно.

Тому Git відхиляє підтвердження.

~/alpha $ printf '2' > data/number.txt
 
~/alpha $ git checkout deputy
 
Switched to branch 'deputy'


Користувач помічає, що він випадково відредагував data/number.txt і присвоює йому назад 2. Потім він успішно підтверджує deputy.



Об'єднання з предком
~/alpha $ git merge master
 
Already up-to-date.


Користувач включає master в deputy. Об'єднання двох гілок означає об'єднання двох комітів. Перший комміт – той, на який вказує deputy: приймає. Другий отримати той, на який вказує master: дає. Для об'єднання Git нічого не робить. Він повідомляє, що він вже «Already up-to-date».

Серія об'єднань у графі інтерпретується як серія змін вмісту репозиторію. Це означає, що при об'єднанні якщо комміт дає виходить предком коміта приймаючого, Git нічого не робить. Ці зміни вже внесені.

Об'єднання з нащадком
~/alpha $ git checkout master
 
Switched to branch 'master'


Користувач підтверджує master.



~/alpha $ git merge deputy
 
Fast forward


Він включає deputy в master. Git виявляє, що комміт приймаючого, а2, є предком коміта дає, а3. Він може зробити об'єднання з швидкою перемотуванням вперед.

Він бере комміт дає і граф, на який він вказує. Він вносить файлові записи в графи робочої копії і індексу. Потім він «перемотує» master, щоб той вказував на а3.



Серія комітів на графі інтерпретується як серія змін вмісту репозиторію. Це означає, що при об'єднанні, якщо дає є нащадком приймаючого, історія не змінюється. Вже існує послідовність комітів, що описують потрібні зміни: послідовність комітів між приймаючим і дає. Але, хоча історія Git не змінюється, граф Git змінюється. Конкретне посилання, на яку вказує HEAD, оновлюється, щоб вказувати на комміт дає.

Об'єднати два коміта з різних родоводів
~/alpha $ printf '4' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m 'a4'
 
[master 7b7bd9a] a4


Користувач записує 4 до вмісту number.txt і коммитит зміна в master.

~/alpha $ git checkout deputy
 
Switched to branch 'deputy'
 
~/alpha $ printf 'b' > data/letter.txt
 
~/alpha $ git add data/letter.txt
 
~/alpha $ git commit -m 'b3'
 
[deputy 982dffb] b3


Користувач підтверджує deputy. Він записує «b» до вмісту data/letter.txt і коммитит зміна в deputy.



У комітів можуть бути спільні батьки. Це означає, що в історії комітів можна створювати нові родоводи.

У комітів може бути кілька батьків. Це означає, що різні родоводи можна об'єднати комітом з двома батьками: об'єднуючим комітом.

~/alpha $ git merge master -m 'b4'
 
Merge made by the 'recursive' strategy.


Користувач об'єднує master з deputy.

Git виявляє, що приймає, b3, дає, a4, знаходяться в різних родоводів. Він виконує об'єднує комміт. У цього процесу вісім кроків.

1. Git записує хеш дає коміта в файл alpha/.git/MERGE_HEAD. Наявність цього файлу повідомляє Git, що він знаходиться в процесі об'єднання.

2. По-друге, Git знаходить базовий комміт: новий предок, загальний для дає і бере комітів.



У комітів є батьки. Це означає, що можна знайти точку, в якій розділилися дві родоводів. Git відстежує ланцюжок тому від b3, щоб знайти всіх його предків, і тому від a4 з тією ж метою. Він знаходить самого нового з їх спільних предків, а3. Це базовий комміт.

3. Git створюють індекси для базового, дає і бере коміта з їх деревоподібних графів.

4. Git створює diff, що об'єднує зміни, які приймає і дає коміти виробили з базовим. Цей diff – список шляхів до файлів, що вказують на зміни: додавання, видалення, модифікацію або конфлікти.

Git отримує список всіх файлів, наявних в індексах базового коміта, одержує і дає. Для кожного він порівнює записи в індексах і вирішує, яким чином потрібно змінювати файл. Він пише відповідну запис в diff. У нашому випадку В diff потрапляють дві запису.

Перша запис data/letter.txt. Вміст цього файлу – «a» в базовому, «b» в одержуючому і «a», дає. Вміст у базового і одержує розрізняється. Але у базового і дає – збігається. Git бачить, що вміст змінено отримують, а не дає. Запис для diff data/letter.txt – це зміна, а не конфлікт.

Другий запис у diff – для data/number.txt. У цьому випадку, вміст збігається у базового і отримує, а у що дає воно інше. Запис для diff data/number.txt — також зміна.

Є можливість знайти базовий комміт об'єднання. Це означає, що якщо файл відрізняється від базового тільки в одержуючому або дає, Git може автоматично провести об'єднання файлу. Це зменшує роботу, яку повинен виконати користувач.

5. Зміни, описані записами в diff, застосовуються до робочої копії. Вмісту data/letter.txt присвоюється значення b, а вмісту data/number.txt – 4.

6. Зміни, описані записами в diff, застосовуються до індексу. Запис, що відноситься до data/letter.txt, вказує на блоб b, а запис data/number.txt – на блоб 4.

7. Підтверджується оновлений індекс:

tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
 
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
 
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
 
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
 
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
 

 
b4


Зверніть увагу, що у коміта два батька.

8. Git направляє поточну гілку, deputy, на новий комміт.



Об'єднання двох комітів з різних родоводів, що змінюють один і той же файл
~/alpha $ git checkout master
 
Switched to branch 'master'
 
~/alpha $ git merge deputy
 
Fast forward


Користувач підтверджує master. Він об'єднує deputy і master. Це призводить до перемотуванні master до коміта b4. Тепер master і deputy вказують на один і той же комміт.



~/alpha $ git checkout deputy
 
Switched to branch 'deputy'
 
~/alpha $ printf '5' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m 'b5'
 
[deputy bd797c2] b5


Користувач підтверджує deputy. Він встановлює вміст data/number.txt в 5 і підтверджує зміну в deputy.

~/alpha $ git checkout master
 
Switched to branch 'master'
 
~/alpha $ printf '6' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m 'b6'
 
[master 4c3ce18] b6


Користувач підтверджує master. Він встановлює вміст data/number.txt у 6 і підтверджує зміну в master.



~/alpha $ git merge deputy
 
CONFLICT in data/number.txt
 
Automatic merge failed; fix conflicts and
 
commit the result.


Користувач об'єднує deputy і master. Виявляється конфлікт і об'єднання припиняється. Процес об'єднання з конфліктом робить ті ж шість перших кроків, що і процес об'єднання без конфлікту: визначити .git/MERGE_HEAD, знайти базовий комміт, створити індекси для базового, приймає і дає комітів, створити diff, оновити робочу копію і відновити індекс. З-за конфлікту сьомий крок з комітом і восьмий з оновленням посилання не виконуються. Пройдемо з ним заново і подивимося, що виходить.

1. Git записує хеш дає коміта в файл .git/MERGE_HEAD.



2. Git знаходить базовий комміт, b4.

3. Git генерує індекси для базового, приймає і дає комітів.

4. Git генерує diff, комбінує зміни базового коміта, вироблені отримують і дає коммитами. Цей diff – список шляхів до файлів, що вказує на зміни: додавання, видалення, модифікацію або конфлікт.

В даному випадку diff містить лише один запис: data/number.txt. Вона відзначена як конфлікт, оскільки вміст data/number.txt відрізняється дає, приймає і базовому коммитах.

5. Зміни, обумовлені записами в diff, застосовуються до робочої копії. В області конфлікту Git виводить обидві версії в файл робочої копії. Вміст файлу ata/number.txt стає наступним:

<<<<<<< HEAD
 
6
 
=======
 
5
 
>>>>>>> deputy


6. Зміни, визначені записами в diff, застосовуються до індексу. Записи в індексі мають унікальний ідентифікатор у вигляді комбінації шляху до файлу та етапи. У запису файлу без конфлікту етап дорівнює 0. Перед об'єднанням індекс виглядав так, де нулі – це номери етапів:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
 
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb


Після запису diff в індекс той виглядає так:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
 
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
 
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
 
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61


Запис data/letter.txt на етапі 0 така ж, яка і була до об'єднання. Запис data/number.txt з етапом 0 зникла. Замість неї з'явилися три нових. У запису з етапом 1 хеш від базового вмісту data/number.txt. У запису з етапом 3 хеш від вмісту дає data/number.txt. Присутність трьох записів говорить Git, що для data/number.txt виник конфлікт.

Об'єднання припиняється.

~/alpha $ printf '11' > data/number.txt
 
~/alpha $ git add data/number.txt


Користувач інтегрує вміст двох конфліктуючих версій, встановлюючи вміст data/number.txt рівним 11. Він додає файл в індекс. Git додає блоб, містить 11. Додавання конфліктного файлу говорить Git про те, що конфлікт вирішено. Git видаляє входження data/number.txt для етапів 1, 2 і 3 з індексу. Він додає запис data/number.txt на етапі 0 з хешем від нового блоба. Тепер індекс містить записи:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
 
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
 

 
~/alpha $ git commit -m 'b11'
 
[master 251a513] b11


7. Користувач коммитит. Git бачить в репозиторії .git/MERGE_HEAD, що говорить йому про те, що об'єднання знаходиться в процесі. Він перевіряє індекс і не виявляє конфліктів. Він створює новий комміт, b11, для запису вмісту дозволеного об'єднання. Він видаляє файл .git/MERGE_HEAD. Це і завершує об'єднання.

8. Git направляє поточну гілку, master, на новий комміт.



Видалення файлу
Діаграма для графа Git включає історію комітів, дерева і блобы останнього коміта, робочу копію та індекс:



~/alpha $ git rm data/letter.txt
 
rm 'data/letter.txt'


Користувач вказує Git видалити data/letter.txt. Файл видалено з робочої копії. З індексу видаляється запис.



~/alpha $ git commit -m '11'
 
[master d14c7d2] 11


Користувач робить комміт. Зазвичай при коммите, Git будує граф, який представляє вміст індексу. data/letter.txt не включений в граф, оскільки його немає в індексі.



Копіювати репозиторій
~/alpha $ cd ..
 
~ $ cp -R alpha bravo


Користувач копіює вміст репозиторію alpha/ в директорію bravo/. Це призводить до наступній структурі:

~
 
├── alpha
 
| └── data
 
| └── number.txt
 
└── bravo
 
└── data
 
└── number.txt


Тепер в директорії bravo є новий граф Git:



Зв'язати репозиторій з іншим сховищем
~ $ cd alpha
 
~/alpha $ git remote add bravo ../bravo


Користувач повертається в репозиторій alpha. Він призначає bravo віддаленим сховищем для alpha. Це додає кілька рядків у файл alpha/.git/config:

[remote "bravo"]
 
url = ../bravo/


Рядки говорять, що існує віддалений репозиторій bravo в директорії ../bravo.

Отримати гілку з віддаленого сховища
~/alpha $ cd ../bravo
 
~/bravo $ printf '12' > data/number.txt
 
~/bravo $ git add data/number.txt
 
~/bravo $ git commit -m '12'
 
[master 94cd04d] 12


Користувач переходить до дерева bravo. Він присвоює вмісту data/number.txt значення 12 і коммитит зміна в master на bravo.



~/bravo $ cd ../alpha
 
~/alpha $ git fetch bravo master
 
Unpacking objects: 100%
 
From ../bravo
 
* branch master -> FETCH_HEAD


Користувач переходить у версії alpha. Він копіює master з bravo в alpha. У цього процесу чотири кроки.

1. Git отримує хеш коміта, на який bravo вказує master. Це хеш коміта 12.

2. Git складає список всіх об'єктів, від яких залежить комміт 12: сам об'єкт коміта, об'єкти в його графі, коміти предка коміта 12, і об'єкти з їх графів. Він видаляє з цього списку всі об'єкти, які вже є в базі alpha. Решту він копіює в alpha/.git/objects/.

3. Вмісту конкретного файлу посилання в alpha/.git/refs/remotes/bravo/master присвоюється хеш коміта 12.

4. Вміст alpha/.git/FETCH_HEAD стає наступним:

94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo


Це означає, що остання команда fetch дістала комміт 12 з master з bravo.



Об'єкти можна копіювати. Це означає, що у різних репозиторіїв може бути спільна історія.

Сховища можуть зберігати посилання на віддалені гілки типу alpha/.git/refs/remotes/bravo/master. Це означає, що репозиторій може локально записувати стан гілки віддаленого сховища. Він коректний під час його видобування, але стане застарілим у разі зміни віддаленої гілки.

Об'єднання FETCH_HEAD
~/alpha $ git merge FETCH_HEAD
 
Updating d14c7d2..94cd04d
 
Fast forward


Користувач об'єднує FETCH_HEAD. FETCH_HEAD – це просто ще одне посилання. Вона вказує на комміт 12, дає. HEAD вказує на комміт 11, що приймає. Git робить об'єднання-перемотування і направляє master на комміт 12.



Отримати гілка віддаленого сховища
~/alpha $ git pull bravo master
 
Already up-to-date.


Користувач переносить master з bravo в alpha. Pull – це скорочення для «скопіювати і об'єднати FETCH_HEAD». Git виконує обидві команди і повідомляє, що master «Already up-to-date».

Клонувати репозиторій
~/alpha $ cd ..
 
~ $ git clone alpha charlie
 
Cloning into 'charlie'


Користувач переїжджає у верхній каталог. Він клонує alpha в charlie. Клонування призводить до результатів, подібним з командою cp, яку користувач виконав для створення репозиторію bravo. Git створює нову директорію charlie. Він ініціалізує charlie в якості сховища, додає alpha в якості віддаленого під ім'ям origin, отримує origin і об'єднує FETCH_HEAD.

Розмістити (push) гілку на підтвердженої гілки на віддаленому сховищі
~ $ cd alpha
 
~/alpha $ printf '13' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m '13'
 
[master 3238468] 13


Користувач повертається в репозиторій alpha. Присвоює вмісту data/number.txt значення 13 і коммитит зміна в master на alpha.

~/alpha $ git remote add charlie ../charlie


Він призначає charlie віддаленим сховищем alpha.

~/alpha $ git push charlie master
 
Writing objects: 100%
 
remote error: refusing to update checked out
 
branch: refs/heads/master because it will make
 
the index and work tree inconsistent


Він розміщує master на charlie.

Всі об'єкти, необхідні для коміта 13, копіюються у charlie.

У цей момент процес розміщення зупиняється. Git, як завжди, повідомляє користувачеві, що пішло не так. Він відмовляється розміщувати на гілки, яка підтверджена віддалено. Це має сенс. Розміщення оновило б віддалений індекс і HEAD. Це призвело б до плутанини, якщо б хтось ще редагував робочу копію на віддаленому сховищі.

В цей момент користувач може створити нову гілку, об'єднати комміт 13 з нею та розмістити цю гілку на charlie. Але користувачам потрібно репозиторій, на якому можна розміщувати, що завгодно. Їм потрібен центральний репозиторій, на якому можна розміщувати, і з якого можна отримувати (pull), але на який безпосередньо ніхто не коммитит. Їм потрібно щось типу віддаленого GitHub. Їм потрібен чистий (bare) репозиторій.

Клонируем чистий (bare) репозиторій
~/alpha $ cd ..
 
~ $ git clone alpha delta --bare
 
Cloning into bare repository 'delta'


Користувач переходить у верхню директорію. Він створює клон delta в якості чистого репозиторію. Це звичайний клон з двома особливостями. Файл config говорить про те, що репозиторій чистий. І файли зазвичай зберігаються в директорії .git, зберігаються в корені репозиторію:

delta
 
├── HEAD
 
├── config
 
├── objects
 
└── refs




Розмістити гілку в чистому репозиторії
~ $ cd alpha
 
~/alpha $ git remote add delta ../delta


Користувач повертається в репозиторій alpha. Він призначає delta віддаленим сховищем для alpha.

~/alpha $ printf '14' > data/number.txt
 
~/alpha $ git add data/number.txt
 
~/alpha $ git commit -m '14'
 
[master cb51da8] 14


Він присвоює вмісту значення 14 коммитит зміна в master на alpha.



~/alpha $ git push delta master
 
Writing objects: 100%
 
To ../delta
 
3238468..cb51da8 master -> master


Він розміщує master в delta. Розміщення проходить у три етапи.

1. Всі об'єкти, необхідні для коміта 14 на гілки master, копіюються з alpha/.git/objects/ delta/objects/.

2. delta/refs/heads/master оновлюється до коміта 14.

3. alpha/.git/refs/remotes/delta/master направляють на комміт 14. В alpha знаходиться актуальна запис стану delta.



Підсумок

Git побудований на графі. Майже всі команди Git маніпулюють цим графом. Щоб зрозуміти Git, сконцентруйтеся на властивостях графа, а не процедурах або командах.

Щоб дізнатися про Git ще більше, вивчайте директорію .git. Це не страшно. Загляньте всередину. Змініть вміст файлів і подивіться, що станеться. Створіть комміт вручну. Спробуйте показати, як сильно ви зможете зламати сховище. Потім полагодите його.
Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.