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

Для кожного з прикладів, які ви побачите в даному документі, я використовував Git версії 1.5.4.5 (а перекладач — 1.7.9.5).

Джон Віґлі (John Wiegley)

http://ftp.newartisans.com/pub/git.from.bottom.up.pdf

Ліцензія

Цей документ надається під умовами ліцензії Creative Commons Attribution-Share Alike 3.0, зміст якої можна переглянути за адресою:

http://creativecommons.org/licenses/by-sa/3.0/us/

Якщо коротко — ви можете використовувати вміст документа з будь-якою метою, як особистою, так і комерційною, до тих пір поки згадується автор. Аналогічно, документ можна змінювати, робити похідні роботи та переклади, до тих пір поки ці переклади та похідні надаються громадськості на таких самих умовах, як і оригінальний документ.

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

Перед тим як ми почнемо, ось кілька термінів які потрібно згадати спершу, так як вони надалі з'являтимуться постійно :

Репозиторій Репозиторій — це набір коммітів, кожен з яких є архівом того, як виглядало робоче дерево проекту в якийсь момент у минулому, не залежно на вашій машині, чи ще десь. Також в ньому означена голова (HEAD), яка ідентифікує гілку чи комміт, від якої відгалужується поточне робоче дерево. Ну й на останок, він містить множину гілок (branches) та міток (tags), які ідентифікують певні комміти за іменем.

Індекс На відміну від інших, подібних інструментів які ви могли використовувати, Git не комітить зміни прямо з робочого дерева в репозиторій. Натомість, зміни спершу заносяться у реєстр що називається індексом. Думайте про нього як про спосіб "підтвердження" своїх змін, одну за одною, перед тим як зробити комміт (який запише всі ваші підтверджені зміни за раз). Декому легше називати це "staging area" замість індексу.

Робоче дерево Робоче дерево — це будь-яка директорія на вашій файловій системі, яка має пов'язаний з собою репозиторій (на що зазвичай вказує присутність в ній піддиректорії яка називається .git). Воно включає всі файли та піддиректорії тієї директорії.

Комміт Комміт - це миттєвий знімок стану вашого робочого дерева в певний момент часу. Комміт на який вказує HEAD в момент створення нового комміта стає його батьком. Таким чином утворюється поняття "історії ревізій".

Гілка Гілка́ - це просто назва для комміта (про комміти скоро поговоримо ширше), яка також називається посиланням. Це батьки комміта що визначають його історію, і таким чином під терміном "гілка розробки" йдеться також про них.

Мітка Мітка (tag) - це теж назва для комміта, подібно до гілки, окрім того що він завжди вказує на один і той же комміт, та може мати власний текст опису.

master Головна лінія розробки в більшості репозиторіїв відбувається в гілці яку називають "master"́. Хоча це загальноприйнята домовленість, але ця гілка не є якоюсь особливою.

HEAD HEAD використовується вашим репозиторієм для визначення того від якого комміта походитсь поточне дерево: Якщо ви перейшли на гілку, HEAD символічно посилатиметься на ту гілку, вказуючи що назва гілки повинна оновитись після наступного комміта. Якщо ви перейшли на конкретний комміт, HEAD посилатиметься лише на той комміт. Це називається detached HEAD, і стається, наприклад, коли ви переходите на тег.

Хід подій, зазвичай, є таким: Після створення репозиторію, ваша робота проходить в робочому дереві. Як тільки робота досягне певної віхи - виправлення зневадження, кінця робочого дня, моменту коли все компілюється - ви додаєте зміни в індекс. Як тільки індекс міститиме все що ви хотіли закомітити, ви записуєте його вміст в репозиторій. Ось проста діаграма що показує типовий життєвиий цикл проекту:

З цією базовою картинкою в головіНасправді команда checkout копіює вміст з репозиторію в індекс, а потім в робоче дерево. Але так як користувач ніколи не бачить таке використання індексу під час отримання стану робочого дерева, я вирішив що буде більше сенсу не відображати це на діаграмі. наступні розділи спробують описати як кожна з цих сутностей бере участь в роботі Git.

Репозиторій: відстеження вмісту директорії Як ми вже згадували, Git займається тривіальним - він зберігає миттєві знімки робочого дерева. Багато його внутрішніх конструкцій можна зрозуміти в термінах цієї базової задачі.

Структура репозиторію Git багато в чому повторює структуру файлової системи UNIX. Файлова система починається з кореневої директорії, яка зазвичай містить інші директорії, більшість з яких мають листки, чи, по іншому, файли, які містять дані. Мета-дані цих файлів містяться як в директоріях (імена), так і в і-нодах які посилаються на вміст тих файлів (їхні типи, права доступу і т.п.). Кожен і-нод має унікальний номер що ідентифікує вміст пов'язаного з ним файлу. І хоча ви можете мати багато елементів директорії що вказують на один і-нод (хардлінків), власником вмісту що зберігається у файловій системі є і-нод.

Внутрішньо Git поділяє, на диво, подібну структуру, проте з однією-двома ключовими відмінностями. По-перше, він зберігає вміст ваших файлів в блобах (blob), які також є листками в чомусь, страшенно подібному на директорію, що називається деревом. Так як і-нод унікально ідентифікується номером присвоєним системою, кожен блоб ідентифікується SHA1 хешем його вмісту та розміру. Для більшості цілей це лише звичайне число, як і-нод, але воно має дві додаткові властивості: воно перевіряє що вміст блобу ніколи змінюється і що той самий вміст завжди буде записуватись в той самий блоб, не залежно де він буде з'являтись: в різних коммітах, в різних репозиторіях - навіть в різних "кінцях" Інтернету. Якщо багато дерев посилаються на ваш блог, це просто як хардлінкінг: блоб не зникне з вашого репозиторію, доки на нього буде хоч одне посилання.

Відмінністю блоба Git і файлу файлової системи є те, що блоб не містить жодних метаданих. Вся ця інформація зберігається в дереві що тримає блоб. Одне дерево може тримати вміст блобу як файл "foo", що був створений в серпні 2004-го, а інше - як файл "bar" що був створений на 9 років пізніше. В нормальній файловій системі два файли з однаковим вмістом, але з різними метаданими завжди зберігатимуться як два різні незалежні файли. Чому така різниця існує? В основному тому, що файлова система створена для зберігання файлів що змінюють свій вміст, а Git - ні. Факт того що дані в репозиторії Git є незмінними - це те на чому все працює і чому потрібен був інший дизайн. І як виявляється, такий дизайн дозволяє набагато компактніше зберігання, тому що всі об'єкти що мають ідентичний вміст зберігатимуться в одному блобі не залежно від того де знаходяться.

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

$ mkdir sample; cd sample $ echo 'Hello, world!' > greeting

Тут я створив нову директорію файлової системи з іменем "sample", яка містить файл з банально передбачуваним вмістом. Я ще навіть не створив репозиторію, але прямо зараз можу почати використовувати деякі команди Git щоб зрозуміти що він збирається зробити. Перш за все, я хочу знати під яким хешем Git збирається зберігати вміст мого файлу:

$ git hash-object greeting af5626b4a114abcb82d63db7c8082c3c4756e51b

Якщо ви виконаєте цю команду на своїй системі, ви отримаєте точнісінько такий самий хеш. Навіть незважаючи на те що ми створюємо два різні репозиторії (ймовірно в різних кінцях світу), наш блоб "greeting" в обох репозиторіях матиме однаковий хеш. Я можу навіть втягнути комміти з вашого репозиторія і Git розбереться що ми зберігаємо однаковий вміст, тому запише лише одну копію цього вмісту!

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

$ git init $ git add greeting $ git commit -m "Added my greeting"

На цьому етапі, наш блоб буде в системі саме так як ми очікували, записаний з іменем хеша яке ми бачили вище. Як правило, Git потребує щонайменше стільки символів хешу, скільки потрібно щоб однозначно визначити об'єкт в межах репозиторію. Зазвичай шести-семи чисел достатньо:

$ git cat-file -t af5626b blob $ git cat-file blob af5626b Hello, world!

Це саме те! Я навіть не дивився в якому комміті знаходиться файл, чи в якому дереві, але знаючи лише вміст я припустив що він там, і він таки там. Він завжди матиме такий самий ідентифікатор, незалежно від того як довго житиме репозиторій, чи де саме в ньому знаходитиметься файл. Конкретний вміст цього файлу гарантовано збережений назавжди.

Таким чином блоб в Git являє собою фундаментальну одиницю даних в Git. Насправді, вся система стосується управління блобами.

Блоби зберігаються в деревах Вміст ваших файлів зберігається в блобах, але ці блоби майже не мають ніяких ознак. Вони не мають ні імен, ні структури. Це, в кінці кінців, просто "блоби".

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

$ git ls-tree HEAD 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting

Ось воно! Перший комміт додав мій файл "greeting" до репозиторію. Цей комміт містить одне дерево Git, яке має один листок: блоб з вмістом файлу "greeting".

Хоча я можу побачити дерево передаючи HEAD команді git ls-tree, я ще не бачив об'єкт дерева на який посилається наш комміт. Ось кілька інших команд для того щоб підкреслити відмінність між деревом та коммітом і відкрити моє дерево:

$ git rev-parse HEAD 374bf425a1a1383fd8e0dea4aba877898627749c # буде іншим на вашій системі. $ git cat-file -t HEAD commit $ git cat-file commit HEAD tree 0563f77d884e4f79ce95117e2d686d7d6e282887 author Bunyk <[email protected]> 1388923552 +0200 committer Bunyk <[email protected]> 1388923552 +0200

Added my greeting

Перша команда перетворює псевдонім HEAD на ідентифікатор комміта на який HEAD посилається, друга перевіряє тип об'єкта, третя показує ідентифікатор дерева що прив'язане до комміту та іншу інформацію комміту. Хеш комміта є унікальним для мого репозиторію - тому що він включає в себе моє ім'я та дату коли я зробив комміт - але хеш для дерева повинен бути спільним для вашого і мого прикладу, тому що ці дерева містять однаковий блоб з однаковим іменем.

Давайте перевіримо що це насправді один і той самий об'єкт дерева:

$ git ls-tree 0563f77 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting

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

$ find .git/objects/ -type f | sort .git/objects/05/63f77d884e4f79ce95117e2d686d7d6e282887 .git/objects/37/4bf425a1a1383fd8e0dea4aba877898627749c .git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b

З того що відобразила команда я можу побачити, що ввесь мій репозиторій містить три об'єкти, хеш кожного з яких ми бачили в попередніх прикладах. Давайте останній раз з цікавості поглянемо на типи цих об'єктів:

$ git cat-file -t 0563f77d884e4f79ce95117e2d686d7d6e282887 tree