Переместите самые последние коммиты в новую ветку с помощью Git


4986

Я хотел бы переместить последние несколько коммитов, которые я совершил, чтобы освоить, в новую ветку и вернуть мастер до того, как эти коммиты были сделаны. К сожалению, мой Git-fu еще недостаточно силен, какая-нибудь помощь?

Т.е. как можно из этого выйти

master A - B - C - D - E

к этому?

newbranch     C - D - E
             /
master A - B 

114
Примечание: Я задал противоположный вопрос здесь
Benjol


7
Были ли комментарии здесь удалены? Я спрашиваю, потому что во время моего двухмесячного посещения этого вопроса я всегда прокручиваю этот комментарий.
Теджас Кале

Ответы:


6453

Переезд в существующую ветку

Если вы хотите переместить ваши коммиты в существующую ветку , это будет выглядеть так:

git checkout existingbranch
git merge master         # Bring the commits here
git checkout master
git reset --keep HEAD~3  # Move master back by 3 commits.
git checkout existingbranch

--keepОпция сохраняет любые неподтвержденные изменения , которые могут возникнуть в несвязанных файлах, или откатится , если эти изменения должны быть перезаписаны - аналогично тому , что git checkoutделает. Если он прерывается, внесите git stashизменения и повторите попытку или используйте, --hardчтобы потерять изменения (даже из файлов, которые не изменились между фиксациями!)

Переезд в новую ветку

Этот метод работает, создавая новую ветку с первой командой ( git branch newbranch), но не переключаясь на нее. Затем мы откатываем текущую ветку (master) и переключаемся на новую ветку, чтобы продолжить работу.

git branch newbranch      # Create a new branch, containing all current commits
git reset --keep HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
git checkout newbranch    # Go to the new branch that still has the desired commits
# Warning: after this it's not safe to do a rebase in newbranch without extra care.

Но удостоверьтесь, сколько человек вернется. В качестве альтернативы, вместо того HEAD~3, чтобы просто указать хеш коммита (или ссылку origin/master), к которой вы хотите вернуться, например:

git reset --keep a1b2c3d4

ВНИМАНИЕ: В Git версии 2.0 и более поздних, если вы позже git rebaseдобавите новую ветку к исходной ( master) ветке, вам может понадобиться явная --no-fork-pointопция во время перебазирования, чтобы избежать потери коммитов, перемещенных вами из главной ветки. Имея branch.autosetuprebase alwaysнабор делает это более вероятно. См . Ответ Джона Меллора для деталей.


249
И, в частности, не пытайтесь вернуться дальше, чем тот момент, когда вы в последний раз выдавали коммиты в другой репозиторий, из которого кто-то другой мог вытащить.
Грег Хьюгилл

107
Хотите знать, если вы можете объяснить, почему это работает. Для меня вы создаете новую ветку, удаляете 3 коммита из старой ветки, в которой вы все еще находитесь, и затем проверяете ветку, которую вы создали. Итак, как коммиты, которые вы удалили, волшебным образом отображаются в новой ветке?
Джонатан Думайн

142
@Jonathan Dumaine: Потому что я создал новую ветку перед удалением коммитов из старой ветки. Они все еще там в новой ветке.
Сыкора

86
ветви в git - это просто маркеры, которые указывают на коммиты в истории, ничего не клонируется, не создается и не удаляется (кроме маркеров)
knittl

218
Также обратите внимание: не делайте этого с незафиксированными изменениями в вашей рабочей копии! Это только укусило меня! :(
Адам Таттл

1039

Для тех, кто интересуется, почему это работает (как я был сначала):

Вы хотите вернуться к C и переместить D и E в новую ветвь. Вот как это выглядит на первый взгляд:

A-B-C-D-E (HEAD)
        ↑
      master

После git branch newBranch:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

После git reset --hard HEAD~2:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

Поскольку ветвь является просто указателем, мастер указал на последний коммит. Когда вы сделали newBranch , вы просто сделали новый указатель на последний коммит. Затем с помощью git resetвы переместили главный указатель назад на две фиксации. Но так как вы не переместили newBranch , он все еще указывает на коммит, который он сделал изначально.


56
Мне также нужно было сделать, git push origin master --forceчтобы изменения отображались в основном репозитории.
Дженан

9
Этот ответ приводит к потере коммитов: в следующий раз вы git rebaseбудете молча отбрасывать 3 коммита newbranch. Смотрите мой ответ для деталей и более безопасных альтернатив.
Джон Меллор

14
@ Джон, это чепуха. Перебазирование без знания того, что вы делаете, приводит к потере коммитов. Если вы потеряли коммиты, мне жаль вас, но этот ответ не потерял ваши коммиты. Обратите внимание, что origin/masterне отображается на диаграмме выше. Если вы нажмете, origin/masterа затем внесете изменения выше, конечно, все будет смешно. Но это проблема «Доктор, мне больно, когда я делаю это». И это выходит за рамки того, что задал первоначальный вопрос. Я предлагаю вам написать свой вопрос, чтобы изучить свой сценарий, а не угонять этот.
Райан Ланди

1
@ Джон, в своем ответе ты сказал: «Не делай этого! git branch -t newbranch». Вернитесь и прочитайте ответы снова. Никто не предлагал это сделать.
Райан Ланди

1
@Kyralessa, конечно, но если вы посмотрите на диаграмму в вопросе, newbranchстанет ясно, что они хотят базироваться на существующей локальной masterветке. После выполнения обслуживаемого ответа, когда пользователь получает вокруг работает git rebaseв newbranch, мерзавец будет напоминать им , что они забыли установить вверх по течению ветви, так что они будут работать , git branch --set-upstream-to=masterто git rebaseи та же проблема. Они могут также использовать git branch -t newbranchв первую очередь.
Джон Меллор

455

В основном...

Метод, раскрытый Sykora, является лучшим вариантом в этом случае. Но иногда это не самый простой и не общий метод. Для общего метода используйте git cherry-pick :

Чтобы достичь того, чего хочет OP, это двухэтапный процесс:

Шаг 1 - Обратите внимание, какой коммит от мастера вы хотите на newbranch

казнить

git checkout master
git log

Обратите внимание на хеши (скажем, 3) коммитов, на которые вы хотите newbranch. Здесь я буду использовать:
C commit: 9aa1233
D commit: 453ac3d
E commit:612ecb3

Примечание: вы можете использовать первые семь символов или весь хеш коммита

Шаг 2 - Положите их на newbranch

git checkout newbranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

ИЛИ (на Git 1.7.2+, используйте диапазоны)

git checkout newbranch
git cherry-pick 612ecb3~1..9aa1233

git cherry-pick применяет эти три коммита к newbranch.


13
Разве ОП не пытался перейти от мастера к новому отделению? Если вы выбираете вишню на мастер-сервере, вы добавляете в ветку master - фактически добавляете коммиты, которые у него уже были. И это не сдвигает назад ни одну ветвь головы к B. Или есть что-то тонкое и классное, которое я не получаю?
RaveTheTadpole

6
Это работает очень хорошо, если вы случайно фиксируете неправильную ветвь, не являющуюся главной, когда вы должны были создать новую ветвь функций.
Julianc

5
+1 за полезный подход в некоторых ситуациях. Это хорошо, если вы хотите только вставить свои собственные коммиты (которые чередуются с другими) в новую ветку.
Тайлер В.

8
Это лучший ответ. Таким образом, вы можете перемещать коммиты в любую ветку.
Skywinder

8
Важен ли порядок сбора вишни?
Кон Псих

328

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

git checkout -b newbranch # switch to a new branch
git branch -f master HEAD~3 # make master point to some older commit

Старая версия - прежде чем я узнал оgit branch -f

git checkout -b newbranch # switch to a new branch
git push . +HEAD~3:master # make master point to some older commit 

Будучи в состоянии , pushчтобы .это хороший трюк , чтобы знать.


1
Текущий каталог. Я думаю, это будет работать, только если вы находитесь в верхнем каталоге.
aragaer

1
Местный толчок вызывает улыбку, но, если подумать, чем он git branch -fздесь отличается ?
до

2
@GerardSexton .является текущим директором. Git может нажать на удаленные или GIT URL-адреса. path to local directoryподдерживается синтаксис Git URL. Смотрите раздел GIT URLS в git help clone.
слабый

31
Я не знаю, почему это не оценено выше. Умер просто, и без малой, но потенциальной опасности сброса мерзавца - трудно.
Кузнец

4
@ Godsmith Полагаю, люди предпочитают три простые команды двум немного более непонятным командам. Кроме того, ответы, получившие наибольшее количество голосов, получают больше откликов, так как отображаются первыми.
JS_Riddler

323

Большинство предыдущих ответов опасно неправильны!

Не делайте этого:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

Поскольку в следующий раз, когда вы запустите git rebase(или git pull --rebase), эти 3 коммита будут молча отброшены newbranch! (см. объяснение ниже)

Вместо этого сделайте это:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • Сначала он отбрасывает 3 самых последних коммита ( --keepэто как --hard, но безопаснее, так как терпит неудачу, а не выбрасывает незафиксированные изменения).
  • Тогда это разветвляется newbranch.
  • Затем он выбирает те 3 коммита обратно newbranch. Поскольку на них больше не ссылается ветвь, она делает это с помощью git reflog : HEAD@{2}коммит, который HEADиспользовался для ссылки на 2 операции назад, то есть до того, как мы 1. извлекли newbranchи 2. использовали git resetдля отмены 3 коммитов.

Предупреждение: reflog включен по умолчанию, но если вы отключили его вручную (например, с помощью «чистого» хранилища git), вы не сможете вернуть 3 коммитов после запуска git reset --keep HEAD~3.

Альтернатива, которая не зависит от reflog:

# newbranch will omit the 3 most recent commits.
git checkout -b newbranch HEAD~3
git branch --set-upstream-to=oldbranch
# Cherry-picks the extra commits from oldbranch.
git cherry-pick ..oldbranch
# Discards the 3 most recent commits from oldbranch.
git branch --force oldbranch oldbranch~3

(если вы предпочитаете, вы можете написать @{-1}- ранее проверенную ветку - вместо oldbranch).


Техническое объяснение

Зачем git rebaseотказываться от 3 коммитов после первого примера? Это связано git rebaseс тем, что без аргументов --fork-pointпо умолчанию эта опция включена, которая использует локальный журнал reflog, чтобы попытаться быть устойчивым к принудительной ветке восходящей ветки.

Предположим, что вы разветвились от origin / master, когда он содержал коммиты M1, M2, M3, а затем сделали три коммита самостоятельно:

M1--M2--M3  <-- origin/master
         \
          T1--T2--T3  <-- topic

но затем кто-то переписывает историю, принудительно нажимая origin / master, чтобы удалить M2:

M1--M3'  <-- origin/master
 \
  M2--M3--T1--T2--T3  <-- topic

Используя ваш локальный reflog, git rebaseможно увидеть, что вы разветвились из более ранней инкарнации origin / master ветви, и, следовательно, что коммиты M2 и M3 на самом деле не являются частью вашей тематической ветви. Следовательно, это разумно предполагает, что, поскольку M2 был удален из вышестоящей ветки, вы больше не захотите его в своей ветке тем, как только ветвь темы будет перебазирована:

M1--M3'  <-- origin/master
     \
      T1'--T2'--T3'  <-- topic (rebased)

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

Так что причина того, что следующие команды не работают:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

потому что они оставляют reflog в неправильном состоянии. Git считает, newbranchчто он раздвоил ветку upstream в ревизии, которая включает в себя 3 коммита, затем reset --hardпереписывает историю апстрима для удаления коммитов, и поэтому при следующем запуске git rebaseон отбрасывает их, как и любой другой коммит, который был удален из апстрима.

Но в данном конкретном случае мы хотим, чтобы эти 3 коммита рассматривались как часть ветки темы. Чтобы достичь этого, нам нужно отключить апстрим в более ранней ревизии, которая не включает 3 коммита. Это то, что делают мои предложенные решения, поэтому они оба оставляют reflog в правильном состоянии.

Для получения дополнительной информации см. Определения --fork-pointв документах git rebase и git merge-base .


15
Этот ответ говорит: «НЕ делай этого!» выше того, что никто не предлагал делать.
Райан Ланди

4
Большинство людей не переписывают опубликованную историю, особенно на master. Так что нет, они не опасно ошибаются.
Уолф

4
@ Kyralessa, то, на что -tвы ссылаетесь, git branchпроисходит неявно, если вы git config --global branch.autosetuprebase alwaysустановили. Даже если вы этого не сделаете, я уже объяснил вам, что такая же проблема возникает, если вы настраиваете отслеживание после выполнения этих команд, что, скорее всего, намеревается сделать ОП, учитывая их вопрос.
Джон Меллор

2
@RockLee, да, общий способ исправления таких ситуаций - создать новую ветвь (newbranch2) из ​​безопасной начальной точки, а затем выбрать все коммиты, которые вы хотите сохранить (от badnewbranch до newbranch2). Cherry-picking даст коммитам новые хэши, так что вы сможете безопасно перебазировать newbranch2 (и теперь можете удалять badnewbranch).
Джон Меллор

2
@Walf, вы неправильно поняли: git rebase разработан так, чтобы быть устойчивым к переизбыткам, переписавшим свою историю. К сожалению, побочные эффекты этой устойчивости влияют на всех, даже если ни они, ни их предшественники никогда не переписывают историю.
Джон Меллор

150

Гораздо проще решение с использованием git stash

Вот гораздо более простое решение для фиксации в неправильной ветви. Начиная с ветки masterс тремя ошибочными коммитами:

git reset HEAD~3
git stash
git checkout newbranch
git stash pop

Когда использовать это?

  • Если вашей основной целью является откат master
  • Вы хотите сохранить изменения файла
  • Вас не волнуют сообщения о ошибочных коммитах
  • Вы еще не нажали
  • Вы хотите, чтобы это было легко запомнить
  • Вам не нужны такие сложности, как временные / новые ветки, поиск и копирование хэшей коммитов и другие головные боли

Что это делает, по номеру строки

  1. Отменяет последние три коммита (и их сообщения) master, но оставляет все рабочие файлы нетронутыми
  2. Сохраняет все изменения рабочего файла, делая masterрабочее дерево точно равным состоянию HEAD ~ 3
  3. Переключение на существующую ветку newbranch
  4. Применяет скрытые изменения к вашему рабочему каталогу и очищает тайник

Теперь вы можете использовать git addи git commitкак обычно. Все новые коммиты будут добавлены в newbranch.

Что это не делает

  • Он не оставляет случайных временных веток, загромождающих ваше дерево
  • Он не сохраняет ошибочные сообщения о коммите, поэтому вам нужно добавить новое сообщение о коммите в этот новый коммит
  • Обновить! Используйте стрелку вверх для прокрутки буфера команд, чтобы повторно применить предыдущий коммит с его сообщением коммита (спасибо @ARK)

цели

ОП заявил, что цель состояла в том, чтобы «вернуть мастера до того, как эти коммиты были сделаны», не теряя изменений, и это решение делает это.

Я делаю это, по крайней мере, один раз в неделю, когда я делаю новые коммиты masterвместо develop. Обычно у меня есть только один коммит для отката, в этом случае использование git reset HEAD^в строке 1 - более простой способ отката только одного коммита.

Не делайте этого, если вы отправили изменения мастера вверх по течению

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


4
Спасибо, я так рад, что прочитал много чего, чтобы добраться сюда, потому что это довольно распространенный случай и для меня. Мы такие нетипичные?
Джим Мак

6
Я думаю, что мы совершенно типичны, и «К сожалению, я совершил мастеринг по ошибке» является наиболее распространенным вариантом использования для необходимости отменить несколько или менее коммитов. К счастью, это решение настолько простое, что я запомнил его сейчас.
Шлем

9
Это должен быть принятый ответ. Это просто, легко понять и легко запомнить
Сина Мадани

1
Я не думаю, что тайнинг необходим. Я просто сделал это без и работал хорошо.
Кампос

1
Вы также можете легко вернуть свои сообщения коммита, если они есть в истории CLI (командной строки). Мне довелось использовать git addи git commitкоманды, и команды, которые я использовал, поэтому все, что мне нужно было сделать, это нажать стрелку вверх и войти несколько раз, и бум! Все вернулось, но теперь на правильной ветке.
Люк Гедеон,

30

Это не «перемещает» их в техническом смысле, но имеет тот же эффект:

A--B--C  (branch-foo)
 \    ^-- I wanted them here!
  \
   D--E--F--G  (branch-bar)
      ^--^--^-- Opps wrong branch!

While on branch-bar:
$ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range)

A--B--C  (branch-foo)
 \
  \
   D-(E--F--G) detached
   ^-- (branch-bar)

Switch to branch-foo
$ git cherry-pick E..G

A--B--C--E'--F'--G' (branch-foo)
 \   E--F--G detached (This can be ignored)
  \ /
   D--H--I (branch-bar)

Now you won't need to worry about the detached branch because it is basically
like they are in the trash can waiting for the day it gets garbage collected.
Eventually some time in the far future it will look like:

A--B--C--E'--F'--G'--L--M--N--... (branch-foo)
 \
  \
   D--H--I--J--K--.... (branch-bar)

1
Разве вы не можете использовать rebaseдля того же?
Берги

Да, вы можете использовать альтернативную rebaseветку в сценарии выше.
Сукима

24

Чтобы сделать это без переписывания истории (т.е. если вы уже выдвинули коммиты):

git checkout master
git revert <commitID(s)>
git checkout -b new-branch
git cherry-pick <commitID(s)>

Обе ветви могут быть вытолкнуты без усилия!


Но тогда вам придется иметь дело со сценарием возврата, который, в зависимости от ваших обстоятельств, может быть намного сложнее. Если вы отмените коммит на ветке, Git все равно увидит, что коммиты уже произошли, поэтому для отмены вы должны отменить откат. Это сжигает довольно много людей, особенно когда они отменяют слияние и пытаются слить ветку обратно, только чтобы обнаружить, что Git полагает, что она уже слила эту ветку (что полностью верно).
Макото

1
Вот почему я выбираю коммиты в конце на новую ветку. Таким образом, git видит их как новые коммиты, что решает вашу проблему.
teh_senaus

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

5
Я не слежу за вашим аргументом - смысл этого ответа в том, что вы не меняете историю, просто добавляете новые коммиты (что фактически отменяет повтор изменений). Эти новые коммиты могут быть переданы и объединены как обычно.
teh_senaus

13

Была только эта ситуация:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

Я выполнил:

git branch newbranch 
git reset --hard HEAD~8 
git checkout newbranch

Я ожидал этого коммита, я был бы ГОЛОВОМ, но коммит Л это сейчас ...

Чтобы быть в нужном месте в истории, проще работать с хэшем коммита

git branch newbranch 
git reset --hard #########
git checkout newbranch

9

Как я могу пойти от этого

A - B - C - D - E 
                |
                master

к этому?

A - B - C - D - E 
    |           |
    master      newbranch

С двумя командами

  • git branch -m master newbranch

дающий

A - B - C - D - E 
                |
                newbranch

а также

  • Git Branch Master B

дающий

A - B - C - D - E
    |           |
    master      newbranch

Да, это работает и довольно легко. Sourcetree GUI немного смущен изменениями, внесенными в оболочку git, но после выборки все снова в порядке.
Ларс К.

Да, они как в вопросе. Предполагается, что первая пара диаграмм эквивалентна тем, которые есть в вопросе, просто перерисовать так, как мне хотелось бы для иллюстрации в ответе. В основном переименуйте основную ветвь в newbranch и создайте новую основную ветвь там, где вы хотите.
Иван

Я считаю, что это правильный ответ.
Леонардо Эррера

4

Если вам просто нужно переместить все ваши невыполненные коммиты в новую ветку , то вам просто нужно,

  1. создать в новую ветвь от текущей:git branch new-branch-name

  2. нажмите вашу новую ветку :git push origin new-branch-name

  3. верните вашу старую (текущую) ветвь в последнее нажатое / стабильное состояние:git reset --hard origin/old-branch-name

Некоторые люди также имеют другие, upstreamsа не origin, они должны использовать соответствующиеupstream


3

1) Создайте новую ветку, которая переместит все ваши изменения в new_branch.

git checkout -b new_branch

2) Затем вернитесь к старой ветке.

git checkout master

3) сделать git rebase

git rebase -i <short-hash-of-B-commit>

4) Затем открытый редактор содержит информацию о последних 3 коммитах.

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) Изменить pick к dropво всех этих 3 фиксаций. Затем сохраните и закройте редактор.

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) Теперь последние 3 коммита удалены из текущей ветви ( master). Теперь принудительно нажмите на ветку со +знаком перед именем ветви.

git push origin +master

2

Вы можете сделать это всего за 3 простых шага, которые я использовал.

1) создать новую ветку, где вы хотите зафиксировать недавнее обновление.

git branch <branch name>

2) Найти недавний коммит-идентификатор для коммита на новой ветке.

git log

3) Скопируйте это примечание id коммита, что Список последних коммитов находится сверху. так что вы можете найти свой коммит. Вы также найдете это через сообщение.

git cherry-pick d34bcef232f6c...

Вы также можете указать некоторый диапазон идентификатора коммита.

git cherry-pick d34bcef...86d2aec

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

Теперь вы можете нажать свой код

git push


0

Еще один способ сделать это:

[1] Переименуйте masterветку в свою newbranch(если вы находитесь на masterветке):

git branch -m newbranch

[2] Создайте masterветку из коммита, который вы хотите:

git checkout -b master <seven_char_commit_id>

например git checkout -b master a34bc22

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.