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

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 reflog

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

  • 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 fetch

Конфлікт виникає, коли обидві гілки змінили ті самі рядки (або одна змінила файл, а інша - видалила). 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

Що робити:

  1. Відредагувати файл: залишити потрібну версію, поєднати обидві чи написати третю. Прибрати всі маркери.
  2. Перевірити, що код працює: тести, запуск.
  3. Позначити конфлікт розв'язаним і завершити:
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.

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

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.