Слияние двух GIT коммитов в одной ветке - PullRequest
2 голосов
/ 29 января 2020

Я заново создал проблему, с которой сталкивался несколько раз. Это базовая c ситуация:

Создайте файл file1.txt со следующим содержимым:

Здравствуйте,
Добро пожаловать в мой файл.
До свидания.

$ git add file1.txt
$ git commit -m “Initial commit”

Добавьте вторую строку тела в file1.txt. ** Примечание: «Случайно» удалите «Welcome to my file» при добавлении этой строки.

Здравствуйте,
Это вторая строка.
До свидания.

$ git add file1.txt
$ git commit -m “Added second line”


$ git log
commit ccd8.. (HEAD -> master)
Author: ____
Date:   Tue Jan 28 11:50:11 2020 -0800

Added second line

commit 6d83..
Author: ___
Date:   Tue Jan 28 11:49:36 2020 -0800

Initial commit

Как лучше всего объединить эти два коммита? Цель состоит в том, чтобы получить файл file1.txt с содержимым:

Здравствуйте,
Добро пожаловать в мой файл.
Это вторая строка.
До свидания.

То, что я до сих пор пробовал, было:

$ git checkout 6d83..
$ git branch tmp
$ git checkout master
$ git merge tmp

Но я получаю сообщение «Уже в курсе». git rebase - лучшее, что можно сделать здесь? Почему создание временной ветки с последующим объединением не работает?

Ответы [ 4 ]

4 голосов
/ 29 января 2020

Проблема здесь в том, что в отношении Git, удаление удаленной строки является правильным ответом .

Помните, что Git Основой c единицы хранения является коммит. Каждый коммит имеет:

  • некоторые данные: снимок всех ваших файлов; и
  • некоторые метаданные: информация о коммите. Это включает в себя, кто это сделал, когда (отметки даты и времени) и почему (ваше или сообщение коммитера). Последний и самый важный фрагмент метаданных для Git - это родительский коммит га sh ID.

Каждый коммит имеет уникальный идентификатор ha sh , Этот идентификатор ha sh назначается для коммита в тот момент, когда вы его делаете. С этого момента этот идентификатор sh зарезервирован для , а commit. Только этот коммит может иметь этот идентификатор. 1

Между тем, как мы только что отметили, каждый коммит получает в своих метаданных идентификатор ha sh ID. Технически, каждый коммит может хранить столько ха sh идентификаторов, сколько хочет Git, но они должны иметь ха sh идентификаторы уже существующих коммитов. 2 Большинство коммитов хранит ровно один другой коммит ha sh ID: родитель (единственное число) коммита. (Коммиты слияния хранят два, что делает их коммитами слияния, и самый первый коммит, который кто-то делает в новом, полностью пустом репозитории, не может иметь родителя - ранее не было коммита, на который можно было бы сослаться - так что он просто не т.)

В таком случае, возможно, у вас были какие-то более ранние коммиты или нет. Мы просто нарисуем график, который предполагает, что вы сделали:

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

Коммит, чей идентификатор ha sh равен H (H означает реальный идентификатор ha sh, который выглядит случайным образом) запоминает идентификатор ha sh его родительского, ранее существовавшего коммита G, который запоминает идентификатор ha sh его родительский F и т. д. Эти стрелки, направленные назад, встроенные в метаданные каждого коммита, показывают, как Git находит коммитов, за исключением самого коммита H, который является последним коммитом.

Способ, которым Git находит последний коммит любой ветви, заключается в том, что ветвь name , такая как master, содержит идентификатор ha sh коммита. Итак, чтобы сделать рисование более полным, давайте начнем рисовать. Поскольку ничто из любого коммита не может измениться после того, как мы его сделаем, мы можем лениться и перестать рисовать эти стрелки в виде стрелок, если мы помним, что они указывают назад:

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

Теперь давайте сделаем ваш новый коммит, который добавит этот новый файл, file1.txt. Commit H вообще не имеет file1.txt - у него есть другие файлы, но не file1.txt. Мы git add file1.txt и запускаем git commit и предоставляем сообщение журнала. Git создает новый коммит, который получает новый уникальный большой уродливый га sh ID, но мы просто назовем его I. Git устанавливает родителя на H, так что I указывает на H:

...--F--G--H   <-- master
            \
             I

, а затем, в качестве последнего шага git commit, Git пишет I Фактический га sh ID в имени master:

...--F--G--H
            \
             I   <-- master

(Нет причин продолжать рисовать I на отдельной строке, поэтому мы не будем.)

Теперь вы редактируете файл и, с помощью обычного процесса, делаете новый коммит J. Commit J имеет I в качестве родителя, а Git записывает J ха sh ID в имя master:

...--F--G--H--I--J   <-- master

Нет ничего для объединить здесь, т. е. вы не можете использовать git merge, чтобы делать то, что вы хотите. У вас есть линейная цепочка коммитов, заканчивающаяся на J. С J мы go обратно до I, с I до H и т. Д.


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

Если Git не может придумать уникальный идентификатор ha sh, он не даст вам совершить коммит! На самом деле этого никогда не происходит, хотя это теоретическая возможность. См. Также Как недавно обнаруженное столкновение SHA-1 влияет на Git?

2 Идентификатор ha sh нового коммита, который мы собираемся сделать create зависит от ha sh ID родительского коммита (ов). Таким образом, даже если мы выясним, какой у sh ID будет новый создаваемый коммит, если его родителем является существующий коммит X , для любого X , если мы затем вставим этот ха sh ID в метаданные коммита перед его созданием, в конце концов получает другой га sh ID. Так что невозможно для коммита ссылаться на себя, и не позволяет просто поместить туда какой-нибудь случайный мусор. Поэтому каждый коммит всегда ссылается на некоторый более ранний коммит.

Короче говоря, при коммите вы можете go назад по времени к его родителю ... но вы можете только go назад во времени. Вы не можете go пересылать его будущим дочерним элементам.

Вследствие этого вы не можете ни изменить ни один коммит, ни удалить более ранний коммит, не удалив также все последующие. (Git делает особенно трудным удаление коммитов. Сравните с Mercurial, где вы запускаете hg strip -r <rev>, и он удаляет этот коммит и всех его потомков. У вас все еще нет выбора в отношении дочерних элементов, но это легко убрать коммит.)


Объединение

Что такое объединение в Git, обычно происходит, когда у нас более одной ветви name . Давайте вернемся к случаю, когда мы просто зафиксировали H в качестве последнего коммита на master. (Мы можем использовать git reset --hard HEAD~2 для достижения этой цели, что заставляет master снова указывать прямо на H, а также настраивает рабочие области - индекс Git и наше рабочее дерево, где мы можем видеть файлы - для отражения коммита H еще раз. I и J будут продолжать существовать, и по умолчанию могут быть получены как минимум еще 30 дней. Но мы просто притворимся, что никогда не делали I и J на всех.) Итак, у нас есть это:

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

Теперь мы создадим новую ветку или две. Когда мы делаем это, нам нужно добавить еще одну вещь к нашему рисунку. Если есть только одно имя ветви, master, это, вероятно, ветвь, которую мы используем. Но что, если мы добавим dev в качестве второго имени? Какое имя мы используем?

Git ответ на этот вопрос заключается в использовании специального имени HEAD. Это специальное имя обычно , прикрепленное , к одному из названий вашей ветви. (Он может присоединяться только к одному или ни к одному: никогда не к одному.) Мы добавим второе имя ветви, dev, но оставим HEAD присоединенным к master:

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

Сейчас мы создадим новые коммиты I и J обычным способом. Давайте нарисуем их в:

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

Обратите внимание, что dev не переместился: он все еще указывает на существующий коммит H. Имя master теперь указывает на новый коммит J.

Теперь давайте создадим два коммита на dev. Мы начинаем с git checkout dev. Это присоединяет наш HEAD к dev, а также извлекает содержимое коммита H для работы с / on:

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

коммиты в хранилище не изменились! Но у файлов, которые мы видим и с которыми работаем, текущая ветвь равна dev, а текущая фиксация равна H. 3 Теперь мы делаем два больше новых коммитов. Допускается любое число, но два упрощают иллюстрацию:

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

Теперь мы можем запустить git merge. Мы выбираем одну ветвь для использования - мы git checkout master или git checkout dev - и затем запускаем git merge и даем ей имя другой ветви. 4 Давайте git checkout master и git merge dev так, чтобы HEAD и текущий коммит, идентифицирующий J вместо L: 5

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

Git теперь должен найти best коммит это на обеих ветках. В этом случае это очевидно: это commit H. Мы доберемся от J, пройдя два шага назад, и доберемся от L, вернувшись на два шага назад. Если бы цепочка вдоль дна была длиннее, нам пришлось бы go отстать на 3 или 4 или столько же шагов, но до тех пор, пока мы можем получить до commit H, commit H будет лучшим общим коммитом.

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

Git теперь будет запускать две git diff операции:

  • git diff --find-renames <em><code>hash-of-H hash-of-J, чтобы узнать, что мы изменили , master, так как общий коммит H; и
  • git diff --find-renames <em><code>hash-of-H hash-of-L, чтобы узнать, что они изменились, на dev, так как общий коммит H .

Что делает git merge, это объединяет эти изменения, а затем применяет объединенные изменения к снимку в коммите H - базе объединения. Таким образом мы сохраняем наши изменения и добавляем их.

Именно поэтому слияния в основном симметричны c. Если бы мы проверили dev, то есть, зафиксировали L и запустили git merge master, Git все равно нашли бы общий коммит H в качестве базы слияния. Он будет запускать те же две команды git diff (в другом порядке, но кого это волнует?). Затем эти различия будут объединены в один большой объединенный набор и применены к снимку из коммита H. Результат будет таким же.

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

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

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

и у нас есть слияние. Снимок , который идет с объединением, является результатом применения к H комбинированных изменений от H -vs- J и изменений от H -vs- L. родители слияния - это, как обычно, предыдущий коммит, и другой коммит, который мы выбрали при выполнении git merge dev.

Теперь, когда это слияние существует, попытка слияния L или даже K в master невозможно. Причина в том, что лучший общий коммит между L и M - это коммит L ..., который уже является частью истории M. Если мы отступим от M вдоль строки bottom , мы достигнем L. История, которая в Git состоит из коммитов, включая их связи, гласит, что L здесь уже объединено.


3 Когда вы спрашиваете Git: Что в HEAD? у вас есть два способа выразить это. Вы можете спросить Git: Какое имя ветви находится в HEAD? Или вы можете спросить: Что коммит делает HEAD выбрать ? Два разных вопроса получают два разных ответа. В режиме «отделяемого ГОЛОВА», в котором HEAD не привязан ни к какому имени ветви, первое выдает ошибку вместо ответа. Второй вопрос почти всегда работает.

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

4 Фактически , git merge работает с идентификаторами коммитов ha sh, поэтому мы можем присвоить ему идентификатор ha sh любого коммита, который мы хотим. Но обычно мы, люди, работаем по именам.

5 Результат слияния, как правило, одинаков во всех отношениях, за исключением того, какой родитель указан первым. Если мы используем конкретные аргументы флага для git merge, результат слияния может быть другим, однако.


Cherry-picking

Есть что-то, что мы можем сделать хоть. При любой цепочке коммитов, будь то форк, например:

          o--P--C--o--o   <-- branch1
         /
...--o--o
         \
          o--o--H   <-- branch2 (HEAD)

или просто линейная цепочка, например:

...--o--o--P--C--o--o--H   <-- branch (HEAD)

, мы можем выбрать коммит C, a ребенок, чей родитель P, и запустите на нем git cherry-pick. (Обычно вы используете здесь C ID * * *1327*.) Это означает, что Git заставляет

  • найти коммит P, C родителя: это просто, потому что C содержит P идентификатор ha sh внутри него;
  • рассматривает P как базу слияния , C как "их" коммит и текущий коммит H - выбранный HEAD - как "наш" коммит, и выполните полноценное трехстороннее слияние, как обычно.

Так что Git теперь будет diff P vs C, чтобы увидеть, что "они" сделали, diff P vs H, чтобы увидеть, что мы сделали, и объединить эти два набора изменений. Git применит объединенные изменения к снимку в P. Если все пойдет хорошо, Git передаст получившиеся файлы как новый снимок C' - копия коммита C - с использованием исходного сообщения C и т. Д. Это не сделает коммитом слияния, а скорее просто обычным коммитом:

          o--P--C--o--o   <-- branch1
         /
...--o--o
         \
          o--o--H--C'  <-- branch2 (HEAD)

или:

...--o--o--P--C--o--o--H--C'  <-- branch (HEAD)

Это имеет больше смысла для cherry-выберите коммит из другой ветки, как на верхней диаграмме; но вы можете выбрать коммит из своей истории, чтобы применить те же изменения. Это особенно полезно, если какой-то коммит между C и C' был коммитом, который un-did , что бы ни случилось в C. 6


6 Git имеет команду git revert, чтобы сделать такие коммиты. Вы указываете на какого-то дочернего элемента, и Git выполняет такое же трехстороннее слияние, что и для вишневого пика, за исключением того, что база слияния на этот раз C, и «их» коммит P. (Наша команда / HEAD - это, как всегда, команда HEAD.) Упражнение: попробуйте получить разность C против P в указанном порядке. Что произойдет, если вы объедините этот набор изменений с C против HEAD в указанном порядке?


Обратите внимание, что все эти операции выполняются для целых коммитов

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

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

1 голос
/ 29 января 2020

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

git difftool ccd8:file1.txt file1.txt

После правильного исправления и сохранения после выхода из редактора

git add file1.txt

Если вы еще не сделали pu sh, вы можете изменить предыдущий коммит

git commit --amend

Или создайте новый с восстановленной линией

git commit -m "Recovered line"
0 голосов
/ 31 января 2020

Один простой способ сделать это - git checkout -p @^ file.txt, который найдет все различия между версией вашего рабочего дерева и версией дедушки и предложит ее применить, и вы сможете редактировать предлагаемые блоки.

Cherrypick как правило, просто сокращение для diff | примените -3, если вы хотите вернуть все изменения @ ^, вы также можете попробовать git diff @^!|git apply -3, это может оставить вам некоторые конфликты для разрешения, но даже не бойтесь их, они редко, но нормально. Практика с хорошим инструментом слияния / различий. Мне нравится vimdiff, решение тривиальных конфликтов чертовски быстро быстро . Что-то вроде битвы за новый бит флага или что-то обычно занимает несколько секунд.

0 голосов
/ 29 января 2020

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

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