Разбить предыдущий коммит на несколько коммитов

1225

Без создания ветки и выполнения какой-то сложной работы над новой веткой, возможно ли разбить один коммит на несколько разных коммитов после его фиксации в локальном репозитории?

koblas
источник
36
Хорошим источником для изучения того, как это сделать, является Pro Git §6.4 Git Tools - Переписывание истории , в разделе «Разделение коммита».
2
Документы, ссылки на которые приведены выше, превосходны и лучше объяснены, чем ответы ниже.
Blaisorblade
2
Я предлагаю использовать этот псевдоним stackoverflow.com/a/19267103/301717 . Это позволяет разделить коммит, используяgit autorebase split COMMIT_ID
Jérôme Pouiller
Проще всего обойтись без интерактивного перебазирования - это (вероятно) создать новую ветвь, начинающуюся с коммита до того, который вы хотите разделить, cherry-pick -n за коммит, сбросить, спрятать, зафиксировать перемещение файла, повторно применить кэш и зафиксируйте изменения, а затем либо объединитесь с предыдущей веткой, либо выберите те коммиты, которые последовали. (Затем переключите прежнее имя ветки на текущий руководитель.) (Вероятно, лучше последовать совету MBO и сделать интерактивную перебазировку.) (Скопировано из ответа 2010 года ниже)
Уильям Перселл
1
Я столкнулся с этой проблемой после того, как случайно раздавил два коммита во время перебазирования в более раннем коммите. Мой способ , чтобы исправить это было проверка сжатую совершить, git reset HEAD~, git stash, то git cherry-pickпервым совершить в сквош, а затем git stash pop. Мой случай с вишней весьма специфичен, но git stashи git stash popвесьма удобен для других.
SOFe

Ответы:

1802

git rebase -i сделаю это.

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

Теперь вы должны решить, какие коммиты вы хотите разделить.

A) Разделение самого последнего коммита

Чтобы разделить ваш последний коммит, сначала:

$ git reset HEAD~

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

Б) Разделить коммит дальше назад

Это требует перебазирования , то есть переписывания истории. Чтобы найти правильный коммит, у вас есть несколько вариантов:

  • Если это было три коммитов назад, то

    $ git rebase -i HEAD~3
    

    где 3сколько коммитов это обратно.

  • Если это было дальше в дереве, чем вы хотите сосчитать, то

    $ git rebase -i 123abcd~
    

    где 123abcdSHA1 коммита, который вы хотите разделить.

  • Если вы находитесь в другой ветке (например, ветке функций), которую вы планируете объединить в master:

    $ git rebase -i master
    

Когда вы получите экран редактирования rebase, найдите коммит, который вы хотите разбить на части. В начале этой строки заменить pickна edit( eдля краткости). Сохраните буфер и выйдите. Rebase теперь остановится сразу после коммита, который вы хотите отредактировать. Затем:

$ git reset HEAD~

Зафиксируйте фрагменты индивидуально обычным способом, выполнив столько коммитов, сколько вам нужно, затем

$ git rebase --continue
Уэйн Конрад
источник
2
Спасибо за этот ответ. Я хотел, чтобы некоторые предварительно зафиксированные файлы находились в области подготовки, поэтому инструкции для меня были немного другими. Прежде , чем я мог git rebase --continue, я на самом деле был git add (files to be added), git commit, а затем git stash(для остальных файлов). После git rebase --continueэтого я git checkout stash .получал оставшиеся файлы
Эрик Ху,
18
Ответ manojlds на самом деле содержит эту ссылку на документацию по git-scm , которая также очень четко объясняет процесс разделения коммитов.
56
Вы также захотите воспользоваться преимуществом git add -pдобавления только частичных разделов файлов, возможно, с eвозможностью редактирования различий, чтобы зафиксировать только часть фрагмента. git stashтакже полезно, если вы хотите продолжить работу, но удалите ее из текущего коммита.
Крейг Рингер
2
Если вы хотите разделить и изменить порядок коммитов, сначала мне нужно разделить, а затем изменить порядок отдельно, используя другую git rebase -i HEAD^3команду. Таким образом, если раскол пойдет не так, вам не придется отменять столько работы.
Дэвид М. Ллойд
4
@kralyk Файлы, которые были недавно зафиксированы в HEAD, останутся на диске после git reset HEAD~. Они не потеряны.
Уэйн Конрад
312

Из руководства git- rebase (раздел SPLITTING COMMITS)

В интерактивном режиме вы можете пометить коммиты с помощью действия «изменить». Однако это не обязательно означает, что git rebase ожидает, что результатом этого редактирования будет ровно один коммит. Действительно, вы можете отменить коммит или добавить другие коммиты. Это можно использовать для разделения коммита на две части:

  • Начните интерактивную перебазировку git rebase -i <commit>^, где <commit>находится коммит, который вы хотите разделить. Фактически, подойдет любой диапазон фиксации, если он содержит этот коммит.

  • Пометьте коммит, который вы хотите разделить, с помощью действия «изменить».

  • Когда дело доходит до редактирования этого коммита, выполняйте git reset HEAD^. В результате HEAD перематывается на единицу, и индекс следует за ним. Однако рабочее дерево остается прежним.

  • Теперь добавьте изменения в индекс, которые вы хотите иметь при первом коммите. Вы можете использовать git add(возможно, в интерактивном режиме) или git gui(или оба), чтобы сделать это.

  • Зафиксируйте текущий текущий индекс с любым соответствующим сообщением.

  • Повторяйте последние два шага, пока ваше рабочее дерево не станет чистым.

  • Продолжите ребаз с git rebase --continue.

MBO
источник
12
На Windows вы используете ~вместо ^.
Кевин Кушик
13
Слово предостережения: при таком подходе я потерял сообщение коммита.
user420667
11
@ user420667 Да, конечно. Мы делаем resetкоммит, в конце концов - сообщение включено. Если вы знаете, что собираетесь разбивать коммит, но хотите сохранить некоторые / все его сообщения, разумно сделать копию этого сообщения. Итак, git showсделайте коммит перед rebaseзагрузкой, или, если вы забыли или предпочли это: вернитесь к нему позже через reflog. Ничто из этого на самом деле не будет «потеряно», пока не будет собрано мусор через 2 недели или что-то еще.
underscore_d
4
~и ^это разные вещи, даже на Windows. Вы по-прежнему хотите использовать каретку ^, поэтому вам нужно просто избежать ее в соответствии с вашей оболочкой. В PowerShell это так HEAD`^. С cmd.exe вы можете удвоить его, чтобы сбежать, как HEAD^^. В большинстве (всех?) Оболочек можно заключать в кавычки, например "HEAD^".
AndrewF
7
Вы также можете сделать git commit --reuse-message=abcd123. Краткий вариант для этого есть -C.
j0057
41

Используйте, git rebase --interactiveчтобы отредактировать этот предыдущий коммит, запустить git reset HEAD~, а затем git add -pдобавить некоторые, затем сделать коммит, затем добавить еще несколько и сделать еще один коммит столько раз, сколько захотите. Когда вы закончите, запустите git rebase --continue, и у вас будут все коммиты split ранее в вашем стеке.

Важно : обратите внимание, что вы можете поиграть и внести все необходимые изменения, и вам не нужно беспокоиться о потере старых изменений, потому что вы всегда можете запустить, git reflogчтобы найти точку в вашем проекте, которая содержит требуемые изменения (давайте назовем это a8c4ab) , а затем git reset a8c4ab.

Вот серия команд, чтобы показать, как это работает:

mkdir git-test; cd git-test; git init

Теперь добавьте файл A

vi A

добавьте эту строку:

one

git commit -am one

затем добавьте эту строку в A:

two

git commit -am two

затем добавьте эту строку в A:

three

git commit -am three

Теперь файл A выглядит так:

one
two
three

и наш git logвыглядит следующим образом (ну, я используюgit log --pretty=oneline --pretty="%h %cn %cr ---- %s"

bfb8e46 Rose Perrone 4 seconds ago ---- three
2b613bc Rose Perrone 14 seconds ago ---- two
9aac58f Rose Perrone 24 seconds ago ---- one

Допустим, мы хотим разделить второй коммит two.

git rebase --interactive HEAD~2

Это вызывает сообщение, которое выглядит так:

pick 2b613bc two
pick bfb8e46 three

Измените первый pickна a eдля редактирования этого коммита.

git reset HEAD~

git diff показывает нам, что мы только что сделали unstaged обязательство, которое мы сделали для второго коммита:

diff --git a/A b/A
index 5626abf..814f4a4 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two

Давайте внесем это изменение и добавим «и третий» к этой строке в файле A.

git add .

Это обычно точка во время интерактивного перебазирования, где мы запускаем git rebase --continue, потому что мы обычно просто хотим вернуться в наш стек коммитов, чтобы отредактировать более ранний коммит. Но на этот раз мы хотим создать новый коммит. Итак, мы побежим git commit -am 'two and a third'. Теперь мы редактируем файл Aи добавляем строку two and two thirds.

git add . git commit -am 'two and two thirds' git rebase --continue

У нас есть конфликт с нашим коммитом three, поэтому давайте разрешим его:

Мы изменим

one
<<<<<<< HEAD
two and a third
two and two thirds
=======
two
three
>>>>>>> bfb8e46... three

в

one
two and a third
two and two thirds
three

git add .; git rebase --continue

Теперь наш git log -pвыглядит так:

commit e59ca35bae8360439823d66d459238779e5b4892
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 13:57:00 2013 -0700

    three

diff --git a/A b/A
index 5aef867..dd8fb63 100644
--- a/A
+++ b/A
@@ -1,3 +1,4 @@
 one
 two and a third
 two and two thirds
+three

commit 4a283ba9bf83ef664541b467acdd0bb4d770ab8e
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 14:07:07 2013 -0700

    two and two thirds

diff --git a/A b/A
index 575010a..5aef867 100644
--- a/A
+++ b/A
@@ -1,2 +1,3 @@
 one
 two and a third
+two and two thirds

commit 704d323ca1bc7c45ed8b1714d924adcdc83dfa44
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 14:06:40 2013 -0700

    two and a third

diff --git a/A b/A
index 5626abf..575010a 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two and a third

commit 9aac58f3893488ec643fecab3c85f5a2f481586f
Author: Rose Perrone <roseperrone@fake.com>
Date:   Sun Jul 7 13:56:40 2013 -0700

    one

diff --git a/A b/A
new file mode 100644
index 0000000..5626abf
--- /dev/null
+++ b/A
@@ -0,0 +1 @@
+one
Роза Перроне
источник
39

В предыдущих ответах рассказывалось о том, git rebase -iкак редактировать коммит, который вы хотите разделить, и коммитить его по частям.

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

Получив коммит, который вы хотите разделить, используя rebase -iи пометив его edit, у вас есть два варианта.

  1. После использования git reset HEAD~, пройдитесь по патчам индивидуально, используя, git add -pчтобы выбрать те, которые вы хотите в каждом коммите.

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

Вариант 2 полезен, если вы разделяете большой коммит, так как он позволяет вам проверить, что промежуточные версии создаются и работают правильно как часть слияния. Это происходит следующим образом.

После использования rebase -iи editфиксации, используйте

git reset --soft HEAD~

отменить фиксацию, но оставить зафиксированные файлы в индексе. Вы также можете сделать смешанный сброс, пропустив --soft, в зависимости от того, насколько близок к конечному результату ваш первоначальный коммит. Разница лишь в том, начинаете ли вы со всех этапов изменений или со всеми без изменений.

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

Как только вы будете довольны, подготовьте / разархивируйте файлы по мере необходимости (я хотел бы использовать git guiдля этого) и зафиксируйте изменения через пользовательский интерфейс или командную строку

git commit

Это первый сделанный коммит. Теперь вы хотите восстановить свою рабочую копию в состояние, которое она имела после разделения коммита, чтобы вы могли принять больше изменений для вашего следующего коммита. Чтобы найти sha1 коммита, который вы редактируете, используйте git status. В первых нескольких строках состояния вы увидите команду rebase, которая выполняется в данный момент, в которой вы можете найти sha1 вашего исходного коммита:

$ git status
interactive rebase in progress; onto be83b41
Last commands done (3 commands done):
   pick 4847406 US135756: add debugging to the file download code
   e 65dfb6a US135756: write data and download from remote
  (see more in file .git/rebase-merge/done)
...

В этом случае редактируемый мной коммит имеет sha1 65dfb6a. Зная это, я могу проверить содержимое этого коммита в моем рабочем каталоге, используя форму, git checkoutкоторая принимает как коммит, так и местоположение файла. Здесь я использую .в качестве местоположения файла замену всей рабочей копии:

git checkout 65dfb6a .

Не пропустите точку на конце!

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

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

Если вы хотите повторно использовать исходное сообщение о коммите для одного или нескольких коммитов, вы можете использовать его прямо из рабочих файлов rebase:

git commit --file .git/rebase-merge/message

Наконец, как только вы совершите все изменения,

git rebase --continue

продолжит и завершит операцию перебазирования.

Энди Мортимер
источник
3
Спасибо!!! Это должен быть принятый ответ. Сэкономил бы мне сегодня много времени и боли. Это единственный ответ, когда результат окончательного коммита приводит вас в то же состояние, что и редактируемый коммит.
Даг Кобурн
1
Мне нравится, как вы используете оригинальное сообщение о коммите.
Саламандар
Используя вариант 2, когда я это делаю, git checkout *Sha I'm Editing* .он всегда говорит Updated 0 paths from *Some Sha That's Not In Git Log*и не дает никаких изменений.
Нумен
18

git rebase --interactiveможет использоваться для разделения коммита на коммиты меньшего размера. В документации Git по rebase есть краткое описание этого процесса - Splitting Commits :

В интерактивном режиме вы можете пометить коммиты с помощью действия «изменить». Однако это не обязательно означает, git rebaseчто в результате этого редактирования будет ровно один коммит. Действительно, вы можете отменить коммит или добавить другие коммиты. Это можно использовать для разделения коммита на две части:

  • Начните интерактивную перебазировку git rebase -i <commit>^, где <commit>находится коммит, который вы хотите разделить. Фактически, подойдет любой диапазон фиксации, если он содержит этот коммит.

  • Пометьте коммит, который вы хотите разделить, с помощью действия «изменить».

  • Когда дело доходит до редактирования этого коммита, выполняйте git reset HEAD^. В результате HEAD перематывается на единицу, и индекс следует за ним. Однако рабочее дерево остается прежним.

  • Теперь добавьте изменения в индекс, которые вы хотите иметь при первом коммите. Вы можете использовать git add(возможно, в интерактивном режиме) или git gui (или оба), чтобы сделать это.

  • Зафиксируйте текущий текущий индекс с любым соответствующим сообщением.

  • Повторяйте последние два шага, пока ваше рабочее дерево не станет чистым.

  • Продолжите ребаз с git rebase --continue.

Если вы не совсем уверены, что промежуточные ревизии являются согласованными (они компилируются, проходят тестовый набор и т. Д.), Вы должны использовать, git stashчтобы скрыть еще не зафиксированные изменения после каждой фиксации, тестирования и изменения фиксации, если необходимы исправления ,


источник
В Windows запомните ^это управляющий символ для командной строки: он должен быть удвоен. К примеру, вопрос git reset HEAD^^вместо git reset HEAD^.
Фредерик,
@ Фредерик: я никогда не сталкивался с этим. По крайней мере, в PowerShell это не так. Затем с помощью ^двойного сброса два коммита выше текущего заголовка.
Фарвей
@ Farway, попробуйте в классической командной строке. PowerShell - совсем другой зверь, его экранирующий символ - задняя часть.
Фредерик
Подводя итог: "HEAD^"в cmd.exe или PowerShell, HEAD^^в cmd.exe, HEAD`^в PowerShell. Полезно узнать о том, как работают оболочки - и ваша конкретная оболочка - (то есть, как команда превращается в отдельные части, которые передаются в программу), чтобы вы могли адаптировать команды онлайн в правильные символы для вашей конкретной оболочки. (Не
относится только
12

Теперь в последней версии TortoiseGit для Windows вы можете сделать это очень легко.

Откройте диалоговое окно rebase, настройте его и выполните следующие шаги.

  • Щелкните правой кнопкой мыши на коммите, который вы хотите разделить, и выберите « Edit» (среди выбора, сквоша, удаления ...).
  • Нажмите « Start», чтобы начать перебазирование.
  • Как только он прибудет в коммит для разделения, проверьте Edit/Splitкнопку " " и нажмите " Amend" напрямую. Откроется диалоговое окно фиксации.
    Редактировать / Разделить коммит
  • Отмените выбор файлов, которые вы хотите поместить в отдельный коммит.
  • Отредактируйте сообщение о коммите и нажмите « commit».
  • Пока не появятся файлы для фиксации, диалог фиксации будет открываться снова и снова. Когда больше нет файла для фиксации, он все равно спросит вас, хотите ли вы добавить еще один коммит.

Очень полезно, спасибо TortoiseGit!

Микаэль Майер
источник
Спасибо, это было действительно полезно для меня!
ivan.ukr
10

Вы можете сделать интерактивный ребаз git rebase -i. Справочная страница содержит именно то, что вы хотите:

http://git-scm.com/docs/git-rebase#_splitting_commits

manojlds
источник
14
Предоставить немного больше контекста о том, как подходить к решению проблем, чем просто дать RTFM, было бы немного более полезно.
Джордан Деа-Мэтсон
8

Обратите внимание, что есть также git reset --soft HEAD^. Это похоже на git reset(по умолчанию --mixed), но оно сохраняет содержимое индекса. Так что если вы добавили / удалили файлы, они уже есть в индексе.

Оказывается очень полезным в случае гигантских коммитов.

lethalman
источник
3

Вот как разделить один коммит в IntelliJ IDEA , PyCharm , PhpStorm и т. Д.

  1. В окне журнала контроля версий выберите коммит, который вы хотите разделить, щелкните правой кнопкой мыши и выберите Interactively Rebase from Here

  2. Отметьте тот, который вы хотите разделить edit, нажмитеStart Rebasing

  3. Вы должны увидеть желтую метку, означающую, что для HEAD установлена ​​эта фиксация. Щелкните правой кнопкой мыши на этом коммите, выберитеUndo Commit

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

bzuo
источник
2

Проще всего обойтись без интерактивного перебазирования - это (вероятно) создать новую ветвь, начинающуюся с коммита до того, который вы хотите разделить, cherry-pick -n за коммит, сбросить, спрятать, зафиксировать перемещение файла, повторно применить кэш и зафиксируйте изменения, а затем либо объединитесь с предыдущей веткой, либо выберите те коммиты, которые последовали. (Затем переключите прежнее имя ветки на текущий заголовок.) (Вероятно, лучше последовать совету MBO и сделать интерактивную перебазировку.)

Уильям Перселл
источник
в соответствии со стандартами SO в наши дни это следует квалифицировать как отсутствие ответа; но это все еще может быть полезно для других, поэтому, если вы не возражаете, пожалуйста, переместите это в комментарии к оригинальному сообщению
YakovL
@ ЯковЛ Кажется разумным. По принципу минимального действия я не буду удалять ответ, но я бы не стал возражать, если кто-то другой сделает это.
Уильям Перселл
это было бы намного проще, чем все rebase -iпредложения. Я думаю, что это не привлекло большого внимания из-за отсутствия какого-либо форматирования. Может быть, вы могли бы просмотреть его, теперь, когда у вас есть 126 тыс. Баллов и, вероятно, знаете, как это сделать. ;)
erikbwork
2

Я думаю, что лучший способ, которым я пользуюсь git rebase -i. Я создал видео, чтобы показать шаги для разделения коммита: https://www.youtube.com/watch?v=3EzOz7e1ADI

Нгуен Си Тхань Сон
источник
1

Если у вас есть это:

A - B <- mybranch

Где вы зафиксировали некоторый контент в коммите B:

/modules/a/file1
/modules/a/file2
/modules/b/file3
/modules/b/file4

Но вы хотите разделить B на C - D и получить такой результат:

A - C - D <-mybranch

Вы можете, например, разделить контент (контент из разных каталогов в разных коммитах) ...

Сбросьте ветку обратно до коммита перед тем, чтобы разделить:

git checkout mybranch
git reset --hard A

Создайте первый коммит (C):

git checkout B /modules/a
git add -u
git commit -m "content of /modules/a"

Создайте второй коммит (D):

git checkout B /modules/b
git add -u
git commit -m "content of /modules/b"
Мартин Г
источник
Что делать, если есть коммиты выше B?
CoolMind
1

Прошло уже более 8 лет, но, возможно, кто-то найдет это полезным в любом случае. Я был в состоянии сделать трюк без rebase -i. Идея состоит в том, чтобы привести git в то же состояние, в котором он находился до этого git commit:

# first rewind back (mind the dot,
# though it can be any valid path,
# for instance if you want to apply only a subset of the commit)
git reset --hard <previous-commit> .

# apply the changes
git checkout <commit-you-want-to-split>

# we're almost there, but the changes are in the index at the moment,
# hence one more step (exactly as git gently suggests):
# (use "git reset HEAD <file>..." to unstage)
git reset

После этого вы увидите это блестящее, Unstaged changes after reset:и ваш репо находится в состоянии, как будто вы собираетесь зафиксировать все эти файлы. Отныне вы можете легко совершить это снова, как обычно. Надеюсь, поможет.

Станислав Евгеньевич Говоров
источник
0

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

git rebase -i <sha1_before_split>
# mark the targeted commit with 'edit'
git reset HEAD^
git add ...
git commit -m "First part"
git add ...
git commit -m "Second part"
git rebase --continue

Кредиты к сообщению в блоге Эммануила Бернарда .

Sparkofska
источник