Питання на співбесіді з Git
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
Головна мета рев'ю - покращити здоров'я кодової бази, а не знайти ідеальне рішення. Корисно йти від загального до деталей: немає сенсу обговорювати назву змінної в коді, який узагалі не варто вливати.
Порядок перегляду:
- Опис і мета - чи зрозуміло, яку проблему розв'язує PR, і чи потрібна ця зміна взагалі;
- Дизайн - чи правильне місце для коду, чи вписується в архітектуру, чи не надто складно;
- Тести - чи покривають нову поведінку й крайні випадки, чи падали б вони без зміни;
- Логіка й коректність - граничні умови, null, порожні колекції, конкурентний доступ, обробка помилок;
- Безпека й продуктивність - авторизація, валідація вводу, N+1, запити в циклі, великі вибірки без пагінації;
- Читабельність - назви, зрозумілість, коментарі там, де «чому» неочевидне;
- Стиль - лише те, що не перевіряє автоматика.
На що звертати особливу увагу в Laravel:
- міграції - чи безпечні на великій таблиці, чи є відкат, чи сумісні зі старою версією коду під час деплою;
- масове присвоєння і
$fillable, перевірка прав (Gate, політики) у контролерах і Livewire-діях; - черги - ідемпотентність задач, що буде при повторі;
- запити -
with()для зв'язків, індекси під новіwhere.
Що віддати автоматиці:
- форматування - Pint;
- типові помилки й типи - PHPStan/Larastan;
- тести - CI.
Людина не повинна писати «тут пробіл» - на це є інструменти, а увага рецензента дорожча.
Чого не робити:
- вимагати переписати під свій смак, якщо рішення автора теж нормальне;
- рев'ювити диф без контексту - іноді треба відкрити весь файл чи запустити гілку локально (
gh pr checkout 412); - затягувати: швидкий відгук важливіший за ідеальний.
Докладніше в документації: Google: що шукати під час code review
Текстовий коментар не передає інтонації: те, що рецензент вважав дрібною порадою, автор може прочитати як різку вимогу. Тому важливо явно позначати вагу коментаря і писати про код, а не про людину.
Conventional Comments - домовленість про формат коментарів: мітка на початку показує, чого саме чекає рецензент.
issue: тут N+1 - для кожної вакансії окремий запит до company.
Додай ->with('company') у запит вище.
suggestion (non-blocking): можна винести умову в scope `published()`,
вона повторюється в трьох місцях.
nitpick: `$data` -> `$vacancyAttributes`, так зрозуміліше.
question: чи може тут прийти порожній масив? Якщо так - впаде на array_key_first.
praise: дуже зручно, що експорт іде потоково, - пам'ять не росте.
Мітки й значення:
| Мітка | Що означає |
|---|---|
issue |
проблема, яку треба виправити |
suggestion |
пропозиція покращення |
question |
рецензент не впевнений, хоче зрозуміти |
nitpick |
дрібниця на смак, не блокує |
praise |
щось зроблено добре |
(blocking) / (non-blocking) |
чи блокує злиття |
Принципи доброго коментаря:
- пояснювати чому: не «зроби інакше», а «так буде N+1, бо...»;
- питати, а не стверджувати, коли не впевнені: можливо, автор знає те, чого не знаєте ви;
- «ми» і «код», а не «ти»: «тут можна спростити», а не «ти ускладнив»;
- пропонувати рішення чи напрямок, а не лише вказувати на проблему;
- хвалити вдалі рішення - це теж інформація: що варто повторювати;
- не дублювати: однакова проблема в десяти місцях - один коментар «і так само нижче».
Автору: відповідати на кожен коментар (виправлено, не згоден - ось чому, винесу в окрему задачу) і не сприймати рев'ю особисто - рецензують код, а не людину.
Кнопка злиття pull request на GitHub має три варіанти, і вони по-різному формують історію main.
Create a merge commit (git merge --no-ff):
* Merge pull request #412 from acme/csv-export
|\
| * add tests
| * fix typo
| * export service
|/
* previous commit
- зберігаються всі коміти гілки й сам факт злиття;
- легко відкотити весь PR одним
git revert -m 1 <merge>; - історія з «fix typo» і «wip» потрапляє в
main.
Squash and merge:
* Експорт вакансій у CSV (#412)
* previous commit
- усі коміти PR стискаються в один новий коміт;
- лінійна чиста історія: один PR - один коміт, легко шукати й відкочувати;
- проміжні коміти автора губляться (лишаються на сторінці PR);
- автор після злиття не може просто продовжити роботу в тій самій гілці - коміт у
mainмає інший SHA, і наступний PR покаже старі зміни ще раз.
Rebase and merge:
- кожен коміт PR переноситься на
mainокремо, без коміту злиття - лінійна історія з усіма комітами; - GitHub завжди створює нові SHA й оновлює дані комітера, навіть якщо можна було б просто перемотати гілку, і відкидає порожні коміти;
- доречно, коли автор ретельно впорядкував коміти й кожен має сенс сам по собі.
Як обрати:
| Ситуація | Підходить |
|---|---|
| коміти в PR «брудні», потрібна чиста історія | Squash |
| коміти атомарні й осмислені | Rebase |
| важливо бачити межі PR у графі | Merge commit |
Політика команди: у налаштуваннях репозиторію можна залишити лише один дозволений спосіб, щоб історія була однорідною. Для squash варто налаштувати, що заголовок коміту береться із заголовка PR, - тоді якість історії залежить від якості заголовків PR.
CODEOWNERS - файл, що визначає людей чи команди, відповідальні за частини репозиторію. Коли PR змінює їхні файли, GitHub автоматично запитує в них рев'ю.
Де лежить: .github/CODEOWNERS, корінь репозиторію чи docs/ - GitHub шукає в такому порядку і бере перший знайдений. Файл діє для тієї гілки, в якій лежить.
# За замовчуванням - уся команда бекенду
* @acme/backend
# Міграції - обов'язково команда даних
/database/migrations/ @acme/data
# Платежі - конкретні люди
/app/Billing/ @olena @acme/payments
# Фронтенд
*.vue @acme/frontend
/resources/js/ @acme/frontend
# Захистити сам файл і конфіги CI
/.github/ @acme/leads
Синтаксис: шаблони як у .gitignore, останній збіг має пріоритет. Тому загальне правило * іде першим, а конкретніші - нижче. Якщо для файла останнє правило не містить власників, у нього їх немає.
Вимоги: власники мають мати права на запис у репозиторій; команда має бути видимою й теж мати права на запис.
Справжня сила - разом із захистом гілки: правило Require review from Code Owners (у branch protection чи ruleset) не дає злити PR, доки його не схвалить власник змінених файлів. Без цього CODEOWNERS - лише автоматичне запрошення.
Практичні поради:
- захистити
.github/: інакше будь-хто може в PR змінити CODEOWNERS чи workflow CI і обійти правила; - команди, а не люди, де можливо: людина йде у відпустку - рев'ю блокується;
- не робити власником усього одну людину - вона стане вузьким місцем;
- GitHub показує помилки синтаксису CODEOWNERS прямо в інтерфейсі файла - варто перевіряти після змін.
Перший рецензент PR - його автор. Перегляд власного дифу на GitHub (а не в редакторі) за кілька хвилин знаходить те, на що інакше пішов би раунд рев'ю.
Чеклист перед запитом рев'ю:
- переглянути весь диф у вкладці Files changed - так, як побачить рецензент;
- прибрати
dd(),dump(),console.log, закоментований код, випадкові файли; - CI зелений, тести на нову поведінку є;
- опис оновлено: що, навіщо, як перевірити;
- залишити власні коментарі до неочевидних місць: «тут свідомо без транзакції, бо...» - це зменшує кількість запитань;
- PR не містить сторонніх змін, що потрапили «заодно».
Як оновлювати PR після коментарів:
- нові коміти, а не переписування історії під час рев'ю: рецензент бачить лише те, що змінилося з останнього перегляду. Після
force pushGitHub губить зв'язок частини коментарів з кодом, і доводиться переглядати все заново; - fixup-коміти зручні, якщо історію треба буде впорядкувати перед злиттям:
git commit --fixup=a1b2c3d
# перед злиттям, коли рев'ю завершене:
git rebase -i --autosquash main
А якщо репозиторій вливає через squash - впорядковувати взагалі не потрібно;
- відповідати на кожен коментар: «виправлено в 4f2e1a0», «не згоден, бо...», «винесу в #430»;
- не закривати чужі обговорення без відповіді - рецензент має бачити, що його почули;
- повторно запросити рев'ю (Re-request review), коли все виправлено, - інакше рецензент не дізнається, що черга знову за ним.
Якщо коментарів дуже багато - часто це сигнал, що варто поговорити голосом чи переглянути підхід, а не виправляти по одному.
Для повідомлень комітів є два хуки:
prepare-commit-msg- викликається до відкриття редактора; може підготувати чи доповнити текст;commit-msg- викликається після введення повідомлення; може перевірити його і заборонити коміт.
Обидва отримують шлях до файла з повідомленням першим аргументом.
Перевірка Conventional Commits у commit-msg:
#!/bin/sh
pattern='^(feat|fix|docs|refactor|test|chore|perf|ci|build)(\([a-z0-9-]+\))?!?: .{1,72}$'
if ! head -1 "$1" | grep -qE "$pattern"; then
echo "Повідомлення не відповідає Conventional Commits:"
echo " feat(vacancies): add CSV export"
exit 1
fi
Номер задачі з назви гілки в prepare-commit-msg:
#!/bin/sh
# гілка feature/ABC-123-csv-export -> префікс [ABC-123]
case "$2" in merge|squash|commit) exit 0 ;; esac # не чіпати merge, squash і --amend
ticket=$(git branch --show-current | grep -oE '[A-Z]+-[0-9]+')
[ -n "$ticket" ] && ! grep -q "$ticket" "$1" && sed -i.bak "1s/^/[$ticket] /" "$1" && rm -f "$1.bak"
Другий аргумент - джерело повідомлення (message при -m, merge, squash, commit при --amend чи -c), щоб не дописувати префікс у службові повідомлення.
Готові інструменти:
- commitlint (Node) - правила для Conventional Commits, часто разом з Husky;
- CaptainHook має вбудовані дії для перевірки повідомлень (довжина рядків, регулярні вирази, загальноприйняті правила оформлення повідомлень);
- GrumPHP - задача
git_commit_message.
Чому перевірка потрібна і в CI: хук локальний, і git commit --no-verify його пропускає. Якщо від формату залежать змінлог і версіонування, перевіряйте повідомлення ще й у CI - або заголовок PR, якщо зливаєте через squash: тоді саме він стає повідомленням коміту в main.
Тести, що працюють з базою, найкраще ганяти на тій самій СУБД, що й на продакшені: SQLite пропускає помилки, специфічні для PostgreSQL чи MySQL (суворіший GROUP BY, типи, регістр у LIKE).
Service containers - Docker-контейнери, які GitHub Actions піднімає поруч із job:
jobs:
tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_DB: testing
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:8-alpine
ports: ["6379:6379"]
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: app
DB_PASSWORD: secret
REDIS_HOST: 127.0.0.1
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
extensions: pdo_pgsql, redis
- id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- run: composer install --no-interaction --prefer-dist
- run: php artisan test --parallel
Ключові моменти:
- healthcheck у
options- GitHub чекає, доки база стане здоровою, перш ніж запускати кроки. Без нього тести можуть стартувати раніше, ніж PostgreSQL готовий; - адреса: job працює прямо на runner-і, тому сервіси доступні як
127.0.0.1через опубліковані порти. Якщо job сам виконується в контейнері (container:), сервіси доступні за назвою (postgres), і порти публікувати не треба; - кеш Composer за хешем
composer.lock- залежності не завантажуються щоразу; кешується каталог завантажень Composer, а неvendor/; - змінні
envперекривають.env- конфігурація тестів без окремого файла.
Прискорення: --parallel для тестів, окремі jobs для стилю (Pint) і статичного аналізу (PHPStan), щоб вони йшли паралельно з тестами, і concurrency з cancel-in-progress: true - скасовувати застарілі запуски при новому push у ту саму гілку.
Докладніше в документації: GitHub Actions: service containers
actions/checkout за замовчуванням завантажує лише один коміт - той, що запустив workflow (fetch-depth: 1). Для збирання й тестів цього досить, і це швидко. Але все, що потребує історії, ламається.
Типові симптоми:
fatal: No names found, cannot describe anything. # git describe без тегів
fatal: ambiguous argument 'origin/main...HEAD' # немає гілки main
fatal: bad object ... # немає потрібного коміту
git describeчи визначення версії з тегів - тегів немає;git diff origin/main...HEAD,pint --diff=main, «запускати тести лише для змінених файлів» - немаєmainі спільного предка;- генерація змінлогу з повідомлень комітів - немає комітів;
git logдля дати останньої зміни файла - показує один коміт;- інструменти на кшталт SonarQube скаржаться на неповну історію для blame.
Варіанти виправлення:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # уся історія, усі гілки й теги
0 - найпростіше, але у великому репозиторії checkout стає помітно повільнішим.
Точніше - догрузити лише потрібне:
- uses: actions/checkout@v7
- run: git fetch --no-tags --depth=50 origin main
- run: vendor/bin/pint --test --diff=origin/main
Або fetch-tags: true з невеликою глибиною, якщо потрібні лише теги.
Нюанс pull request-ів: для події pull_request checkout за замовчуванням бере тимчасовий коміт злиття PR з базовою гілкою (refs/pull/N/merge), а не останній коміт гілки PR. Тести перевіряють результат злиття, що добре, але git log покаже коміт злиття, якого немає в гілці. Якщо потрібна саме голова гілки - ref: ${{ github.event.pull_request.head.sha }}.
Правило: глибина історії в CI - свідомий вибір: мінімум для швидкості, а для кроків, яким потрібна історія, - явно догрузити рівно стільки, скільки треба.
Видалити секрет з історії складно, а після публікації - вже запізно: ключ треба вважати скомпрометованим. Тому головне - не дати йому потрапити в коміт. Захист будують у кілька шарів.
1. Структура проєкту:
- секрети лише в
.env, який у.gitignore; у репозиторії -.env.exampleз порожніми значеннями; - у коді - лише
config('services.stripe.secret'), ніяких ключів у тестах, сидерах і фікстурах; - для CI - секрети платформи (
${{ secrets.NAME }}), а не файли в репозиторії.
2. Локальна перевірка до коміту - сканер секретів у pre-commit:
#!/bin/sh
gitleaks git --pre-commit --staged --redact --verbose
gitleaks шукає за сотнями шаблонів (ключі AWS, Stripe, GitHub-токени, приватні ключі) і за ентропією рядків. Хибні спрацювання - через .gitleaksignore чи конфігурацію.
3. Захист на сервері - push protection на GitHub:
- для користувачів увімкнено за замовчуванням: GitHub блокує ваш push із розпізнаним секретом у публічні репозиторії;
- для репозиторіїв (потрібна GitHub Secret Protection) - вмикається адміністратором і блокує push із секретами у конкретний репозиторій, зокрема з командного рядка, через вебінтерфейс і API.
При блокуванні push GitHub показує, у якому файлі й коміті знайдено секрет. Обхід можливий із зазначенням причини (хибне спрацювання, тестовий ключ), і для захисту на рівні репозиторію він створює сповіщення й запис у журналі аудиту.
4. Сканування в CI - той самий gitleaks на кожен PR: ловить те, що пройшло повз локальний хук через --no-verify чи відсутність хука.
5. Мінімізація шкоди:
- ключі з мінімальними правами й обмеженим терміном дії;
- окремі ключі для розробки, staging і продакшену;
- план дій на випадок витоку: спочатку відкликати ключ, а вже потім чистити історію.
Чому шари: кожен окремо можна обійти - хук не встановлено, push protection не знає формату вашого внутрішнього токена. Разом вони ловлять переважну більшість випадків.
Коли Composer встановлює пакет з дистрибутива (--prefer-dist, поведінка за замовчуванням для стабільних версій), він завантажує ZIP-архів, який GitHub генерує через git archive. Атрибут export-ignore у .gitattributes виключає файли з цього архіву.
# .gitattributes пакета
/.github export-ignore
/tests export-ignore
/docs export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/phpunit.xml.dist export-ignore
/phpstan.neon.dist export-ignore
/pint.json export-ignore
/CHANGELOG.md export-ignore
Перевірити результат локально:
git archive HEAD | tar -t
Виключені файли в списку не з'являться.
Навіщо це:
- менший
vendor/у кожному проєкті, що використовує пакет: тести, документація й конфіги CI пакета там нікому не потрібні; - швидше встановлення і менші образи Docker;
- менше шуму для інструментів, що сканують
vendor/(автодоповнення IDE, статичний аналіз); - не публікувати зайве - внутрішні скрипти, фікстури з даними.
Що не можна виключати:
src/,composer.json, файли, на які посилається автозавантаження;LICENSE- ліцензія має йти разом з кодом;README.mdзазвичай лишають;- ресурси, потрібні пакету під час роботи: шаблони, конфіги, міграції, файли перекладів.
Помилка тут ламає пакет лише при встановленні з дистрибутива: локально в репозиторії все працює, тому варто перевіряти через git archive чи тестове встановлення в чистий проєкт.
Пов'язане: --prefer-source завантажує повний клон репозиторію, і export-ignore там не діє. Інший корисний атрибут - export-subst: підставляє дані коміту (наприклад, $Format:%H$) у файли при створенні архіву.
Коміти в Git майже ніколи не зникають одразу. reset --hard, rebase чи видалення гілки лише прибирають посилання на коміти, а самі об'єкти лишаються в репозиторії, доки їх не прибере збирач сміття (за замовчуванням недосяжні записи reflog живуть 30 днів).
git reflog - журнал того, куди вказував HEAD (і кожна гілка) після кожної операції:
git reflog
9f8e7d6 HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: Send invoice email
d4e5f6a HEAD@{2}: commit: Add invoice model
Знайшли стан до помилки - повертаємося:
git reset --hard HEAD@{1} # повернути гілку туди
# або обережніше:
git branch rescue a1b2c3d # створити гілку на втраченому коміті
Після невдалого rebase шукають у reflog запис перед rebase (start). Ще простіше - ORIG_HEAD: rebase, merge і reset зберігають у ньому попереднє положення.
Що reflog не врятує:
- Незакомічені зміни після
reset --hardчиcheckout -- file: їх у репозиторії не було. Лише якщо їх додавали в індекс (git add), об'єкти можна знайти черезgit fsck --lost-found. - Чужий репозиторій: reflog локальний. Коміт, втрачений на сервері після force push, шукають у reflog того, хто його мав.
- Записи, старші за термін зберігання, після
git gc.
Висновок для команди: закомітьте - і зміни майже неможливо втратити. Небезпечна лише незакомічена робота.
Git - це сховище об'єктів, адресованих за хешем вмісту, і набір вказівників на них.
Чотири типи об'єктів:
- blob - вміст файлу (без імені й прав).
- tree - каталог: список імен з посиланнями на blob-и й вкладені tree.
- commit - посилання на кореневий tree (повний знімок проєкту), на батьківські коміти, автор, дата, повідомлення.
- tag (анотований) - іменований вказівник на об'єкт з власними метаданими.
Ідентифікатор кожного об'єкта - хеш його вмісту (SHA-1, у нових репозиторіях можливий SHA-256). Однаковий файл у двох комітах - той самий blob, збережений один раз. Змінити старий коміт неможливо: інший вміст - інший хеш, тобто вже інший об'єкт.
Гілка - файл з одним хешем. refs/heads/main містить хеш останнього коміту. Новий коміт просто переписує цей хеш. Тому створення гілки миттєве й нічого не копіює.
HEAD - вказівник на поточну гілку (ref: refs/heads/main) або напряму на коміт («detached HEAD»).
git cat-file -p HEAD # вміст коміту: tree, parent, author
git cat-file -p HEAD^{tree} # дерево каталогу
cat .git/refs/heads/main # гілка - це просто хеш
Що з цього випливає:
- Коміт зберігає знімок, а не різницю. Diff Git обчислює на льоту, порівнюючи дерева. (Для економії місця об'єкти пакуються в pack-файли з дельта-стисненням, але це деталь зберігання.)
- Rebase не «переносить» коміти - він створює нові з іншими батьками й хешами.
- Видалення гілки не видаляє коміти, лише вказівник - звідси можливість їх відновити.
rerere - «reuse recorded resolution», повторне використання записаних розв'язань конфліктів. Git запам'ятовує, як ви розв'язали конфлікт, і коли той самий конфлікт виникає знову, застосовує розв'язання автоматично.
git config --global rerere.enabled true
Як це працює:
- виникає конфлікт - Git записує «образ» конфлікту (обидві версії фрагмента) в
.git/rr-cache; - ви розв'язуєте конфлікт і робите коміт - Git записує результат;
- наступного разу при такому самому конфлікті Git підставляє збережене розв'язання:
Resolved 'app/Models/Invoice.php' using previous resolution.
Файл лишається не доданим до індексу - розв'язання варто переглянути й зробити git add (з rerere.autoUpdate true Git додає сам).
Коли це заощаджує час:
- довгоживуча гілка, яку регулярно оновлюють rebase-ом на
main: при кожному rebase ті самі конфлікти повторюються коміт за комітом; - пробне злиття: злити гілку, щоб перевірити, чи все збирається, скасувати злиття - а при справжньому злитті пізніше конфлікти вже розв'язані;
- скасований rebase: розв'язали половину конфліктів, зробили
git rebase --abort, почали знову - розв'язане вже не треба повторювати; - інтеграційні гілки (як у Git Flow чи при підготовці релізу), куди ті самі гілки зливаються багато разів.
Команди:
git rerere status # файли, для яких записано розв'язання
git rerere diff # що саме буде застосовано
git rerere forget app/Models/Invoice.php # забути неправильне розв'язання
Ризики:
- збережене помилкове розв'язання застосовуватиметься знову й знову - тому
forget; - rerere зіставляє текст конфлікту, а не зміст: якщо код навколо змінився, розв'язання може бути формально застосовне, але логічно хибне. Тести після злиття обов'язкові;
- кеш локальний і не передається колегам; старі записи прибирає
git gc(за замовчуванням через 60 днів для розв'язаних і 15 - для нерозв'язаних).
Для змін, які зачіпають усю історію (а не кілька останніх комітів), interactive rebase не підходить. Інструмент для цього - git-filter-repo, який рекомендує сам проєкт Git замість застарілого й повільного git filter-branch.
brew install git-filter-repo # чи pip install git-filter-repo
Типові задачі:
# видалити файл з усієї історії
git filter-repo --invert-paths --path storage/dump.sql
# видалити всі файли, більші за 10 МБ
git filter-repo --strip-blobs-bigger-than 10M
# лишити лише підкаталог (виділити пакет в окремий репозиторій)
git filter-repo --subdirectory-filter packages/billing
# замінити текст у всіх файлах історії (наприклад, ключ)
git filter-repo --replace-text replacements.txt
Змінити автора - через файл .mailmap:
Olena Petrenko <olena@company.com> <olena@old-laptop.local>
git filter-repo --mailmap .mailmap
Що відбувається: кожен коміт, починаючи з першого зміненого, отримує новий хеш - бо змінюється вміст або батько. Фактично це новий репозиторій зі схожою історією.
Наслідки, які треба спланувати:
- усі клони застаріли: колеги мають зробити свіжий клон, а не
pull- інакше старі коміти повернуться при наступному злитті; - force push усіх гілок і тегів, тимчасове зняття захисту гілок;
- відкриті pull request посилаються на старі коміти - їх доведеться перестворити;
- посилання на коміти в задачах, документації, changelog стають недійсними;
- копії на сервері: GitHub ще деякий час зберігає старі об'єкти в кеші й у форках - для повного видалення чутливих даних потрібне звернення в підтримку.
Захисні механізми filter-repo: за замовчуванням він відмовляється працювати не на свіжому клоні (щоб не зіпсувати робочий репозиторій) і видаляє origin після переписування, щоб випадковий push не перезаписав сервер.
Альтернатива для великих файлів - BFG Repo-Cleaner: простіший, але менш гнучкий.
Перед тим як переписувати - перевірити, чи не простіше залишити історію як є: великий файл у минулому лише збільшує розмір клону, а витік секрету лікується ротацією ключа, а не переписуванням історії.
Трибічне злиття (three-way merge) порівнює не дві версії, а три:
- base - спільний предок обох гілок (merge-base);
- ours - поточна гілка;
- theirs - гілка, яку зливають.
git merge-base main feature # хеш спільного предка
Правило для кожного фрагмента:
| base | ours | theirs | результат |
|---|---|---|---|
| X | X | Y | Y (змінили лише вони) |
| X | Y | X | Y (змінили лише ми) |
| X | Y | Y | Y (змінили однаково) |
| X | Y | Z | конфлікт |
Саме спільний предок дозволяє зрозуміти, хто змінив рядок. Порівнюючи лише дві версії, неможливо відрізнити «ми додали рядок» від «вони його видалили».
Конфлікт із базою легше розв'язувати, якщо бачити всі три версії:
git config --global merge.conflictStyle zdiff3
<<<<<<< HEAD
$total = round($sum * 1.2, 2);
||||||| base
$total = $sum * 1.2;
=======
$total = $sum * $vatRate;
>>>>>>> feature
Видно: одна сторона додала округлення, інша - змінну ставки. Правильний результат поєднує обидві зміни.
Кілька спільних предків (criss-cross merge - гілки зливалися одна в одну кілька разів): рекурсивна стратегія спершу зливає предків між собою у «віртуальну базу», а потім використовує її.
Стратегія ort («Ostensibly Recursive's Twin») - за замовчуванням з Git 2.34 замість recursive:
- той самий результат для звичайних випадків, але значно швидше на великих репозиторіях і з великою кількістю перейменувань;
- краще розпізнає перейменування й не торкається робочого каталогу, поки результат не обчислено;
- лежить в основі
git merge-tree --write-tree- злиття без робочого каталогу (так сервіси на кшталт GitHub перевіряють, чи можна злити pull request).
Інші стратегії й параметри:
-X ours/-X theirs- у конфліктних фрагментах автоматично взяти свою чи їхню версію (неконфліктні зміни обох сторін зберігаються);-s ours- ігнорувати зміни іншої гілки повністю, лише записати факт злиття (не плутати з-X ours);octopus- злиття більше ніж двох гілок за раз, без ручних конфліктів.
Що не розв'язує жодна стратегія: семантичні конфлікти. Одна гілка перейменувала метод, інша додала новий виклик старого імені - текстово конфлікту немає, а код зламаний. Тому після злиття запускають тести.
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії