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

3467

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

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

master A - B - C - D - E

к этому?

newbranch     C - D - E
             /
master A - B 
  • 87
    Примечание: я задал противоположный вопрос здесь
  • 0
    eddmann.com/posts/… этот работает
Показать ещё 1 комментарий
Теги:
git-branch
branching-and-merging

8 ответов

4382
Лучший ответ

Переход к новой ветке

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

# Note: Any changes not committed will be lost.
git branch newbranch      # Create a new branch, saving the desired commits
git reset --hard HEAD~3   # Move master back by 3 commits (GONE from master)
git checkout newbranch    # Go to the new branch that still has the desired commits

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

git reset --hard a1b2c3d4

* 1 только будет "потерять" фиксацию из главной ветки, но не беспокойтесь, у вас будут эти коммиты в newbranch!

ПРЕДУПРЕЖДЕНИЕ: При использовании Git версии 2.0 и более поздних версий, если позже git rebase новая ветка на исходной ветки (master), вам может понадобиться явная опция --no-fork-point во время rebase, чтобы избежать потери переносимых коммитов. Наличие branch.autosetuprebase always set делает это более вероятным. Подробнее см. John Mellor.

Переход к существующей ветке

ПРЕДУПРЕЖДЕНИЕ: Метод выше работает, потому что вы создаете новую ветвь с первой командой: git branch newbranch. Если вы хотите использовать существующую ветку, перед выполнением git reset --hard HEAD~3 вам необходимо объединить свои изменения в существующую ветку. Если вы сначала не объедините свои изменения, они будут потеряны. Итак, если вы работаете с существующей ветвью, это будет выглядеть так:

git checkout existingbranch
git merge master
git checkout master
git reset --hard HEAD~3 # Go back 3 commits. You *will* lose uncommitted work.
git checkout existingbranch
  • 212
    И, в частности, не пытайтесь вернуться дальше, чем тот момент, когда вы в последний раз выдавали коммиты в другой репозиторий, из которого кто-то другой мог вытащить.
  • 92
    Хотите знать, если вы можете объяснить, почему это работает. Для меня вы создаете новую ветку, удаляете 3 коммита из старой ветки, в которой вы все еще находитесь, и затем проверяете ветку, которую вы создали. Итак, как коммиты, которые вы удалили, волшебным образом отображаются в новой ветке?
Показать ещё 28 комментариев
841

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

Вы хотите вернуться на 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, он все же указывает на фиксацию, которую она изначально сделала.

  • 38
    Вам нужен --hard, потому что в противном случае git оставляет изменения из коммитов сброса в рабочем каталоге (или, по крайней мере, сделал для меня).
  • 0
    @ Андрей, ты прав. Мое мышление было немного запутанным месяц назад, когда я написал это. Исправлена.
Показать ещё 9 комментариев
300

В общем...

В этом случае наилучшим вариантом является метод, открытый 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
    Разве ОП не пытался перейти от мастера к новому отделению? Если вы выбираете вишню на мастер-сервере, вы добавляете в основную ветку - фактически добавляете коммиты, которые у него уже были. И это не сдвигает назад ни одну ветвь головы к B. Или есть что-то тонкое и классное, которое я не получаю?
  • 0
    Ну, это пример, но вы правы, в случае, предложенном ФП, он должен работать на newbranch. Я редактирую, чтобы избежать ошибок. Благодарю.
Показать ещё 14 комментариев
246

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

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

Возможность push to . - это хороший трюк.

Позднее редактирование: Теперь, когда я знаю о git branch -f, правильный способ сделать это:

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

То же самое, но менее "волшебное"

  • 0
    А что есть. ?
  • 1
    Текущий каталог. Я думаю, это будет работать, только если вы находитесь в верхнем каталоге.
Показать ещё 10 комментариев
187

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

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

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, который использует локальный рефлок, чтобы попытаться быть надежным против того, чтобы восходящая ветка была принудительно нажата.

Предположим, что вы разветвлялись от источника/хозяина, когда он содержал коммиты 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

Используя ваш локальный рефлок, 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 как разветвленную ветвь вверх по ревизии, которая включает в себя 3 коммита, затем reset --hard перезаписывает историю восходящего потока, чтобы удалить коммиты, и поэтому в следующий раз при запуске git rebase он отбрасывает их как любой другой фиксатор, который был удален из восходящего потока.

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

Подробнее см. определение --fork-point в git rebase и git merge- базы.

  • 0
    Я думаю, что я был бы более удовлетворен этим ответом, если бы вы могли продемонстрировать, что «неправильное» состояние рефлога будет отличаться от правильного состояния в этих обстоятельствах.
  • 0
    В вашем сценарии, если я оставлю M2 и M3 в моей ветке тем и затем попытаюсь объединить мою ветку тем обратно в master, то не буду ли я снова вносить изменения, которые кто-то еще пытался удалить (то есть M2)? Казалось бы, это нежелательное поведение, нет? Для меня имеет смысл, что если кто-то собирается переписать историю, вы захотите, чтобы ваша ветка темы отслеживала новую историю, чтобы вы не перезаписывали историю при слиянии.
Показать ещё 20 комментариев
25

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

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 для того же?
  • 0
    Да, вы могли бы альтернативно использовать rebase в отдельной ветке в сценарии выше.
17

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

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

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

  • 0
    Но тогда вам придется иметь дело со сценарием возврата, который, в зависимости от ваших обстоятельств, может быть намного сложнее. Если вы отмените коммит на ветке, Git все равно увидит, что коммиты уже произошли, поэтому для отмены вы должны отменить откат. Это сжигает довольно много людей, особенно когда они отменяют слияние и пытаются слить ветку обратно, только чтобы обнаружить, что Git полагает, что она уже слила эту ветку (что полностью верно).
  • 1
    Вот почему я выбираю коммиты в конце на новую ветку. Таким образом, git видит их как новые коммиты, что решает вашу проблему.
Показать ещё 2 комментария
11

Если бы такая ситуация:

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

Я ожидал, что фиксация будет HEAD, но commit L это сейчас...

Чтобы приземлиться в нужном месте в истории, его легче работать с хешем фиксации

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню