Git
Chapters ▾ 2nd Edition

7.13 Інструменти Git - Заміна

Заміна

Як ми вже наголошували, обʼєкти в базі даних Git незмінні, проте Git надає цікавий засіб вдавати заміну обʼєктів своєї бази на інші.

Команда replace дозволяє задати обʼєкт у Git та сказати "щоразу як ти звертаєшся до цього обʼєкту, вдай нібито це _інший обʼєкт". Це найбільш корисно для заміни одного коміту вашої історії на інший без потреби переписувати всю історію за допомогою, наприклад, git filter-branch.

Наприклад, скажімо ви маєте неосяжну історію коду та бажаєте розділити репозиторій на один з короткою історією для нових розробників та один з набагато довшою та більшою історією для людей, що зацікавлені у видобуванні інформації. Ви можете прищепити одну історію до іншої, замінюючи (replaceing) найдавніший коміт у новій лінії останнім комітом старої. Це зручно, адже означає, що вам нема потреби насправді переписувати кожен коміт нової історії, як вам зазвичай доводиться робити, щоб поєднати їх разом (адже батьківство впливає на SHA-1 суми).

Спробуймо це. Візьмімо існуючий репозиторій, розділимо його на два, один новітній, інший історичний, та потім побачимо, як можна воззʼєднати їх без редагування значень SHA-1 новітнього репозиторія за допомогою replace.

Ми використаємо простий репозиторій з пʼятьма простими комітами:

$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Ми бажаємо розбити це на дві лінії історії. Одна буде з першого до четвертого коміту - це буде історичною. Друга лінія буде містити лише четвертий та пʼятий коміти - це буде новітня історія.

replace1

Ну, створити давню історію просто: треба лише покласти гілку в історію, а потім надіслати цю гілку до гілки master нового віддаленого репозиторія.

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
replace2

Тепер ми можемо надіслати нову гілку history до гілки master нового репозиторія:

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

Добре, отже наша історія опублікована. Тепер складніше завдання — зрізати нашу новітню історію, щоб зменшити її. Нам потрібне перекривання, щоб ми могли замінити коміт одного репозиторія еквівалентним комітом з іншого, отже ми збираємося зрізати до четвертого та пʼятого комітів (і четвертий коміт перекривається).

$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

У даному випадку корисно створити базовий коміт, що має інструкції щодо розширення історії, щоб інші розробники знали, що треба робити, якщо наштовхнуться на перший коміт зрізаної історії, а потребують більшого. Отже, ми збираємось створити обʼєкт первинного коміту в якості нашої базової точки з інструкціями, потім перебазуємо решту комітів (четвертий та пʼятий) поверх нього.

Щоб це зробити, нам треба вибрати точку розриву, у даному випадку це третій коміт, тобто 9c68fdc словами SHA. Отже, наш базовий коміт буде засновано на цьому дереві. Ми можемо створити наш базовий коміт за допомогою команди commit-tree, яка просто бере дерево та поверне нам SHA-1 новісінького коміту без батьків.

$ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Note

Команда commit-tree є однією з набору команд, які, зазвичай, називають кухонними (plumbing). Це команди, які, щиро кажучи, створені не для безпосереднього використання, а для використання іншими командами Git, щоб виконувати менші завдання. Коли ж доводиться робити більш чудернацькі речі, на кшталт описаного тут, вони дозволяють робити дійсно низькорівневі речі, проте, не призначені для щоденного користування. Ви можете дізнатись більше про кухонні команди в Кухонні та парадні команди

replace3

Добре, отже тепер, коли в нас є базовий коміт, ми можемо перебазувати решту нашої історії поверх нього за допомогою git rebase --onto. Параметр --onto треба встановити у SHA-1 суму, щойно отриману від commit-tree, а місцем перебазування буде третій коміт (батько першого коміту, який треба зберегти, 9c68fdc):

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
replace4

Добре, отже тепер ми переписали нашу новітню історію поверх технічного базового коміту, який тепер містить інструкції щодо відновлення повної історії, якщо комусь треба. Тепер ми можемо надіслати цю історію до нового проекту, та тепер при клонуванні цього репозиторія, вони побачать лише останні два коміти, а потім базовий коміт з інструкціями.

Поміняймося роллю з кимось, хто клонує проект вперше, та потребує повної історії. Щоб отримати дані історії після клонування зрізаного репозиторія, потрібно додати друге віддалене сховище — історичний репозиторій, та отримати з нього зміни:

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

Тепер співробітники матимуть нові коміти в гілці master, а історичні коміти у гілці project-history/master.

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Щоб їх обʼєднати, можна просто викликати git replace з комітом, який ви хочете замінити, та комітом, яким ви бажаєте його замінити. Отже, ми хочемо замінити "четвертий" коміт з гілки master "четвертим" комітом з гілки project-history/master:

$ git replace 81a708d c6e1e95

Тепер, якщо подивитись на історію гілки master, виявляється, вона матиме такий вигляд:

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Вражає, еге ж? Без необхідності змінювати всі SHA-1 суми першоджерела, ми впорались замінити один коміт історії геть іншим, і всі звичайні інструменти (bisect, blame тощо) працюватимуть, як ми й очікуємо.

replace5

Що цікаво, тут досі зазначено 81a708d як SHA-1, хоч насправді використовуються дані коміту c6e1e95, яким ми його замінили. Навіть якщо виконати таку команду, як cat-file, вона все одно покаже замінені дані:

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

Памʼятайте, що справжній батько коміту 81a708d був наш коміт-заповнювач (622e88e), а не 9c68fdce, як тут зазначено.

Ще одна цікава річ: відомості про це зберігаються в посиланнях:

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

Це означає, що поділитися нашою заміною з іншими просто, адже ми можемо надіслати це посилання до сервера, а інші можуть легко завантажити його. Це не дуже корисно в розглянутому тут сценарії з прищепленням історії (адже тоді всім доведеться завантажувати обидві історії, то ж яка в тому користь?), проте це може бути корисним за інших обставин.