"Зміст цієї статті народився з цієї хабр статті та роздумів про найпростіший шлях синхронізації субмодулів у деяких моїх проєктах.
Що то є за звір такий - GIT-дерево
Усім добре відомо, що кожен git-комміт має хеш. Наприклад 3eda1bf626755efa6f683af88b91337daf304862. Маючи хеш ми можемо побачити як будується коміт:
$ git cat-file -p 5e45ecb
tree 8640790949c12690fc71f9abadd7b57ec0539376
parent 930741d1f5fd2a78258aa1999bb4be897ba3d015
author Mark Tareshawty <tareby...@github.com> 1542718283 -0500
committer Mark Tareshawty <tareby...@github.com> 1542718283 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
Fix scoping so that we don't break the naming convention
Це і є весь коміт
- tree - посилання на конкретний стан проєкту
- parent - посилання на батьківський коміт (можливий варіант з декільками батьками. Наприклад при мержі. Тоді головним вважається перший)
- author - автор + дата
- committer - той, хто робив коміт + дата
- gpgsig - пидпис, якщо є
- message - текст комментара
Кожен коміт описує конкретний стан проєкту цілком, а те, що ми бачимо як зміни, що вносяться ним, - це насправді динамічно обчислювальна різниця, як порівняти з попереднім станом. Також варто зазначити, що всі коміти імутабельні (незмінні), тобто, наприклад, під час rebase/cherry-pick/amend створюються абсолютно нові коміти.
tree (дерево) - по суті це просто папка з конкретним імутабельним вмістом. Об'єкт типу tree містить поіменний список файлів із конкретним вмістом (blobs) і підпапок (trees). А дерево, на яке вказує кожен коміт - це коренева папка проєкту, точніше її певний стан.
Побачити як будується дерево можна за допомогою тієї ж cat-file, але з хешем дерава.
$ git cat-file 8640790
------
100644 blob 7ab08294a46f158c51460be3e7df6a190e15023b .env.example
100644 blob 0a1a4d1ad9ff3f35b67678ca893811e91b423af5 .gemset
040000 tree 033aa38ce0eab11fe229067c14ccce95e2b8b601 .github
100644 blob ca49bb7ffa6273b0be4ce7ba1accba456032fb11 .gitignore
100644 blob c99d2e7396e14ac072c63ec8419d9b8fede28d86 .rspec
100644 blob 65e77a2f59f635a8f24eb4714e8e43745c5c0eb9 .rubocop.yml
....
Коміт - це...
Тепер зрозуміло, що насправді відбувається, коли виконується коміт
- стан проєкту staging (зберігається як новий об'єкт tree і береться його хеш)
- посилання на поточний (батьківський) коміт
- автора + комітера + дві дати
- текст коміту
Це все зберігається, хешується, і виходить новий об'єкт commit. І команда автоматично піднімає на нього покажчик поточної гілки.
commit-tree
І нарешті ми можемо перейти до опису потрібної нам команди git commit-tree. Формально це одна з низькорівневих команд, тому вона рідко згадується і використовується. Ми не будемо розглядати пов'язані з нею інші низькорівневі команди (такі як git write-tree, git-update-index, вони також відомі як plumbing commands). Нам цікавий тільки один окремий наслідок: за допомогою цієї команди ми можемо легко скопіювати (перевикористати) дерево стану проєкту з будь-якого іншого коміту.
Подивимося на її виклик
$ git commit-tree 4c835c2 -m ""Fixed bug"" -p a8fc5e3
d9aded78bf57ca906322e26883644f5f36cfdca5
Команда git commit-tree теж створює коміт, але низькорівневим способом. Тут необхідно явно вказати вже наявне дерево (tree) 4c835c2 і посилання на батьківський коміт a8fc5e3. І вона повертає хеш нового коміту d9aded7, а положення гілки не змінює (тому цей коміт ніби зависає в повітрі).
По суті - це вся теорія.
Приклади використання
Синхронізація з іншою гілкою
Синхронізувати стан проєкту на гілці alpha з гілкою beta. Тобто потрібно створити на гілці alpha такий новий коміт, щоб стан проєкту став точно таким же, як на гілці beta.
Знаходимо хеш дерева
$ git cat-file -p 280c30f
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a <---
parent 560b449675513bc8f8f4d6cda56a922d4e36917a
author Baur <atanov...@gmail.com> 1540619512 +0600
committer Baur <atanov...@gmail.com> 1540619512 +0600
Тепер будуємо коміт, що вказує на це дерево
$ git commit-tree 3c1afe7 -m ""Synced with branch 'beta'"" -p HEAD
eb804d403d4ec0dbeee36aa09da706052a7cc687
Kоміт створено, команда вивела його хеш. У нього таке ж дерево, як у коміту на гілці origin/beta. І оскільки цей коміт є прямим нащадком поточної гілки. Причому це значення буде завжди унікальним, адже воно обчислюється не тільки від дерева, а й від автора і часу. Сам коміт завис у повітрі, оскільки поки що він не входить до жодної з гілок. Щоб включити його до гілки, досить просто зробити fast-forward merge
$ git merge --ff xxxxxxx
Updating 64fafc7..xxxxxxx
Fast-forward
Azure.txt | 3 ---
Bill.txt | 6 +-----
Linus.txt | 15 +++++++++++++++
3 files changed, 16 insertions(+), 8 deletions(-)
Тепер стан проєкту на гілці alpha точно такий самий, як на гілці beta. А якщо подивитись на те, що ж усе-таки змінив цей коміт, ми побачимо: він ревертнув усі власні коміти (зміни) гілки alpha та додав усі унікальні коміти (зміни) гілки beta, відносно їхнього спільного коміту-попередника.
Порівняння двох гілок
З першого прикладу видно, що створений коміт показує всі ті зміни, які є фактичною різницею між цими двома гілками. Ця його властивість дозволяє легко порівняти одну гілку з іншою. Достатньо створити третю тимчасову гілку і в ній зробити подібний коміт.
Створимо на поточному коміті гілку temp і встанемо на неї
$ git checkout -b temp
І нам залишилося зробити те саме, що і в попередньому прикладі. Але цього разу ми вкладемося в один рядок. Для цього ми скористаємося спеціальним синтаксисом для звернення до дерева коміту origin/beta^{tree} або що те ж саме 280c30f^{tree}.
$ merge --ff $(git commit-tree origin/beta^{tree} -m ""Diff with branch 'beta'"" -p HEAD)
По суті ми матеріалізували у вигляді комміта дифф між двома гілками! Зрозуміло, такий ""порівняльний"" коміт ми можемо створити взагалі для будь-яких двох комітів (станів) у репозиторії.
Revert декількох комітів
Треба відкотити кілька останніх комітів.
Припустимо, що нам потрібно відкотити два останні коміти на гілці alpha. Є два класичні способи це зробити:
- Два рази виконати
git revert- на кожен коміт git reset, тобто скидання положення гілки
Але можна це зробити і третім способом:
$ git merge --ff $(git commit-tree 7a714bf^{tree} -m ""Reverted to commit 7a714bf"" -p HEAD)
Це додасть один новий коміт, який ніби відкочує зміни двох попередніх комітів. На відміну від першого способу, створюється всього один коміт, навіть якщо треба відкотити десять останніх комітів. А відмінність способу з git reset полягає в тому, що ми не викидаємо ці коміти з самої гілки.
Далі, якщо потім потрібно повернути початковий стан гілки, то це можна зробити аналогічно
$ git merge --ff $(git commit-tree 64fafc7^{tree} -m ""Reverted back to commit 64fafc7"" -p HEAD)
При цьому в історії гілки залишаться ці два коміти, за якими буде видно, що її відкочували і повертали назад.
Цікавий варіант, якщо потрібно зробити реверт лише частково. Тоді алгоритм буде такий самий, але з додаванням ще 2-х кроків.
-
git reset HEAD~1тобто після повного реверту ми повертаємося на крок назад. -
І тепер достатньо застейджити потрібні файли і зробити коміт, а інші зміни можна відкинути.
$ git add Bill.txt $ git commit -m ""Reverted file Bill.txt to 7a714bf"" $ git reset --hard HEAD
Штучний merge
І тепер та сама команда, але вже з двома батьківськими комітами
$ git merge --ff $(git commit-tree alpha^{tree} -m ""Merge 'alpha' into 'master', but take 'alpha' tree"" -p HEAD -p alpha)
ALIAS
За допомогою наступного git аліасу копіювання дерева стає дуже простою командою. Створюємо аліас у конфігурації поточного репозиторію.
$ git config alias.copy '!git merge --ff $(git commit-tree ${1}^{tree} -p HEAD -m ""Tree copy from ${1}"")''
І тепер достатньо виконати git copy xxx, де xxx - будь-яке посилання на коміт.
$ git copy xxx
Це буде те ж саме що і
$ git merge --ff $(git commit-tree xxx^{tree} -p HEAD -m ""Tree copy from xxx"")
Приклади
$ git copy a8fc5e3
$ git copy origin/beta
$ git copy HEAD~3
виправити стандартний текст створеного коміту на довільний можна через git amend.
$ git commit --amend -m ""Just for test""
Або запустивши редактор:
$ git commit --amend
Висновки
Це справді неперевешений інструмент для тонкої роботи з git. Я дякую авторові з хабра, що розповів про такі команди. Дякую всім, хто дочитав цю статтю до завершення. Користуйтесь з насолодою.