Git Объединение выбранных веток в новую ветвь с сохранением доступа к веткам

git github bitbucket git-bash git-commit

459 просмотра

1 ответ

Что именно я набираю, чтобы перейти: (Я также чувствую от других людей, что мои рисунки подсказывают, что я не совсем понимаю git - терпите меня.)

               -<>-<>-<>-<>- (B)
             /            
-----master-            
             \         
               --<>-<>- (A)

where '<>' is a commit.

к этому:

                    (merge A and B into C)

               --------------o-> (C, new 'clean' branch off master)
              /             /
             /-<>-<>-<>-<>-/ (B)
            //            /
-----master--            /
              \         /
               --<>-<>-/ (A)

where 'o' is a merge of A and B into C.

И смогу ли я тогда еще к git check-outветкам (A)и (B)?

И / или я мог бы сделать это:

               --------------o-<>-(C)
              /             /
             /-<>-<>-<>-<>-/-<>-<>-(B)
            //            /
-----master--            /
              \         /
               --<>-<>-/-<>-<>-<>-(A)

Если вы можете, даже в каком-то смысле, не могли бы вы объяснить? Благодарю.

Автор: JohnG79 Источник Размещён: 29.09.2019 09:51

Ответы (1)


1 плюс

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

$ git cat-file -p HEAD

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

tree 142feb985388972de41ba56af8bc066f1e22ccf9
parent 62ebe03b9e8d5a6a37ea2b726d64b109aec0508c
author A U Thor <thor@example.com> 1501864272 -0700
committer A U Thor <thor@example.com> 1501864272 -0700

this is some commit

It has a commit message.

Вот и все - это все, что вам нужно для фиксации! Хотя здесь многое скрывается на виду. В частности, есть treeи parentлиния, которые имеют эти большие уродливые идентификаторы хэша. На самом деле, имя HEADвыступает в качестве замены для другого:

$ git rev-parse HEAD
4384e3cde2ce8ecd194202e171ae16333d241326

(опять же, ваш номер будет другим).

Эти хэш-идентификаторы являются «истинными именами» каждого коммита (или - как и для treeнекоторых других объектов Git). Эти хеш-идентификаторы на самом деле являются криптографическими контрольными суммами содержимого коммита (или другого типа объекта, подобного tree). Если вам известно содержимое - последовательность байтов, составляющих объект, - а также его тип и размер, вы можете сами вычислить этот хэш-идентификатор, хотя реальной причины для беспокойства нет.

Что в коммите

Как видно из вышесказанного, коммит хранит относительно небольшое количество информации. Фактический объект, эта короткая строка текстовых строк, попадает в базу данных Git и получает уникальный хэш-идентификатор. Этот идентификатор хеша является его «истинным именем»: когда Git хочет увидеть, что находится в коммите, вы даете Git что-то, что создает идентификатор, и Git извлекает сам объект из базы данных Git. Внутри объекта коммита мы имеем:

  • Дерево . В нем хранится исходное дерево, которое вы сохранили (путем git addввода и, в конечном итоге, git commit- последний git commitшаг сначала записывает дерево, а затем коммит).
  • Родитель . Это хэш-идентификатор другого коммита. Мы вернемся к этому через минуту.
  • Автор и коммиттер: они содержат имя человека , который написал код (то есть автор) и сделал коммит. Они разделяются в случае, если кто-то отправляет вам патч по электронной почте: тогда другой человек является автором, но вы являетесь коммиттером. (Git родился за несколько дней до сайтов для совместной работы, таких как GitHub, поэтому рассылка исправлений по электронной почте была довольно распространенной.) Они хранят адрес электронной почты и отметку времени, вместе с отметкой времени в этой нечетной числовой паре.
  • Сообщение журнала . Это просто текст произвольной формы, что бы вы ни хотели предоставить. Единственное, что Git интерпретирует здесь, это пустая строка, отделяющая тему сообщения журнала от остальной части сообщения журнала (и даже тогда, только для форматирования: например, git log --onelinevs git log).

Создание коммитов, начиная с полностью пустого репозитория

Предположим, у нас есть полностью пустой репозиторий без коммитов. Если бы мы пошли рисовать коммиты, мы бы просто получили пустой рисунок или пустую доску. Итак, давайте сделаем первый коммит, запустив git addнекоторые файлы, такие как README, и запустив git commit.

Этот первый коммит получает какой-то большой уродливый хеш-идентификатор, но давайте просто назовем его «коммит А» и нарисуем его:

A

Это единственный коммит. Так ... каков его родитель?

Ответ таков: у него нет родителей. Это первый коммит, поэтому не может. Так что у него нет parentлинии в конце концов. Это делает его корневым коммитом .

Давайте сделаем второй коммит, создав полезный файл, а не просто README. Тогда мы будем git addэтот файл и git commit. Новый коммит получает еще один большой уродливый хэш-идентификатор, но мы его просто назовем B. Давайте нарисуем это в:

A <-B

Если мы посмотрим на Bс git cat-file -p <hash for B>, мы увидим , что на этот раз у нас есть parentлиния, и это показывает хэш A. Мы говорим, что B«указывает на» A; Aэто Bродитель

Если мы сделаем третий коммит Cи посмотрим на него, то увидим, что Cродительский элемент - Bэто хеш:

A <-B <-C

Так что теперь Cуказывает на B, Bуказывает на A, и Aявляется корневым коммитом и нигде не указывает. Вот как работают коммиты Git: каждый указывает назад , на своего родителя. Цепочка обратных указателей заканчивается, когда мы достигаем корневого коммита.

Теперь все эти внутренние указатели исправлены, как и все остальное в коммите. Вы не можете изменить что - либо в какой - либо совершить, когда - либо, потому что его большая уродливая хэш - ID является криптографическая контрольная сумма из содержимого этой фиксации. Если вам как-то удастся что-то изменить, криптографическая контрольная сумма тоже изменится. У вас будет новый, другой коммит.

Поскольку все внутренние указатели фиксированы (и всегда указывают назад), нам не нужно их рисовать:

A--B--C

достаточно. Но - вот где имена ветвей и имя HEADвходят - нам нужно знать, с чего начать . Хеш-идентификаторы выглядят довольно случайными, в отличие от нашего простого простого, A-B-Cгде мы знаем порядок букв. Если у вас есть два идентификатора, например:

62ebe03b9e8d5a6a37ea2b726d64b109aec0508c
3e05c534314fd5933ff483e73f54567a20c94a69

невозможно сказать, в каком порядке они идут, по крайней мере, не по удостоверениям личности. Таким образом, нам нужно знать, какой из них является последним коммитом, т. Е. Коммитом типа tip для некоторой ветви master. Затем мы можем начать с самого последнего коммита и работать в обратном направлении, следуя этим родительским ссылкам по одной за раз. Если мы сможем найти коммит C, Cто найдем Bи Bнайдем A.

Хэш-идентификаторы хранилищ имен филиалов

Что делает Git, так это хранит хэш-идентификатор коммитов tip ветви в (другой) базе данных. Вместо использования хеш-идентификаторов в качестве ключей здесь используются ключи с именами ветвей, а их значения не являются фактическими объектами, а представляют собой просто хеш-идентификаторы коммитов tip.

(Эта «база данных» - по крайней мере в настоящее время - в основном просто набор файлов: .git/refs/heads/masterэто файл, содержащий хеш-идентификатор для master. Таким образом, «обновление базы данных» просто означает «запись нового хеш-идентификатора в файл». Но этот метод делает не очень хорошо работает в Windows, поскольку это означает, что masterи MASTER, которые должны быть двумя разными ветвями, используют один и тот же файл, что вызывает всевозможные проблемы. На данный момент никогда не используйте два имени ветвей, которые отличаются только регистром.)

Итак, теперь давайте посмотрим на добавление нового коммита Dв нашу серию из трех коммитов. Сначала нарисуем имя master:

A--B--C   <-- master

Имя masterсодержит хэш-идентификатор Cна данный момент, что позволяет нам (или Git) находить C, делать с ним все, что мы хотим, и использовать Cдля поиска B. Затем мы используем Bдля поиска A, а затем, поскольку Aэто корневой коммит, мы закончили. Мы говорим, что masterуказывает на C.

Теперь мы добавляем или меняем некоторые файлы и git commit. Git записывает новое дерево как обычно, а затем пишет новый коммит D. DРодитель будет C:

A--B--C   <-- master
       \
        D

и, наконец, Git просто заполняет Dхэш, независимо от того, каким он оказывается master:

A--B--C
       \
        D   <-- master

Теперь masterуказывает на D, поэтому в следующий раз, когда мы будем работать, masterмы начнем с коммита D, затем Dвернёмся к родительской стрелке назад Cи так далее. Указывая на D, имя ветки masterтеперь имеет в Dкачестве подсказки коммит. (И, конечно же, больше нет причин рисовать график с изломом, подобным этому.)

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

Несколько филиалов

Теперь давайте посмотрим на создание более чем одной ветви, и зачем нам это нужно HEAD.

Мы продолжим с нашими четырьмя коммитами:

A--B--C--D   <-- master

Теперь давайте создадим новую ветку develop, используя git branch developили git checkout -b develop. Поскольку имена ветвей - это просто файлы (или записи базы данных), содержащие хэш-идентификаторы, мы сделаем так, чтобы новое имя develop также указывало на коммит D:

A--B--C--D   <-- master, develop

Но теперь, когда у нас есть два или более названий ветвей, нам нужно знать: на какой мы ветке? Это где HEADприходит.

HEADВ Git на самом деле просто еще один файл, .git/HEAD, который обычно содержит строку , ref:за которой следует полное название филиала. Если мы находимся master, .git/HEADимеет ref: refs/heads/masterв этом. Если мы находимся develop, .git/HEADимеет ref: refs/heads/developв этом. Эти refs/heads/вещи являются именами файлов, содержащих хэши tip-коммитов, поэтому Git может прочитать READ, получить имя ветки, затем прочитать файл ветки и получить правильный ID хэша.

Давайте нарисуем это тоже, прежде чем мы переключимся на ветку develop:

A--B--C--D   <-- master (HEAD), develop

а затем после того, как мы переключимся на develop:

A--B--C--D   <-- master, develop (HEAD)

Это все, что здесь происходит! Есть еще кое-что, что происходит в других местах при переключении веток, но для работы с графиком все, что git checkoutнужно, это изменить имя, HEADк которому присоединено.

Теперь давайте сделаем новый коммит E. Новый коммит входит как обычно, и его новый родитель - это то HEAD, что говорит, то есть Dтак:

A--B--C--D   <-- master, develop (HEAD)
          \
           E

Теперь мы должны обновить какую-то ветку. Тока ветвь develop, так что это один мы обновляем. Мы пишем Eхэш-идентификатор, и теперь у нас есть:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

Вот и все - это все, что нужно для того, чтобы в Git росли ветви! Мы просто добавляем новый коммит, где бы он ни HEADнаходился сейчас, чтобы родитель нового коммита был старым HEADкоммитом. Затем мы перемещаем ту ветку, которая указывает на новый коммит, который мы только что сделали.

Объединение и слияние коммитов

Теперь, когда у нас есть несколько веток, давайте сделаем еще несколько коммитов для каждой. Нам нужно будет перейти к git checkoutкаждой ветви и сделать некоторые коммиты, но предположим, что в итоге мы получим следующий график:

A--B--C--D--G   <-- master (HEAD)
          \
           E--F   <-- develop

Теперь у нас есть один дополнительный коммит master(это ветвь, в которой мы находимся) и два develop, плюс четыре оригинальных A-B-C-Dкоммита, которые есть в обеих ветвях.

(Между прочим, это особенность Git, не встречающаяся во многих других системах контроля версий. В большинстве VCS ветвь, в которой фиксация включена, устанавливается при выполнении фиксации, так же, как устанавливаются родители коммитов. в то время в камне. Но в Git имена веток - это очень легкие пушистые вещи, которые просто указывают на один коммит: конец ветки. Таким образом, набор ветвей, которые какой-то коммит "включен", определяется путем нахождения всех ветвей имена, а затем следуя всем стрелкам, направленным назад, чтобы увидеть, какие коммиты достижимы , начиная с того, с каких кончиков веток. Эта концепция достижимости очень важна, скоро, хотя мы не доберемся до этой публикации. Смотрите также http: //think-like-a-git.net/ например.)

Теперь давайте бегем, git merge developчтобы объединить developкоммиты обратно в master. Помните, что мы в настоящее время на master -JUST взгляд на HEADна чертеже. Таким образом , Git будет использовать имя , developчтобы найти его кончик фиксации, что Fи имя , HEADчтобы найти наш наконечник фиксации, который G.

Затем Git будет использовать этот график, который мы рисовали, чтобы найти общий базовый коммит слияния . Вот, это коммит D. Коммит D- это то место, где эти две ветви сначала снова объединяются.

Основной процесс слияния в Git несколько сложен и запутан, но если все идет хорошо - и обычно это происходит - нам не нужно углубляться в это. Мы можем просто знать, что Git сравнивает коммит Dс коммитом, Gчтобы увидеть, что мы сделали master, и сравнивает коммит Dс коммитом, Fчтобы увидеть, что они сделали develop. Затем Git объединяет оба набора изменений, следя за тем, чтобы все, что сделано в обеих ветвях, делалось ровно один раз.

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

Результат этого процесса слияния, это слияние, как-глагол, является источником дерева, и вы знаете , что мы делаем с деревом, не так ли? Мы делаем обязательство! Вот что Git делает дальше: он делает новый коммит. Новый коммит работает во многом как любой обычный коммит. У него есть дерево, которое Git только что создал. У него есть автор, коммиттер и сообщение коммита. И у него есть родитель, который является нашим текущим или HEADкоммитом ... и другой , второй родитель, который является коммитом, который мы слили!

Давайте нарисуем наш коммит слияния Hс двумя родительскими стрелками, указывающими назад:

A--B--C--D--G---H   <-- master (HEAD)
          \    /
           E--F   <-- develop

(Мы не сделали - потому что это слишком сложно - учли тот факт, что первый родитель есть, Gа второй - есть F, но позже это полезное свойство.)

Как и при каждом коммите, новый коммит переходит в текущую ветвь и продвигает имя ветки вперед. Так что masterтеперь указывает на новый коммит слияния H. Это Hчто указывает обратно на обоих Gи F.

Этот вид коммита, этот коммит слияния , также использует слово «слияние». В этом случае «слияние» является прилагательным, но мы (и Git) часто просто называем это «слияние», используя слово «слияние» в качестве существительного. Таким образом, слияние , существительное, относится к коммиту слияния с слиянием как прилагательным. Коммит слияния - это просто любой коммит по крайней мере с двумя родителями .

Мы делаем коммит слиянием, запустив git merge. Однако есть небольшая загвоздка: git mergeне всегда выполняется коммит слияния. Он может выполнять слияние глагола, не делая прилагательного, и на самом деле, он даже не всегда делает глагол рода. Мы можем заставить Git использовать коммит слияния git merge --no-ff, даже в том случае, когда он может пропустить всю работу.

На данный момент мы просто воспользуемся --no-ff , заставив Git сделать реальное слияние. Но сначала мы увидим, зачем нам это нужно --no-ff , а затем, во-вторых, почему мы не должны были беспокоиться!

Вернуться к вашей проблеме из вашего вопроса

Давайте перерисовать ваши графики по-моему, потому что мой путь лучше. :-) У вас есть это для начала:

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

(Здесь ничего не помечено, HEADпотому что сейчас мы не знаем или не заботимся о HEADтом, кто из них, если это вообще что-то из этого.)

Теперь вы хотите создать новую ветку, branch-Cуказывающую на фиксацию A, и сделать ее текущей веткой. Самый быстрый способ сделать это, предполагая, что все уже чисто, это использовать:

$ git checkout -b branch-C master

который перемещается к (извлекает в индекс и рабочее дерево) коммиту, идентифицированному с помощью master(commit A), затем создает новую ветвь, branch-Cуказывающую на этот коммит, а затем делает HEADветвь имени branch-C.

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master, branch-C (HEAD)
         \
          F--G   <-- branch-A

Теперь мы запустим первый, git mergeчтобы подобрать branch-A:

$ git merge --no-ff branch-A

Это позволит сравнить текущий коммит Aс коммитом на основе слияния, что Aснова. (Это причина, по которой мы нуждаемся --no-ff: база слияния - это текущий коммит!) Затем он сравнивает текущий коммит с коммитом G. Git объединит изменения, что означает «взять G», и сделает новый коммит слияния в нашей текущей ветке. Название masterбудет продолжать указывать A, но сейчас я просто перестану рисовать его полностью из-за ограничений в искусстве ASCII:

          B--C--D--E   <-- branch-B
         /
--o--o--A------H   <-- branch-C (HEAD)
         \    /
          F--G   <-- branch-A

Далее мы объединяем branch-B:

$ git merge branch-B

Это позволит сравнить базовую фиксацию слияния Aдля фиксации H, а также сравнить Aс E. (На этот раз база слияния не является текущим коммитом, поэтому нам не нужно --no-ff.) Git, как обычно, попытается объединить изменения - объединить как глагол - и, если это удастся, Git сделает еще один коммит слияния (объединить как существительное или прилагательное), которое мы можем нарисовать так:

          B--C--D--E   <-- branch-B
         /          \
--o--o--A------H-----I   <-- branch-C (HEAD)
         \    /
          F--G   <-- branch-A

Обратите внимание, что ни одно из других имен не изменилось вообще. Филиалы branch-Aи branch-Bдо сих пор указывают на их первоначальные коммиты. Ветвь masterвсе еще указывает на A(и если бы это была доска или бумага или что-то подобное, мы могли бы держать это нарисованным). Теперь имя branch-Cуказывает на второй из двух использованных нами коммитов слияния, поскольку каждое из наших слияний может указывать только на два коммита, а не на три сразу.

Git имеет три вида слияния

Если по какой-то причине вам не нравится иметь два слияния, Git предлагает нечто, называемое слиянием осьминога , которое может объединить более двух кончиков веток одновременно. Но нет никакого требования делать слияние осьминога, поэтому я просто упомяну это здесь для полноты.

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

Нам не нужно одно из слияний

Мы начали с того git merge --no-ff branch-A, что нам нужно было использовать --no-ffGit для быстрого слияния. Мы также отметили почему : причина в том, что база слияния, зафиксированная Aв нашем чертеже, была той же самой, что и branch-Cуказанная в то время.

То , как мы сделали Git объединить «изменения» происходит от совершения Aсовершить A(все нули этих «изменений») с изменениями он обнаружил , идущие от совершения Aсовершить , Gчтобы использовать --no-ff: Хорошо, Git, я знаю , что вы можете сделать это как быстрая перемотка без слияния, но в конце я хочу реального слияния, поэтому представьте, что вы усердно работали и сделайте коммит слияния. Если бы мы пропустили эту опцию, Git просто «сдвинул бы метку ветви вперед», идя против направления стрелок внутреннего коммита. Начнем с:

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master, branch-C (HEAD)
         \
          F--G   <-- branch-A

и тогда Git сделает это:

          B--C--D--E   <-- branch-B
         /
--o--o--A   <-- master
         \
          F--G   <-- branch-A, branch-C (HEAD)

Тогда, когда мы сделали вторую слияние-за которой мы не и до сих пор не нужно --no-ff-Git бы найти слияния базы A, сравнить Aпротив G, сравнить Aпротив E, объединить изменения , чтобы сделать новый treeобъект, и сделать новый совершить Hиз ряда результат:

          B--C--D-----E   <-- branch-B
         /             \
--o--o--A   <-- master  H   <-- branch-C (HEAD)
         \             /
          F-----------G   <-- branch-A

Как и раньше, ни одна из других меток не двигается вообще (и на этот раз мы можем нарисовать имя, masterнемного растянув график). Мы получаем только один коммит слияния Hвместо двух коммитов слияния H--I.

Почему ты можешь хотеть --no-ffтак или иначе

Если мы сделаем два слияния, используя git merge --no-ff, исходное дерево, которое мы получим, когда Git объединит все наши изменения, будет таким же, как исходное дерево, которое мы получим, если мы допустим одно слияние в прямом направлении. Но окончательный график отличается.

Граф коммитов в Git - это история. Если вы хотите знать, что происходило в прошлом, то, что у вас есть - то, на что вы можете посмотреть - это график коммитов . График состоит из всех коммитов, а коммиты хранят имена и даты авторов и коммиттеров, а также сообщения журнала. Они ссылаются на сохраненные исходные деревья и предоставляют родительские ссылки, составляющие график.

Это означает, что в будущем, если вы захотите узнать, что вы сделали два слияния, вы должны сделать два слияния сейчас. Но если в будущем вам все равно, сколько git merge команд вы выполнили, вы можете позволить любому количеству этих git mergeшагов быть операциями ускоренной пересылки (без слияния). Они не оставляют следов на графике коммитов - они просто перемещают одну метку имени ветви с одного коммита на другой - поэтому в будущем вы не сможете точно сказать, произошло ли это когда-либо. График не хранит движение имени; это только коммиты.

Автор: torek Размещён: 26.08.2017 06:02
Вопросы из категории :
32x32