Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Внутрішня будова Git»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

Каталог .git - це і є репозиторій. Робочий каталог лише показує одну з версій. Скопіюйте .git - і отримаєте всю історію; видаліть - і залишаться тільки поточні файли без історії.

.git/
├── HEAD            ← на що зараз вказує HEAD (зазвичай ref: refs/heads/main)
├── config          ← налаштування цього репозиторію, remotes, гілки
├── index           ← індекс (staging area), бінарний файл
├── objects/        ← база об'єктів: blob, tree, commit, tag
│   ├── 1a/2485...  ← окремі стиснені об'єкти (loose objects)
│   └── pack/       ← упаковані об'єкти (packfiles)
├── refs/
│   ├── heads/      ← локальні гілки
│   ├── remotes/    ← віддалені гілки (origin/main)
│   └── tags/       ← теги
├── packed-refs     ← багато ref-ів одним файлом
├── logs/           ← reflog: історія переміщень HEAD і гілок
├── hooks/          ← скрипти хуків (*.sample - приклади)
└── info/exclude    ← персональні ігноровані шаблони

Найважливіше:

  • objects/ - увесь вміст: кожна версія кожного файлу, кожен каталог, кожен коміт. Імена файлів - хеші вмісту, перші два символи хешу - назва підкаталогу;
  • refs/ і packed-refs - імена: гілки й теги - це лише файли з хешем коміту;
  • HEAD - де ви зараз;
  • index - що піде в наступний коміт;
  • logs/ - страховка: звідси git reflog відновлює «втрачені» коміти.

Що можна побачити самостійно:

cat .git/HEAD
cat .git/refs/heads/main          # хеш останнього коміту (якщо не упаковано)
find .git/objects -type f | head
git count-objects -vH             # скільки об'єктів і скільки вони займають

Чого не робити:

  • не редагувати файли в .git вручну без розуміння - для цього є команди (git update-ref, git config);
  • не комітити .git іншого репозиторію в свій (вкладені репозиторії - через підмодулі);
  • не синхронізувати .git через Dropbox чи iCloud - одночасний запис з двох машин псує репозиторій.

git worktree і підмодулі використовують файл .git замість каталогу - у ньому лише шлях до справжнього каталогу репозиторію.

Докладніше в документації: Pro Git: plumbing і porcelain

Перемикання гілки (git switch чи git checkout) - це три кроки:

  1. порівняти дерева: Git бере tree поточного коміту й tree цільового коміту і визначає, які файли відрізняються;
  2. оновити індекс і робочий каталог: змінені файли переписуються, відсутні в цільовій гілці - видаляються, нові - створюються. Однакові файли не чіпаються взагалі - саме тому перемикання між схожими гілками миттєве навіть у великому проєкті;
  3. оновити HEAD: записати ref: refs/heads/<гілка>.

Незакомічені зміни переносяться разом з вами. Якщо ви змінили файл, а в цільовій гілці цей файл такий самий, як у поточній, - Git просто залишить вашу зміну. Тому «я почав роботу не в тій гілці» виправляється простим git switch -c правильна-гілка - зміни перейдуть.

Коли Git відмовляється:

error: Your local changes to the following files would be overwritten by checkout:
	config/services.php
Please commit your changes or stash them before you switch branches.
Aborting

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

Те саме для невідстежуваних файлів:

error: The following untracked working tree files would be overwritten by checkout:
	app/Actions/Export.php

У цільовій гілці є файл з таким шляхом, а у вас лежить невідстежуваний файл з тим самим іменем.

Варіанти:

  • закомітити зміни (можна тимчасовий коміт в поточній гілці);
  • git stash (з -u для невідстежуваних), перемкнутися, потім git stash pop там, де потрібно;
  • git switch -m гілка - спробувати злити ваші зміни з версією цільової гілки (можуть виникнути конфлікти);
  • відкинути зміни - git restore, якщо вони не потрібні.

Чого не варто робити - git checkout -f / git switch --discard-changes без розуміння: це перемикання з безповоротним знищенням незакомічених змін.

Невідстежувані й проігноровані файли (vendor/, .env) переживають перемикання - Git їх не чіпає. Тому після переходу на стару гілку vendor/ може не відповідати її composer.lock: потрібен composer install.

Докладніше в документації: git checkout

Git відстежує вміст файлів, а не каталоги. Каталог існує в репозиторії лише як tree-об'єкт, і tree створюється тільки тоді, коли в ньому є хоча б один файл. Порожній каталог нічого не додає до дерева - тому git add empty/ нічого не робить, а після клонування його немає.

Як зберегти порожній каталог - покласти в нього файл:

touch storage/logs/.gitkeep

.gitkeep - лише угода, а не особливий файл для Git. Laravel використовує інший прийом - .gitignore всередині каталогу:

# storage/logs/.gitignore
*
!.gitignore

Каталог існує в репозиторії (завдяки цьому файлу), а його вміст ігнорується.

Права файлів - лише кілька режимів. Tree зберігає для кожного запису режим, ім'я і хеш:

git ls-tree HEAD
# 100644 blob 7898192...   a.txt       ← звичайний файл
# 100755 blob 1a24852...   run.sh      ← виконуваний файл
# 120000 blob 8d14cbf...   link        ← символьне посилання
# 040000 tree 3c4e9cd...   app         ← каталог
# 160000 commit 9f2b1e0... vendor/pkg  ← підмодуль (gitlink)
Режим Значення
100644 звичайний файл
100755 виконуваний файл
120000 symlink - blob містить шлях, на який він вказує
040000 каталог (tree)
160000 підмодуль - посилання на коміт іншого репозиторію

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

Типова проблема - «змінений» файл без змін у вмісті:

old mode 100644
new mode 100755

Хтось виконав chmod +x, або файлова система (Windows, змонтований диск) не підтримує біт виконання. Якщо зміни прав не мають значення для проєкту:

git config core.fileMode false

А щоб справді зробити скрипт виконуваним у репозиторії на системі без підтримки прав - git update-index --chmod=+x deploy.sh.

Докладніше в документації: git ls-tree

Хеш коміту обчислюється з усього вмісту коміту:

git cat-file -p HEAD
# tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614
# parent 84ba0356e6199a1cb8a555fef6d09aebe1c203f1
# author Olena <olena@example.com> 1759500000 +0300
# committer Olena <olena@example.com> 1759500000 +0300
#
# Add invoice export

У нього входять хеш дерева (знімок усіх файлів) і хеш батьківського коміту. А хеш батька, у свою чергу, залежить від його батька - і так до першого коміту.

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

  • один хеш гарантує всю історію: якщо у вас і в колеги main вказує на той самий хеш, у вас ідентичні всі файли й уся історія до цього моменту;
  • непомітно підмінити старий коміт чи файл неможливо - зміниться хеш, і це видно;
  • переписування історії (rebase, commit --amend) завжди створює нові коміти з новими хешами - звідси конфлікти з колегами, які вже мають старі.

Чому коміти з однаковим вмістом мають різні хеші: у коміті є час і автор. Два однакові git commit з різницею в секунду - різні коміти. Тому cherry-pick одного коміту у дві гілки дає два різні хеші.

Перевірка цілісності - git fsck:

git fsck
git fsck --full --strict

Перевіряє, що кожен об'єкт відповідає своєму хешу, всі посилання (з коміту на tree, з tree на blob-и) ведуть на існуючі об'єкти, і повідомляє про пошкоджені, відсутні й недосяжні об'єкти.

Де це корисно:

  • після збою диска чи синхронізації .git через хмарні сервіси;
  • fetch і clone перевіряють об'єкти автоматично (transfer.fsckObjects вмикає повнішу перевірку), тож пошкоджені дані з сервера не потраплять у ваш репозиторій непомітно.

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

Докладніше в документації: git fsck

Кожен коміт зберігає посилання на батьківські коміти. Разом вони утворюють спрямований ациклічний граф (DAG):

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

Скільки батьків може мати коміт:

Батьків Що це
0 кореневий коміт - перший у репозиторії
1 звичайний коміт
2 merge-коміт - результат злиття двох гілок
3+ octopus merge - злиття кількох гілок одночасно (рідко)
git cat-file -p HEAD   # merge-коміт
# tree ...
# parent 99fe6a1...     ← перший батько: гілка, в яку зливали (main)
# parent 5b8c2d0...     ← другий батько: гілка, яку зливали (feature)

Порядок батьків має значення: перший батько - це «основна лінія». git log --first-parent показує історію main лише з merge-комітів, без внутрішніх комітів кожної гілки - зручно бачити, які фічі й коли потрапили в основну гілку.

Подивитися граф:

git log --graph --oneline --all
# *   23933de Merge branch 'feature'
# |\
# | * 5b8c2d0 Add export
# * | 99fe6a1 Fix typo
# |/
# * 84ba035 Init

Що випливає з графової моделі:

  • гілка - лише вказівник на одну вершину графа; «вміст гілки» - усі коміти, досяжні з неї по батьках;
  • спільний предок (merge base) двох гілок - найближча вершина, досяжна з обох: від неї Git рахує зміни при злитті (git merge-base main feature);
  • «чи злито гілку» = чи досяжний її коміт з main (git branch --merged);
  • merge-коміт не містить «різниці» - як і будь-який коміт, він зберігає повний знімок, а зміни Git показує, порівнюючи з батьками.

Докладніше в документації: Pro Git: розгалуження й злиття