git постепенная перебазировка / полное слияние с веткой - PullRequest
0 голосов
/ 20 марта 2020

Добрый день,

В настоящее время я пытаюсь выяснить, как я могу «перезаписать» ветку, не используя что-то drasti c, например «rebase».

Например:

branch::master
|- dir_a
|  |- file_a
|  |- file_b
|- file_c

branch::dev
|- dir_a
|  |- file_b
|- file_c        [edited compared to branch::master]


branch::master   [after merge from branch::dev with strategy 'ours' (keep changes from branch::dev)]
|- dir_a
|  |- file_a     [is also merged, but should be deleted]
|  |- file_b
|- file_c        [edited version from branch::dev]

Чтобы решить вышеупомянутую проблему, я мог бы сделать 'rebase', но это сломало бы кучу вещей, так как ветвь master (head) не является частной / личной.

Итак вопрос в следующем: Есть ли хороший способ полностью перезаписать / заменить содержимое главной ветви, чтобы не нарушать зависимости от текущей основной ветви?


информация, которая может влияют на возможности:

  • может быть разница между несколькими коммитами между двумя ветвями (сумма также неизвестна)
  • предпочтительно ветвь dev должна продолжаться (также для причины зависимости).
  • желательно, чтобы история основной ветки оставалась неизменной

Если есть какие-либо дополнительные разъяснения, не стесняйтесь спрашивать. Спасибо, что подумали!

Ответы [ 2 ]

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

Есть ли хороший способ полностью перезаписать / заменить содержимое главной ветки ...

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

. Следует иметь в виду, что каждая ветвь имя просто говорит мой последний коммит _____ (заполните бланк с необработанным коммитом ha sh ID). Коммиты, которые находятся "на" ветви, зависят от того, что commit .

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

Каждый коммит содержит две вещи: как основные данные, полный и полный снимок всех файлов, и как его метаданные - информацию о коммите. Это включает в себя имя и адрес электронной почты человека, который его сделал, и сообщение в журнале, объясняющее , почему они совершили этот коммит. Но самое важное, по крайней мере для Git, заключается в следующем: когда вы или кто-либо делаете новый коммит, Git записывает необработанный га sh ID его непосредственного родитель коммит. Этот идентификатор ha sh является идентификатором ha sh идентификатора коммита, который вы только что получили, go, который был последним коммитом в ветви. Опять же, никакая часть этих данных (снимок) или метаданных (включая родительский га sh ID) никогда не может измениться с этого момента, так что это означает, что этот коммит запоминает предыдущий commit-tip commit.

Следовательно, «содержимое ветки» может быть:

  • raw ha sh ID последнего коммита в ветке; или
  • с идентификатором ha sh и, следовательно, с этим коммитом, плюс идентификатор ha sh, сохраненный в этом коммите и, следовательно, с предыдущим коммитом, плюс другой идентификатор ha sh, сохраненный в этом коммите и, следовательно, с другим commit, plus ...

Если мы рисуем эти коммиты, что требует предположений об их идентификаторах ha sh; здесь я просто заменю каждый фактический идентификатор ha sh одиночными заглавными буквами - мы получим такую ​​картинку:

             I--J   <-- master
            /
...--F--G--H
            \
             K--L   <-- dev

То есть name master содержит raw ha sh ID commit J (что бы это ни было на самом деле). Commit J содержит необработанный идентификатор транзакции ha sh I, который содержит идентификатор транзакции ha sh H. Между тем имя dev содержит га sh идентификатор коммита L, который содержит K, который содержит H.

Обратите внимание, что коммит через H на обе ветви.

В этой ситуации ветви объединяются на H, по крайней мере, при просмотре так, как Git просматривает их: начиная с конца и работает в обратном направлении, один коммит за раз.


1 Технически, индекс содержит имена файлов, их режим (+ x или -x) и ссылка на содержимое в замороженном формате.


Как обычно растут ветви

Чтобы сделать новый коммит, вы можете начать с git checkout <em>name</em>. Допустим, имя в этом случае master. Это находит фиксацию , на которую указывает имя - в данном случае J - и копирует содержимое фиксации только для чтения из фиксации, в индекс Git и в вашу работу. -tree. В рабочем дереве вы можете просматривать и редактировать свои файлы. Это также прикрепляет специальное имя HEAD к имени master:

             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

Git теперь можно увидеть, что текущая ветвь равна master (видя где HEAD прилагается), а текущий коммит равен J (видя, где master указывает). Теперь в вашем рабочем дереве есть файлы, с которыми вы работаете - обычные файлы, не подвергнутые сублимационной сушке (сжатые и Git только для зафиксированных файлов), а в индексе Git есть копии 1 из высушенных замораживанием файлов из J, готовых к go в новый коммит.

Теперь вы можете изменять рабочее дерево так, как вам нравится. Когда вы закончите, вы можете запустить git add для различных файлов. При этом каждый добавленный файл content копируется обратно в индекс Git, сжимая их в высушенную для замораживания форму, которую Git будет хранить в коммите, заменяя предыдущую копию. Или вы можете git add новый файл или git rm существующий файл; в любом случае Git соответственно обновляет индекс.

Затем вы запускаете git commit. Git просто упаковывает все, что есть в index , добавляет ваше имя и текущее время, добавляет ваше сообщение журнала и записывает это как новый коммит. parent нового коммита - это текущий коммит , а идентификатор ha sh - это уникальная контрольная сумма содержимого нового коммита, которая больше не может быть изменена. Мы назовем этот новый коммит N (пропуская M). N указывает на J:

                  N
                 /
             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

, но теперь мы получаем хитрость, которая делает ветки полезными: Git теперь записывает идентификатор ha sh нового коммита в имя ветви . Итак, теперь у нас есть:

             I--J--N   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

Обратите внимание, что HEAD не изменился: он все еще присоединен к master.

Теперь давайте избавимся от совершить N. На самом деле он не будет go удален - он все еще будет в нашем хранилище - но мы организуем для name master, чтобы снова идентифицировать commit J. Для этого мы находим фактический идентификатор ha sh, равный J, или любой прокси для него и используем git reset --hard:

git reset --hard <hash-of-J>

Теперь у нас есть:

                  N   [abandoned]
                 /
             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

Если вы сделаете это в вашем хранилище, без использования git push в отправьте новый коммит N в какое-то другое Git хранилище, только у вас будет коммит N. Никто больше никогда не узнает, что вы сделали это!

Использование git reset произвольно

Вы можете, с помощью git reset, переместить любую ветвь на любую коммит. Но если вы перемещаете ветвь «назад», как мы только что сделали, коммиты, которые «падают с конца», становятся трудными для поиска. Предположим, вы заставили имя master указать, чтобы совершить L. Как же тогда найти коммит J? Как насчет коммита I? Кажется, они исчезли!

Этот может быть тем, что вы хотите. Но если у какого-нибудь другого Git репозитория уже есть коммитов I и J, возможно, кто-то, использующий этот репозиторий, создал new коммитов, которые ссылаются на J. Они не будут благодарны вам за то, что вы попросили их забыть все свои коммиты, а также I и J.

Если все в порядке, чтобы все остальные забыли I и J, вы можете просто сбросить настройки. master чтобы указать на L. Затем вам нужно использовать git push --force или его эквивалент, чтобы убедить какой-нибудь другой Git репозиторий забыть также I и J (и, возможно, дополнительные коммиты), и всех остальных, кто использует клон этого хранилище должно убедиться, что их Git забудет I и J и т. д.

Это самый быстрый и простой способ сопоставления master dev, но это также самый разрушительный для всех остальных. Git "нравится", когда ветви растут, и "не нравится", когда они теряют коммиты.

Как работает слияние (сокращенно)

Давайте теперь посмотрим, что произойдет, если вы запустите git merge dev (master снова указывает на J). Git нуждается, для этой операции слияния, три ввода фиксирует. Одним из них является ваш текущий коммит, слияние которого вызывает ours. Одним из них является любой другой коммит по вашему выбору, который объединяет вызовы theirs. третий один - который в некотором смысле является первым; по крайней мере, он получает номер 1 в мгновение - Git находит сам по себе.

Используя git merge dev, вы выбрали коммит L в качестве коммита theirs. Это потому, что имя dev выбирает коммит L. Git теперь работает в обратном направлении от обеих подсказок ветвления, чтобы найти лучший общий коммит , который в данном случае явно является коммитом H. Git называет это базисом слияния .

Помните, что каждый коммит содержит снимок. Так что commit H имеет снимок, а commit J имеет его, а commit L - один. Git может в любое время сравнить любые два снимка , чтобы увидеть разницу. Команда, которая делает это для вас - git diff. Операция объединения хочет знать, что отличается, поэтому она запускает внутренний эквивалент:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed on master
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed on dev

Git теперь может объединить эти два набора изменений и применить комбинированный изменяет снимок в H (не J, не L, а H: база слияния). Если мы добавили файл, а они этого не сделали, Git добавит файл. Если мы удалили файл, а они этого не сделали, Git удалит файл. Если они изменили файл, а мы нет, Git изменит файл.

Результатом, если все пойдет хорошо, будет комбинация наших изменений и их изменений. Git вставит этот результат обратно в индекс - место, из которого Git делает коммиты, - одновременно обновляя наше рабочее дерево, чтобы мы могли видеть, что он сделал. Затем, поскольку все прошло хорошо, Git делает новый коммит.

Новый коммит, который мы назовем M для merge, имеет не одного, а двух родителей , Родитель first такой же, как обычно: M ссылается на существующий коммит J, где master был моментом go. Но Git добавляет коммит L, который мы выбрали для слияния, в качестве второго родителя. Итак, теперь картинка выглядит так: 2

             I--J
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             K--L   <-- dev

Commit M имеет снимок. Его снимок был сделан Git (а не вами), который объединил изменения от общей начальной точки - базы объединения - и применил объединенные изменения к базе объединения снимок.

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


2 Фиксация N все еще существует, но без возможности найти это, Вы больше не можете видеть его, и нам не нужно беспокоиться, чтобы нарисовать его. В конце концов, Git удалит его полностью - обычно через некоторое время после, по крайней мере, 30 дней. А до тех пор вы можете вернуть его обратно, если хотите: вам просто нужно найти его га sh ID.


Это нормальное слияние; вам может потребоваться перезапись слияния

Предположим, что вы можете сказать Git: Начать слияние, но пока не делать коммит слияния M. Git найдет слияние Основываясь как обычно, объедините ваши изменения и их изменения как обычно, и обновите свое рабочее дерево и индекс Git как обычно ... и затем остановитесь, не делая фиксацию.

Вы можете сделать точно что, используя git merge --no-commit. Сделав это, вы можете заменить результат слияния на что угодно. Поместите любой файл в свое рабочее дерево и используйте git add, чтобы Git скопировал его в индекс. Индексная копия, которая будет go в новом коммите слияния, теперь соответствует любому файлу, который вы поместите в рабочем дереве.

Здесь вы можете добавлять новые файлы и удалять их полностью. Что бы вы ни делали, это до вы: вы контролируете все на этом этапе. Вы можете сделать слияние любым контентом, который вам нужен.

То, что вы хотите - в зависимости от вашего вопроса, в любом случае; подумайте, действительно ли это действительно , что вы хотите - это полностью игнорировать разницу от базы слияния H до вашего коммита J, и просто взять разницу от H до их comimt L. Конечно, это будет точно соответствовать снимку в коммите L.

Существует быстрый и простой способ, с помощью git merge -s ours, сказать Git, что он должен полностью игнорировать изменения их ветви и просто используйте вашу версию всего, т. Е. Для принятия J. К сожалению, это противоположно тому, что вы хотите здесь: вы запрашиваете способ полностью игнорировать изменения вашей ветки и просто использовать их версию всего, то есть принять коммит L.

К счастью, есть довольно волшебная команда 3 , которую вы можете использовать, чтобы было быстро и легко, чтобы получить то, что вы хотите здесь:

git merge --no-commit dev
git read-tree --reset -u dev

Эта команда git read-tree -u сообщает Git: Заменить индекс и содержимое моего рабочего дерева файлами, которые хранятся в коммите, который я здесь называю. Опция --reset сообщает Git выбрасывать конфликтные записи, если слияние порождает конфликты слияния (и должно быть безвредным, если нет). Итак, теперь индекс и ваше рабочее дерево соответствуют снимку в L. Теперь вы можете завершить sh слияние: 4

git merge --continue

Это сделает новый коммит слияния M из содержимого, хранящегося в индексе, что - благодаря -u опцию команды git read-tree - вы также можете увидеть в вашем рабочем дереве. Итак, теперь у вас есть:

             I--J
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             K--L   <-- dev

, где контент коммита M точно соответствует контент коммита L. Первый родительский из M по-прежнему J, а второй по-прежнему L, как это было бы для любого другого слияния. Но вы «убили» изменения, которые были у вас в I и J, в то время как только добавили коммит к master. Все остальные Gits будут достаточно счастливы, чтобы добавить совершить M к их коллекциям подтверждений.


3 Это не совсем волхвов c на всех. Это команда plumbing , предназначенная для использования в сценариях, а не для пользователей. Команда read-tree поразительно сложна: она реализует (некоторые из) слияния, операции с поддеревьями и все другие виды хитрости. Это существенная часть причины, по которой существует индекс, который в противном случае является болезненным.

4 Если хотите, вы можете вместо этого запустить git commit. В старых Git версиях, в которых нет git merge --continue, вам придется использовать git commit. Можно использовать git commit напрямую: все, что делает git merge --continue, это сначала проверяет, что есть слияние с fini sh, а затем запускает git commit для вас. Используя git merge --continue, вы проверяете, что действительно завершаете слияние.


Рассмотрите и эту опцию

Предположим, вы не хотите слияния. То есть предположим, что вы хотите сохранить прогрессию:

             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

так, как есть, но добавьте новый коммит O к master, у которого parent равен J , но чей контент совпадает с L на dev?

Это тоже легко сделать. Сделав недавно git checkout master, чтобы ваш индекс и рабочее дерево соответствовали J, вы теперь запускаете:

git read-tree -u dev

(без предшествующего слияния не может быть конфликтов слияния, поэтому --reset не требуется).

Это заменяет индекс Git и содержимое вашего рабочего дерева, читая их из коммита L, как мы это делали в примере слияния. Теперь вы можете запустить git commit, чтобы сделать O:

             I--J--O   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

content commit O теперь соответствует содержимому L; история , полученная при запуске с master - коммит O - и обратная работа читает O, затем J, затем I, затем H и т. д.

(Какой из этих вариантов вы хотите? Это ваше дело.)

0 голосов
/ 22 марта 2020

Если вы хотите выполнить слияние без реального слияния, просто присоединитесь к истории, выбрав «нашу» стратегию слияния:

git checkout dev
git merge -s ours master

Ваша история запишет слияние dev с master, history of обе ветви останутся нетронутыми, но содержимое файла будет выглядеть так: master никогда не существовало (они отражают только dev содержимое)

...