Подробнее о git rebase

Добавлено 4 ноября 2021 в 02:48

Одно из основных преимуществ Git – возможность редактировать историю. В отличие от других систем контроля версий, которые рассматривают историю как священную запись, в git мы можем изменить историю в соответствии с нашими потребностями. Это дает нам множество мощных инструментов и позволяет вести хорошую историю коммитов точно так же, как мы используем рефакторинг для поддержания хороших практик разработки программного обеспечения. Эти инструменты могут немного напугать новичка или даже среднего пользователя git, но данное руководство поможет развенчать миф о мощном git-rebase.

Предупреждение

Как правило, не рекомендуется изменять историю общедоступных или стабильных веток. Редактировать историю функциональных веток и личных форков можно, а редактировать коммиты, которые вы еще не отправили на сервер, всегда нормально. Используйте git push -f, чтобы после редактирования ваших коммитов принудительно отправить изменения в личный форк или функциональную ветку.

Несмотря на страшное предупреждение, стоит отметить, что всё, что упоминается в данном руководстве, является неразрушающими операциями. На самом деле в git довольно сложно навсегда потерять данные. Как исправить ошибки, если вы совершите ошибки, рассказывается в конце этого руководства.

Настройка песочницы

Мы не хотим испортить ни один из ваших реальных репозиториев, поэтому в этом руководстве мы будем работать с репозиторием песочницы. Выполните следующие команды, чтобы начать:

git init /tmp/rebase-sandbox
cd /tmp/rebase-sandbox
git commit --allow-empty -m"Initial commit"

Мы добавляем пустой начальный коммит, чтобы упростить остальную часть руководства, потому что для перебазирования первоначального коммита вашего репозитория требуются специальные команды (а именно git rebase --root).

Если у вас возникнут проблемы, просто запустите rm -rf /tmp/rebase-sandbox и выполните эти шаги еще раз, чтобы начать всё сначала. Каждый шаг этого руководства можно выполнять в новой песочнице, поэтому нет необходимости повторно выполнять каждую задачу.

Содержание

Внесение изменений в ваш последний коммит

Начнем с простого: исправления вашего последнего коммита. Добавим файл в нашу песочницу – и ошибемся:

echo "Hello wrold!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

Исправить эту ошибку довольно просто. Мы можем просто отредактировать файл и закоммитить его с помощью --amend, например, так:

echo "Hello world!" >greeting.txt
git commit -a --amend

Указание -a автоматически индексирует (т.е. git add) все файлы, о которых git уже знает, а --amend помещает изменения в самый последний коммит. Сохраните и выйдите из текстового редактора (у вас есть возможность изменить сообщение коммита, если хотите). Вы можете увидеть исправленный коммит, запустив git show:

commit f5f19fbf6d35b2db37dcac3a55289ff9602e4d00 (HEAD -> master)
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun Apr 28 11:09:47 2019 -0400

    Add greeting.txt

diff --git a/greeting.txt b/greeting.txt
new file mode 100644
index 0000000..cd08755
--- /dev/null
+++ b/greeting.txt
@@ -0,0 +1 @@
+Hello world!

Исправление старых коммитов

Внесение поправок с --amend работает только для самого последнего коммита. Но что, если вам нужно исправить более старый коммит? Начнем с соответствующей настройки нашей песочницы:

echo "Hello!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

echo "Goodbye world!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"

Похоже, в файле greeting.txt отсутствует слово «world». Давайте обычно напишем коммит, который исправляет это:

echo "Hello world!" >greeting.txt
git commit -a -m"fixup greeting.txt"

Итак, теперь файлы выглядят правильно, но наша история могла бы быть лучше – давайте воспользуемся новым коммитом, чтобы «исправить» последний. Для этого нам нужно ввести новый инструмент: интерактивное перебазирование. Так как мы собираемся отредактировать последние три коммита, поэтому запустим git rebase -i HEAD~3 (-i для интерактивности). Это откроет ваш текстовый редактор с чем-то вроде этого:

pick 8d3fc77 Add greeting.txt
pick 2a73a77 Add farewell.txt
pick 0b9d0bb fixup greeting.txt

# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit's log message

Это план перебазирования, и, отредактировав этот файл, вы можете указать git, как редактировать историю. Я обрезал пояснение в файле до деталей, относящихся к этой части руководства по перебазированию, но не стесняйтесь прочитать его полностью.

Когда мы сохраняем и закрываем наш редактор, git удаляет все эти коммиты из своей истории, а затем поочередно выполняет каждую строку. По умолчанию он будет выбирать каждый коммит, брать (pick) его из кучи и добавлять в ветку. Если мы вообще не отредактируем этот файл, мы вернемся к тому месту, где начали, забирая каждый коммит как есть. Сейчас мы воспользуемся одной из моих любимых функций: исправление – fixup. Отредактируйте третью строку, чтобы изменить операцию с pick на fixup, и переместите ее сразу после коммита, который мы хотим «исправить»:

pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup greeting.txt
pick 2a73a77 Add farewell.txt

Совет

Мы также можем сократить это слово до f, чтобы ускорить процесс в следующий раз.

Сохраните и выйдите из редактора – git выполнит эти команды. Чтобы проверить результат, можно проверить журнал:

$ git log -2 --oneline
fcff6ae (HEAD -> master) Add farewell.txt
a479e94 Add greeting.txt

Использование git rebase --autosquash

Описанные выше шаги также можно выполнить более автоматизированным образом, воспользовавшись параметром --autosquash команды git rebase в сочетании с параметром --fixup команды git commit:

git commit -a --fixup HEAD^
git rebase -i --autosquash HEAD~3

Это подготовит план перебазирования с переупорядочением коммитов и настроенными действиями:

pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup! Add greeting.txt
pick 2a73a77 Add farewell.txt

# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit's log message

Помимо --fixup, также существует опция --squash для git commit, которая позволяет редактировать сообщение коммита.

Наконец, параметр --autosquash можно опустить, установив через конфигурацию это поведение поведением по умолчанию:

git config --global rebase.autosquash true

Объединение нескольких коммитов в один

В процессе работы вам может быть полезно писать много коммитов по мере прохождения небольших этапов или исправлять ошибки в предыдущих коммитах. Однако прежде чем объединять свою работу с веткой master, может быть полезно «сжать» (squash) эти коммиты вместе, чтобы сделать историю более чистой. Для этого воспользуемся операцией squash. Начнем с написания нескольких коммитов – просто скопируйте и вставьте следующий код, если хотите ускориться:

git checkout -b squash
for c in H e l l o , ' ' w o r l d; do
    echo "$c" >>squash.txt
    git add squash.txt
    git commit -m"Add '$c' to squash.txt"
done

Это создает много коммитов, создающих файл с текстом «Hello, world»! Давайте запустим еще одно интерактивное перебазирование, чтобы объединить их вместе. Обратите внимание, что мы сначала создали тестовую ветку и переключились на нее. Поскольку мы создали ветвление, мы можем быстро перебазировать все коммиты с помощью git rebase -i master. Результат:

pick 1e85199 Add 'H' to squash.txt
pick fff6631 Add 'e' to squash.txt
pick b354c74 Add 'l' to squash.txt
pick 04aaf74 Add 'l' to squash.txt
pick 9b0f720 Add 'o' to squash.txt
pick 66b114d Add ',' to squash.txt
pick dc158cd Add ' ' to squash.txt
pick dfcf9d6 Add 'w' to squash.txt
pick 7a85f34 Add 'o' to squash.txt
pick c275c27 Add 'r' to squash.txt
pick a513fd1 Add 'l' to squash.txt
pick 6b608ae Add 'd' to squash.txt

# Rebase 1af1b46..6b608ae onto 1af1b46 (12 commands)
#
# Commands:
# p, pick <commit> = use commit
# s, squash <commit> = use commit, but meld into previous commit

Совет

Ваша локальная ветка master развивается независимо от удаленной ветки master, а git сохраняет удаленную ветку как origin/master. В сочетании с этим трюком git rebase -i origin/master часто оказывается очень удобным способом перебазировать все коммиты, которые еще не были объединены с удаленной веткой!

Мы собираемся втиснуть все эти изменения в первый коммит. Для этого измените каждую операцию pick на squash, кроме первой строки, например:

pick 1e85199 Add 'H' to squash.txt
squash fff6631 Add 'e' to squash.txt
squash b354c74 Add 'l' to squash.txt
squash 04aaf74 Add 'l' to squash.txt
squash 9b0f720 Add 'o' to squash.txt
squash 66b114d Add ',' to squash.txt
squash dc158cd Add ' ' to squash.txt
squash dfcf9d6 Add 'w' to squash.txt
squash 7a85f34 Add 'o' to squash.txt
squash c275c27 Add 'r' to squash.txt
squash a513fd1 Add 'l' to squash.txt
squash 6b608ae Add 'd' to squash.txt

Когда вы сохраните файл и закроете редактор, git задумается на мгновение, а затем снова откроет редактор, чтобы проверить окончательное сообщение коммита. Вы увидите что-то вроде этого:

# This is a combination of 12 commits.
# This is the 1st commit message:

Add 'H' to squash.txt

# This is the commit message #2:

Add 'e' to squash.txt

# This is the commit message #3:

Add 'l' to squash.txt

# This is the commit message #4:

Add 'l' to squash.txt

# This is the commit message #5:

Add 'o' to squash.txt

# This is the commit message #6:

Add ',' to squash.txt

# This is the commit message #7:

Add ' ' to squash.txt

# This is the commit message #8:

Add 'w' to squash.txt

# This is the commit message #9:

Add 'o' to squash.txt

# This is the commit message #10:

Add 'r' to squash.txt

# This is the commit message #11:

Add 'l' to squash.txt

# This is the commit message #12:

Add 'd' to squash.txt

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
#    squash a513fd1 Add 'l' to squash.txt
#    squash 6b608ae Add 'd' to squash.txt
# No commands remaining.
# You are currently rebasing branch 'squash' on '1af1b46'.
#
# Changes to be committed:
#	new file:   squash.txt
#

По умолчанию это комбинация сообщений всех объединяемых коммитов, но оставлять это так – почти всегда не то, что вам нужно. Тем не менее, сообщения старых коммитов могут быть полезны для справки при написании нового.

Совет

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

Давайте удалим всё и заменим более подходящим сообщением о коммите, например:

Add squash.txt with contents "Hello, world"

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
#    squash a513fd1 Add 'l' to squash.txt
#    squash 6b608ae Add 'd' to squash.txt
# No commands remaining.
# You are currently rebasing branch 'squash' on '1af1b46'.
#
# Changes to be committed:
#	new file:   squash.txt
#

Сохраните и выйдите из редактора, затем проверьте журнал git – успех!

commit c785f476c7dff76f21ce2cad7c51cf2af00a44b6 (HEAD -> squash)
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun Apr 28 14:21:56 2019 -0400

    Add squash.txt with contents "Hello, world"

Прежде чем мы продолжим, давайте перенесем наши изменения в основную ветку и избавимся от этой пустой ветки. Мы можем использовать команду git rebase, как мы используем git merge, но она позволяет избежать коммита слияния:

git checkout master
git rebase squash
git branch -D squash

Обычно мы предпочитаем избегать использования git merge, если только мы не объединяем несвязанные истории. Если у вас есть две расходящиеся ветки, git merge полезна для записи того, когда они были... слиты. В ходе вашей обычной работы перебазирование часто более уместно.

Разделение одного коммита на несколько

Иногда возникает обратная задача – один коммит слишком большой. Давайте разберемся с этим. На этот раз давайте напишем реальный код. Начните с простой программы на C (для ускорения вы можете скопировать и вставить этот фрагмент в свою консоль):

cat <<EOF >main.c
int main(int argc, char *argv[]) {
    return 0;
}
EOF

Закоммитим это первым.

git add main.c
git commit -m"Add C program skeleton"

Далее немного расширим программу:

cat <<EOF >main.c
#include <stdio.h>

const char *get_name() {
    static char buf[128];
    scanf("%s", buf);
    return buf;
}

int main(int argc, char *argv[]) {
    printf("What's your name? ");
    const char *name = get_name();
    printf("Hello, %s!\n", name);
    return 0;
}
EOF

После того, как мы закоммитили и это, мы будем готовы узнать, как его разделить.

git commit -a -m"Flesh out C program"

Первый шаг – запустить интерактивное перебазирование. Давайте перебазируем оба коммита с помощью git rebase -i HEAD~2, дающей нам следующий план перебазирования:

pick 237b246 Add C program skeleton
pick b3f188b Flesh out C program

# Rebase c785f47..b3f188b onto c785f47 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# e, edit <commit> = use commit, but stop for amending

Измените команду второго коммита с pick на edit, затем сохраните и закройте редактор. Git задумается на секунду, а затем представит вам следующее:

Stopped at b3f188b...  Flesh out C program
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

Мы могли бы следовать этим инструкциям, чтобы добавить новые изменения в коммит, но вместо этого сделаем «мягкий сброс», запустив git reset HEAD^.

Примечание

На самом деле это «смешанный сброс». «Мягкий сброс» (выполненный с помощью git reset --soft) сохранит изменения поэтапно, Поэтому вам не нужно будет добавлять их снова, и вы сможете закоммитить их все за один раз. Но это не то, что здесь нам нужно. Мы хотим выборочно добавить в индекс части изменений, чтобы разделить коммит.

Если после этого вы запустите git status, то увидите, что он отменяет последний коммит и добавляет его изменения в рабочий каталог:

Last commands done (2 commands done):
   pick 237b246 Add C program skeleton
   edit b3f188b Flesh out C program
No commands remaining.
You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
  (Once your working directory is clean, run "git rebase --continue")

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)

  modified:   main.c

no changes added to commit (use "git add" and/or "git commit -a")

Чтобы разделить его, мы собираемся использовать интерактивный режим команды commit. Он позволяет нам выборочно фиксировать только определенные изменения из рабочего каталога. Запустите git commit -p, чтобы запустить этот процесс, и вы увидите следующее приглашение:

diff --git a/main.c b/main.c
index b1d9c2c..3463610 100644
--- a/main.c
+++ b/main.c
@@ -1,3 +1,14 @@
+#include <stdio.h>
+
+const char *get_name() {
+    static char buf[128];
+    scanf("%s", buf);
+    return buf;
+}
+
 int main(int argc, char *argv[]) {
+    printf("What's your name? ");
+    const char *name = get_name();
+    printf("Hello, %s!\n", name);
     return 0;
 }
Stage this hunk [y,n,q,a,d,s,e,?]? 

Git представил вам только один «кусок» (то есть одно изменение), которое следует рассмотреть. Однако он слишком большой – давайте воспользуемся командой s, чтобы «разделить» (split) этот кусок на более мелкие части.

Split into 2 hunks.
@@ -1 +1,9 @@
+#include <stdio.h>
+
+const char *get_name() {
+    static char buf[128];
+    scanf("%s", buf);
+    return buf;
+}
+
 int main(int argc, char *argv[]) {
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? 

Совет

Если вас интересуют другие варианты, нажмите ?, чтобы получить о них краткую информацию.

Этот кусок выглядит лучше – единичное, изолированное изменение. Давайте нажмем y, чтобы ответить на вопрос (и добавить этот «кусок» в индекс), затем q, чтобы «выйти» (quit) из интерактивного сеанса и продолжить работу с коммитом. Откроется ваш текстовый редактор, чтобы попросить вас ввести подходящее сообщение коммита.

Add get_name function to C program

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto c785f47
# Last commands done (2 commands done):
#    pick 237b246 Add C program skeleton
#    edit b3f188b Flesh out C program
# No commands remaining.
# You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
#
# Changes to be committed:
#	modified:   main.c
#
# Changes not staged for commit:
#	modified:   main.c
#

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

git commit -a -m"Prompt user for their name"
git rebase --continue

Эта последняя команда сообщает git, что мы закончили редактирование этого коммита и переходим к следующей команде rebase. Вот и всё! Запустите git log, чтобы увидеть, что получилось:

$ git log -3 --oneline
fe19cc3 (HEAD -> master) Prompt user for their name
659a489 Add get_name function to C program
237b246 Add C program skeleton

Изменение порядка коммитов

Это довольно просто. Начнем с настройки нашей песочницы:

echo "Goodbye now!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"

echo "Hello there!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

echo "How're you doing?" >inquiry.txt
git add inquiry.txt
git commit -m"Add inquiry.txt"

Вывод git log теперь должен выглядеть так:

f03baa5 (HEAD -> master) Add inquiry.txt
a4cebf7 Add greeting.txt
90bb015 Add farewell.txt

Ясно, что всё это идет не по порядку. Давайте выполним интерактивное перебазирование последних 3 коммитов, чтобы решить эту проблему. Запустите git rebase -i HEAD~3, и появится следующий план перебазирования:

pick 90bb015 Add farewell.txt
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt

# Rebase fe19cc3..f03baa5 onto fe19cc3 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
#
# These lines can be re-ordered; they are executed from top to bottom.

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

pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
pick 90bb015 Add farewell.txt

Сохраните и закройте редактор, а всё остальное Git сделает за вас. Обратите внимание, что на практике это может привести к конфликтам – о разрешении конфликтов читайте дальше.

git pull --rebase

Если вы писали какие-то коммиты в ветке <branch>, которая была обновлена ​, скажем, в удаленном источнике origin, обычно git pull создает коммит слияния. В этом отношении поведение git pull по умолчанию эквивалентно:

git fetch origin <branch>
git merge origin/<branch>

Это предполагает, что локальная ветвь <branch> настроена для отслеживания ветки <branch> с удаленного источника origin, то есть:

$ git config branch.<branch>.remote
origin
$ git config branch.<branch>.merge
refs/heads/<branch>

Есть еще один вариант, который часто бывает более полезным и ведет к гораздо более чистой истории: git pull --rebase. В отличие от подхода слияния, это в основном эквивалентно следующему:

git fetch origin
git rebase origin/<branch>

Подход merge проще и понятнее, но подход rebase почти всегда – это то, что вам нужно, если вы понимаете, как использовать git rebase. Если хотите, вы можете установить его как поведение по умолчанию, например:

git config --global pull.rebase true

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

Использование git rebase для... перебазирования

По иронии судьбы, функция git rebase, которую я использую меньше всего, – это та, в честь которой она названа: перебазирование веток. Скажем, у вас есть следующие ветки:

A--B--C--D--> master
   \--E--F--> feature-1
      \--G--> feature-2

Оказывается, feature-2 не зависит от каких-либо изменений в feature-1, то есть от коммита E; поэтому вы можете просто перебазировать ее на master. Это можно сделать так:

git rebase --onto master feature-1 feature-2

Неинтерактивное перебазирование выполняет для всех вовлеченных коммитов операцию по умолчанию (pick), которая просто воспроизводит коммиты в feature-2, которых нет в feature-1, поверх master. Ваша история теперь выглядит так:

A--B--C--D--> master
   |     \--G--> feature-2
   \--E--F--> feature-1

Разрешение конфликтов

Подробности разрешения конфликтов слияния выходят за рамки этого руководства. Предполагая, что вы знакомы с разрешением конфликтов в целом, вот особенности, относящиеся к перемещению.

Иногда при перебазировании возникает конфликт слияния, с которым можно справиться так же, как и с любым другим конфликтом слияния. Git установит маркеры конфликтов в затронутых файлах, git status покажет вам, что вам нужно разрешить, и вы можете пометить файлы как разрешенные с помощью git add или git rm. Однако в контексте git rebase есть несколько вариантов, о которых вам следует знать.

Во-первых, как завершить разрешение конфликта. Вместо git commit, которую вы будете использовать при разрешении конфликтов, возникающих в git merge, подходящей командой для перебазирования будет git rebase --continue. Однако есть еще один вариант: git rebase --skip. Это пропустит коммит, над которым вы работаете, и он не будет включен в перебазирование. Это наиболее часто встречающийся вариант при выполнении неинтерактивного перебазирования, когда git не понимает, что коммит, который он извлек из «другой» ветки, является обновленной версией коммита, с которым он конфликтует в «нашей» ветке.

Если вы хотите взглянуть на разницу в коммитах, которую Git не удалось воспроизвести, и которая привела к конфликту слияния, вы можете использовать git rebase --show-current-patch или его эквивалент git show REBASE_HEAD.

Наконец, стоит отметить, что при использовании git checkout --ours или --theirs для быстрого разрешения конфликтующих путей путем извлечения определенной версии из индекса значение этих параметров меняется на противоположное по сравнению с обычным конфликтом слияния git merge: во время перебазирования --theirs относится к изменениям из перебазируемой ветки (REBASE_HEAD), а --ours относится к изменениям из ветки, на которую мы перебазируем (HEAD).

Помогите! Я всё сломал!

Вне всяких сомнений, иногда бывает сложно выполнить перебазирование. Если вы допустили ошибку и при этом потеряли нужные коммиты, то git reflog поможет вам сэкономить время. Выполнение этой команды покажет вам каждую операцию, которая изменила ref (ссылки), то есть ветки и теги. Каждая строка показывает вам, на что указывала старая ссылка, и вы можете использовать git cherry-pick, git checkout, git show или любую другую операцию для коммитов git, которых считали потерянным.

Теги

Gitgit mergegit rebaseКонфликт слиянияПеребазированиеСистемы контроля версий

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.