Питання на співбесіді з Git
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
У великому монорепозиторії робочий каталог може містити сотні тисяч файлів, з яких розробнику потрібні кілька каталогів. Sparse-checkout лишає в робочому каталозі лише вказані шляхи - решта файлів є в історії, але не на диску.
git clone --filter=blob:none --sparse https://github.com/acme/monorepo.git
cd monorepo
git sparse-checkout set apps/billing packages/ui
git sparse-checkout add packages/auth
git sparse-checkout list
git sparse-checkout disable # повернути всі файли
Режим cone (за замовчуванням) - шаблони лише на рівні каталогів:
- до робочого каталогу потрапляють файли в корені репозиторію, вказані каталоги цілком і файли в їхніх батьківських каталогах (без вкладених підкаталогів);
- Git перевіряє шляхи значно швидше, ніж у режимі довільних шаблонів у стилі
.gitignore, тому для великих репозиторіїв cone рекомендований.
Що це дає:
git status,git checkout,git switchпрацюють з меншою кількістю файлів - значно швидше;- IDE індексує лише потрібний код;
- менше місця на диску.
Найкраще - разом з частковим клоном (--filter=blob:none): тоді вміст файлів поза sparse-набором навіть не завантажується з сервера.
Як інші операції поводяться з невидимими файлами:
- коміти, злиття й rebase працюють з усім деревом - Git знає про всі файли;
- конфлікт у файлі поза sparse-набором - Git тимчасово розміщує файл у робочому каталозі для розв'язання;
git grepіgit log -- pathможуть шукати й поза набором.
Типове застосування:
- монорепозиторій: розробник фронтенду бачить лише
apps/webі спільні пакети; - CI: збирання конкретного сервісу без виписування всього репозиторію - разом з
--depth 1і--filter; - документація в репозиторії з великим кодом.
Обмеження:
- інструменти, яким потрібні файли поза набором (наприклад, Composer з
path-репозиторіями на сусідні пакети, збирачі, що шукають конфіги в корені), ламаються - набір має містити всі залежності; - для невеликого проєкту накладні витрати не виправдані.
Обидва способи зменшують обсяг завантаження, але обрізають різні речі.
Поверхневий клон (--depth 1) обрізає історію: комітів до певної глибини просто немає.
Частковий клон (--filter) завантажує всю історію комітів, але не всі об'єкти - пропущені догружаються з сервера на вимогу:
git clone --filter=blob:none https://github.com/acme/app.git # blobless
git clone --filter=tree:0 https://github.com/acme/app.git # treeless
git clone --filter=blob:limit=1m https://github.com/acme/app.git # без файлів понад 1 МБ
| Поверхневий | Без blob-ів (blob:none) |
Без дерев (tree:0) |
|
|---|---|---|---|
| коміти | лише останні | усі | усі |
| дерева (каталоги) | лише останні | усі | на вимогу |
| вміст файлів | лише останні | лише для checkout, решта на вимогу | на вимогу |
git log |
обрізаний | повний | повний |
git log -p, blame |
обмежені | працюють, догружаючи файли | повільні, багато догрузок |
| для кого | CI, одноразові збирання | розробники | CI, якому потрібна історія комітів |
Чому blobless клон - добрий вибір для розробника великого репозиторію:
- історія повна:
log,merge-base,describe, перемикання гілок працюють як звичайно; - старі версії великих файлів не завантажуються, поки не знадобляться;
git blameчи перегляд старого коміту догружають потрібні об'єкти - помітна, але одноразова затримка.
Чому поверхневий клон - для CI: найменший обсяг і жодних мережевих запитів пізніше. Але все, що потребує історії, не працює.
Treeless клон економить найбільше серед часткових, але операції з історією файлів роблять багато запитів до сервера - для щоденної роботи зазвичай повільно.
Що потрібно:
- підтримка на сервері (GitHub, GitLab, Bitbucket підтримують);
- доступ до сервера при операціях, що потребують відсутніх об'єктів - офлайн частина команд не працюватиме;
- не поєднувати бездумно з
--depth- зазвичай обирають одне.
Разом зі sparse-checkout частковий клон дає найкращий ефект для монорепозиторію: не завантажуються ні старі версії, ні файли з чужих каталогів.
Докладніше в документації: GitHub Blog: partial clone і shallow clone
Кілька Laravel-застосунків використовують спільний код: модуль авторизації, клієнт API, набір Blade-компонентів. Є три основні способи.
1. Submodule - посилання на коміт іншого репозиторію:
git submodule add git@github.com:acme/shared.git packages/shared
- точна версія, окрема історія, окремі права доступу;
- незручно: додаткові кроки при клонуванні й оновленні, detached HEAD, легко забути оновити вказівник.
2. Subtree - код копіюється в репозиторій разом з історією:
git subtree add --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree pull --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree push --prefix=packages/shared git@github.com:acme/shared.git feature-x
- для інших учасників це просто звичайні файли - клон працює без додаткових кроків;
- зміни можна відправити назад в оригінальний репозиторій;
- команди довгі, легко змішати в одному коміті зміни спільного й основного коду;
- історія основного репозиторію росте.
3. Composer-пакет - стандартний спосіб для PHP:
{
"repositories": [
{ "type": "vcs", "url": "git@github.com:acme/shared.git" }
],
"require": { "acme/shared": "^2.1" }
}
- семантичні версії й діапазони, власні залежності пакета, автозавантаження, сервіс-провайдер Laravel;
- оновлення -
composer update acme/shared, аcomposer.lockфіксує точну версію; - приватні пакети - через Private Packagist, Satis чи
vcs-репозиторій з доступом.
Для локальної розробки пакета разом із застосунком - path-репозиторій:
{ "repositories": [{ "type": "path", "url": "../shared" }] }
Composer створює символьне посилання - зміни в пакеті видно одразу.
Порівняння:
| Submodule | Subtree | Composer | |
|---|---|---|---|
| версіонування | коміт | коміт | семантичні версії |
| залежності пакета | ні | ні | так |
| простота для команди | низька | висока | висока |
| зміни в обидва боки | так | так (subtree push) |
у репозиторії пакета |
Для PHP-коду Composer майже завжди кращий. Submodule і subtree доречні для того, що не є PHP-пакетом: документація, спільні конфіги, ресурси. А якщо спільний код змінюється разом із застосунками постійно, - можливо, вам потрібен монорепозиторій.
Монорепозиторій - кілька проєктів (застосунки, пакети, сервіси) в одному репозиторії. Polyrepo - кожен проєкт в окремому репозиторії.
Переваги монорепозиторію:
- атомарні зміни: змінити API пакета й усіх його споживачів одним комітом і одним pull request. У polyrepo це кілька узгоджених релізів у правильному порядку;
- немає пекла версій: усі проєкти завжди використовують поточну версію спільного коду;
- спільні інструменти: одна конфігурація Pint, PHPStan, CI, однакові правила;
- видимість: легко знайти всі використання функції, зробити рефакторинг по всьому коду;
- простіший онбординг: один клон - увесь контекст.
Проблеми монорепозиторію:
- розмір і продуктивність Git:
status,clone,checkoutповільнішають (лікується частковим клоном, sparse-checkout, fsmonitor); - CI: запускати все на кожну зміну - дорого. Потрібні інструменти, що визначають, які проєкти зачеплені зміною (Nx, Turborepo, Bazel, Pants чи власні скрипти по
git diff); - права доступу: Git не обмежує доступ до каталогів - бачать усі все (CODEOWNERS лише керує рев'ю);
- зв'язність: легко створити залежності, яких не мало б бути, - потрібні правила меж між модулями.
Переваги polyrepo:
- незалежні релізи, власний темп і власні інструменти кожної команди;
- чіткі межі й права доступу;
- маленькі швидкі репозиторії.
Проблеми polyrepo:
- зміни через кілька репозиторіїв - кілька pull request, порядок випусків, тимчасова несумісність;
- розбіжність версій: сервіси застрягають на старих версіях спільних пакетів;
- дублювання конфігурацій CI, лінтерів, шаблонів.
Гібрид, яким користується Laravel: розробка в монорепозиторії laravel/framework, а компоненти автоматично розділяються в окремі репозиторії лише для читання (illuminate/database, illuminate/support), щоб їх можна було встановити окремо.
Як обрати:
- кілька тісно пов'язаних проєктів однієї команди (застосунок, адмінка, спільні пакети) - монорепозиторій дає більше, ніж коштує;
- незалежні продукти різних команд з різним циклом випусків - polyrepo;
- ключове питання: як часто зміна в одному проєкті вимагає зміни в іншому. Часто - монорепозиторій, рідко - окремі репозиторії.
З часом у репозиторії накопичуються незапаковані об'єкти (кожен новий коміт створює окремі файли), недосяжні об'єкти (після rebase, reset, видалення гілок) і багато pack-файлів після кожного fetch. Це сповільнює операції й займає місце.
git gc (garbage collection):
- пакує окремі об'єкти в pack-файли з дельта-стисненням;
- видаляє недосяжні об'єкти, старші за термін (за замовчуванням 2 тижні, і лише ті, на які не посилається reflog - а reflog зберігає записи 90 днів, недосяжні - 30);
- упаковує посилання в
packed-refs, оновлює commit-graph.
git gc # звичайне прибирання
git gc --aggressive # повільне глибоке перепакування - рідко потрібне
git gc --prune=now # видалити недосяжні об'єкти негайно
git gc --auto Git запускає сам після деяких команд (commit, merge, fetch), коли незапакованих об'єктів понад 6700 чи pack-файлів понад 50. Тому вручну запускати gc зазвичай не потрібно.
Проблема автоматичного gc у великих репозиторіях: він запускається посеред роботи й може блокувати на хвилини.
git maintenance - сучасна заміна: обслуговування у фоні за розкладом:
git maintenance start
Реєструє репозиторій і задачі в системному планувальнику (launchd на macOS, systemd чи cron на Linux, Task Scheduler на Windows):
| Задача | Частота | Що робить |
|---|---|---|
prefetch |
щогодини | фоновий fetch у спеціальні ref-и - ваш git fetch потім майже миттєвий |
commit-graph |
щогодини | оновлює граф комітів для швидкого log і злиттів |
loose-objects |
щодня | пакує окремі об'єкти |
incremental-repack |
щодня | поступово об'єднує pack-файли без повного перепакування |
git maintenance run --task=gc # вручну конкретну задачу
git maintenance stop
Коли запускати вручну:
- після видалення великих файлів з історії (
git filter-repo) -git gc --prune=now, щоб звільнити місце; - після масового імпорту чи міграції репозиторію;
- для невеликого проєкту нічого робити не треба - автоматичного
gcдостатньо.
На сервері (GitHub, GitLab) обслуговування виконує хостинг.
Git читає конфігурацію з кількох файлів. Пізніший рівень перекриває раніший:
| Рівень | Файл | Прапорець |
|---|---|---|
| system | /etc/gitconfig (чи в каталозі встановлення) |
--system |
| global | ~/.gitconfig чи ~/.config/git/config |
--global |
| local | .git/config репозиторію |
--local (за замовчуванням) |
| worktree | .git/config.worktree |
--worktree |
| команда | git -c key=value ... |
- |
git config --global user.name "Olena Kovalenko"
git config user.email olena@company.com # лише цей репозиторій
git config --list --show-origin # усі значення і звідки вони
git config --show-origin --get user.email
--show-origin - перший інструмент, коли «налаштування не працює»: видно, який файл перекрив значення.
Умовні включення - різні налаштування для роботи й особистих проєктів:
# ~/.gitconfig
[user]
name = Olena Kovalenko
email = olena@personal.dev
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
# ~/.gitconfig-work
[user]
email = olena@company.com
signingkey = ~/.ssh/work_ed25519.pub
Усі репозиторії в ~/work/ автоматично отримують робочу пошту - більше ніяких комітів з особистою адресою в корпоративному репозиторії.
Умови includeIf:
gitdir:- шлях до репозиторію (gitdir/i:- без урахування регістру);onbranch:- поточна гілка;hasconfig:remote.*.url:- адреса віддаленого репозиторію, наприклад усі репозиторії зgithub.com/company/**.
Що важливо:
- слеш у кінці
gitdir:~/work/означає «цей каталог і все всередині»; .git/configне комітиться і не поширюється через клон - спільні налаштування команди передаються інакше (документація, скрипт налаштування,.gitattributesі.editorconfigу репозиторії);- у CI змінні оточення
GIT_AUTHOR_NAME,GIT_COMMITTER_EMAILперекривають конфігурацію.
Буває, що в одному файлі змішалися дві задачі: виправлення помилки й рефакторинг. git add -p (--patch) дозволяє додати в індекс лише окремі фрагменти (hunks) змін і зробити з них два чисті коміти.
git add -p app/Services/InvoiceService.php
Git показує кожен фрагмент і питає, що з ним робити:
@@ -42,7 +42,7 @@ public function total(): int
- return $this->lines->sum('price');
+ return $this->lines->sum(fn ($line) => $line->price * $line->quantity);
(1/3) Stage this hunk [y,n,q,a,d,s,e,?]?
Основні відповіді:
| Клавіша | Дія |
|---|---|
y / n |
додати / пропустити фрагмент |
s |
розбити фрагмент на менші (якщо між змінами є незмінені рядки) |
e |
відредагувати фрагмент вручну - для змін у сусідніх рядках |
a / d |
додати / пропустити всі решта фрагменти файлу |
q |
вийти |
Типовий процес:
git add -p # обрати фрагменти виправлення
git diff --staged # перевірити, що в індексі
git commit -m "Fix invoice total for multiple quantities"
git add -p # тепер рефакторинг
git commit -m "Extract line total calculation"
Той самий режим є в інших командах:
git restore -p- відкинути частину змін у робочому каталозі;git restore --staged -p- прибрати частину з індексу;git stash push -p- відкласти лише вибрані фрагменти;git commit -p- обрати фрагменти й одразу закомітити.
Про що пам'ятати:
- нові файли
add -pне показує, бо Git їх ще не відстежує. Спершуgit add -N файл(intent-to-add) - тоді вміст з'явиться як фрагменти; - проміжний стан може не працювати: закомічена половина змін не перевірялася окремо. Перед пушем корисно прогнати тести на кожному коміті чи хоча б на останньому;
- IDE (PhpStorm, VS Code) дають той самий механізм через галочки біля рядків - під капотом те саме часткове додавання в індекс.
git checkout історично робить дві принципово різні речі:
git checkout feature # перемкнути гілку
git checkout -- config.php # відкинути зміни у файлі (безповоротно!)
Одна команда і для безпечної операції, і для руйнівної. git checkout name перемкне гілку, якщо гілка name існує, а якщо ні, але є такий файл - мовчки перезапише файл. Саме через це в Git 2.23 з'явилися дві вузькі команди.
git switch - лише гілки:
git switch main
git switch -c feature/export # створити й перемкнутися
git switch -c fix origin/fix # нова локальна гілка від віддаленої
git switch - # повернутися на попередню гілку
git switch --detach v2.4.0 # явно перейти в detached HEAD
git restore - лише файли:
git restore config.php # відкинути зміни в робочому каталозі
git restore --staged config.php # прибрати з індексу, зміни лишаються
git restore --source=HEAD~3 config.php # взяти версію файлу з іншого коміту
git restore --staged --worktree . # повністю повернути все до HEAD
Відповідність старих і нових команд:
| Було | Стало |
|---|---|
git checkout branch |
git switch branch |
git checkout -b new |
git switch -c new |
git checkout -- file |
git restore file |
git reset HEAD file |
git restore --staged file |
git checkout abc123 -- file |
git restore --source=abc123 file |
Чому це важливо:
- намір явний:
switchніколи не чіпає незакомічені зміни у файлах без потреби, аrestoreніколи не перемикає гілку; - detached HEAD не трапляється випадково:
git switch v2.4.0відмовиться працювати з тегом без--detach, тоді якcheckoutмовчки перейде в detached HEAD; git statusсам підказує нові команди («use git restore...»).
git checkout нікуди не дівся і працює, як раніше, - у скриптах і старих інструкціях він зустрічатиметься ще довго. Але для щоденної роботи switch і restore безпечніші.
Обережно: git restore файл так само безповоротно відкидає незакомічені зміни, як і checkout --. Їх немає ні в reflog, ні в stash.
.gitignore діє лише на невідстежувані файли. Якщо файл уже закомічено, Git продовжує відстежувати його зміни, хоч би що було написано в .gitignore.
Типова ситуація: .env чи .idea/ випадково потрапили в репозиторій, потім їх додали в .gitignore - а git status все одно показує зміни.
Рішення - прибрати файл з індексу, не видаляючи з диска:
git rm --cached .env
git rm -r --cached .idea/
git commit -m "Stop tracking local environment files"
--cachedвидаляє файл лише з індексу - на диску він лишається;- після коміту файл стає невідстежуваним, і тепер
.gitignoreна нього діє.
Пастка для команди: у колег після git pull цей файл буде видалено з диска, бо для Git коміт означає «файл видалено». Їхній локальний .env зникне. Попередьте команду або попросіть зберегти копію перед оновленням.
Чим відрізняються варіанти:
| Команда | Індекс | Диск |
|---|---|---|
git rm file |
видаляє | видаляє |
git rm --cached file |
видаляє | залишає |
rm file |
залишає (до git add) |
видаляє |
Масово привести репозиторій у відповідність з .gitignore:
git rm -r --cached .
git add .
git commit -m "Apply .gitignore"
Перед комітом обов'язково перегляньте git status - так можна випадково прибрати потрібні файли.
Перейменування - git mv:
git mv app/Helpers.php app/Support/Helpers.php
Це те саме, що mv + git rm старого + git add нового. Git не зберігає факт перейменування - він визначає його при порівнянні за схожістю вмісту.
Якщо файл містив секрет - git rm --cached прибирає його лише з наступних комітів. В історії він лишається, і секрет треба вважати скомпрометованим: змінити ключі, а історію чистити окремими інструментами.
git clean видаляє з робочого каталогу файли, які Git не відстежує: тимчасові файли, результати збирання, згенерований код, залишки експериментів.
Головне - ці файли не можна відновити засобами Git. Їх немає в жодному коміті, reflog чи stash. Тому git clean за замовчуванням навіть відмовляється працювати без явного прапорця (clean.requireForce=true).
Безпечний порядок - спершу подивитися:
git clean -n # dry run: що буде видалено
git clean -nd # разом з невідстежуваними каталогами
git clean -f # видалити файли
git clean -fd # файли й каталоги
Прапорці:
| Прапорець | Що робить |
|---|---|
-n |
лише показати (dry run) |
-f |
справді видалити |
-d |
включно з каталогами |
-x |
також проігноровані файли (.env, vendor/, node_modules/) |
-X |
лише проігноровані файли |
-i |
інтерактивний вибір |
-e <шаблон> |
додатково виключити шаблон |
-x - найнебезпечніший прапорець. git clean -fdx у Laravel-проєкті видалить .env, vendor/, node_modules/, storage/*.key, локальну базу SQLite - усе, що ігнорується. Корисно для «чистого» збирання в CI, але вдома варто запускати лише після -n.
git clean -fdx -e .env -n # усе, окрім .env, - спершу перевірка
Інтерактивний режим зручний, коли більшість файлів треба видалити, а кілька - залишити:
git clean -id
Типові сценарії:
- повернути репозиторій до стану щойно клонованого:
git restore --staged --worktree .(відстежувані) +git clean -fd(невідстежувані); - прибрати лише результати збирання, не чіпаючи нові вихідні файли:
git clean -fX; - CI на повторно використовуваному runner-і -
git clean -ffdxперед збиранням (подвійний-fвидаляє й вкладені репозиторії).
Альтернатива без ризику: якщо не впевнені, git stash -u прибере невідстежувані файли, але збереже їх у stash.
Команди Git поділяються на два рівні:
- porcelain («порцеляна») - команди для людей:
commit,switch,merge,log. Їхній вивід і поведінка можуть змінюватися між версіями; - plumbing («сантехніка») - низькорівневі команди, з яких побудовано porcelain: читання й запис об'єктів, оновлення ref-ів. Їхній вивід стабільний - на них розраховані скрипти.
Коміт вручну - те, що робить git commit під капотом:
# 1. записати вміст файлу як blob
echo 'Hello' | git hash-object -w --stdin
# e965047ad7c57865823c7d992b1d046ea66edf78
# 2. додати blob в індекс під ім'ям
git update-index --add --cacheinfo 100644,e965047ad7c57865823c7d992b1d046ea66edf78,hello.txt
# 3. записати індекс як tree
git write-tree
# 7b3ce6f...
# 4. створити коміт з цим деревом і батьком
echo 'Add hello' | git commit-tree 7b3ce6f -p HEAD
# 4a1f2c9...
# 5. пересунути гілку на новий коміт
git update-ref refs/heads/main 4a1f2c9
Робочий каталог при цьому взагалі не використано: файл hello.txt на диску не з'являвся.
Основні plumbing-команди:
| Команда | Що робить |
|---|---|
hash-object |
обчислити хеш, з -w - записати blob |
cat-file |
прочитати об'єкт |
ls-tree, ls-files |
вміст дерева й індексу |
write-tree, commit-tree |
створити tree й коміт |
update-ref, symbolic-ref |
змінити ref безпечно, з записом у reflog |
rev-parse |
перетворити ім'я (гілка, HEAD~2) на хеш |
for-each-ref |
перелік ref-ів у заданому форматі |
merge-tree |
злиття без робочого каталогу |
Де це корисно на практиці:
- скрипти й CI:
git rev-parse --abbrev-ref HEAD,git for-each-ref --format='%(refname:short)' refs/heads/замість розбору виводуgit branch, який може змінитися; - операції без робочого каталогу: сервери Git (GitHub, GitLab) роблять злиття й коміти через
merge-treeіcommit-tree, не створюючи файлів на диску; - розуміння: знаючи plumbing, легко пояснити, що роблять
rebase,stash,cherry-pick, - вони лише комбінують ці кроки.
Порада для скриптів: використовуйте porcelain-команди з --porcelain-виводом (git status --porcelain=v2) або plumbing - і ніколи не розбирайте вивід, призначений для людей.
git cat-file - інструмент для читання будь-якого об'єкта з бази:
git cat-file -t a1b2c3d # тип: blob, tree, commit, tag
git cat-file -s a1b2c3d # розмір у байтах
git cat-file -p a1b2c3d # вміст у зручному вигляді
git cat-file -e a1b2c3d # лише перевірити існування (код виходу)
Подорож від коміту до файлу:
git cat-file -p HEAD
# tree 3c4e9cd...
# parent 84ba035...
# ...
git cat-file -p 3c4e9cd # кореневе дерево
# 100644 blob 1f7a7a4... composer.json
# 040000 tree 8e2b7f1... app
git cat-file -p 8e2b7f1 # дерево каталогу app/
git cat-file -p HEAD:composer.json # одразу вміст файлу в коміті
Blob - лише вміст файлу. У ньому немає імені, прав чи дати: файли з однаковим вмістом у різних місцях і різних комітах - один blob. Перейменування файлу не створює нового blob-а, лише змінює запис у tree.
Хеш blob-а - SHA-1 від заголовка й вмісту: blob <розмір>\0<вміст>. Тому його можна обчислити й без Git:
printf 'Hello\n' | git hash-object --stdin
printf 'blob 6\0Hello\n' | shasum
# обидві команди дають e965047ad7c57865823c7d992b1d046ea66edf78
Tree - відсортований список записів режим тип хеш ім'я. Він відповідає одному каталогу й посилається на blob-и (файли) і інші tree (підкаталоги).
Що випливає зі структури:
- зміна одного файлу в глибокому каталозі створює новий blob, нові tree для кожного каталогу на шляху до кореня й новий коміт. Усі інші tree й blob-и перевикористовуються - коміт великого проєкту з однією зміною займає кілька сотень байтів;
- порівняння двох комітів дешеве: однакові хеші піддерев означають однакові каталоги, і Git не заходить у них;
- ім'я і вміст розділені, тож Git дізнається про перейменування лише порівнянням.
Масова робота: git cat-file --batch і --batch-check читають багато об'єктів за один процес - так працюють інструменти аналізу репозиторіїв (git cat-file --batch-all-objects --batch-check - перелік усіх об'єктів з розмірами, щоб знайти найбільші).
.git/index - бінарний файл, що описує знімок для наступного коміту. Для кожного файлу він зберігає:
- шлях, режим і хеш blob-а вмісту;
- дані
statз файлової системи: розмір, час зміни (mtime, ctime), inode, пристрій, uid/gid; - номер стадії (stage) - для конфліктів злиття.
git ls-files --stage
# 100644 1f7a7a4... 0 composer.json
# 100644 e9c3d2b... 0 app/Models/User.php
git ls-files --debug app/Models/User.php # разом з даними stat
Як git status працює швидко. Обчислювати хеш кожного файлу в проєкті з десятками тисяч файлів було б повільно. Тому Git спершу порівнює дані stat:
- якщо розмір і час зміни файлу збігаються з тими, що в індексі, - файл вважається незмінним без читання вмісту;
- лише для файлів з іншими
statGit читає вміст, хешує й порівнює з blob-ом в індексі; - якщо вміст той самий (наприклад, файл перезбережено без змін), Git оновлює дані
statв індексі, щоб наступного разу не перевіряти.
«Racy git» - файл змінено в ту саму секунду, що й записано індекс: час збігається, хоча вміст інший. Git розпізнає цей випадок і перевіряє такі файли за вмістом.
Стадії при конфлікті: для конфліктного файлу індекс містить до трьох записів - стадія 1 (спільний предок), 2 (наша версія), 3 (їхня). git add після розв'язання замінює їх одним записом стадії 0.
git ls-files -u # конфліктні записи
git show :2:config/app.php # наша версія
git show :3:config/app.php # їхня версія
Прапорці в індексі:
git update-index --assume-unchanged file- обіцянка Git не перевіряти файл (оптимізація для повільних файлових систем);git update-index --skip-worktree file- ігнорувати локальні зміни відстежуваного файлу (локальна конфігурація). Обидва - локальні й не передаються іншим;git ls-files -vпоказує такі файли малими літерами.
Для великих репозиторіїв: core.untrackedCache, core.fsmonitor і index.version=4 (стиснення шляхів) зменшують час status з секунд до мілісекунд.
Пошкоджений індекс («index file corrupt») не означає втрату історії: rm .git/index && git reset відтворить його з HEAD, зберігши файли робочого каталогу.
Refspec - правило відповідності між ref-ами віддаленого репозиторію і вашого локального. Його формат:
+<джерело>:<призначення>
- джерело - ref на тому боці, звідки беремо;
- призначення - куди записати локально;
+- дозволити оновлення без fast-forward (наприклад, після force push на сервері).
Refspec за замовчуванням записується при git clone чи git remote add:
# .git/config
[remote "origin"]
url = git@github.com:acme/shop.git
fetch = +refs/heads/*:refs/remotes/origin/*
Читається так: «усі гілки сервера (refs/heads/*) записати як refs/remotes/origin/*». Звідси й з'являються origin/main, origin/feature-x - це ваші локальні копії стану гілок сервера на момент останнього fetch.
Що можна налаштувати:
# завантажувати лише main і релізні гілки
fetch = +refs/heads/main:refs/remotes/origin/main
fetch = +refs/heads/release/*:refs/remotes/origin/release/*
# додатково отримувати pull request-и GitHub
fetch = +refs/pull/*/head:refs/remotes/origin/pr/*
Після цього git fetch створить origin/pr/142 для кожного PR, і будь-який з них можна переглянути локально.
Refspec у командах:
git fetch origin main # лише main, результат у FETCH_HEAD і origin/main
git fetch origin pull/142/head:pr-142 # PR одразу в локальну гілку pr-142
git push origin feature:review/feature # запушити локальну feature під іншим ім'ям
git push origin :old-branch # порожнє джерело - видалити гілку на сервері
git push origin HEAD:refs/for/main # Gerrit: відправити на рецензію
Push теж використовує refspec: git push origin main - скорочення для main:main. Налаштування push.default визначає, що відправляє git push без аргументів (simple - поточну гілку в однойменну upstream).
Корисні наслідки:
origin/mainоновлюється лише приfetch/pull, а не сам по собі - тому «origin/main відстає від GitHub» лікуєтьсяgit fetch;fetch --prune(чиfetch.prune=true) видаляєorigin/*для гілок, видалених на сервері;- у CI з великим репозиторієм вузький refspec (лише потрібна гілка) суттєво зменшує обсяг завантаження.
Ref - ім'я, що вказує на об'єкт (зазвичай коміт). Класичне зберігання - один файл на ref:
.git/refs/heads/main → 23933defbcb6...
.git/refs/remotes/origin/main → 23933defbcb6...
.git/refs/tags/v2.4.0 → 9fceb02d0ae5...
Просто й наочно, але з тисячами гілок і тегів (типово для великих проєктів і серверів) з'являються проблеми: тисячі дрібних файлів, повільний перелік, а на файлових системах без урахування регістру (macOS, Windows) гілки Feature і feature конфліктують.
packed-refs - багато ref-ів одним текстовим файлом:
git pack-refs --all
cat .git/packed-refs
# # pack-refs with: peeled fully-peeled sorted
# 84ba0356e619... refs/heads/f
# 23933defbcb6... refs/heads/main
# 9fceb02d0ae5... refs/tags/v2.4.0
# ^4a1f2c9b7e3d... ← «peeled»: коміт, на який вказує анотований тег
Як Git шукає ref: спершу окремий файл у refs/, потім packed-refs. Окремий файл має пріоритет - тому оновлення гілки просто створює новий файл, не переписуючи packed-refs. git gc періодично пакує ref-и автоматично.
Наслідок для скриптів: cat .git/refs/heads/main може не знайти файл - гілка є, але упакована. Правильно - через команди:
git rev-parse refs/heads/main
git for-each-ref --format='%(refname) %(objectname)' refs/tags/
git update-ref refs/heads/main <хеш> # атомарно, із записом у reflog
Атомарність: оновлення ref-а йде через lock-файл (main.lock) і перейменування. Звідси повідомлення Unable to create '.git/refs/heads/main.lock': File exists - залишок після аварійно завершеного процесу Git; якщо інших процесів Git немає, файл можна видалити.
Reftable - новий бінарний формат зберігання ref-ів (з Git 2.45), створений у JGit для серверів з мільйонами ref-ів:
- атомарні транзакції для багатьох ref-ів одночасно;
- швидкий пошук і перелік, без проблем з регістром імен;
- reflog у тому самому сховищі.
git init --ref-format=reftable
git refs migrate --ref-format=reftable # перетворити наявний репозиторій
Для звичайного проєкту різниця непомітна, але у великих монорепозиторіях і на серверах reftable прибирає вузьке місце з тисячами ref-ів.
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії