Git: віддалені репозиторії й командна робота
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
fetch, pull і push, робочі процеси й рев'ю, теги й релізи, налаштування, worktree й субмодулі, внутрішня будова - питання всіх рівнів, від junior до senior.
- За спробу
- 20
- У пулі
- 52
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
36 питаньКоміти в 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 не «переносить» коміти - він створює нові з іншими батьками й хешами.
- Видалення гілки не видаляє коміти, лише вказівник - звідси можливість їх відновити.
git fetchзавантажує нові коміти й гілки з віддаленого репозиторію і оновлює віддалені гілки (origin/main). Ваші локальні гілки й робочі файли не змінюються.git pull=fetch+ інтеграція змін у поточну гілку (за замовчуваннямmerge, абоrebase, якщо так налаштовано).
git fetch
git log main..origin/main # що нового на сервері, ще до злиття
git diff main origin/main # які саме зміни
git merge origin/main # або git rebase origin/main
Чому fetch корисний: спершу подивитися, що змінилося, і лише потім вирішити, як інтегрувати. pull робить усе одразу, і при конфлікті ви опиняєтеся посеред злиття без попередження.
Налаштування pull, яке варто знати:
git config --global pull.rebase true # pull через rebase - без зайвих комітів злиття
git config --global pull.ff only # або: лише перемотування, інакше помилка
Без явного налаштування сучасний Git при розбіжності гілок відмовляється робити pull і просить обрати стратегію.
Типова ситуація: «у мене не той код, що на сервері» - часто просто не зроблено fetch, і origin/main у вас застарілий.
Конфлікт виникає, коли обидві гілки змінили ті самі рядки (або одна змінила файл, а інша - видалила). Git не знає, яка версія правильна, і зупиняє злиття.
git merge feature
# CONFLICT (content): Merge conflict in app/Services/Billing.php
git status # список файлів з конфліктами
У файлі з'являються маркери:
<<<<<<< HEAD
$total = $subtotal * 1.2;
=======
$total = $subtotal + $this->tax($subtotal);
>>>>>>> feature
Що робити:
- Відредагувати файл: залишити потрібну версію, поєднати обидві чи написати третю. Прибрати всі маркери.
- Перевірити, що код працює: тести, запуск.
- Позначити конфлікт розв'язаним і завершити:
git add app/Services/Billing.php
git commit # для rebase: git rebase --continue
Передумали - повернути все як до злиття: git merge --abort (чи git rebase --abort).
Корисне:
git config --global merge.conflictStyle zdiff3- маркери показують ще й спільного предка: видно не лише «що в кожній гілці», а й «що було до обох змін». Розв'язувати значно легше.- IDE (PhpStorm, VS Code) мають тристоронній редактор конфліктів.
- Для файлів, які генеруються (
composer.lock,package-lock.json), руками не мержать: беруть одну версію й заново виконують команду, що її створила.
Як конфліктів менше: короткоживучі гілки, часта інтеграція з main, невеликі pull request.
Докладніше в документації: git merge: розв'язання конфліктів
Після rebase чи commit --amend історія локальної гілки розходиться з віддаленою, і звичайний git push відмовляється. git push --force перезаписує віддалену гілку вашою версією безумовно.
Чим це небезпечно: якщо колега встиг відправити в цю гілку свої коміти, force push їх знищить на сервері - ви навіть не побачите, що вони там були.
--force-with-lease перезаписує гілку, лише якщо вона на сервері досі в тому стані, який ви бачили під час останнього fetch. Якщо хтось додав коміти - push відхиляється, і ви спершу інтегруєте їхні зміни.
git rebase main
git push --force-with-lease
Пастка: «той стан, що ви бачили» - це ваш origin/feature. Якщо ви зробили git fetch (чи IDE зробила його у фоні), не подивившись, що прийшло, lease вважатиме нові чужі коміти «баченими». Суворіший варіант - --force-if-includes (Git 2.30+), який вимагає, щоб віддалені коміти були інтегровані в вашу гілку.
Правила команди:
- Force push лише у свої гілки. У
main,develop, релізні гілки - ніколи. - Захищені гілки на GitHub/GitLab забороняють force push і вимагають pull request з рев'ю.
- Якщо над гілкою працює кілька людей - домовитися перед переписуванням історії або обійтися merge.
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.