Мастер слияния добавляет все изменения в мою ветку - PullRequest
0 голосов
/ 17 апреля 2020

Я работаю над веткой, я изменил 5 файлов или около того. При этом другие отправили изменения в> 100 файлов в мастер. Теперь, работая над своей веткой, я хочу время от времени merge master в моей локальной ветке. Я бы сделал это так:

git checkout master
git pull
git checkout my-branch
git merge master
git push

Но сейчас, для по какой-то причине все файлы, которые были изменены другими пользователями на master, добавляются в мои изменения. Так что, если бы я на самом деле push после merge master, это показало бы, что я изменил> 100 файлов вместо просто 5. Что я делаю неправильно? Спасибо.

Ответы [ 2 ]

1 голос
/ 17 апреля 2020

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

Есть некоторые ключевые вещи, которые необходимо знать о Git, файлах и фиксациях:

  • Что хранит Git на уровне, с которым вы взаимодействуете это, совершает . Такие имена веток, как master, полезны, но на самом деле они просто помогают Git - и вы - находите коммиты Мы посмотрим, как это работает в данный момент.

  • Коммиты хранят файлы, но вы обычно работаете с целым коммитом за раз. Вы говорите Git: получить мне коммит X для некоторого X , который идентифицирует коммит, и вы получите все файлы для этого коммита. У вас либо есть фиксация - и, следовательно, все файлы - или у вас нет фиксации вообще, и, следовательно, у вас нет ни одного файла.

  • Каждый коммит имеет уникальный идентификатор , Этот идентификатор является га sh ID и представляет собой большую некрасивую строку случайных букв и цифр, таких как 9fadedd637b312089337d73c3ed8447e9f0aa775. То, что ha sh ID, как только он существует, означает , что коммит, и никогда никакой другой коммит.

  • Содержимое любого коммита полностью, полностью, 100% только для чтения. Ни файлы, хранящиеся в коммите, ни метаданные коммита не могут быть изменены. (Причина этого заключается в том, что идентификатор ha sh является криптографической контрольной суммой c содержимого коммита. Если вы извлекаете коммит, изменяете любой из его битов и помещаете его обратно, вы получаете новый, другой коммит, с новым, другим идентификатором ha sh. Старый коммит все еще там: вы только что добавили один больше коммит.)

  • Снимок всех файлов каждого коммита - это просто снимок. То есть фиксирует не магазин изменения вообще.

  • Но когда вы посмотрите на коммит, Git часто показывает вам изменений . Это трюк! Но это также хорошая вещь, потому что это, как правило, более интересно. raw ha sh ID одного предыдущего или parent commit. Таким образом, с учетом любого одного коммита X , Git может выполнить резервное копирование на один шаг, чтобы найти коммит, который приходит до X . Этот коммит также имеет моментальный снимок.

Git может - и делает - просто извлекает два снимка, родительский и дочерний, и сравнивает их. Для каждого файла, который один и тот же , Git вообще ничего не говорит. Для каждого файла, который отличается , Git показывает рецепт: Начните с родительской копии файла. Добавьте эту строку здесь. Удалите это там. Повторите при необходимости, и когда вы закончите добавлять и удалять, у вас будет версия файла, которая находится в дочернем коммите.

Когда у вас есть простая строка коммитов, все в Строка, вы можете нарисовать их, или думать о них, как это:

... <-F <-G <-H ...

, где H обозначает некоторый идентификатор ha sh, который находит коммит. Сам коммит H содержит ha sh ID своего родителя, который мы просто назовем G. Это позволяет Git найти G. G содержит идентификатор ha sh его родительский элемент, F, что позволяет Git найти F и т. Д.

A имя ветви подобно master просто содержит идентификатор ha sh последнего коммита в цепочке . Последний коммит указывает назад на своего родителя, который снова указывает назад и так далее. Таким образом, мы можем нарисовать это как:

...--F--G--H   <-- master

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

...--G--H   <-- master

, а затем добавить новое имя ветви, чтобы мы могли делать новые коммиты, не касаясь нашего master:

...--G--H   <-- master, dev

, но в конечном итоге мы будем добавьте новый коммит в нашу ветку. Давайте добавим специальное имя HEAD к dev, чтобы помнить, что это имя, которое мы используем - имя, которое мы использовали при запуске git checkout dev - и нарисуем его так:

...--G--H   <-- master, dev (HEAD)

Теперь мы сделаем новый коммит. Он получит какой-то большой уродливый случайный идентификатор ha sh, но мы просто назовем его I и нарисуем так:

          I
         /
...--G--H   <-- master, dev (HEAD)

I указывает на H, потому что H является текущим коммитом , когда мы делаем I.

Теперь приходит хитрый трюк: Git записывает I идентификатор ha sh в название филиала. Измененное имя ветви - это то, к которому HEAD присоединено: dev. Так что теперь dev указывает на I вместо H:

          I   <-- dev (HEAD)
         /
...--G--H   <-- master

Нет существующий коммит изменился. (В конце концов, никто не может.) Но наш новый коммит I теперь существует и указывает на существующий коммит H, а теперь наше имя dev указывает на коммит I, что теперь текущий коммит.

Когда мы делаем новый коммит J, Git делает то же самое, давая нам:

          I--J   <-- dev (HEAD)
         /
...--G--H   <-- master

На этом этапе, однако, мы можем запустить git checkout master и git pull (или git fetch && git merge) и получить новые коммиты, сделанные кем-то другим. Просто для симметрии я нарисую два коммита, которые сделал кто-то другой. Это продвигает нашу master вверх по сравнению с двумя новыми коммитами:

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

Текущая ветвь теперь master, а текущая фиксация сейчас L. Вы можете удивиться, почему я нарисовал их в отдельной строке: в основном следует подчеркнуть, что коммиты до H находятся на в обеих ветвях . Этот странный факт - фиксация может происходить более чем на одной ветви за раз - несколько присущ Git.

Теперь мы можем запустить git checkout dev, чтобы подготовиться к слиянию master в dev. Этот первый шаг просто перемещает HEAD к dev:

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

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

В наши коммиты I-J, мы сделали некоторые изменения в некоторых файлах. В их коммитах K-L они - кем бы они ни были - внесли некоторые изменения в некоторые файлы. Мы собираемся сделать новый коммит слияния , и этот коммит слияния будет содержать снимок, как и каждый коммит. Что должно go в этом снимке?

Ответ таков: нам бы хотелось, чтобы этот снимок сочетал нашу работу с их работой. То есть мы хотели бы начать с каждого файла из общего, общего коммита . Лучшая общая общая отправная точка ясна из диаграммы: это коммит H. Этот коммит находится на обеих ветках. Так же и G, но H лучше лучше , потому что это самое близкое к J и L.

Итак, Git начнется с того, что находится в H. Это будет сравнение H против J, чтобы увидеть, что мы изменили. У каждого файла, который мы изменили, есть рецепт: добавить несколько строк, удалить несколько строк. Затем Git начнется снова с того, что находится в H, и сравните H с L, чтобы увидеть, что они изменились. У каждого файла, который они изменили, есть рецепт: добавить несколько строк, удалить несколько строк.

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

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

Объединив все файлы в меру своих возможностей, Git теперь либо останавливается с конфликтами слияния, либо не имеет никаких конфликтов слияния и продолжает делать коммит слияния . Давайте предположим, что не было никаких конфликтов, чтобы упростить задачу.

Единственное, что является особенным в этом коммите слияния, это то, что вместо одного родителя у него есть два. Мы можем нарисовать его так:

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

Родитель first нового коммита M - это commit J, который продвигает ветку dev на один шаг как обычно. Родитель second нового коммита M является коммитом L, который по-прежнему является коммитом коммита ветви master. С именем master ничего не происходит, и * существующий коммит не изменился (так как никто не может), но новый коммит слияния M делает так, что коммиты K и L теперь находятся на ветке dev тоже, вместе с коммитами до J.

Почему слияния работают

Если мы сейчас спросим Git: , где появилась какая-то конкретная строка (скажем, строка 42) некоторого конкретного файла F из , Git может посмотреть снимок в M, а затем и снимки в J и L. Если строка 42 F соответствует M и J, но отличается в M и L, то строка 42 "взята из" J: объединить сохранить строку из J. Git теперь откатится еще на один коммит до I, чтобы увидеть, совпадает ли строка 42 в F с I и J. Если они там другие, Git скажет, что строка 42 пришла от человека, который сделал коммит I, в день, когда он сделал коммит I.

Если строка 42 из F однако совпадает в M и L и отличается в J, что означает, что слияние сохранило строку 42 из L. Поэтому Git следует вернуться к L, а затем K и т. Д. При необходимости.

Если строка 42 совпадает с M, L, и J, он, вероятно, прошел без изменений с H, и Git продолжит маршировать назад, по одному коммиту за раз, чтобы увидеть, изменилось ли оно при переходе G -to- H, или оно пришло от еще более раннего изменения.

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

Вы не должны извлекать чужие изменения (если они не ошибочны)

Результат любого слияния автоматически правильный файл. Будущее слияние будет предполагать, что все, что вы вставили , правильно. Если вы извлекаете их изменения , это означает, что вы говорите, что их код был неверным и его следует забыть.

Если это действительно так, то вы можете удалить этот код, но вы вероятно, следует сделать это в другом отдельном коммите, а не делать это непосредственно в слиянии.

Дополнительные примечания о слияниях ускоренной перемотки

Хотя мы не рассмотрели это здесь должным образом, Ответ Чака Лу упоминает ускоренные слияния . Предположим, что мы рисуем серию коммитов, как это:

...--C--D--E   <-- branch1 (HEAD)
            \
             F--G--H   <-- branch2

, что указывает на то, что у нас есть ветвь branch1, и, следовательно, коммит E, проверенный прямо сейчас. Если мы запустим git merge branch2, Git обнаружит, что наилучшим общим коммитом в обеих ветвях является текущий коммит E. В этом случае Git не нужно выполнять слияние. С учетом этой опции Git вместо этого выполнит операцию fast-forward , по сути, выполнив git checkout commit H, но перетащив в ответ имя ветви branch1 вперед. :

...--C--D--E
            \
             F--G--H   <-- branch1 (HEAD), branch2

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

Когда Git делает это Операция также сравнивает снимок старого коммита E с новым текущим коммитом H. Для каждого файла, который изменился, он сообщает вам об этом изменении.

Вы можете увидеть то же самое сравнение, выполнив:

git diff --stat <hash-of-E> HEAD

Так как HEAD теперь называет имена commit H, этот git diff сравнивает снимок в E со снимком в H - в точности то же самое, что git pull сделал - и поэтому печатает ту же информацию снова.

Когда вы делаете реальное слияние (как мы сделали с M), информация, которую вы видите прямо в то время, основана на сравнении вашего предыдущего коммита (J) и этого в M. Поскольку M объединяет изменения с обеих "сторон" ветви, но J имеет ваши изменения, что вы видите их изменения . Однако вы можете запустить git diff --stat master dev для сравнения коммита L с коммитом M: на этот раз вы увидите, что произошло слияние с "вашей стороны" ветви.

Трудно увидеть что в реальном слиянии M в общем из-за его двух родителей. Вам нужны две отдельные команды git diff, чтобы увидеть это правильно, правда. Команда git show может сделать это автоматически, если вы укажете флаг -m, но мы не будем здесь это рассматривать.

1 голос
/ 17 апреля 2020

Существует два вида git слияния, fast-forward и no-fast-forward.

Похоже, вы столкнулись с типом no-fast-forward, который генерирует новый коммит слияния.

Если вы не хотите генерировать коммит слияния, вы можете попробовать git rebase.

git checkout master  
git pull  
git rebase master my-branch (might encounter conflicts here)  
git push  

Вы можете найти анимацию демо о перебазировании здесь

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