Как изменить начальную точку ветки?

git github git-branch

8609 просмотра

3 ответа

3525 Репутация автора

Обычно я создаю ветку, выполняя команду вроде git checkout -b [branch-name] [starting-branch]. В одном случае я забыл включить starting-branch, и теперь я хочу исправить это. Как мне это сделать после того, как ветка уже создана?

Автор: Daniel Kobe Источник Размещён: 17.07.2016 11:58

Ответы (3)


-3 плюса

679 Репутация автора

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

git branch -f <branch-name> <starting-branch>

Обратите внимание, что если branch-nameтекущая ветвь, вы должны сначала переключиться на другую ветку, например, с помощью git checkout master.

Автор: werkritter Размещён: 18.07.2016 12:13

1 плюс

15648 Репутация автора

Если у вас нет коммитов в новой ветке, проще использовать git reset --hard.

Если у вас есть коммиты в новой ветке ...

Если ваша ветка начинается с более старого коммита, который вы хотите, просто выполните «git rebase».

Если, скорее всего, ваша ветка запускается с новой фиксации или с совершенно другой ветки, используйте 'git rebase --onto'

Автор: Philippe Размещён: 18.07.2016 07:31

30 плюса

233002 Репутация автора

Решение

Короткий ответ в том , что когда - то у вас есть несколько коммитов, вы хотите git rebaseих, используя длинную форму git rebase: . Чтобы узнать, как определить каждый из них, см. (Очень) длинный ответ ниже. (К сожалению, он вышел из-под контроля, и у меня нет времени его сокращать.)git rebase --onto <em>newbase</em> <em>upstream</em>

Проблема у вас есть в том , что в Git, филиалы не имеют «отправную точку» -при крайней мере, не в каком - либо полезном способе.

Термин «ветвь» в Git неоднозначен

Первая проблема здесь заключается в том, что в Git слово «ветвь» имеет как минимум два разных значения. Обычно, когда мы свободно говорим о «ветви», из контекста становится ясно, имеем ли мы в виду название ветви - то, что это слово, похожее на masterили developили feature-X- или то, что я называю «происхождение ветви» или «структура ветви», или более неформально, "ДАГлет". 1 Смотрите также Что именно мы подразумеваем под «ветвью»?

В данном конкретном случае, к сожалению, вы имеете в виду оба из них одновременно.


1 Термин DAG является сокращением от «Направленный ациклический граф», который представляет собой граф фиксации: набор вершин или узлов и направленные (от дочернего к родительскому) ребра, так что нет циклов через направленные ребра от любого узла назад. к себе. К этому я просто добавляю уменьшительный суффикс "-let" . Полученное слово имеет счастливое сходство слова Aglet , плюс определенное созвучие со словом «крестик», делая звук немного опасно: «? Это DAGlet , которую я вижу перед собой»

Нарисуйте график коммитов

Всякий раз, когда вам нужно разобраться с этими проблемами, это помогает нарисовать график того, что у вас есть сейчас, или, по крайней мере, какое-то полезное подмножество того, что у вас есть сейчас. Конечно, есть много способов нарисовать это (см. Этот связанный вопрос для нескольких вариантов, включая некоторые плохие :-)), но в виде простого текста в ответе StackOverflow я обычно рисую их так:

...--o--o--o           <-- master
         \
          o--o--o--o   <-- develop

Круглые oузлы представляют коммиты , а также имена ветвей masterи developуказывают на один конкретный коммит- наконечник в каждой ветке.

В Git каждый коммит указывает на свой родительский коммит, и именно так Git формирует ветвящиеся структуры. Под «структурами ветвей» я подразумеваю здесь определенные подмножества общей части предков графа или то, что я называю DAGlets. Имя masterуказывает на самый верхний коммит masterветви, и этот коммит указывает назад (влево) на другой коммит, который является предыдущим коммитом в ветви, и этот коммит снова указывает влево, и так далее.

Когда нам нужно поговорить о конкретных коммитах в этом графе, мы можем использовать их фактические имена, которые представляют собой большие уродливые 40-символьные хеши, которые идентифицируют каждый объект Git. Хотя они действительно неуклюжи, поэтому я заменяю маленькие круглые oбуквы с заглавными:

...--A--B--C           <-- master
         \
          D--E--F--G   <-- develop

и теперь легко сказать, например, что имя masterуказывает на фиксацию C, Cуказывает на Bи Bуказывает на то A, что указывает на большую историю, о которой мы на самом деле не заботимся, и поэтому просто оставляем как ....

Где начинается ветка?

Теперь, совершенно очевидно, для нас с вами, основываясь на этом графическом рисунке, ветвь develop, для которой выполняется коммит наконечника G, начинается с коммита D. Но это не очевидно для Git - и если мы рисуем один и тот же график немного по-другому, это может быть менее очевидно для вас и меня тоже. Например, посмотрите на этот рисунок:

          o             <-- X
         /
...--o--o--o--o--o--o   <-- Y

Очевидно, что ветвь Xимеет только один коммит, а основная строка есть Y, верно? Но давайте добавим несколько букв:

          C             <-- X
         /
...--A--B--D--E--F--G   <-- Y

и затем двигайтесь Yвниз по линии:

          C            <-- X
         /
...--A--B
         \
          D--E--F--G   <-- Y

а затем посмотрите, что произойдет, если мы перейдем Cк главной линии и поймем , что Xесть masterи Yесть develop? В какой ветке все- таки есть коммит B?

В Git коммиты могут быть на многих ветках одновременно; DAGlets до вас

Ответ Git на этой дилеммы состоит в том, что коммиты Aи Bнаходятся на обеих ветвях. Начало ветки Xнаходится далеко слева, в ...части. Но так начинается ветка Y. Что касается Git, ветвь «запускается» с любого корневого коммита (ов), который он может найти в графе.

Это важно иметь в виду в целом. Git не имеет реального представления о том, где «начался» филиал, поэтому мы вынуждены предоставить ему дополнительную информацию. Иногда эта информация подразумевается, а иногда она является явной. В общем, также важно помнить, что коммиты часто находятся во многих ветвях, поэтому вместо указания веток мы обычно указываем коммиты.

Мы просто часто используем имена филиалов, чтобы сделать это. Но если мы дадим Git только имя ветки и скажем найти всех предков коммитов tip этой ветки, Git вернется в историю.

В вашем случае, если вы напишите имя developи попросите Git выбрать этот коммит и его предков , вы получите коммиты D-E-F-G(которые вы хотели) и коммит B, и коммит A, и так далее (что вы не сделали). Хитрость заключается в том, чтобы как-то определить, какие коммиты вам не нужны, а какие коммиты вы делаете.

Обычно мы используем двухточечный X..Yсинтаксис

В большинстве команд Git, когда мы хотим выбрать какой-то конкретный DAGlet, мы используем синтаксис из двух точек, описанный в gitrevisions , например master..develop. Большинство 2-х команд Git, которые работают с несколькими коммитами, рассматривают это как: «Выберите все коммиты, начиная с кончика developветви, но затем вычтите из этого набора набор всех коммитов, начиная с кончика masterветви». Оглянитесь на нашем графике рисунок masterи develop: это говорит «Принимают ли фиксации , начиная с Gи работают в обратное направление» -Какой заставляет нас слишком много , так как она включает в себя фиксации Bи Aи earlier- « но исключает фиксации , начиная с Cи двигаясь в обратном направление.» Это что исключает часть, которая дает нам то, что мы хотим.

Следовательно, написание master..develop- это то, как мы называем коммиты D-E-F-G, и у нас Git вычисляет это автоматически для нас, без необходимости сначала сидеть и вытягивать большую часть графика.


2 Существуют два заметных исключения git rebase, которые есть в своем собственном разделе чуть ниже, и git diff. Команда git diffобрабатывается X..Yкак просто смысл X Y, то есть фактически полностью игнорирует две точки. Обратите внимание, что это имеет совсем другой эффект, чем вычитание набора: в нашем случае, git diff master..developпросто дифференцирует дерево для фиксации Cпротив дерева для фиксации G, даже если master..developникогда не было фиксации Cв первом наборе.

Другими словами, математически, master..developобычно ancestors(develop) - ancestors(master), когда ancestorsфункция включает в себя указанный коммит, т. Е. Тестирует ≤, а не просто <. Обратите внимание, что ancestors(develop)не включает коммит Cвообще. Заданная операция вычитания просто игнорирует присутствие Cв наборе ancestors(master). Но когда вы кормите это git diff, оно не игнорирует C: оно не отличается, скажем, Bот G. Несмотря на то , что может быть разумным , что нужно сделать, git diffа не ворует три -dot master...developсинтаксиса для достижения этой цели .

Git's rebaseнемного особенный

rebaseКоманда почти всегда используется для перемещения 3 один из этих DAGlet совершающих-подмножеств из одной точки на графике к другому. Фактически, это то, что rebase определено или изначально было определено сделать. (Теперь у него есть необычный интерактивный режим перебазирования, который делает это и куча других операций редактирования истории. У Mercurial есть похожая команда hg histedit, с немного лучшим именем и гораздо более строгой семантикой по умолчанию. 4 )

Поскольку мы всегда (или почти всегда) хотим переместить DAGlet, мы строим git rebaseэтот набор подмножеств для нас. И, поскольку мы всегда (или почти всегда) хотим, чтобы DAGlet приходил сразу после кончика какой-либо другой ветви, по git rebaseумолчанию выбирается целевая (или --onto) фиксация с использованием имени ветви, а затем используется это же имя ветви в X..Yсинтаксисе. , 5


3 Технически, git rebaseфактически копирует коммиты, а не перемещает их. Это необходимо, потому что коммиты неизменны , как и все внутренние объекты Git. Истинное имя, хэш SHA-1 коммита, представляет собой контрольную сумму битов, составляющих коммит, поэтому каждый раз, когда вы меняете что-либо - включая что-то столь же простое, как родительский идентификатор - вы должны сделать новый , немного другой , совершать.

4 В Mercurial, вполне в отличие от Git, ветви действительно ли есть отправные точки, и, более важны для histedit-commits записывать их фазы : секрета, проекта, или опубликовано. Редактирование истории легко применимо к секретным или черновым коммитам, а не к опубликованным коммитам. Это также верно и для Git, но, поскольку Git не имеет понятия фаз фиксации, в ребазе Git должны использоваться эти другие методы.

- Технически <upstream>и --ontoаргументы могут быть просто сырой совершать идентификаторы. Обратите внимание, 1234567..developчто он прекрасно работает как селектор диапазона, и вы можете сделать перебаз, --onto 1234567чтобы разместить новые коммиты после коммита 1234567. Единственное место, которое git rebaseдействительно нуждается в имени ветки, - это имя текущей ветки, которое она обычно просто читает из HEADлюбого места. Тем не менее, мы обычно хотим использовать имя, поэтому я так все и опишу здесь.


То есть, если мы в настоящее время на ветке develop, и в этой ситуации, которую мы нарисовали раньше:

...--A--B--C           <-- master
         \
          D--E--F--G   <-- develop

мы, вероятно, просто хотим переместить D-E-F-Gцепочку на кончик master, чтобы получить это:

...--A--B--C              <-- master
            \
             D'-E'-F'-G'  <-- develop

(Причина, по которой я изменил имена с, D-E-F-Gна D'-E'-F'-G'то, что rebase вынужден копировать оригинальные коммиты, а не перемещать их. Новые копии так же хороши, как и оригиналы, и мы можем использовать одно и то же одно буквенное имя, но мы должны по крайней мере, заметьте, хотя и смутно, что это на самом деле копии. Для этого и нужны «главные» знаки, 'символы.)

Потому что это то, что мы обычно хотим, git rebaseбудет делать это автоматически, если мы просто назовем другую ветвь. То есть мы сейчас на связи develop:

$ git checkout develop

и мы хотим перебазироваться фиксаций , которые находятся на ветви developи находятся не на master, перемещая их к кончику master. Мы могли бы выразить это как , но тогда мы должны были бы напечатать слово дважды (такая ужасная судьба). Таким образом, вместо этого Git выводит это, когда мы просто набираем:git <em>somecmd</em> master..develop mastermasterrebase

$ git rebase master

Имя masterстановится левой стороной ..двухточечного селектора DAGlet, и имя master также становится целью перебазирования; и Git затем возвращается D-E-F-Gна C. Git получает наш филиал по имени , developпуть считывания текущего имени филиала. На самом деле, он использует ярлык, который заключается в том, что когда вам нужно текущее имя ветки, вы можете просто написать HEADвместо этого. Так master..developи master..HEADзначат одно и то же, потому что HEAD есть develop .

Гит rebaseназывает это имя <upstream>. То есть, когда мы говорим git rebase master, претензии Git, в документации, что masterявляется <upstream>аргументом git rebase. Затем команда rebase воздействует на коммиты <upstream>..HEAD, копируя их после любого коммита <upstream>.

Это скоро станет для нас проблемой, а пока просто отметьте это.

(У Rebase также есть скрытая, но желательная побочная особенность - пропуск любого из D-E-F-Gкоммитов, который достаточно похож на коммит C. Для наших целей мы можем игнорировать это.)

Что не так с другим ответом на этот вопрос

В случае, если другой ответ будет удален или станет одним из нескольких других ответов, я приведу его здесь как «используйте git branch -fдля перемещения метки ветви». Недостаток в другом ответе - и, возможно, что более важно, именно тогда , когда это проблема - становится очевидным, когда мы рисуем наш график DAGlets.

Имена ветвей уникальны, но коммиты не обязательно так

Давайте посмотрим, что происходит, когда вы бежите git checkout -b newbranch starting-point. Это заставляет Git искать в текущем графе заданную начальную точку и указывать метку новой ветви для этой конкретной фиксации. (Я знаю , что я сказал выше , что филиалы не имеют начальную точку Это по - прежнему в основном верно:. Мы давая git checkoutкоманду отправной точкой в настоящее время , но Git собирается установить его и, самое главное, забыть его.) Давайте скажем, что starting-pointэто другое название ветки, и давайте нарисуем целую кучу веток:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

Поскольку у нас есть четыре филиала названия , у нас есть четыре филиала советы : четыре ветви кончику коммиты, которые были определены имена brAчерез brD. Мы выбираем один и создаем новое имя ветви, newbranchкоторое указывает на тот же коммит, что и один из этих четырех. Я произвольно выбрал brAздесь:

          o--o--o--o     <-- brA, newbranch
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

Теперь у нас есть пять имен и пять ... ну, четыре? ... хорошо, некоторые советы фиксируются. Сложно бит, что brAи newbranchоба указывают на тот же наконечник фиксации.

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

          o--o--o--o     <-- brA, HEAD -> newbranch
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

На данный момент четыре коммита, которые раньше были только на ветке brA, теперь находятся на обоих brAи newbranch. И, к тому же, Git больше не знает, что newbranchначинается на кончике brA. Что касается Git, то brAи newbranchте , и другие содержат эти четыре коммита, а также все более ранние, и оба они «начинают» где-то в далеком прошлом.

Когда мы делаем новые коммиты, текущее имя перемещается

Так как мы находимся на ветке newbranch, если мы сделаем новый коммит сейчас, родитель нового коммита будет старым коммитом tip, и Git откорректирует имя ветки так, newbranchчтобы оно указывало на новый коммит:

                     o   <-- HEAD -> newbranch
                    /
          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

Обратите внимание, что ни одна из других меток не сместилась: четыре «старых» ветки остаются на месте, HEADменяется только ветка current ( ). Это изменяется, чтобы приспособить новый коммит, который мы только что сделали.

Обратите внимание, что Git по-прежнему не подозревает, что ветка newbranch«началась» в brA. Теперь это как раз тот случай, когда в нем newbranchсодержится один коммит, brAкоторого нет, плюс четыре коммита, которые они оба содержат, плюс все те более ранние коммиты.

какая git branch -f does

Использование git branch -fпозволяет нам перемещать метку ветки . Допустим, по какой-то таинственной причине мы не хотим, чтобы метка ветви brBуказывала на то, что она делает в нашем текущем чертеже. Вместо этого мы хотим, чтобы он указывал на тот же коммит, что и brC. Мы можем использовать git branch -fдля изменения места, в которое brBуказывает точка, т. Е. Для перемещения метки:

$ git branch -f brB brC

                     o   <-- HEAD -> newbranch
                    /
          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    [abandoned]
            \
             o--o--o     <-- brC, brB
                 \
                  o--o   <-- brD

Это заставляет Git «забыть» или «отказаться» от тех трех коммитов, которые были только brBранее. Это, вероятно , плохая идея, почему же мы решили сделать эту странную вещь? -Так мы , вероятно , хотим , чтобы положить brBобратно.

Reflogs

К счастью, «заброшенные» коммиты обычно запоминаются в том, что Git называет рефлогами . Reflogs используют расширенный синтаксис . Часть выбора обычно представляет собой либо число, либо дату, например, или . Каждый раз, когда Git обновляет имя ветви для указания какого-либо коммита, он записывает запись reflog для этой ветви с идентификатором указанного коммита, отметкой времени и необязательным сообщением. Беги, чтобы увидеть это. Команда записала новую цель как , увеличивая все старые числа, поэтому теперь называет предыдущую подсказку commit. Так:name@{<em>selector</em>}brB@{1}brB@{yesterday}git reflog brBgit branch -fbrB@{0}brB@{1}

$ git branch -f brB 'brB@{1}'
    # you may not need the quotes, 'brB@{...}' --
    # I need them in my shell, otherwise the shell
    # eats the braces.  Some shells do, some don't.

вернет его обратно (а также перенумерует все числа снова: каждое обновление заменяет старое @{0}и делает его @{1}, и @{1}становится @{2}, и так далее).

Во всяком случае, предположим, что мы делаем все, git checkout -b newbranchпока мы находимся brC, и не упомянуть brA. То есть мы начнем с:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- HEAD -> brC
                 \
                  o--o   <-- brD

и беги git checkout -b newbranch. Тогда мы получаем это:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC, HEAD -> newbranch
                 \
                  o--o   <-- brD

Если мы предназначены , чтобы сделать newbranchточку фиксации brA, мы можем на самом деле сделать это прямо сейчас, с git branch -f. Но допустим, что мы делаем новый коммит, прежде чем осознаем, что newbranchначали не с той точки. Давайте нарисуем это в:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \  \
                 |   o   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

Если мы используем git branch -fсейчас, мы откажемся - потеряем - обязательство, которое мы только что сделали. Вместо этого мы хотим перебазировать его в коммит, на который brAуказывает ветвь .

Простые git rebaseкопии слишком много

Что если вместо использования git branch -fмы используем git rebase brA? Давайте проанализируем это, используя - что еще - наши DAGlets. Мы начинаем с вышеприведенного рисунка выше, с вытянутой ногой, идущей к brD, хотя в конце мы получаем, чтобы игнорировать эту ногу, и с секцией, идущей к brB, большую часть которой мы также можем игнорировать. То, что мы не можем игнорировать, это все то, что находится посередине, которое мы получаем, отслеживая линии назад.

Команда git rebaseв этой форме будет использоваться brA..newbranchдля выбора коммитов для копирования. Итак, начиная со всего DAGlet, давайте пометим (с *) все коммиты, которые включены (или содержатся) newbranch:

          o--o--o--o     <-- brA
         /
...--*--*--*--o--o--o    <-- brB
            \
             *--*--*     <-- brC
                 \  \
                 |   *   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

Теперь давайте снимем пометку (с x) всех коммитов, которые включены (или содержатся) brA:

          x--x--x--x     <-- brA
         /
...--x--x--*--o--o--o    <-- brB
            \
             *--*--*     <-- brC
                 \  \
                 |   *   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

Все, что остается - все *коммиты - это те, которые git rebaseбудут копировать. Это слишком много!

Нам нужно git rebaseскопировать только один коммит. Это означает, что для <upstream>аргумента нам нужно дать git rebaseимя brC. 6 Таким образом, Git будет использовать brC..HEADдля выбора коммитов для копирования, что будет только тем коммитом, который нам нужно скопировать.

Но - увы! - теперь у нас есть большая проблема, потому что мы хотим git rebaseскопировать коммит в точку сразу после того, как <upstream>мы ее только что дали. То есть он хочет скопировать коммиты сразу после brC. Вот где коммиты сейчас! (Ну, один коммит.) Так что это совсем не хорошо!

К счастью, git rebaseесть запасной люк, в частности, --ontoаргумент. Я уже упоминал об этом несколько раз, но сейчас, когда нам это нужно. Мы хотим, чтобы копии отправлялись сразу после этого brA, так что мы предоставим это в качестве --ontoаргумента. Git rebaseиспользует <upstream> по умолчанию , но если мы даем ему --onto, он использует это вместо. Так:

$ git branch   # just checking...
  brA
  brB
  brC
  brD
  master
* newbranch

Хорошо, хорошо, мы все еще на связи newbranch. (Обратите внимание, что и git statusздесь это работает, и если вы используете одну из этих необычных настроек командной строки, вы даже можете указать текущее имя вашей ветки, чтобы вам не приходилось запускать ее git statusтак часто.)

$ git rebase --onto brA brC

Теперь Git выберет фиксации в brC..HEAD, который является правильным набором фиксаций для копирования, а затем скопировать их сразу после наконечника brA, который является правильным местом , чтобы скопировать их на . Как только копии будут готовы, Git откажется от оригинальных коммитов 7 и сделает newbranchуказание на имя нового, самого совершенного, скопированного коммита.

Обратите внимание, что это работает, даже если у вас нет новых коммитов в новой ветке. Это тот случай, когда git branch -f тоже работает. Когда коммитов нет, это git rebaseтщательно копирует все их нули :-), а затем делает имя newbranch, указывает на тот же коммит, что и brA. Следовательно, git branch -fэто не всегда неправильно; но git rebaseвсегда прав - хотя и несколько неуклюже: вы должны определить обе точки <upstream>и --ontoточки вручную.


6 Или, как мы отмечали в предыдущей сноске, мы можем дать git rebaseидентификатор коммита, на который brCуказывает имя . В любом случае, мы должны предоставить это в качестве upstreamаргумента.

7 За исключением, конечно, записи reflog newbranch@{1}будут помнить старый, теперь заброшенный, tip commit. Дополнительные записи reflog для newbranchмогут помнить еще больше коммитов, и достаточно вспомнить коммит tip, чтобы все его предки остались живы. Срок действия записей reflog истекает - через 30 дней для некоторых случаев и 90 для других по умолчанию, но по умолчанию это дает вам примерно месяц или около того, чтобы восстановиться после ошибок.

Автор: torek Размещён: 18.07.2016 07:33
32x32