trees


В этой статье я расскажу об одном полезном, но малоизвестном приеме работы с 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

Все команды, включая скрипт, работают и под 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. Есть два классических способа это сделать:


  1. Два раза выполнить git revert — на каждый коммит
  2. 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

windows

Файл появится тут: 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)


  1. koropovskiy
    20.12.2018 13:45
    +1

    Готово. Теперь состояние проекта на ветке alpha в точности такое же, как на ветке beta.

    либо это делается простым reset на ветку beta.
    либо правильнее написать «состояние проекта на ветке alpha содержит изменения ветки beta»

    Если содержит, то включает ли получившийся коммит (Synced with branch beta) изменения «Added Linux Kernel» или нет?


    1. capslocky Автор
      20.12.2018 14:10

      reset переместит ветку alpha на тот же коммит, где находится ветка beta, при этом мы теряем все уникальные коммиты ветки alpha — в большинстве случаев это и требуется. Но в моем абстрактном примере ветки alpha и beta топологически остаются независимы.

      Если говорить об изменениях, то можно сказать так: этот новый коммит ревертнул все уникальные изменения на ветке alpha и добавил все уникальные изменения из ветки beta (включая изменения коммита «Added Linux kernel»).


      1. koropovskiy
        20.12.2018 16:24

        Спасибо. теперь понятнее, да и остальные примеры стали яснее по механике.


  1. MisterParser
    20.12.2018 22:13

    Спасибо. Я пока только читаю pro git, но и ваша статья очень кстати, правда я понял только самый первый случай про копирование дерева, а остальное пока для меня магия.


  1. 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
    


    1. 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
      

      То получим так:
      Скрытый текст


  1. Volodar
    21.12.2018 07:52

    Спасибо за описание подобных техник! На работе сейчас как частые откаты, реверсы и ребейзы, подчерпнул из статьи полезные для себя штуки, о которых даже не догадывался ранее. Завтра же опробую все на проекте.


  1. 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, без служебных команд.


    1. capslocky Автор
      21.12.2018 15:46

      Да, так тоже можно сделать. Но это не очевидно, на самом деле. Не все точно представляют, как работают разные опции reset. А если человек знает, что состояние проекта в коммите — это всего лишь дерево, то такая задача становится тривиальной.


      1. SVlad
        21.12.2018 16:24

        Для меня, наоборот, reset — базовая функция гита, перенос указателя ветки на другой коммит в графе состояний — тривиальна и очевидна.

        если человек знает, что состояние проекта в коммите — это всего лишь дерево

        Знать, что коммит — это снимок состояния рабочей директории нужно и для reset. Это вообще одна из основы git. Но что бы воспользоваться rests, этих знаний достаточно, а вот что бы вручную создать коммит служебными утилитами, нужно ещё и знать о внутреннем устройстве, что излишне для нормальной работы с git.


      1. 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 
        


        1. 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"
          

          коммит — это снимок состояния рабочей директории
          Именно поэтому когда впервые возникла такая необходимость я подумал: значит, должен существовать прямой и элегантный способ копирования этого снимка. И теперь я поделился этим способом со всеми читателями хабра.