Модуль 1: Привид у машині — Внутрішня будова Git
Модуль 1: Привид у машині — Внутрішня будова Git
Розділ «Модуль 1: Привид у машині — Внутрішня будова Git»Складність: [СЕРЕДНЯ] Час на виконання: 90 хвилин Передумови: Модуль 0.6 “Основи Git” курсу “З нуля до терміналу” (init, add, commit, push, pull) Наступний модуль: Модуль 2: Мистецтво розгалуження
Що ви зможете зробити
Розділ «Що ви зможете зробити»До кінця цього модуля ви зможете:
- Діагностувати стан репозиторію, перевіряючи директорію
.gitта її об’єкти. - Порівняти ролі об’єктів blob, tree та commit у представленні історії проєкту.
- Впровадити зміни в область підготовки (index) та продемонструвати її проміжну роль у процесі створення commit.
- Оцінити наслідки моделі адресованого за вмістом сховища (content-addressable storage) Git для цілісності даних та історії.
- Використовувати службові (plumbing) команди Git для ручного створення та перевірки об’єктів репозиторію.
Чому це важливо
Розділ «Чому це важливо»Уявіть, що ваша команда розгортає критичне оновлення у продуктовий кластер Kubernetes. П’ятниця, вечір, усі вже мріють про вихідні. Раптом у Slack спалахує термінове повідомлення: “Продакшн лежить! Pods падають через відсутність ConfigMap!”. Починається паніка. Останній deploy був успішним, але чомусь життєво важлива конфігурація зникла. Розробники гарячково намагаються зробити відкат, але знайти точний попередній стан не вдається. Через кілька годин болісних пошуків старший інженер зрештою відновлює втрачений елемент, ретельно досліджуючи старі логи та локальні копії. Виявилося, що хтось випадково видалив гілку локально, вважаючи, що видаляє лише вказівник, а не реальний код, і наступний force-push поширив помилку.
Цей сценарій нічного жаху, хоч і екстремальний, підкреслює загальну вразливість: брак глибокого розуміння того, як Git насправді працює. Багато інженерів сприймають Git як “чорну скриньку” — магічний інструмент, який якось зберігає їхній код. Вони знають git add, git commit, git push, але коли щось іде не так, коли історія переписується або критичний файл таємниче зникає, “чорна скринька” не дає відповідей. Без розуміння внутрішніх механізмів Git — як він зберігає дані, пов’язує історію та керує вказівниками — ви перебуваєте у владі його налаштувань за замовчуванням. Цей модуль відкриє завісу, демістифікуючи внутрішнього “привида” Git. Ви вивчите фундаментальні будівельні блоки кожного репозиторію Git, отримавши можливість не просто використовувати Git, а справді розуміти, діагностувати та відновлювати дані навіть після найбільш заплутаних збоїв системи контролю версій. Розуміючи Git на такому рівні, ви станете впевненішим, вмілішим та стійкішим інженером, готовим до будь-яких викликів у роботі з версіями.
Основні розділи
Розділ «Основні розділи»1. Директорія .git: мозок вашого репозиторію
Розділ «1. Директорія .git: мозок вашого репозиторію»Кожного разу, коли ви запускаєте git init, Git створює приховану директорію .git у корені вашого проєкту. Ця директорія — не просто папка; це весь мозок вашого репозиторію. Вона містить усю інформацію, необхідну Git для керування історією вашого проєкту: від кожної версії файлу до кожного повідомлення commit, гілки та тегу. Якщо ви втратите цю директорію, ви втратите всю історію Git вашого проєкту.
Давайте зазирнемо всередину щойно ініціалізованого репозиторію.
# Створити нову порожню директоріюmkdir my-git-repocd my-git-repo
# Ініціалізувати репозиторій Gitgit init
# Вивести вміст директорії .gitls -F .gitОчікуваний результат:
HEAD config description hooks/ info/ objects/ refs/HEAD: Вказівник на поточний вибраний commit. Зазвичай він вказує на гілку.config: Налаштування конфігурації, специфічні для проєкту.description: Використовується GitWeb (веб-інтерфейсом для репозиторіїв Git) для опису проєкту.hooks/: Скрипти на стороні клієнта або сервера, які Git може виконувати до або після команд (наприклад, pre-commit, post-receive).info/: Містить глобальний файл виключень для ігнорованих шаблонів, подібний до.gitignore.objects/: Тут Git зберігає всі ваші дані — фактичний вміст ваших файлів, директорій та метадані commit. Це сховище, адресоване за вмістом.refs/: Містить вказівники на commit, зокрема для гілок (heads) та тегів (tags).
Директорія objects/ є найважливішою. Саме тут відбувається справжня магія Git.
Зупиніться та подумайте: Як ви гадаєте, що відбувається всередині директорії
objects/, коли ви вперше виконуєтеgit addфайлу? Чи збереже Git весь вміст файлу чи лише різницю (diff)?
2. Об’єкти Git: Blobs, Trees та Commits
Розділ «2. Об’єкти Git: Blobs, Trees та Commits»Git — це фундаментально система керування вмістом, а не система керування файлами. Він зберігає історію вашого проєкту як серію взаємопов’язаних об’єктів, кожен з яких ідентифікується унікальним хешем SHA-1. Існує чотири основних типи об’єктів Git, але ми зосередимося на трьох головних: blob, tree та commit.
2.1 Blobs (двійкові великі об’єкти)
Розділ «2.1 Blobs (двійкові великі об’єкти)»Об’єкт blob зберігає вміст файлу. Він не зберігає ім’я файлу, шлях або будь-які метадані — лише необроблені дані. Якщо два файли у вашому репозиторії (навіть у різних директоріях) мають абсолютно однаковий вміст, Git зберігає лише один об’єкт blob для обох. Це ключова частина ефективності Git.
Давайте створимо файл і подивимося на його blob:
# Переконайтеся, що ми в my-git-repocd my-git-repo
# Створити зразок Kubernetes ConfigMapcat <<EOF > configmap.yamlapiVersion: v1kind: ConfigMapmetadata: name: my-app-configdata: app.properties: | environment=dev database.url=jdbc:postgresql://localhost:5432/myapp_dev log4j.properties: | log4j.rootLogger=INFO, stdoutEOF
# Підготувати файл (це створить об'єкт blob)git add configmap.yaml
# Перевірити базу даних об'єктів Gitfind .git/objects -type fВи побачите новий файл у .git/objects/. Його назва матиме вигляд xx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, де xx — це перші два символи хешу SHA-1, а решта — інші 38 символів.
Тепер скористаємося службовою (plumbing) командою, щоб перевірити цей об’єкт blob. Службові команди — це низькорівневі команди, розроблені для використання скриптами або іншими командами Git. Високорівневі (porcelain) команди (як-от git add, git commit) — це зручні для користувача команди верхнього рівня.
# Отримати хеш SHA-1 підготовленого файлуBLOB_HASH=$(git hash-object -w configmap.yaml)echo "Blob Hash: $BLOB_HASH"
# Прочитати вміст об'єкта blobgit cat-file -p "$BLOB_HASH"Очікуваний результат (схожий на):
Blob Hash: 9d8c... (ваш хеш буде іншим)apiVersion: v1kind: ConfigMapmetadata: name: my-app-configdata: app.properties: | environment=dev database.url=jdbc:postgresql://localhost:5432/myapp_dev log4j.properties: | log4j.rootLogger=INFO, stdoutЗауважте, що git cat-file -p вивела точний вміст configmap.yaml без жодної інформації про назву файлу.
2.2 Trees (дерева)
Розділ «2.2 Trees (дерева)»Об’єкти tree подібні до записів у директорії. Вони зберігають список вказівників на blob та інші об’єкти tree разом із відповідними іменами файлів, режимами доступу (permissions) та типами об’єктів. Саме так Git відновлює стан вашого проєкту в будь-якому конкретному commit. Об’єкт tree представляє знімок (snapshot) директорії в певний момент часу.
Коли ви виконуєте git commit, Git бере поточний стан вашої області підготовки та перетворює його на ієрархію об’єктів tree (для директорій) та blob (для файлів).
Давайте зафіксуємо наш configmap.yaml і перевіримо отримане дерево:
# Зафіксувати файл (commit)git commit -m "Add initial ConfigMap"
# Отримати хеш SHA-1 останнього commitCOMMIT_HASH=$(git rev-parse HEAD)echo "Commit Hash: $COMMIT_HASH"
# Прочитати об'єкт commit, щоб знайти його кореневе деревоgit cat-file -p "$COMMIT_HASH"Результат git cat-file -p "$COMMIT_HASH" покаже щось на кшталт tree <tree_hash>. Скопіюйте цей <tree_hash>.
# Прочитати вміст кореневого об'єкта treeTREE_HASH=<the_tree_hash_from_above> # Замініть на ваш реальний хеш дереваgit cat-file -p "$TREE_HASH"Очікуваний результат (схожий на):
100644 blob 9d8c... configmap.yamlЦе показує, що наше кореневе дерево містить один запис: об’єкт blob (9d8c...) з назвою configmap.yaml та режимом доступу 100644.
2.3 Commits (коміти)
Розділ «2.3 Commits (коміти)»Об’єкти commit пов’язують усе разом. Об’єкт commit містить:
- Вказівник на кореневий об’єкт tree (знімок файлів вашого проєкту в цьому commit).
- Вказівники на один або кілька об’єктів parent commit (батьківських комітів), що пов’язують історію.
- Інформацію про автора та того, хто зафіксував зміни (ім’я, email, мітка часу).
- Повідомлення commit.
Цей ланцюжок об’єктів commit, кожен з яких вказує на свого батька, утворює спрямований ациклічний граф (DAG), який і є історією вашого проєкту.
Давайте ще раз перевіримо наш об’єкт commit:
# Прочитати об'єкт commitgit cat-file -p "$COMMIT_HASH"Очікуваний результат (схожий на):
tree 1a2b3c4d5e6f7890abcdef1234567890abcdefauthor Your Name <your.email@example.com> 1678886400 +0000committer Your Name <your.email@example.com> 1678886400 +0000
Add initial ConfigMapТут tree 1a2b3c4d... вказує на кореневий об’єкт tree для цього commit. Якби це був не найперший commit, ви б також побачили рядок parent <parent_commit_hash>.
Зупиніться та поміркуйте: Який підхід ви б обрали для швидкої перевірки повідомлення останнього commit:
git logчиgit cat-file -p <commit_hash>, і чому?
3. Область підготовки (Index)
Розділ «3. Область підготовки (Index)»Область підготовки, також відома як index, є критично важливим проміжним кроком між вашою робочою директорією та історією репозиторію. Це двійковий файл (.git/index), який зберігає інформацію про файли, які Git включить у наступний commit. Це не ваша робоча директорія і не ваш останній commit. Це пропозиція наступного commit.
Коли ви запускаєте git add <file>, Git не просто кладе файл у базу даних objects/ як blob (хоча він справді створює blob). Натомість він оновлює index інформацією про шлях до файлу, права доступу та вказівник на об’єкт blob.
Давайте змінимо наш configmap.yaml, підготуємо його та подивимося на index:
# Змінити configmap.yamlcat <<EOF > configmap.yamlapiVersion: v1kind: ConfigMapmetadata: name: my-app-configdata: app.properties: | environment=prod database.url=jdbc:postgresql://production.db.svc/myapp_prod log4j.properties: | log4j.rootLogger=WARN, fileEOF
# Підготувати змінений файлgit add configmap.yaml
# Перевірити indexgit ls-files --stageОчікуваний результат (схожий на):
100644 2f1a... 0 configmap.yamlДругий стовпець 2f1a... — це хеш SHA-1 нового об’єкта blob для зміненого configmap.yaml. Це підтверджує, що index тепер вказує на оновлений вміст. Якби ви зараз виконали git commit, він створив би новий об’єкт commit, що вказує на новий об’єкт tree, який, своєю чергу, вказував би на цей новий blob.
4. Адресоване за вмістом сховище та DAG
Розділ «4. Адресоване за вмістом сховище та DAG»Дизайн Git є революційним, оскільки він є адресованим за вмістом. Це означає, що замість того, щоб посилатися на файли за їхніми іменами, Git посилається на їхній вміст через криптографічний хеш (SHA-1). Кожна частина даних — blob, tree, commit — зберігається як об’єкт, ім’ям якого є хеш SHA-1 його вмісту.
Це має глибокі наслідки:
- Цілісність: Якщо зміниться хоча б один біт файлу, його хеш SHA-1 зміниться, і Git негайно зрозуміє, що це інша версія. Це робить Git неймовірно стійким до випадкового пошкодження даних.
- Ефективність: Дублікати файлів або ідентичні версії файлів зберігаються лише один раз.
- Незмінність (Immutability): Після того як об’єкт створено в базі даних об’єктів Git, його ніколи не можна змінити. Будь-яка зміна вмісту призводить до створення нового об’єкта.
Спосіб, у який ці незмінні об’єкти пов’язуються між собою, утворює Спрямований ациклічний граф (Directed Acyclic Graph, DAG). Кожен об’єкт commit вказує на свого батька (або батьків), створюючи хронологічний ланцюжок. Ця структура дозволяє Git ефективно відстежувати історію, гілки, злиття (merges) та перебазування (rebases).
graph TD CommitC --> CommitB CommitB --> CommitA CommitA -- initial --> RootTreeA CommitB --> RootTreeB CommitC --> RootTreeC
RootTreeA --> BlobA1 RootTreeA --> TreeA_dir
TreeA_dir --> BlobA2
RootTreeB --> BlobB1 RootTreeB --> TreeB_dir
TreeB_dir --> BlobB2
RootTreeC --> BlobC1 RootTreeC --> TreeC_dir
TreeC_dir --> BlobC2
style CommitA fill:#f9f,stroke:#333,stroke-width:2px style CommitB fill:#f9f,stroke:#333,stroke-width:2px style CommitC fill:#f9f,stroke:#333,stroke-width:2px style RootTreeA fill:#bbf,stroke:#333,stroke-width:2px style RootTreeB fill:#bbf,stroke:#333,stroke-width:2px style RootTreeC fill:#bbf,stroke:#333,stroke-width:2px style BlobA1 fill:#bfb,stroke:#333,stroke-width:2px style BlobA2 fill:#bfb,stroke:#333,stroke-width:2px style BlobB1 fill:#bfb,stroke:#333,stroke-width:2px style BlobB2 fill:#bfb,stroke:#333,stroke-width:2px style BlobC1 fill:#bfb,stroke:#333,stroke-width:2px style BlobC2 fill:#bfb,stroke:#333,stroke-width:2px
linkStyle 0 stroke-width:2px,fill:none,stroke:red; linkStyle 1 stroke-width:2px,fill:none,stroke:red; linkStyle 2 stroke-width:2px,fill:none,stroke:green; linkStyle 3 stroke-width:2px,fill:none,stroke:green; linkStyle 4 stroke-width:2px,fill:none,stroke:green; linkStyle 5 stroke-width:2px,fill:none,stroke:blue; linkStyle 6 stroke-width:2px,fill:none,stroke:blue; linkStyle 7 stroke-width:2px,fill:none,stroke:blue; linkStyle 8 stroke-width:2px,fill:none,stroke:blue; linkStyle 9 stroke-width:2px,fill:none,stroke:blue; linkStyle 10 stroke-width:2px,fill:none,stroke:blue; linkStyle 11 stroke-width:2px,fill:none,stroke:blue;На діаграмі:
- Рожеві прямокутники представляють Об’єкти Commit. Кожен вказує на свого батька (червоні стрілки) та своє кореневе дерево (зелені стрілки).
- Сині прямокутники представляють Об’єкти Tree (директорії). Кожен вказує на blobs (світло-сині стрілки) або інші дерева (не показано для спрощення).
- Зелені прямокутники представляють Об’єкти Blob (вміст файлів).
5. Refs, HEAD та гілки як вказівники
Розділ «5. Refs, HEAD та гілки як вказівники»Гілки в Git — це не окремі копії вашого коду; це надзвичайно легкі вказівники (посилання, або “refs”) на конкретні об’єкти commit. Гілка просто каже Git: “це поточний край цієї лінії розробки”.
- Refs: Зберігаються у
.git/refs/. Зокрема,.git/refs/heads/містить файли, названі на честь ваших гілок, і кожен файл містить хеш SHA-1 коміту, на який вказує ця гілка. Подібним чином.git/refs/tags/містить вказівники для тегів. - HEAD: Цей спеціальний ref є вказівником на гілку, над якою ви зараз працюєте. Зазвичай це символічне посилання, тобто воно вказує на інший ref (гілку). Коли ви виконуєте
git checkout <branch-name>, ви просто змінюєте те, на що вказуєHEAD. КолиHEADвказує безпосередньо на commit (а не на гілку), ви перебуваєте в стані “detached HEAD”.
Давайте подивимося на наш поточний HEAD та посилання на гілку:
# Переглянути, на що вказує HEADcat .git/HEAD
# Переглянути посилання на гілку main (припускаючи, що 'main' — ваша гілка за замовчуванням)cat .git/refs/heads/mainОчікуваний результат (схожий на):
# cat .git/HEADref: refs/heads/main
# cat .git/refs/heads/main2f1a... (це буде хеш вашого останнього commit)Це чітко показує, що HEAD вказує на гілку main, а гілка main вказує на хеш SHA-1 вашого останнього commit. Коли ви робите новий commit, Git створює новий об’єкт commit, а потім переміщує вказівник гілки (і, відповідно, HEAD) на цей новий commit. Ось чому гілки в Git такі “дешеві” та швидкі.
Повчальна історія: Зниклий ConfigMap
Розділ «Повчальна історія: Зниклий ConfigMap»Один середній стартап, назвемо його “KubeFlow Inc.”, мав проблеми з розбіжністю конфігурацій (configuration drift) між своїми середовищами розробки та тестування Kubernetes. Їхній основний додаток покладався на критичний ConfigMap для рядків підключення до бази даних та feature flags. Одного разу молодша розробниця, якій доручили очистити старі гілки функцій, вирішила видалити локальну гілку під назвою feature/db-migration. Вона правильно розуміла, що git branch -d feature/db-migration видалить вказівник на гілку. Проте раніше вона також виконала експериментальний git rebase у цій гілці, який випадково скопіював історію гілки main, включно з тимчасовим commit, який видаляв критичний ConfigMap для тестування, а потім одразу скасував це видалення. Коли вона видалила гілку, вона не усвідомила, що сам об’єкт commit, який був короткий час єдиним місцем, де на цей ConfigMap було правильне посилання в її локальному репозиторії, став кандидатом на видалення збирачем сміття (garbage collection). Вона думала, що видаляє лише мітку. Через день її локальна гілка main була примусово відправлена (force-push) до віддаленого репозиторію. Наступний deploy з main раптово пропустив ConfigMap, що спричинило каскад збоїв.
Інцидент зрештою вирішив старший інженер, який використав git reflog, щоб знайти SHA-1 “втраченого” commit перед rebase, а потім використав git checkout <sha>, щоб відновити файли. Цей випадок підкреслює важливість розуміння того, що git branch -d видаляє лише вказівник; базові об’єкти commit залишаються на деякий час, але якщо жоден ref не вказує на них, вони зрештою можуть бути видалені збирачем сміття. Якби цей ConfigMap існував тільки в тій перебазованій, а потім видаленій гілці, ситуація була б набагато гіршою без reflog.
Чи знали ви?
Розділ «Чи знали ви?»- Git був спочатку розроблений Лінусом Торвальдсом у 2005 році для розробки ядра Linux. Він був незадоволений існуючими SCM, особливо пропрієтарними. Перший кандидат у реліз був анонсований 7 квітня 2005 року.
- Вразливість до колізій SHA-1 була відомо продемонстрована Google у 2017 році за допомогою “розбитого” (shattered) PDF. Хоча для Git це теоретична загроза через специфічне використання SHA-1 (додавання префіксів типів об’єктів), проєкт Git перейшов на SHA-256 для хешів commit у версії 2.29, щоб підготуватися до майбутніх криптографічних викликів. Проте ядро моделі об’єктів все ще значною мірою покладається на SHA-1 для blobs та trees.
- Директорія Git
.git/objectsвикористовує формат “packfile” для ефективності. Хоча окремі об’єкти спочатку зберігаються як вільні об’єкти (один файл на об’єкт), Git періодично “пакує” їх у поодинокі файли (з розширенням.pack), щоб заощадити місце на диску та підвищити продуктивність. Ця техніка стиснення означає, що часто всередині цих pack-файлів зберігаються лише відмінності між версіями. - Початковою ціллю дизайну Git була підтримка розподіленої нелінійної розробки, обробка великих проєктів (як ядро Linux) та надзвичайна швидкість. Він досягає такої швидкості частково завдяки тому, що під час типових операцій він майже повністю обмежений швидкістю дискового вводу-виводу (disk I/O), а не мережевого (network I/O), оскільки більшість історії та об’єктів зберігаються локально.
Типові помилки
Розділ «Типові помилки»| Помилка | Чому це трапляється | Як це виправити |
|---|---|---|
Неправильне розуміння git add | Віра в те, що git add додає файли безпосередньо в історію репозиторію. | Пам’ятайте, що git add готує зміни в index. Вона створює об’єкти blob, але не фіксує їх. Тільки git commit переміщує підготовлені зміни в історію репозиторію. |
Втрата історії після rebase | Force-push після необачного rebase, що переписує віддалену історію. | Розумійте, що rebase переписує історію. Використовуйте git reflog, щоб знайти втрачені коміти, якщо працюєте локально. Ніколи не робіть force-push у спільну гілку без крайньої обережності та координації з командою. |
| Стан Detached HEAD | Checkout конкретного хешу commit або старого тегу безпосередньо, а не гілки. | git status попередить вас. Щоб повернутися до нормального стану, створіть нову гілку з вашого поточного стану detached HEAD (git branch <new-branch-name>), а потім git checkout <new-branch-name>. |
Видалення папки .git | Думка про те, що це просто метадані або кеш, які безпечно видалити, як node_modules. | Ніколи не видаляйте .git! Вона містить усю історію вашого проєкту. Якщо ви випадково видалите її, єдиний вихід — знову клонувати репозиторію з віддаленого сервера, втративши будь-яку незафіксовану локальну історію. |
Нерозуміння HEAD | Плутання HEAD з поточною гілкою або думка, що це завжди main. | HEAD вказує на вашу поточну гілку (наприклад, main), а ця гілка потім вказує на останній commit у цій гілці. HEAD — це фактично “де ви зараз” у графі комітів. |
Ігнорування проблем .gitignore | Файли все ще відстежуються, попри те, що вони є в .gitignore, бо вони були додані раніше. | .gitignore ігнорує лише невідстежувані (untracked) файли. Якщо файл уже був зафіксований, він не ігноруватиметься. Використовуйте git rm --cached <file>, щоб припинити його відстеження (залишивши його в робочій директорії), а потім зафіксуйте видалення. |
Сліпе використання git reset --hard | Надто агресивне використання git reset --hard без розуміння його деструктивної природи. | git reset --hard відкидає зміни в робочій директорії, області підготовки та переміщує вказівник гілки. Використовуйте git reset --soft (тільки переміщує вказівник, зберігаючи зміни підготовленими) або git restore (вибірково відкидає зміни в робочій директорії). |
Контрольні запитання
Розділ «Контрольні запитання»-
Ви запустили `git gc`, щоб очистити свій репозиторій. Тепер `git log` усе ще правильно показує всі останні коміти, але виконання `git show
Ви б дослідили директорію `.git/objects/`, щоб визначити точний стан відсутніх даних. Спочатку потрібно знайти хеш blob, пов'язаний з цим файлом у конкретному коміті, виконавши `git ls-tree:configmap.yaml` таємничо видає помилку для одного конкретного історичного коміту. Де б ви шукали причину, щоб діагностувати, чи був базовий blob пошкоджений, чи помилково видалений збирачем сміття, і які команди підтвердили б це? `. Отримавши хеш SHA-1 об'єкта blob, ви можете перевірити, чи існує файл об'єкта в `.git/objects/ / `, або скористатися `git cat-file -t `. Якщо файл повністю відсутній, він, ймовірно, був видалений збирачем сміття (можливо, тому що він став недоступним до запуску `gc`); якщо файл існує, але `cat-file` повертає помилку, blob був пошкоджений. Цей підхід дозволяє ізолювати, чи є першопричиною вказівник (tree) чи самі дані (blob). -
Ви внесли зміни в маніфест Kubernetes Deployment (`deployment.yaml`) у вашій робочій директорії. Перед запуском `git commit` ви виконуєте `git add deployment.yaml`. Опишіть стан `deployment.yaml` відносно робочої директорії, області підготовки (index) та репозиторію (бази даних об'єктів) після цієї команди `git add`.
Після виконання `git add deployment.yaml` робоча директорія зберігає ваш змінений файл точно в тому вигляді, в якому ви його зберегли, але внутрішній стан Git суттєво змінився. Область підготовки (index) оновлюється новим записом для `deployment.yaml`, який тепер вказує на щойно згенерований об'єкт blob, що представляє поточний знімок файлу. Чому Git це робить? Створюючи цей blob у базі даних `.git/objects/` негайно, Git надійно кешує саме той вміст, який ви збираєтеся зафіксувати. Таке розділення дозволяє вам продовжувати змінювати робочий файл, не впливаючи на підготовлений знімок, що дає вам точний контроль над тим, що потрапить у наступний об'єкт commit. -
Колега примусово відправив (force-push) гілку до віддаленого репозиторію, і тепер критичний маніфест `ServiceAccount`, здається, відсутній в останньому commit у `main`. Ви підозрюєте, що маніфест існував у commit, який був перезаписаний під час force-push. Яку команду Git ви б негайно використали, щоб спробувати знайти хеш SHA-1 відсутнього commit у вашому *локальному* репозиторії, і чому ця команда особливо корисна в такому сценарії?
Ви б використали команду `git reflog`, щоб перевірити локальну історію вказівників ваших гілок та `HEAD`. Ця команда надзвичайно корисна, оскільки вона записує хронологічну історію того, де перебували ваші локальні вказівники, замість того, щоб покладатися лише на прямих предків у графі комітів. Коли force-push переписує історію, оригінальні коміти зазвичай стають "сиротами", але не видаляються збирачем сміття негайно. Ідентифікувавши попередній хеш SHA-1 у reflog, ви можете безпечно переключитися на цей стан (detached state) і зробити cherry-pick або скопіювати відсутній маніфест `ServiceAccount`. -
Вам потрібен необроблений вміст `service.yaml` у commit `a1b2c3d4`, але ви також хочете вручну порівняти його з версією, що була рівно двома комітами раніше. Опишіть два різні підходи з використанням команд Git для отримання цих даних і поясніть, який підхід ефективніший, якщо файл змінювався в кожному commit.
Один із підходів — використовувати `git show a1b2c3d4:service.yaml` та `git show a1b2c3d4~2:service.yaml`, щоб безпосередньо отримати вміст файлів у цих конкретних комітах. Другий підхід із використанням службових (plumbing) команд передбачає спочатку пошук хешів tree за допомогою `git cat-file -p a1b2c3d4`, потім отримання хешів blob у цих деревах за допомогою `git ls-tree` і, нарешті, читання blobs за допомогою `git cat-file -p`. Підхід `git show` значно ефективніший і менш схильний до помилок, оскільки він автоматично розв'язує шлях через об'єкти tree безпосередньо до blob. Службовий підхід вимагає ручного проходження графа дерев, що стає нудним і забирає багато часу. Проте розуміння обох методів пояснює, чому високорівневі (porcelain) команди, як-от `git show`, життєво необхідні для повсякденних операцій, попри складність базового DAG. -
Вас попросили переглянути зміну, що включає великий ConfigMap. Розробник наполягає, що це незначна зміна, але `git diff` показує сотні рядків. Ви підозрюєте, що вони могли змінити відступи або додати коментарі, що спричинило великий diff, навіть якщо семантичний вміст переважно той самий. Яка фундаментальна причина того, що Git відстежує вміст саме так (тобто розглядає будь-яку зміну як новий blob), і в чому головна перевага?
Фундаментальна причина того, що Git відстежує вміст саме так, полягає в його моделі сховища, адресованого за вмістом. Будь-яка зміна, якою б маленькою вона не була (наприклад, зміна відступу або новий коментар), призводить до нового хешу SHA-1 для вмісту файлу, таким чином створюючи новий об'єкт blob. Головною перевагою є цілісність та незмінність даних. Хешуючи вміст, Git гарантує, що після того, як об'єкт збережено, його неможливо змінити без зміни його хешу, що забезпечує надійний механізм виявлення пошкоджень або несанкціонованих змін. Це також робить операції, як-от перемикання між різними версіями, надзвичайно швидкими, оскільки Git потрібно лише отримати конкретні об'єкти, а не обчислювати різниці (diffs) на льоту для кожного файлу. -
Ваша команда впроваджує Git hook для автоматичної перевірки YAML-файлів маніфестів Kubernetes перед їх зафіксуванням (commit). Ви б реалізували це як `pre-commit` hook чи `post-commit` hook, і чому? У своїй відповіді пошліться на структуру директорії `.git`.
Ви б реалізували це як `pre-commit` hook. Директорія `hooks/` у `.git/` містить скрипти, які Git може виконувати в різних точках робочого процесу. `pre-commit` hook запускається до завершення створення commit, що означає, що він може перервати цей процес. Це дозволяє hook перевірити підготовлені зміни (які представляють вміст наступного commit) і, якщо валідація не вдалася (наприклад, `kubeval` або `yamllint` знайшли помилки), повністю скасувати commit. `post-commit` hook запускається після успішного створення commit, що було б занадто пізно, щоб запобігти запису некоректного YAML в історію репозиторію.
Практична вправа: Створення коміту з нуля
Розділ «Практична вправа: Створення коміту з нуля»У цій вправі ви заглибитеся у внутрішню будову Git, вручну створюючи об’єкти Git (blobs, trees та commit) за допомогою службових (plumbing) команд. Це покаже, як Git фундаментально зберігає історію вашого проєкту. Потім ми перевіримо наш вручну створений commit за допомогою високорівневої (porcelain) команди.
Налаштування:
- Переконайтеся, що ви перебуваєте в директорії
my-git-repoз основного контенту, або створіть нову:Terminal window rm -rf my-git-repomkdir my-git-repocd my-git-repogit init
Завдання:
-
Створити об’єкт Blob: Створіть файл
service.yamlдля Kubernetes Service та вручну додайте його вміст як об’єкт blob Git, зберігши отриманий хеш SHA-1.Terminal window # Створити файл service.yamlcat <<EOF > service.yamlapiVersion: v1kind: Servicemetadata:name: my-webapp-servicespec:selector:app: my-webappports:- protocol: TCPport: 80targetPort: 8080EOF# Вручну створити об'єкт blob із вмісту service.yaml# Прапорець -w записує об'єкт у базу даних# Прапорець --stdin зчитує вміст зі стандартного вводуSERVICE_BLOB_HASH=$(git hash-object -w service.yaml)echo "Service Blob Hash: $SERVICE_BLOB_HASH"# Перевірити тип об'єкта та його вмістgit cat-file -t "$SERVICE_BLOB_HASH"git cat-file -p "$SERVICE_BLOB_HASH"- Критерії успіху:
service.yamlіснує у вашій робочій директорії.- Змінна
SERVICE_BLOB_HASHмістить коректний хеш SHA-1. git cat-file -tвиводитьblob.git cat-file -pвиводить точний вмістservice.yaml.
- Критерії успіху:
-
Створити об’єкт Tree: Створіть об’єкт tree, який представляє директорію, що містить
service.yaml. Це вимагатиме невеликої хитрості, оскількиmktreeочікує специфічний формат вводу.Terminal window # Підготувати ввід для git mktree# Формат: <mode> <type> <object_hash> <filename># 100644 для звичайного файлу, blob для типуTREE_INPUT="100644 blob $SERVICE_BLOB_HASH service.yaml"echo "$TREE_INPUT" > tree_input.txt# Вручну створити об'єкт treeROOT_TREE_HASH=$(git mktree < tree_input.txt)echo "Root Tree Hash: $ROOT_TREE_HASH"# Перевірити тип об'єкта та його вмістgit cat-file -t "$ROOT_TREE_HASH"git cat-file -p "$ROOT_TREE_HASH"- Критерії успіху:
- Змінна
ROOT_TREE_HASHмістить коректний хеш SHA-1. git cat-file -tвиводитьtree.git cat-file -pвиводить100644 blob <SERVICE_BLOB_HASH> service.yaml.
- Змінна
- Критерії успіху:
-
Створити об’єкт Commit: Тепер створіть об’єкт commit, який вказує на ваше щойно створене дерево.
Terminal window # Налаштувати фіктивні дані автора/коммітера для комітуexport GIT_AUTHOR_NAME="KubeDojo Learner"export GIT_AUTHOR_EMAIL="learner@kubedojo.io"export GIT_COMMITTER_NAME="KubeDojo Learner"export GIT_COMMITTER_EMAIL="learner@kubedojo.io"# Вручну створити об'єкт commit# Прапорець -m надає повідомлення commit# Прапорець --tree вказує кореневе дерево для цього commitCOMMIT_MESSAGE="Initial commit for service.yaml via plumbing commands"INITIAL_COMMIT_HASH=$(git commit-tree "$ROOT_TREE_HASH" -m "$COMMIT_MESSAGE")echo "Initial Commit Hash: $INITIAL_COMMIT_HASH"# Перевірити тип об'єкта та його вмістgit cat-file -t "$INITIAL_COMMIT_HASH"git cat-file -p "$INITIAL_COMMIT_HASH"- Критерії успіху:
- Змінна
INITIAL_COMMIT_HASHмістить коректний хеш SHA-1. git cat-file -tвиводитьcommit.git cat-file -pвиводить деталі commit, включно з хешем дерева та повідомленням коміту.
- Змінна
- Критерії успіху:
-
Спрямувати HEAD на ваш новий commit: Щоб Git розпізнав цей commit як “останній”, вам потрібно оновити
HEADта посилання на гілку, щоб вони вказували на нього.Terminal window # Оновити гілку main, щоб вона вказувала на наш новий commitgit update-ref HEAD "$INITIAL_COMMIT_HASH" # Це змушує HEAD вказувати безпосередньо на commit (detached HEAD)git update-ref refs/heads/main "$INITIAL_COMMIT_HASH" # Це створює/оновлює посилання на гілку 'main'# Перевірити станgit log --onelinegit status- Критерії успіху:
git log --onelineпоказує ваш “Initial commit for service.yaml via plumbing commands”.git statusвказуєOn branch mainта відсутність комітів (оскільки ми не додавалиservice.yamlдо index за допомогоюgit addдля цього процесу).
- Критерії успіху:
-
Перевірити за допомогою високорівневих (porcelain) команд: Тепер, коли ваш репозиторій вказує на вручну створений commit, скористайтеся звичайними високорівневими командами для перевірки.
Terminal window # Отримати вміст commit у вашу робочу директорію# Примітка: service.yaml з вашої робочої директорії НЕ був частиною цього commit,# тому нам потрібно відновити його з самого commit.git restore --source="$INITIAL_COMMIT_HASH" -- service.yaml# Перевірити вміст файлуcat service.yaml# Видалити тимчасові файлиrm tree_input.txt- Критерії успіху:
cat service.yamlпоказує оригінальний вмістservice.yaml, який ви створили в Завданні 1. Це доводить, що Git успішно відновив файл із ваших вручну створених об’єктів.
- Критерії успіху:
Рішення:
Рішення до Завдання 1: Створити об'єкт Blob
cat <<EOF > service.yamlapiVersion: v1kind: Servicemetadata: name: my-webapp-servicespec: selector: app: my-webapp ports: - protocol: TCP port: 80 targetPort: 8080EOF
SERVICE_BLOB_HASH=$(git hash-object -w service.yaml)echo "Service Blob Hash: $SERVICE_BLOB_HASH"git cat-file -t "$SERVICE_BLOB_HASH"git cat-file -p "$SERVICE_BLOB_HASH"Рішення до Завдання 2: Створити об'єкт Tree
TREE_INPUT="100644 blob $SERVICE_BLOB_HASH service.yaml"echo "$TREE_INPUT" > tree_input.txtROOT_TREE_HASH=$(git mktree < tree_input.txt)echo "Root Tree Hash: $ROOT_TREE_HASH"git cat-file -t "$ROOT_TREE_HASH"git cat-file -p "$ROOT_TREE_HASH"Рішення до Завдання 3: Створити об'єкт Commit
export GIT_AUTHOR_NAME="KubeDojo Learner"export GIT_AUTHOR_EMAIL="learner@kubedojo.io"export GIT_COMMITTER_NAME="KubeDojo Learner"export GIT_COMMITTER_EMAIL="learner@kubedojo.io"
COMMIT_MESSAGE="Initial commit for service.yaml via plumbing commands"INITIAL_COMMIT_HASH=$(git commit-tree "$ROOT_TREE_HASH" -m "$COMMIT_MESSAGE")echo "Initial Commit Hash: $INITIAL_COMMIT_HASH"git cat-file -t "$INITIAL_COMMIT_HASH"git cat-file -p "$INITIAL_COMMIT_HASH"Рішення до Завдання 4: Спрямувати HEAD на ваш новий commit
git update-ref HEAD "$INITIAL_COMMIT_HASH"git update-ref refs/heads/main "$INITIAL_COMMIT_HASH"
git log --onelinegit statusРішення до Завдання 5: Перевірити за допомогою високорівневих (porcelain) команд
git restore --source="$INITIAL_COMMIT_HASH" -- service.yamlcat service.yamlrm tree_input.txtНаступний модуль
Розділ «Наступний модуль»Готові вийти за межі внутрішніх механізмів? У Модулі 2: Мистецтво розгалуження ми дослідимо, як Git використовує свою структуру DAG для створення потужних стратегій розгалуження та злиття, розбираючи типові робочі процеси та складні техніки, як-от octopus merges.