Таємниці втрачених комітів в Git

Git — штука не те, щоб особливо складна, але гнучка. Іноді ця гнучкість призводить до кумедних наслідків. Наприклад, подивіться на цей комміт на GitHub. Він виглядає як нормальний комміт, але якщо ви клон собі цей репозиторій, то такого коміта в ньому не знайдете. Тому що це втрачений комміт, більш відомий як git loose object або ж orphaned commit. Під катом трохи про нутрощі Git, звідки таке береться і що робити, якщо воно вам зустрілося.

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

echo 'test content' | git hash-object-w --stdin


Ця архітектурна особливість породила каламутне висловлювання, що Git відстежує перейменування по вмісту файлу. При перейменуванні об'єкт «комміт» буде містити посилання на об'єкт «вміст файлу», але якщо вміст не змінилося, то це буде посилання на об'єкт, вже наявний у сховище.


Коли розробник створює комміт, Git поміщає в сховище один об'єкт опису коміта і купку об'єктів, що описують файлову структуру та вміст файлів. Таким чином, «коміти» — це пов'язані між собою об'єкти Git в сховище типу ключ-значення.

За замовчуванням Git зберігає вміст файлів цілком: якщо ми поміняли сходинку в 100-килобайтном исходнике, то в сховище буде доданий об'єкт з усіма 100 кілобайтами, стислими з допомогою zlib. Щоб репозиторій надмірно не розпухав, в Git передбачений garbage collector, який запускається при виконанні команди push, при цьому об'єкти переупаковувати в pack-файл, який містить різницю між вихідним файлом і наступною ревізією (diff).


Коли коміти вмирають
У ряді випадків комміт може бути не потрібен. Наприклад, розробник зробив комміт foo, а потім відкотив зміна з допомогою команди reset. Git влаштований таким чином, що не видаляє коміти відразу ж, даючи розробнику можливість «повертати назад» навіть найбільш деструктивні дії. Спеціальна команда reflog дозволяє переглянути журнал операцій, що містить посилання на всі зміни репозиторію.

Але «непотрібні» коміти трапляються не тільки при використанні команди reset. Приміром, популярна операція rebase просто копіює інформацію про коммитах, залишаючи в сховище «оригінал», який нікому вже не потрібно. Щоб такі «загублені» об'єкти не накопичувалися, в Git передбачений механізм складання сміття — вже згаданий вище garbage collector автоматично викликається при виконанні команди push або викликається вручну.

Garbage collector шукає об'єкти, на які немає посилань, і видаляє їх з пам'яті. Величезну роль при цьому відіграє журнал операцій reflog: посилання в ньому мають обмежений термін життя, за замовчуванням 30 днів для об'єкта без посилань і 90 днів для об'єкта з посиланнями. Garbage collector спочатку видаляє з журналу reflog всі посилання з вичерпаним «терміном придатності», а потім видаляє з сховища об'єкти, на які немає посилань. Така архітектура дає розробнику 30 днів, щоб відновити «непотрібний» комміт, який в іншому випадку буде остаточно видалено з репозиторію після закінчення цього терміну.

Що ж сталося на GitHub?
Думаю, ви вже здогадуєтеся. Зазначений комміт виявився непотрібним: швидше за все, автор зробив rebase. Але GitHub показує вміст серверного репозиторію, з якого ніколи не виконується команда push. І garbage collector, швидше за все, теж ніхто не викликає. При цьому при клонуванні такого репозиторію Git передає по мережі тільки ті коміти, на які є посилання, а «втрачені коміти», більш відомі як loose objects, залишаються лежати мертвим вантажем на серверній стороні.

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.