Перебазировать после силового толчка - PullRequest
0 голосов
/ 26 марта 2020

У меня есть ветвь функций A. Затем я начинаю разработку второй функции, зависящей от A, поэтому я основываю свою новую ветвь функций B на A:

git checkout A
git checkout -B B

Я работаю над B, так что теперь в BI есть коммит 1 (из A) и новый коммит 2. Наша компания всегда подавляет все коммиты одного PR как можно больше вместе, поэтому в какой-то момент я толкаю A так, что A имеет только коммит 1 '. Теперь я хочу изменить B на A (или master, после слияния A), но, поскольку я принудительно нажал A, git пытается применить коммит 1, который, очевидно, не удается.

2 метода, чтобы решить эту проблему , что не очень хорошо:

с использованием git cherry-pick:

git checkout B
git checkout -B B2
git log // copy latest commit id
git checkout B
git reset --hard A
git cherry-pick <commit-id>

с использованием мягкого сброса:

git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"

Есть ли "git метод "для выяснения этого? Я знаю, что, может быть, не всегда лучше всего использовать коммиты, но я не могу измениться. Или, может быть, есть лучший способ основать одну ветку на другой?

1 Ответ

1 голос
/ 26 марта 2020

В конечном счете, вы захотите git rebase --onto. Однако иногда вам не нужно делать ничего особенного.

Настройка

Давайте нарисуем вашу начальную ситуацию:

...--A--B   <-- master
         \
          C   <-- feature/A
           \
            D   <-- feature/B

То есть, есть ряд коммитов на некоторой главной строке (я назвал это master здесь, но это может быть develop или что-то еще), затем один коммит на вашем feature/A, затем один коммит на вашем feature/B. Родителем коммита D на вашем feature/B является ваш коммит C на feature/B и feature/A.

Несколько позже вы добавили второй коммит к вашему feature/A, что дает:

...--A--B   <-- master
         \
          C--E   <-- feature/A
           \
            D   <-- feature/B

В конце концов, feature/A должно быть объединено с master, и согласно некоторому правилу политики вы сделали новый коммит F, который является комбинацией C и E, так что теперь у вас есть:

          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   [abandoned]
           \
            D   <-- feature/B

На данный момент вы хотите скопировать D в новый коммит D', который выглядит точно так же, как D с точки зрения его дифференциал по отношению к его родителю, но где родитель D' равен F вместо C.

Git предлагает легкий-i sh способ получить то, что вы хотите:

git checkout feature/B
git rebase --onto feature/A something-goes-here

Проблема в части something-goes-here. Что там происходит?

Копирование некоторых коммитов

Команда git rebase, по сути, представляет собой просто серию git cherry-pick команд, за которыми следует движение метки ветвления. Как вы уже обнаружили, git cherry-pick делает то, что вы хотите: он копирует коммит. Фактически, он может копировать более одного коммита (используя то, что Git вызывает, внутренне, sequencer ).

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

Например, давайте начнем с этой ситуации. На данный момент я вставил новый ярлык saved-A, чтобы запомнить фиксацию E, и добавил имя new-B и добавил HEAD в скобках, чтобы показать, что текущая ветвь - это new-B, а текущий коммит - это коммит F:

          F   <-- feature/A, new-B (HEAD)
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B

Теперь мы можем запустить git cherry-pick feature/B. Мы говорим Git: Сравните коммит D с его родителем C, затем внесите те же самые изменения в то место, где мы сейчас находимся в коммите F, и передайте результат. Если все все идет хорошо, мы получаем:

            D'   <-- new-B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B

Все, что нам нужно сейчас сделать, это нажать имя feature/B, чтобы указать D', а затем сбросить имя new-B:

            D'   <-- feature/B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   [abandoned]

Опять же, первая часть этого именно то, что делает git cherry-pick: скопировать один коммит. последняя часть этого - то, что git rebase делает: перемещает метку ветви как feature/B.

Ключ здесь в том, что git rebase копий некоторые совершает. Какие из них? Ответ по умолчанию - неправильный ответ для вас!

Что делает git rebase, в двух словах

Давайте посмотрим на немного другой рисунок:

...--A--B   <-- target
      \
       C--D--E   <-- current (HEAD)

Здесь мы находимся "на" ветке current, то есть git status скажет on branch current. Коммит-коммит current - это коммит E: E 's ha sh ID - это га sh ID, хранящийся в имени refs/heads/current.

Если мы теперь запустим:

git rebase target

Git будет копировать коммиты C-D-E для новых коммитов C'-D'-E' и помещать новые коммиты поверх target, а затем перемещать имя ветки, например:

          C'-D'-E'   <-- current (HEAD)
         /
...--A--B   <-- target
      \
       C--D--E   [abandoned]

Это обычно то, что мы хотим. Но: Как git rebase узнал, что нужно копировать C-D-E, но не копировать A тоже?

Ответ таков: git rebase использует внутренний список Git некоторые операции коммитов, git rev-list, с остановкой Документация по перебазировке утверждает, что git rebase выполняет:

git rev-list target..HEAD

, что немного похоже на белый l ie: это достаточно близко и показательно. Точные детали хитрее, и мы немного разберемся. А пока давайте посмотрим на target.. часть target..HEAD. Это говорит Git: не перечислять какие-либо коммиты, которые вы можете найти, начиная с цели и работая в обратном направлении.

Поскольку target names коммит B, это означает: не копировать коммит B. Ну, мы уже не собирались копировать commit B, так что ничего страшного. Но это также означает: не копировать коммит A. Почему бы нет? Потому что commit B указывает на коммит A. Коммит A находится в обеих ветках, target и current. Таким образом, мы скопировали бы A, но мы этого не делаем, потому что он в списке не копирует . Есть коммиты до A, но они все в не копируют часть, поэтому ни один из них не копируется.

Следовательно, это коммиты C-D-E которые копируются здесь: они находятся в списке для копирования, и их нельзя остановить, если они находятся в списке «не копировать».

Итак, что делает git rebase, в двух словах, this:

  1. Запомните, к какой ветви HEAD прикреплен.
  2. Перечислите некоторые идентификаторы коммитов ha sh для копирования.
  3. Отделите HEAD от текущая ветвь.
  4. Копировать перечисленные коммиты, по одному, как если бы git cherry-pick.
  5. Переместить имя ветки, к которой было прикреплено HEAD туда, где мы сейчас находимся.
  6. Повторное присоединение HEAD к перемещенной ветви.

Обратите внимание, что на шаге 4 все может go неверно. В частности, копирование commit, как если бы git cherry-pick - независимо от того, использует ли он на самом деле git cherry-pick - может иметь конфликт слияния . Если это так, то перебазирование останавливается посередине с отсоединенной ГОЛОВКОЙ. Вот почему знание о шаге 3 важно. Но мы оставим это для других вопросов и ответов (вместе с подробностями о том, действительно ли rebase использует вишневый кир: иногда это делает, иногда притворяется).

Правда о том, что совершается Копирование

Мы упоминали, что вышеприведенная вещь target..HEAD была белого цвета l ie: упрощение, призванное облегчить понимание того, какие коммиты копируются. Настало время для истины.

  • Во-первых, git rebase обычно пропускает слияния совершаются полностью. Любой коммит, который будет сгенерирован с помощью git rev-list выше, удаляется, если это слияние (имеет двух или более родителей). Пока в вашем списке нет коммитов слияния, это все равно не имеет значения.

  • Во-вторых, git rebase также пропускает коммиты, которые идентификатор патча, эквивалентный некоторым другим коммитам. Для этого используется программа git patch-id. Здесь мы не будем go вдаваться в подробности, кроме как заметим, что для получения части "некоторые другие коммиты" Git фактически должен использовать git rev-list target...HEAD с тремя точками. Это создает список симметри c разница коммитов, достижимых из HEAD, но не цели, а также коммиты, достижимые из target, но не HEAD. Подробнее о достижимости см. Думайте как (а) Git. Затем команда rebase использует git patch-id для каждого коммита в двух списках - который он генерирует для себя, поэтому он знает, какой коммит ha sh идет с каким списком - и выбивает те, которые имеют соответствующие идентификаторы патчей. Эффект этого состоит в том, что если, например, commit B равен уже так же (по вишне), что и commit D, вместо копирования C-D-E, мы просто скопируем C-E, чтобы получить:

              C'-E'   <-- current (HEAD)
             /
    ...--A--B   <-- target
          \
           C--D--E   [abandoned]
    

    , поскольку коммиты B и D "делают то же самое".

  • Последний и самый важный для нас здесь, --onto позволяет нам использовать другую цель .

В приведенном выше примере мы запустили:

git rebase target

и target был нашим стоп-аргументом для git rev-list stop..HEAD и нашей целью, для которой Git поместил копии. Но мы можем запустить:

git rebase --onto target stop

и теперь git rebase будет использовать наш stop аргумент для stop части git rev-list, продолжая при этом использовать target аргумент для места копирования go.

Итак, предположим, что нам дано this сейчас:

...--A--B   <-- target
      \
       C   <-- another
        \
         D--E   <-- current (HEAD)

и мы выполняем:

git rebase --onto target another

Теперь мы сказали Git, что аргумент stop для нашей перебазировки равен another, который выбирает commit C. Наша перебазировка будет использовать git rev-list для another..HEAD или C..E, что означает, что список коммитов для копирования будет состоять всего из D-E.

Этот список будет далее отфильтрован патчем. правила id и no-merges, но пока B не совпадает с D, мы получим:

          D'-E'  <-- current (HEAD)
         /
...--A--B   <-- target
      \
       C   <-- another
        \
         D--E   [abandoned]

То есть мы скопирую только два коммита D-E, которые достижимы с current, опуская коммит C, который доступен с another.

Собираем все вместе

Вот ваши настройки в то время, когда вы хотите выполнить коммит копирование:

          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B (HEAD)

Обратите внимание, что мы добавили имя saved-A, чтобы запомнить, что не для копирования. Мы не хотим копировать коммиты C и E. В любом случае, мы не будем копировать E, но это простой способ запомнить все , а не копировать.

В настоящее время мы извлекли feature/B (commit D). Нам не нужно создавать имя, new-B, поэтому мы этого не делали. Теперь мы просто запустим:

git rebase --onto feature/A saved-A

Git теперь перечислит коммиты для копирования: каждый коммит, который находится в текущей ветви, feature/B, , за исключением каждый коммит, который включен saved-A. Итак, это коммит D.

Git теперь отсоединяет HEAD, переходит к коммиту F - наша --onto цель - и копирует D для получения D'. Это завершает список коммитов для копирования, поэтому, успешно скопировав D в D', Git принудительно перемещает имя feature/B для указания на D' и повторно присоединяет HEAD, давая нам:

            D'  <-- feature/B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   [abandoned]

именно это мы и хотим.

Теперь мы можем удалить имя saved-A.

Что если вы не сохранили имя?

Что, если вы уже выполнили ребазинг feature/A, но забыли сохранить коммит ha sh Идентификатор коммита E где-нибудь?

К счастью, у вас нет для сохранили идентификатор ha sh либо E, либо C. Вы можете:

  • найти их, используя git log, или
  • , использовать git reflog, чтобы найти идентификаторы ha sh, которые feature/A использовал для имен, или
  • делайте все что угодно, чтобы найти их.

Идентификаторы ha ha sh работают, поэтому вы можете просто запустить:

git rebase --onto feature/A <hash-ID-of-E-or-C>

после того, как найдете га sh ID. (Используйте метод «вырезать и вставить» или аналогичный, чтобы получить правильный идентификатор ha sh; ввод его или даже уникальный префикс вручную представляет собой рецепт ошибок.)

Имена рефлогов также работают Довольно часто вы можете сделать:

git rebase --onto feature/A feature/A@{1}

, где feature/A@{1} - это имя reflog, которое вы увидите для ha sh ID коммита E, когда вы запускаете git reflog feature/A в список из предыдущих идентификаторов га sh для feature/A. (feature/A@{2} вероятно имена коммитов C, так что это также сработает.)

Ключ в том, чтобы найти коммиты, которые вы хотите пропустить, и использовать их с git rebase --onto. Установите target в зависимости от того, где копии должны go, и установите точку остановки - то, что документация git rebase вызывает аргумент upstream - га sh удостоверение личности, которое останавливает коммиты, которые вы не не хотите скопировать.

Когда вам не нужно ничего особенного?

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

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

...