Замена

Объекты в Git неизменяемы, но Git предоставляет интересный способ эмулировать замену объектов в своей базе другими объектами.

Команда git replace позволяет вам указать объект Git и сказать «каждый раз, когда встречается этот объект, заменяй его другим». В основном, это бывает полезно для замены одного коммита в вашей истории другим.

Например, в вашем проекте огромная история изменений и вы хотите разбить ваш репозиторий на два?—?один с короткой историей для новых разработчиков, а другой с более длинной историей для людей, интересующихся анализом истории. Вы можете пересадить одну историю на другую, «заменяя» самый первый коммит в короткой истории последним коммитом в длинной истории. Это удобно, так как вам не придется по-настоящему изменять каждый коммит в новой истории, как это вам бы потребовалось делать в случае обычного объединения историй (так как родословная коммитов влияет на SHA-1).

Попробуйте, как это работает. Возьмите существующий репозиторий и разбейте его на два?—?один со свежими правками, а другой с историческими. Затем посмотрите, как можно воссоединить их с помощью операции git replace, не изменяя при этом значений SHA-1 в свежем репозитории.

Вы будете использовать простой репозиторий с пятью коммитами. В расширении Git Graph они выглядят следующим образом.

Он опубликован в удаленном репозитории origin — https://github.com/1C-EDT-Developer/project.

Вы разобьете его на два семейства историй. Одно семейство, которое начинается от первого коммита и заканчивается четвертым, будет историческим.

Второе, состоящее пока только из четвертого и пятого коммитов?—?будет семейством со свежей историей.

Создать историческую часть легко: просто создайте ветку с вершиной на нужном коммите и затем отправьте эту ветку как master в новый удаленный репозиторий.

Чтобы создать ветку на этом коммите нажмите Create branch... в его контекстном меню.

Назовите ее history и сразу извлеките в рабочий каталог ().

Для хранения исторической части вы будете использовать другой удаленный репозиторий — https://github.com/1C-EDT-Developer/project-history. Отправьте только что созданную ветвь history в ветку master этого удаленного репозитория. Воспользуйтесь для этого командной строкой.

> git remote add project-history https://github.com/1C-EDT-Developer/project-history

> git push project-history history:master
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 12 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 871 bytes | 871.00 KiB/s, done.
Total 12 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/1C-EDT-Developer/project-history
 * [new branch]      history -> master

Таким образом, ваша историческая часть опубликована, она выглядит следующим образом.

Теперь вы займетесь более сложной частью?—?усечете свежую историю. Вам необходимо перекрытие, так чтобы вы смогли заменить коммит из одной части коммитом из другой, то есть вы будете обрезать историю, оставив четвертый и пятый коммиты (таким образом четвертый коммит будет входить в пересечение).

В данном случае будет полезным создать базовый коммит, содержащий инструкции о том как раскрыть историю, так другие разработчики будут знать что делать, если они столкнулись с первым коммитом урезанной истории и нуждаются в остальной истории. Итак, далее вы создадите объект заглавного коммита, представляющий вашу отправную точку с инструкциями, а затем перебазируете оставшиеся коммиты (четвертый и пятый) на этот коммит.

Для того, чтобы сделать это, вам нужно выбрать точку разбиения, которой для вас будет третий коммит коммит 3. Таким образом, ваш базовый коммит будет основываться на этом дереве. Вы можете создать свой базовый коммит, используя команду git commit-tree, которая просто берет дерево и возвращает SHA-1 объекта, представляющего новый сиротский коммит.

Этой команде нужно передать идентификатор дерева третьего коммита. Чтобы узнать его, вам понадобится хеш третьего коммита.

Чтобы получить хеш, кликните на третий коммит в расширении Git Graph, и запомните его хеш в буфер обмена.

С помощью команды cat-file и этого хеша получите содержимое третьего коммита:

> git cat-file -p eadc70c91151c288cd8c5b379aa8ad1fa4f3303b
tree d92543337f7cd9281b59967418f2d4b40fb706c1
parent f56f9d004696c1f20659130bf6abf98c61492328
author devmaster <devmaster@example.com> 1732195209 +0300
committer devmaster <devmaster@example.com> 1732195209 +0300

коммит 3

В первой строке вы видите идентификатор дерева: tree d92543337f7cd9281b59967418f2d4b40fb706c1.

Теперь с помощью команды git commit-tree вы можете получить хеш нового сиротского коммита. В сообщении укажите инструкции для получения исторической части вашего проекта, например, 'Get history from https://github.com/1C-EDT-Developer/project-history':

> echo 'Get history from https://github.com/1C-EDT-Developer/project-history' | git commit-tree d92543337f7cd9281b59967418f2d4b40fb706c1
dbcc304c87564fc625dc4a3bf5115ee7fe76a2fd
Примечание: Команда git commit-tree входит в набор команд, которые обычно называются «сантехническими» («plumbing» commands). Это команды, которые обычно не предназначены для непосредственного использования, но вместо этого используются другими командами Git для выполнения небольших задач. Периодически, когда вы занимаетесь необычными задачами подобными текущей, эти команды позволяют вам делать низкоуровневые вещи, но все они не предназначены для повседневного использования.

Хорошо. Теперь когда у вас есть базовый коммит, вы можете перебазировать свою оставшуюся историю на этот коммит используя git rebase --onto. Значением параметра --onto будет SHA-1 хеш коммита, который вы только что получили от команды commit-tree (dbcc304), а перебазируемой точкой будет третий коммит (родитель первого коммита, который вы хотите сохранить, eadc70c):

> git checkout main   
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

> git rebase --onto dbcc304 eadc70c
Successfully rebased and updated refs/heads/main.

Таким образом, вы переписали свою свежую историю поверх вспомогательного базового коммита (Get history from...), который теперь содержит инструкции о том, как при необходимости восстановить полную историю.

Вы можете отправить эту историю в новый проект или перезаписать существующую удаленную ветку main. Когда люди клонируют этот репозиторий, они будут видеть только два свежих коммита и после них базовый коммит с инструкциями.

Допустим, вы решили поместить вашу усеченную свежую историю в прежний репозиторий и перезаписали удаленную ветку main.

> git push --force origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (9/9), 698 bytes | 698.00 KiB/s, done. 
Total 9 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/1C-EDT-Developer/project.git
 + 53becbf...39f0ad3 main -> main (forced update) 

Теперь представьте себя на месте кого-то, кто впервые клонировал проект (https://github.com/1C-EDT-Developer/project) и хочет получить полную историю. Например, это разработчик Василий.

Для получения исторических данных после клонирования усеченного репозитория, Василию нужно добавить в список удаленных репозиториев исторический репозиторий и извлечь из него данные:

> git remote add project-history https://github.com/1C-EDT-Developer/project-history

> git fetch project-history
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 12 (delta 0), reused 12 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (12/12), 851 bytes | 8.00 KiB/s, done.
From https://github.com/1C-EDT-Developer/project-history 
 * [new branch]      master     -> project-history/master

Теперь у Василия его собственные свежие коммиты будут находиться в ветке main, а исторические коммиты в ветке project-history/master.

Для объединения этих веток Василий может просто вызывать команду git replace, указав коммит, который он хочет заменить (старый), и коммит, которым он хочет заменить его (новый). Так Василий хочет заменить коммит 4 в основной ветке на коммит 4 из ветки project-history/master:

> git replace 68c6d44 71e21ba

Теперь история коммитов будет выглядеть следующим образом.

А ветка main примет следующий вид.

Здорово, не правда ли? Не изменяя SHA-1 всех коммитов семейства, вы можете заменить один коммит в своей истории совершенно другим коммитом и все обычные утилиты (git bisect, git blame и т. д.) будут работать как от них это и ожидается.

Как вы помните, коммит 68c6d44 Василий заменил на 71e21ba. Интересно, что сейчас для коммит 4 SHA-1 хеш все равно выводится равный 68c6d44, хотя в действительности он содержит данные коммита 71e21ba, которым Василий его заменил.

> git log --oneline main
39f0ad3 (HEAD -> main, origin/main, origin/HEAD) коммит 5
68c6d44 (replaced) коммит 4
eadc70c коммит 3
f56f9d0 коммит 2
13890b7 коммит 1

Даже если выполнить команду типа cat-file, она отобразит замененные данные:

> git cat-file -p 68c6d44
tree 69aa0c267245e97b79dcf2a8926209b2fd103c63
parent eadc70c91151c288cd8c5b379aa8ad1fa4f3303b
author devmaster <devmaster@example.com> 1732195219 +0300
committer devmaster <devmaster@example.com> 1732195219 +0300

коммит 4

А ведь вы помните, что настоящим родителем коммита 68c6d44 был ваш вспомогательный базовый коммит (dbcc304), а не eadc70c как это отмечено здесь.

По материалам книги Pro Git (авторы Scott Chacon и Ben Straub, издательство Apress). Книга распространяется по лицензии Creative Commons Attribution Non Commercial Share Alike 3.0 license.