В этой статье я расскажу об одном полезном, но малоизвестном приеме работы с git — как можно легко создать коммит, используя дерево из другого коммита. Проще говоря, как получить нужное состояние проекта на какой-либо ветке, если это состояние уже когда-то и где-то было в репозитории раньше. Будет приведено несколько примеров того, как это позволяет элегантно решать некоторые практические задачи. И в частности я расскажу о найденном мной методе, который позволяет значительно упростить исправление множественных конфликтов при rebase. Кроме того, эта статья — отличный способ понять на практике, что из себя представляет коммит в git-е.
Содержание
Теоретическая часть. Про коммиты и деревья
git commit-tree
Практическая часть
1. Синхронизация с другой веткой
2. Сравнение двух веток
3. Реверт ветки
4. Частичный реверт
5. Искуственный merge
6a. Метод rebase через merge — описание
6b. Метод rebase через merge — скрипт
7. Алиас
Заключение
Теоретическая часть. Про коммиты и деревья
Коммит, наверное, самое основное понятие в git-е, давайте посмотрим из чего он состоит. Каждый коммит имеет свой уникальный идентификатор в виде хэша, например 5e45ecb. И с помощью следующей команды, зная хэш, мы можем посмотреть его содержимое.
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 — текст коммита
Напомню, что коммит в git-е (в отличие от других VCS) не описывает сделанные изменения. Тут все наоборот: каждый коммит описывает конкретное состояние проекта целиком, а то что мы видим как вносимые им изменения — это на самом деле динамически вычисляемая разница в сравнении с предыдущим состоянием. Также стоит отметить, что все коммиты иммутабельны (неизменяемы), то есть, например, при rebase/cherry-pick/amend создаются абсолютно новые коммиты.
tree (дерево) — по сути это просто папка с конкретным иммутабельным содержимым. Объект типа tree содержит поименный список файлов с конкретным содержимым (blobs) и подпапок (trees). А дерево, на которое указыват каждый коммит — это корневая папка проекта, точнее ее определенное состояние.
Сделать это можно точно так же и для любого другого объекта (commit / tree / blob), при этом достаточно взять первые несколько уникальных симоволов хэша: 8640790949c12690fc71f9abadd7b57ec0539376 -> 8640790.
git cat-file -p 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
100644 blob 8e8299dcc068356889b365e23c948b92c6dfcd78 .ruby-version
100644 blob 19028f9885948aca2ba61f9d062e9dc21c21ad03 .stylelintrc.json
100644 blob 2f7a032fbc3f4f7195bfd91cb33889a684b572b9 .travis.yml
100644 blob 121615722a6c206a9fe24b9a1c9b647662a460d2 ARCHITECTURE.md
100644 blob 898195daeea0bbf8c5930deeaf1020ba8abab34a Gemfile
100644 blob de7ca707f9fe9172db941b65cdacaba7e024fc06 Gemfile.lock
100644 blob e6ff62fefd071b1a8ca279bae94ddbc4dd17b7a3 Gruntfile.js
100644 blob 0cac5b30fb32d36cce2aeb7d936be7b6207d68c7 MIT-LICENSE.txt
100644 blob c2c566e8cc3d440d3ee8041b79cded416db28136 Procfile
100644 blob d1fb2f575380e1e093a4d82e3f19e51f0b99a0a1 Procfile.dev
100644 blob 3a88e138f10fa65bd2cfe1a1d3292348205508b5 README.md
100644 blob 5366e6e073cc426518894cc379d3a07cf3c9cfb3 Rakefile
100644 blob e6d3d2d3e9d5122c5f75bbeee8ed0917ad38c131 app.json
040000 tree 94f83cf03bd6f1cf14672034877b14604744b7a2 app
040000 tree d4d859e82564250b4c4f2047de21e089e7555475 bin
100644 blob 1f71007621f17334fd6f2dd71c87b7a16867119c config.ru
040000 tree 9e8e4bf5ec44541aefff544672b94ca8a9d07bbf config
040000 tree 31b8d0e1fa2bb789dbd6319e04fc9f115952cf2a db
040000 tree 38e7a13e0e772c2a13e46d2007e239f679045bee doc
040000 tree a6e35ded8b35837660cf786e637912377f845515 lib
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e log
100644 blob 843523565ddee5e00f580d9c4e37fc2478fdaecc package-lock.json
100644 blob 791ee833ad316d75b1d2c83a64a3053fc952d254 package.json
040000 tree 4645317c52675d9889f89b26f4dd4d2ae1d8cbad public
040000 tree 31d3f8ae4a4ffe62787134642743ed32a35dbae2 resources
040000 tree 807ffa29868ef9c25ddb4b4126a4bb7f1b041bf0 script
040000 tree 4c3bf9a7f3679ba059b0f1c214a500d197546462 spec
040000 tree 136c8174412345531a9542cafef25ce558d2664f test
040000 tree e6524eafe066819e4181bc56c503320548d8009b vendor
На самом деле это важнейшая особенность того, как работает git, идентификатор коммита — это действительно хэш от его содержимого. Точно так же как и хэши вложенных в него объектов (trees, blobs).
Теперь посмотрим что происходит когда мы делаем
git commit -m "Fixed bug"
Эта команда создает новый коммит который фиксирует в себе следующее:
- состояние проекта staging (сохраняется как новый объект tree и берется его хэш)
- ссылку на текущий (родительский) коммит
- автора + коммитера + две даты
- текст коммита
Это все сохраяется, хэшируется, и получается новый объект commit. И команда автоматически поднимает на него указатель текущей ветки.
Как мы уже знаем, tree — это объект, который содержит состояние проекта целиком на какой-то определенный момент в прошлом — когда был создан коммит с этим деревом.
Рабочую папку называют working tree / working copy / working directory, что вполне логично.
Также у нас есть — staging area / index — область подготовленных изменений. Но логически это тоже дерево, точнее то состояние, которое сохраняется при коммите как дерево. Поэтому, мне кажется, более логичным это было бы называть staged tree.
git 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, а положение ветки не меняет (поэтому этот коммит как бы зависает в воздухе).
Практическая часть
Примеры использования этой команды демонстрируются на следующем простом репозитории.
Он содержит три ветки:
master — основная ветка
alpha — ветка, на которой мы работаем и находимся
beta — ветка, которая уже была ранее смерджена в master
Все действия легко повторить локально, для этого достаточно клонировать репозиторий, встать на ветку alpha и далее исполнять команды из примеров. Это исходное состояние — общее для всех примеров.
git clone https://github.com/capslocky/git-commit-tree-example.git
cd ./git-commit-tree-example/
git checkout alpha
Все команды, включая скрипт, работают и под windows. Только нужно открыть bash терминал в папке проекта, например, так
"C:\Program Files\Git\git-bash.exe" --cd="D:\path\project"
1. Синхронизация с другой веткой
Задача:
Синхронизировать состояние проекта на ветке alpha с веткой beta. То есть нужно создать на ветке alpha такой новый коммит, чтобы состояние проекта стало точно таким же, как на ветке beta.
Конкретно такая задача, наверное, вряд ли когда-нибудь возникнет, но это самый подходящий кейс, чтобы продемонстрировать подход.
Самое простое решение заключается в том, чтобы взять существующее дерево, на которое указывает ветка beta, и просто указать на него из нового коммита для ветки alpha. Так как это первый пример, вся его логика рассмотрена достаточно подробно.
Для начала найдем хэш коммита, на который указывает ветка beta:
git rev-parse origin/beta
280c30ff81a574f8dd41721726cf60b22fb2eced
280c30f — достаточно взять несколько первых символов
Теперь найдем хэш его дерева, отобразив содержимое коммита через git cat-file:
git cat-file -p 280c30f
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a <---
parent 560b449675513bc8f8f4d6cda56a922d4e36917a
author Baur <atanov...@gmail.com> 1540619512 +0600
committer Baur <atanov...@gmail.com> 1540619512 +0600
Added info about windows
3c1afe7 — это и есть нужное нам дерево
И теперь создадим коммит, указывающий на это дерево, а родительским коммитом укажем текущий коммит:
git commit-tree 3c1afe7 -m "Synced with branch 'beta'" -p HEAD
eb804d403d4ec0dbeee36aa09da706052a7cc687
Всё, коммит создан, команда вывела его хэш. Причем это значение будет всегда уникально, ведь оно вычисляется не только от дерева, но и от автора и времени. Сам коммит завис в воздухе, так как пока он не входит ни в одну из веток. Нам достаточно взять несколько первых символов: eb804d4, это уникальное для каждого случая значение я обозначу как xxxxxxx. Давайте посмотрим на его содержимое:
git cat-file -p xxxxxxx
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a <---
parent 64fafc79e8f6d22f5226490daa5023062299fd6c
author Peter <peter...@gmail.com> 1545230299 +0600
committer Peter <peter...@gmail.com> 1545230299 +0600
Synced with branch 'beta'
Отлично, у него такое же дерево, как у коммита на ветке 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. [update] А если посмотреть на то, что же все-таки изменил этот коммит, мы увидим: он ревертнул все собственные коммиты (изменения) ветки alpha и добавил все уникальные коммиты (изменения) ветки beta, относительно их общего коммита-предка.
2. Сравнение двух веток
Задача:
Сравнить ветку alpha с веткой beta.
Из первого примера видно, что созданный коммит показывает все те изменения, которые являются фактической разницей между этими двумя ветками. Это его свойство позволяет легко сравнить одну ветку с другой. Достаточно создать третью временную ветку и в ней сделать подобный коммит.
Итак, сначала вернем ветку alpha к исходному состоянию
git reset --hard origin/alpha
Создадим на текущем коммите ветку temp и встанем на нее
git checkout -b temp
И нам осталость сделать то же, что и в предыдущем примере. Но в этот раз мы уложимся в одну строчку. Для этого мы воспользуемся специальным синтаксисом для обращения к дереву коммита origin/beta^{tree} или что то же самое 280c30f^{tree}.
git merge --ff $(git commit-tree origin/beta^{tree} -m "Diff with branch 'beta'" -p HEAD)
Готово, по сути мы материализовали в виде коммита дифф между двумя ветками
git show
git diff alpha origin/beta
Разумеется, такой "сравнительный" коммит мы можем создать вообще для любых двух коммитов (состояний) в репозитории.
3. Реверт ветки
Задача:
Откатить несколько последних коммитов.
Вернемся на ветку alpha и удалим ветку temp
git checkout alpha
git branch -D temp
Предположим, что нам нужно откатить два последних коммита на ветке 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)
При этом в истории ветки останутся эти два коммита, по которым будет видно, что ее откатывали и возвращали назад.
4. Частичный реверт
Задача:
Откатить изменения в некоторых файлах за несколько последних коммитов.
Снова вернем ветку alpha к исходному состоянию
git reset --hard origin/alpha
Ветка alpha содержит 3 коммита, в каждом из которых вносятся изменения в файле Bill.txt, в последнем коммите также добавляется файл Azure.txt. Предположим, что нам нужно откатить изменения в файле Bill.txt за последние 2 коммита, при этом не трогая какие-либо другие файлы.
Для начала откатим все файлы на 2 коммита назад
git merge --ff $(git commit-tree 7a714bf^{tree} -m "any text" -p HEAD)
Далее вернем ветку на предыдущий коммит, но не трогая состояние проекта на диске
git reset HEAD~1
И теперь достаточно застейджить нужные файлы и сделать коммит, а прочие изменения можно отбросить.
git add Bill.txt
git commit -m "Reverted file Bill.txt to 7a714bf"
git reset --hard HEAD
5. Искуственный merge
Задача:
Смерджить одну ветку в другую, получив заранее предопределенный результат.
Представим такую ситуацию. В продакшене обнаружен критический баг и его как обычно нужно срочно починить. Однако при этом не ясно сколько времени уйдет на то, чтобы его изучить и сделать корректный хотфикс, поэтому был быстро сделан временный хотфикс, который изолировал взаимодействие с проблемным модулем. Итак, в ветке master появляется коммит с этой временной заплаткой, а спустя какое-то время вместо нее в master нужно смерджить уже полноценный хотфикс.
Таким образом, нам нужно сделать merge ветки alpha в master, но это должен быть не традиционный мердж, когда все уникальные изменения из ветки alpha добавляются в master сверху и возникает конфликт, а мы должны полностью перезаписать master веткой alpha.
Для начала вспомним вообще, что такое merge commit — на самом деле это тот же самый обычный коммит, но просто у него два родительских коммита, это хорошо видно, если посмотреть на его содержимое (то, как формируется его дерево — это уже отдельный вопрос). А кто в кого смерджен определяется просто — первый parent коммит считается главным.
git cat-file -p 7229df8
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a
parent fd54ab7dde87593b9892b6d1ffbf1afd39ba6f9e
parent 280c30ff81a574f8dd41721726cf60b22fb2eced
author Baur <atanov...@gmail.com> 1540619579 +0600
committer Baur <atanov...@gmail.com> 1540619592 +0600
Merge branch 'beta' into 'master'
Сбросим к исходному состоянию текущую ветку alpha и переключимся на master
git reset --hard origin/alpha
git checkout master
И теперь та же команда, но уже с двумя родительскими коммитами
git merge --ff $(git commit-tree alpha^{tree} -m "Merge 'alpha' into 'master', but take 'alpha' tree" -p HEAD -p alpha)
Готово, мы смерджили ветку alpha в master, и нам не пришлось удалять временный код и решать конфликты, так как в данном случае нужно было просто перезаписать последние изменения.
На самом деле это не так уж и странно — создавать merge с деревом, которое копируется из другого коммита. Такие ситуации могут возникнуть, потому что git это очень гибкий инструмент, который позволяет реализовать самые разные подходы для работы с ветками и репозиториями. Но все же самый банальный пример был в нашем репозитории с самого начала — попробуйте его разобрать самостоятельно или откройте спойлер, чтобы прочитать объяснение.
Обратим внимание на то, как ветка beta была смерджена обратно в master. За то время, когда в ней появилось два коммита, в самой ветке master — новых коммитов не было. Это значит, что при мердже beta в master исключены какие-либо конфликты из-за одновременных изменений.
Если бы мы сделали git merge beta — то произошел бы fast-forward merge (поведение по умолчанию), то есть ветка master просто встала бы на том же коммите, где находится ветка beta, и никакого мердж коммита не было бы. Было бы вот так:
Но тут был сделан no-fast-forward merge с помощью команды git merge beta --no-ff. То есть мы форсировали создание мердж коммита, хотя он был и не обязателен. И так как нужное конечное состояние проекта для будущего мерджа было известно — это дерево ветки beta, то git просто скопировал ссылку на это дерево в новый коммит:
git cat-file -p origin/beta
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a <---
parent 560b449675513bc8f8f4d6cda56a922d4e36917a
author Baur <atanov...@gmail.com> 1540619512 +0600
committer Baur <atanov...@gmail.com> 1540619512 +0600
Added info about windows
git cat-file -p 7229df8
tree 3c1afe75f54518dbd82ea7a4e3c4ff50389a573a <---
parent fd54ab7dde87593b9892b6d1ffbf1afd39ba6f9e
parent 280c30ff81a574f8dd41721726cf60b22fb2eced
author Baur <atanov...@gmail.com> 1540619579 +0600
committer Baur <atanov...@gmail.com> 1540619592 +0600
Merge branch 'beta' into 'master'
6a. Метод rebase через merge — описание
Задача:
Нужно сделать "тяжелый" rebase ветки (множество конфликтов на разных коммитах).
Есть такая классическая холиварная тема в git — rebase vs merge. Но холиварить я не буду. Наоборот, я расскажу о том, как их можно подружить в контексте указанной задачи.
Вообще, git был специально спроектирован так, чтобы мы могли эффективно делать merge. И когда я пришел на проект, где воркфлоу основан на rebase, мне первое время было неудобно и непривычно, пока я не разработал техники, которые упростили мою каждодневную работу с git. Одна из них это мой оригинальный метод для осуществления тяжелого rebase-а.
Итак, нам нужно сделать rebase ветки alpha на develop, чтобы потом смерджить ее так же красиво, как была смерджена ветка beta. Если мы как обычно начнем делать rebase, первый и последний коммит породят два разных конфликта в двух разных местах. Но если бы мы вместо rebase просто делали merge, то был бы всего один конфликт в одном месте в одном мердже коммите.
Если хочется убедиться в этом, предлагаю готовые команды под спойлером.
Возвращаем ветки в исходное состояние
git checkout master
git reset --hard origin/master
git checkout alpha
git reset --hard origin/alpha
Создадим и встанем на ветку alpha-rebase-conflicts и сделаем ребейз на master
git checkout -b alpha-rebase-conflicts
git rebase master
Будут конфликты на разных коммитах, включая "фантомный" конфликт.
А теперь попробуем мердж, вернемся на ветку alpha и удалим ветку для ребейза.
git checkout alpha
git branch -D alpha-rebase-conflicts
Переключимся на master и сделаем мердж
git checkout master
git merge alpha
Будет всего один простой конфликт, исправляем его и делаем
git add Bill.txt
git commit -m "Merge branch 'alpha' into 'master'"
Мердж успешно завершен.
git-конфликты это естественная часть нашей жизни, и этот простой пример показывает, что merge в этом отношении однозначно удобнее rebase. В реальном проекте эта разница измеряется гораздо большим объемом потраченного времени и нервов. Поэтому, например, существует неоднозначная рекомендация сквошить перед мерджем все коммиты фича-ветки в один коммит.
Идея этого метода заключается в том, чтобы сделать временный скрытый merge, в котором мы за один раз разрешим все конфликты. Запомнить результат (tree). Далее запустить обычный rebase, но с опцией "автоматически разрешать конфликты, выбирая наши изменения". И в конце добавить на ветку один дополнительный коммит, который восстановит корректное дерево.
Приступим. Снова вернем обе ветки в исходное состояние.
git checkout master
git reset --hard origin/master
git checkout alpha
git reset --hard origin/alpha
Cоздадим временную ветку temp, в которой сделаем merge.
git checkout -b temp
git merge origin/master
Конфликт.
Резолвим один простой конфликт в файле Bill.txt как обычно (в любом редакторе).
Обратим внимание, что тут всего один конфликт, а не два, как при rebase.
git add Bill.txt
git commit -m "Merge branch 'origin/master' into 'temp'"
Возвращаемся на ветку alpha, делаем rebase с автоматическим разрешением всех конфликтов в нашу пользу, и приводим к состоянию ветки temp, а саму ветку temp удаляем.
git checkout alpha
git rebase origin/master -X theirs
git merge --ff $(git commit-tree temp^{tree} -m "Fix after rebase" -p HEAD)
git branch -D temp
И наконец красиво мерджим alpha в master.
git checkout master
git merge alpha --no-ff --no-edit
Отметим, что master, alpha и удаленная ветка temp — все три указывают на одно и то же дерево, хотя это три разных коммита.
Минусы данного метода:
- Нет ручного исправления каждого конфликтного коммита — конфликты решаются автоматом. Такой промежуточный коммит может и не скомпилироваться.
- Добавляется (но не всегда) дополнительный коммит при каждом rebase
Плюсы:
- Мы исправляем только реальные конфликты (никаких фантомных конфликтов)
- Исправление всех конфликтов происходит только один раз
- Первые два пункта дают экономию времени
- Сохраняется полная история коммитов и всех изменений (можно, например, сделать cherry-pick)
- Метод реализован в виде скрипта и может использоваться всегда при необходимости сделать rebase (при этом не требуется каких-либо знаний про деревья и прочее)
6b. Метод rebase через merge — скрипт
Скрипт опубликован здесь: https://github.com/capslocky/git-rebase-via-merge
Проверим его работу на нашем примере. Снова вернем обе ветки в исходное состояние
git checkout master
git reset --hard origin/master
git checkout alpha
git reset --hard origin/alpha
Скачиваем скрипт и делаем его исполняемым
curl -L https://git.io/rebase-via-merge -o ~/git-rebase-via-merge.sh
chmod +x ~/git-rebase-via-merge.sh
Файл появится тут: C:\Users\user-name\git-rebase-via-merge.sh
Меняем ветку по умолчанию, на которую нужно делать ребейз, в нашем случае нужен origin/master
nano ~/git-rebase-via-merge.sh
default_base_branch='origin/master'
Давайте также создадим и встанем на временную ветку, чтобы не трогать саму ветку alpha
git checkout -b alpha-rebase-test
И теперь можно запустить скрипт (вместо традиционного git rebase origin/master)
~/git-rebase-via-merge.sh
$ ~/git-rebase-via-merge.sh
This script will perform rebase via merge.
Current branch:
alpha-rebase-test (64fafc7)
Base branch:
origin/master (9c6b60a)
Continue (c) / Abort (a)
c
Auto-merging Bill.txt
CONFLICT (content): Merge conflict in Bill.txt
Automatic merge failed; fix conflicts and then commit the result.
You have at least one merge conflict.
Fix all conflicts in the following files, stage them up and type 'c':
Bill.txt
Continue (c) / Abort (a)
c
[detached HEAD 785d49e] Hidden temp commit to save result of merging 'origin/master' into 'alpha-rebase-test' as detached head.
Merge succeeded on hidden commit:
785d49e
Starting rebase automatically resolving any conflicts in favor of current branch.
First, rewinding head to replay your work on top of it...
Auto-merging Bill.txt
[detached HEAD a680316] Added history of windows
Author: Baur <atanov...@gmail.com>
Date: Sat Oct 27 11:45:50 2018 +0600
1 file changed, 6 insertions(+), 3 deletions(-)
Committed: 0001 Added history of windows
Auto-merging Bill.txt
[detached HEAD dcd34a8] Replaced history of windows
Author: Baur <atanov...@gmail.com>
Date: Sat Oct 27 11:55:42 2018 +0600
1 file changed, 4 insertions(+), 5 deletions(-)
Committed: 0002 Replaced history of windows
Auto-merging Bill.txt
[detached HEAD 8d6d82c] Added file about Azure and info about Windows 10
Author: Baur <atanov...@gmail.com>
Date: Sat Oct 27 12:06:27 2018 +0600
2 files changed, 5 insertions(+), 3 deletions(-)
create mode 100644 Azure.txt
Committed: 0003 Added file about Azure and info about Windows 10
All done.
Restoring project state from hidden merge with single additional commit.
Updating 8d6d82c..268b320
Fast-forward
Bill.txt | 4 ++++
1 file changed, 4 insertions(+)
Done.
Базовую ветку также можно указать явно
~/git-rebase-via-merge.sh origin/develop
Также стоит упомянуть, что ours / theirs в конфликтах становятся более интуитивными:
ours — это наши изменения на текущей ветке (HEAD)
theirs — это изменения из другой ветки (например, origin/develop)
В то время как при rebase — у них противоположное значение.
7. Алиас
С помощью следующего 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
Заключение
По правде говоря, из описанного я сам регулярно пользуюсь только данным методом для ребейза, очень хорошо помогает на текущем проекте. Иногда делаю сравнение веток, тегов и коммитов. Остальное — единичные случаи. Для большинства приведенные примеры, наверное, не будут актуальны. Тем не менее знать про коммиты и деревья будет всегда полезно. А приемчики "как украсть дерево" или "сделать ребейз без боли" могут однажды пригодится. Если есть идеи зачем еще можно красть деревья (в частности, в практиках devops), пишите, будет интересно обсудить.
Комментарии (12)
MisterParser
20.12.2018 22:13Спасибо. Я пока только читаю pro git, но и ваша статья очень кстати, правда я понял только самый первый случай про копирование дерева, а остальное пока для меня магия.
jcmvbkbc
21.12.2018 00:16Синхронизация с другой веткой
Можно сделать проще, командами checkout, merge и branch:
$ git checkout target -b merge # сделать ветку merge из целевой ветки target $ git merge -s ours source # смёржить в ветку merge исходную ветку source # игнорируя её содержимое $ git branch -d source # удалить ветку source $ git branch -m source # переименовать ветку merge в source
capslocky Автор
21.12.2018 09:01Нет, это уже будет другой способ синхронизации веток. Если выполнить эти команды вместо моей.
git checkout origin/beta -b merge git merge -s ours --no-edit alpha git branch -D alpha git branch -m alpha
То получим так:
Скрытый текстVolodar
21.12.2018 07:52Спасибо за описание подобных техник! На работе сейчас как частые откаты, реверсы и ребейзы, подчерпнул из статьи полезные для себя штуки, о которых даже не догадывался ранее. Завтра же опробую все на проекте.
SVlad
21.12.2018 15:20Синхронизация с другой веткой
А зачем лезть в недра гита и вручную модифицировать коммиты и деревья?
Разве не проще сделать через reset?
git checkout -b temp git reset --hard origin/beta git reset --soft alpha git commit git checkout alpha git merge --ff temp
Команд чуть больше, но все они в семантике git, без служебных команд.capslocky Автор
21.12.2018 15:46Да, так тоже можно сделать. Но это не очевидно, на самом деле. Не все точно представляют, как работают разные опции reset. А если человек знает, что состояние проекта в коммите — это всего лишь дерево, то такая задача становится тривиальной.
SVlad
21.12.2018 16:24Для меня, наоборот, reset — базовая функция гита, перенос указателя ветки на другой коммит в графе состояний — тривиальна и очевидна.
если человек знает, что состояние проекта в коммите — это всего лишь дерево
Знать, что коммит — это снимок состояния рабочей директории нужно и для reset. Это вообще одна из основы git. Но что бы воспользоваться rests, этих знаний достаточно, а вот что бы вручную создать коммит служебными утилитами, нужно ещё и знать о внутреннем устройстве, что излишне для нормальной работы с git.
SVlad
21.12.2018 17:02Можно даже проще, без hard reset:
git checkout -B temp origin/beta ## создаём новую ветку на beta git reset --soft alpha ## перемещаем указатель новой ветки на alpha, не изменяя содержимое рабочей директории git commit ## коммитим ветку на alpha с рабочей директорией от beta git checkout alpha git merge --ff temp
capslocky Автор
21.12.2018 20:08А если использовать stash, то можно даже не создавать временную ветку.
git checkout --detach origin/beta git reset --soft alpha git stash git checkout alpha git stash pop --index git commit -m "Synced with beta"
коммит — это снимок состояния рабочей директории
Именно поэтому когда впервые возникла такая необходимость я подумал: значит, должен существовать прямой и элегантный способ копирования этого снимка. И теперь я поделился этим способом со всеми читателями хабра.
koropovskiy
либо это делается простым reset на ветку beta.
либо правильнее написать «состояние проекта на ветке alpha содержит изменения ветки beta»
Если содержит, то включает ли получившийся коммит (Synced with branch beta) изменения «Added Linux Kernel» или нет?
capslocky Автор
reset переместит ветку alpha на тот же коммит, где находится ветка beta, при этом мы теряем все уникальные коммиты ветки alpha — в большинстве случаев это и требуется. Но в моем абстрактном примере ветки alpha и beta топологически остаются независимы.
Если говорить об изменениях, то можно сказать так: этот новый коммит ревертнул все уникальные изменения на ветке alpha и добавил все уникальные изменения из ветки beta (включая изменения коммита «Added Linux kernel»).
koropovskiy
Спасибо. теперь понятнее, да и остальные примеры стали яснее по механике.