Проблема здесь в том, что в отношении 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 предназначен для операций фиксации за раз.