git pull --rebase потерял коммиты после git push --force коллеги

Я думал, что понял, как работает git pull --rebase, но этот пример меня смущает. Я бы предположил, что следующие два сценария дадут одинаковые результаты, но они отличаются.

во-первых, тот, который работает.

# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556

brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556

# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"

brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"

# Brian pushes first, then Dan runs `git pull --rebase`
brian$ git push

dan$ git pull --rebase
dan$ git log
commit 9e1140410af8f2c06f0188f2da16335ff3a6d04c
Author: Daniel
Date:   Wed Mar 1 09:31:41 2017 -0600

    Add bagels to favorite_foods.txt

commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date:   Wed Mar 1 09:47:09 2017 -0600

    I love root beer

commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date:   Wed Mar 1 09:27:09 2017 -0600

    Shared history

так что все в порядке. В другом сценарии представьте, что Дэн толкнул, а затем Брайан (грубо)толкнул-принудил его совершить. Теперь, когда Дэн бежит git pull --rebase, его фиксации нет.

# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556

brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556

# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"

brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"

# THIS TIME, Dan pushes first, then Brian force pushes.
dan$ git push

brian$ git push --force

dan$ git pull --rebase
dan$ git log  # Notice, Dan's commit is gone!
commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date:   Wed Mar 1 09:47:09 2017 -0600

    I love root beer

commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date:   Wed Mar 1 09:27:09 2017 -0600

    Shared history

происхождение версия ветви имела такое же состояние после Брайана push --force как и в первом сценарии, поэтому я ожидал такого же поведения от git pull --rebase. Я смущен, почему Dan ' commit был потерян.

Я понимаю, что pull -- rebase говорит: "возьмите мои локальные изменения и примените их после удаленных". Я не ожидаю, что местные изменения будут отброшены. Кроме того, если бы Дэн убежал ...--5--> (без --rebase), его фиксация не потеряна.

так почему же Дэн теряет свою локальную фиксацию, когда он бежит git pull --rebase? Сила толчка имеет смысл для меня, но разве это не должно оставить пульт в том же состоянии, как если бы Брайан толкнул первым?

как я могу думать об этом неправильно?

4 ответов


TL; DR: это код точки вилки

вы получаете эффект git rebase --fork-point, который намеренно сбрасывает dan's commit из код репозитории тоже. См. также git rebase-commit выберите в режиме fork-point (хотя в моем ответе там я не упоминаю что-то я здесь).

если вы запустите git rebase себя,выбрать ли это. The это когда:

  • запустить git rebase С это root commit, что является просто причудливым способом сказать "commit без родителей". Мы также можем иметь обязательства с двумя или более родителями; это коммитов слияния. (Впрочем, здесь я ничего не нарисовал. Часто неразумно перебазировать цепочки ветвей, содержащие слияния, поскольку буквально невозможно перебазировать слияние и git rebase есть повторновыполнить слияние, чтобы приблизить его. Обычно git rebase просто опускает сливается полностью, что вызывает другие проблемы.)

    3очевидно, к Принципу Дирихле, любой хэш, который уменьшает более длинную бит-строку до фиксированной длины k-битный ключ обязательно должен иметь столкновения на некоторых входах. Ключевым требованием для хэш-функции Git является то, что она избегает случайных столкновений. В "криптографии" часть не очень важна для Git, это просто затрудняет (но, конечно, не невозможно) для кого-то сознательно причиной столкновения. Коллизии приводят к тому, что Git не может добавлять новые объекты, поэтому они плохи, но-помимо ошибок в реализации-они фактически не нарушают git сам, просто дальнейшее использование Git для ваших собственных данных.


    определение того, что копировать

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

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

    именование a совершить, например, пишет branch - имеет тенденцию выбирать не только это commit, но также и родительская фиксация этой фиксации, родительская родительская и так далее, вплоть до корневой фиксации. (Если есть фиксация слияния, мы обычно выбираем все своих родительских коммитов и следовать за всеми из них назад к корню одновременно. Граф может иметь более одного корня, поэтому это позволяет нам выбирать несколько нитей, возвращающихся к нескольким корням, а также нити ветвей и слияния возвращаясь к одному корню.) Мы называем множество всех коммитов, которые мы находим, начиная с одного коммита и выполняя эти родительские обходы,набор достижимых коммитов.

    для многих целей, в том числе git rebase, нам нужно сделать этот массовый выбор остановка, и для этого мы используем операции git fancy set. Если мы напишем master..branch в качестве селектора ревизий это означает: "все коммиты доступны из кончика филиала, за исключением любых коммитов, доступных из кончика мастер.- Взгляните еще раз на этот график.--169-->

    A--B--C--D     <-- master
        \
         E--F--G   <-- branch
    

    коммиты достижимые из branch are G, F, E, B и A. Коммиты, доступные из master are D, C, B и A. Так что master..branch означает: вычесть набор A+B+C+D из большего набора A+B+E+F+G.

    в вычитании набора, удаляя то, чего никогда не было в первое место тривиально: вы просто ничего не делаете. Поэтому мы удаляем A+B из второго набора, оставляя E+F+G. альтернативно, мне нравится использовать метод, который я не могу нарисовать на StackOverflow: цвет коммитов красный (стоп) и зеленый( go), следуя всем обратным стрелкам на графике, начиная с Красного для коммитов, которые запрещены (master) и зеленый для фиксации (branch). Просто убедитесь, что красный перезаписывает зеленый, или что вы сначала делаете красный и не перекрашиваете их, когда вы делаю зеленый. Это интуитивно очевидно4 это делает зеленый сначала, затем перезаписывает красным; или делает красный сначала и не перезаписи, дает тот же результат.

    в любом случае, это один из способов git rebase выбирает фиксации для копирования. Документация rebase называет это <upstream>. Ты пишешь:

    git checkout branch; git rebase master
    

    и Git знает цвет master фиксирует красный и текущий-ветвь branch фиксирует зеленый, а затем копирует только зеленые. Более того, то же самое имя-master-говорит git rebase куда положить копии. Это довольно элегантно и эффективно: один аргумент,master, предписывает как как скопировать и куда положить копии.

    проблема в том, что не всегда работает.

    Существует несколько распространенных случаев, когда он ломается. Один происходит, когда вы хотите ограничить копии еще больше, например, разбить одну большую ветвь на две меньшие. Другой происходит, когда некоторые, но не все, ваши собственные коммиты уже скопированы (вишневый или сквош-"объединены") в другую ветвь, на которую вы перебазируетесь. Более редкий, но не неслыханный, иногда восходящий поток сознательно отбросил некоторые коммиты, и Вы тоже должны.

    для некоторых из этих случаев git rebase может иметь дело с ними, используя git patch-id: он может фактически сказать, что коммит был скопирован, если два коммита имеют один и тот же идентификатор исправления. Для других, вы должны вручную разделить перебазировать цель (rebase называет это <newbase>) с помощью --onto флаг:

    git rebase --onto <newbase> <upstream>
    

    который ограничивает коммиты для копирования в <upstream>..HEAD, при запуске копий после <target>. Это-разделение таргетинга копий от <upstream> аргумент-означает, что теперь вы можете выбрать любой <upstream> что удаляет право фиксирует, а не некоторый набор, определенный "где копии идти."


    4это фраза, которую математики используют, когда они не хотят писать доказательство. :-)


    на --fork-point опции

    если вы регулярно совершает перебазироваться на origin/whatever филиал (или аналогичный), то есть сам,и регулярно перезагружается специально с целью удаление commits, может быть трудно решить, какие коммиты копировать. Но если у вас есть, в вашем origin/whatever reflog, серия хэшей фиксации, которые показывают, что некоторые коммиты были там раньше, но больше нет, для Git можно использовать это, чтобы отбросить из набора для копирования некоторые или все эти коммиты.

    я не был полностью уверен, как --fork-point реализован внутренне (это не очень хорошо документировано). Для ответ, я сделал тестовый репозиторий. Неудивительно, что этот вариант зависел от порядка: git merge-base --fork-point origin/master topic возвращает отличный результат от git merge-base --fork-point topic origin/master.

    для этого ответа я посмотрел на исходный код. Это показывает, что ГИТ смотрит в reflog из первый аргумент non-option-назовите это arg1 - а затем использует его, чтобы найти базу слияния с помощью далее такой аргумент arg2 разрешен для идентификатора фиксации, полностью игнорируя любые дополнительные аргументы. Исходя из этого, результат git merge-base --fork-point $arg1 $arg2 по сути5 выход из:

    git merge-base $arg2 $(git log -g --format=%H $arg1)
    

    As в документации написано:

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

    как следствие объединить базы не обязательно содержится в каждом из аргументы commit, если указано более двух коммитов. Это отличается от git-show-branch (1) при использовании .

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

    помните, используя --fork-point режим просто изменяет, внутренне, до git rebase (без изменения его --onto target). Предположим, например, что в свое время вверх по течению было:

    ...--o--B1--B2--B3--C--D    <-- upstream
                     \
                      E--F--G   <-- branch
    

    цель, так сказать,--fork-point для обнаружения перезаписи этой формы:

    ...--o-------------C--D     <-- upstream
          \
           B1--B2--B3           <-- upstream@{1}
                    \
                     E--F--G    <-- branch
    

    и" нокаут " совершаетB1, B2 и B3 опции B3 как внутренний оставить на --fork-point опция, Git рассматривает все таким образом:

    ...--o-------------C--D     <-- upstream
          \
           B1--B2--B3--E--F--G  <-- branch
    

    так что все B коммиты являются "нашими". Коммиты на восходящей ветви D, C и CС родителя o (и его родители, по корню).

    в нашем конкретном случае фиксация Дэна-та, которая отбрасывается-напоминает одну из этих B commits. Он упал с --fork-point и хранил с --no-fork-point.


    5если бы, был один использовать --all, полученная несколько объединить базы, команда терпит неудачу и ничего не печатает. Если в результате (в единственном числе) слить базу-это не коммит, который уже в reflog, команда тоже не получается. Первый случай происходит с перекрестными слияниями в качестве ближайшего предка. Второй случай возникает, когда некоторые предок достаточно стар, чтобы истекли из reflog, или никогда не был в нем во-первых (я уверен, что оба варианта возможны). У меня есть пример этого второго вида неудачи прямо здесь:

    $ arg1=origin/next
    $ arg2=stash-exp
    $ git merge-base --all $arg2 $(git log -g --format=%H $arg1)
    3313b78c145ba9212272b5318c111cde12bfef4a
    $ git merge-base --fork-point $arg1 $arg2
    $ echo $?
    1
    

    я думаю идея пропуск 3313b78... вот что это один из тех "возможно гипотетических коммитов", которые я цитирую из документации, но на самом деле это было бы правильным коммитом для использования с git rebase, а это is используется без --fork-point.


    Git до 2.0, и заключение

    в версиях Git до 2.0 (или, возможно, в конце 1.9),git pull это было перебазирование вычислит эту точку развилки, но git rebase никогда этого не делал. Это означало, что если вы хотел такое поведение, вы had использовать git pull чтобы получить его. Теперь что git rebase и --fork-point, вы можете выбрать, когда, чтобы получить его:

    • добавить опцию, если вы определенно do хочу.
    • использовать --no-fork-point или явный восходящий поток (@{u} достаточно, если у вас есть значение по умолчанию вверх по течению), Если вы определенно не хочу.

    если вы запустите git pull, у вас нет .6 ли git pull origin master (предполагая, что текущая ветвь вверх по течению origin/master) подавляет вилку-указывает путь git rebase origin/master было бы, я не знаю: я в основном избегаю git pull.


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


оба сценария не сбивают с толку, вот как push --force строительство.

Я не буду объяснять первый сценарий, потому что Вы тоже это понимаете. Но во втором сценарии, когда brian$ git push --force это происходит, локальный репозиторий Брайана ничего не знает о нажатии Дэна на пульт.

так что возьмите локальную копию Брайана и замените все там в пульте, потому что это force push.

разницу между git pull & git pull --rebase есть

git pull будет сравнивать изменения origin & local. Вот почему даже у вас есть незафиксированные изменения, которые вы можете git pull.

git pull --rebase не будет сравнивать изменения с локальными, но он берет все из источника. Вот почему вы не можете git pull --rebase если есть незафиксированные изменения

надеюсь, вы понимаете, что я сказал. Есть вопросы??


Да, это выглядит правильно. Брайана git push --force собирается установить последнюю фиксацию как ту, которую он имеет в своей локальной системе. Если бы Брайан сначала вытащил последние коммиты, а затем git push --force результаты будут одинаковыми.

вот хороший поток с дополнительной информацией:Force "git push" для перезаписи удаленных файлов


Да, ваше понимание git pull --rebase является правильным.

это потому, что commit 9e11404 нажал на remote, а после этого commit 2f25b9a принудительно обновить удаленный (9e11404 если сила удалена из пульта).

когда дан выполнить git pull --rebase, git обнаружен 9e11404 и origin / branch указывает на 2f25b9a. Но git кажется 9e11404 уже существует в remote (.git\logs\refs\remotes\origin\branch содержание update by push 9e1140410af8f2c06f0188f2da16335ff3a6d04c), поэтому он ничего не делает для перезагрузки. Или вы можете использовать git pull --rebase=interactive чтобы проверить, вы найдете это показывает noop для перебазирования. Если вы хотите вручную rebase 9e11404 в верхней части origin / branch вы можете добавить pick 9e11404 в интерактивном окне результат будет таким, каким вы ожидали.