Хорошие и плохие новости
Вы буквально не можете перезаписать сохраненный файл. Причина этого проста: Git не хранит файлов. Git хранит коммитов .
Теперь верно, что коммиты сохраняют файлы. Это работает так, что вы выбираете какой-то коммит для извлечения , а Git затем извлекает все файлы, содержащиеся в этом коммите . Здесь скрываются многие детали, но важно знать, что в коммите хранится весь снимок , всех файлов , в специальном Git -только формате, который только Git можно использовать. Сначала его нужно извлечь, чтобы использовать.
Никакая часть любого коммита не может быть изменена. Либо у вас есть коммит, а затем у вас есть все файлов; или у вас нет коммита, и поэтому у вас есть нет файлов.
То, что вы можете сделать, используя git push --force
, это убедить другое хранилище Git в Discard совершает . Когда вы это сделаете, все файлы в этом коммите исчезнут. Вы не перезаписали файл. У вас есть удалено всех файлов в этом коммите , удаление этого коммита. (Опять же, здесь скрываются многие детали: иногда вы можете удалить.)
Любые коммиты, оставшиеся у Git, все еще содержат все их файлов. Каждый коммит имеет полный снимок всех своих файлов! Но обязательство, которое вы сказали им выбросить, они выбросили. У них есть нет из этих файлов.
Легко избежать этой участи: не используйте git push --force
, если вы точно не знаете, что вы делают. (Как только вы узнаете, что делаете, вы можете предпочесть git push --force-with-lease
.)
Вам нужно слить или перебазировать
Вы упомянули слияние. Слияние не означает заменить мои файлы своими файлами . Объединение означает, что объединяет мои изменения с его изменениями. Но коммиты не сохраняют изменений. Они хранят снимки. Итак, как это может работать?
Ключ к пониманию этого состоит в том, чтобы понять, что в то время как коммиты сохраняют файлы - в специальном, только для чтения, Git только замороженном формате, именно поэтому коммит имеет быть извлеченным , прежде чем вы сможете работать над ним - это не все, что они хранят. Каждый коммит также хранит некоторые метаданные. Например, git log
: кто сделал коммит, когда и почему (их сообщение в журнале). В этих метаданных каждый коммит записывает свой предыдущий или родительский коммит.
Если вы берете два коммита подряд и сравниваете их снимки, разница между двумя снимками говорит вам, что изменилось. То есть, если мы вытянем цепочку коммитов:
... <-F <-G <-H
, где последний - это коммит с ha sh ID H
- H
означает фактический Git ha sh ID, который слишком велик и уродлив для запоминания или повторения, а затем сам коммит H
содержит в своих метаданных фактический идентификатор ha sh некоторого более раннего коммита, который мы просто вызовем G
. То есть H
указывает на G
. Commit G
хранит идентификатор ha sh своего родителя F
: G
указывает на F
.
Имя ветви в Git - это просто метка, содержащая ha sh ID последнего коммита. Например, если коммит H
является последним коммитом в master
, мы имеем:
...--F--G--H <-- master
, где я стал слишком ленив, чтобы рисовать стрелки между коммитами. (Все части любого коммита замораживаются во времени навсегда, включая стрелки, указывающие назад между коммитами. Поэтому, если мы просто помним, что они указывают назад, не слишком страшно просто нарисовать их без частей стрелки.)
Создание нового коммита на ветви - это просто вопрос Git, выписывающий новый снимок, добавление метаданных и получение идентификатора ha sh. Git устанавливает новый коммит так, чтобы он указывал на тот коммит, который вы имели через мгновение go. Затем Git записывает идентификатор sh нового коммита, каким бы он ни был, в имя ветви . Так что теперь имя указывает на последний коммит.
Вы можете иметь более одного имени ветви, указывающего на какой-то коммит! Это просто означает, что все коммиты находятся на обеих ветвях. Например, если мы начнем с:
...-- G - H <- master </p>
и создадим новое имя branch
, указывающее на существующий коммит H
, мы get:
...--G--H <-- master, branch
Теперь нам нужно узнать, какое имя ветви мы используем. Мы можем прикрепить специальное имя HEAD
к одному из названий веток, например
...--G--H <-- master, branch (HEAD)
, чтобы обозначить, что мы находимся на ветке branch
, а не master
. текущий коммит по-прежнему является коммитом H
, поэтому мы будем работать с файлами из коммита H
.
Чтобы сделать новый коммит I
, у нас есть Git сохранить новый снимок, установить его parent для текущего коммита H
, а затем записать все, что sh ID нового коммита I
попадет в имя текущей ветви:
I <-- branch (HEAD)
/
...--G--H <-- master
Это означает, что значения имени ветви меняются со временем по мере роста ветви. Стрелка, выходящая из имени ветви , перемещается , которую мы можем нарисовать, перемещая все имя и стрелку, как указано выше.
Сами коммиты, как только они сделаны, замораживаются для всех время. Самое большее, мы можем переместить название ветви и стрелку, чтобы имя больше не указывало на фиксацию . Например, предположим, что мы перемещаем имя branch
так, чтобы оно снова указывало на H
:
I ???
/
...--G--H <-- branch (HEAD), master
Git находит , фиксирует с использованием имен - эти имена branch
и master
, например - и затем работает в обратном направлении. Поскольку name не находит I
, коммит I
теперь "потерян". Если вы помните его необработанный идентификатор sh, , вы сможете его найти, но Git не найдет его самостоятельно. Все его файлы - весь его снимок - теперь исчезли; чтобы "восстановить" I
, вам нужно знать его га sh ID.
(Это, в значительной степени, смысл git reset
: он перемещает имена веток. Вы можете заставить его двигаться имя ветви и тем самым «удаляет» кучу коммитов.)
Поскольку коммит запоминает своего родителя, мы также можем Git сравнить снимки очень легко. Вот что делает git log -p
или git show
: вы выбираете какой-то коммит для просмотра, и Git также смотрит на его родительский коммит. Git извлекает оба снимка, родительский и дочерний, и сравнивает их. Для файлов, которые одинаковы, Git ничего не говорит. Для файлов, которые отличаются, Git показывает рецепт, который изменит первый коммит на второй.
Распределенная разработка
Предположим, у вас есть:
I--J <-- yourbranch (HEAD)
/
...--G--H <-- master, origin/master
Этот origin/master
- ваш Git, помнящий master
из другого Git хранилища. Вы сделали пару новых коммитов в своей ветке. Между тем:
... Том изменил «hello.cs» ранее и уже перенес свою ветку на удаленную и слился Джоном.
Давайте посмотрим на что на самом деле произошло в хранилище Тома. Он сделал свое собственное название филиала или использовал свой собственный master
. (На самом деле не имеет значения, какой он сделал. Томские ветви называются Тома , и вы их никогда не увидите - вы используете Джона в качестве origin/master
. У Джона есть репозиторий Джона , с Ионами Джона.) Допустим, Том использовал ветку с именем tom
, чтобы помочь понять, какой Git мы ищем.
Том сделал:
git checkout -b tom
, чтобы получить:
...--G--H <-- master, tom (HEAD), origin/master
Затем Том сделал несколько коммитов, в которых он изменил некоторые файлы. Каждый из этих новых коммитов, которые сделал Том, имеет полные и полные снимки его измененных файлов:
...--G--H <-- master, origin/master
\
K--L <-- tom (HEAD)
(Ваши коммиты будут или уже будут I-J
в наших чертежах, поэтому мы назвали Тома K-L
здесь.) Затем Том отправил свои коммиты Джону, используя git push
.
Действия Джона
Теперь, на этом этапе полезно знать, что all Git Количество репозиториев фиксируется так же . Таким образом, га sh идентификаторы коммитов K-L
в хранилище Джона совпадают с K-L
в Томе. имя , которое идентифицирует коммит L
, так что Git Джона может работать в обратном направлении, это то имя, которое Том сказал использовать - вероятно, не master
; возможно, снова tom
, но давайте использовать pr/123
, чтобы назвать его "запросом на извлечение". Итак, у Джона теперь есть:
...--G--H <-- master (HEAD)
\
K--L <-- pr/123
Обратите внимание, что у Джона нет origin/master
, а его master
равен его текущей ветви. Теперь он объединяет коммит L
.
У Джона есть опция. Он может заставить своего Git совершить реальное слияние, а не использовать опцию Git «fast-forward», чтобы у него было следующее:
...--G--H------M <-- master (HEAD)
\ /
K--L <-- pr/123
Коммит слияния M
имеет свой собственный снимок всех файлов, включая любые изменения hello.cs
. В данном конкретном случае в коммите Джона M
есть hello.cs
, который, вероятно, соответствует hello.cs
в коммите L
. Что делает M
коммит слияния то, что он имеет не только одного, но двух родителей. Фиксируйте M
очков назад как H
, так и L
.
Или он может позволить своему Git использовать «ускоренную перемотку вперед» вместо реального слияния , Допустим, он делает это. В этом случае его репозиторий теперь выглядит следующим образом:
...--G--H--K--L <-- master (HEAD), pr/123
Коммит слияния M
на этот раз не существует: его репозиторий просто использует существующий коммит L
.
В обоих случаях Джон теперь может удалить свое имя pr/123
: оно больше не нужно.
Вернуться к вашему хранилищу
Прямо сейчас у вас все еще есть это:
I--J <-- yourbranch (HEAD)
/
...--G--H <-- master, origin/master
Теперь вы должны получить новых коммитов от Джона. Для этого вам нужна команда git fetch
. Вы запускаете git fetch origin
и ваш Git получает коммиты Джона:
I--J <-- yourbranch (HEAD)
/
...--G--H <-- master
\
K--L origin/master
Здесь git merge
входит.
Вы могли отправлять коммит J
Джону сейчас, возможно, он установит его имя secondimage
или, возможно, yourbranch
. Затем ему придется выяснить, как объединить или перебазировать вашу работу. Или вы можете выполнить слияние или перебазировать себя.
Цель git merge
состоит в том, чтобы объединить работу. У вас есть некоторые изменения, которые вы сделали в коммите I
, который вы можете просмотреть, сравнив снимок в H
со снимком в I
. У вас есть некоторые изменения, которые вы сделали в коммите J
, который вы можете просмотреть, сравнив снимок в I
с снимком в J
. Чтобы просмотреть все ваши изменения одновременно, вы можете сравнить снимок в H
с снимком в J
.
Между тем у Тома есть некоторые сделанные им изменения, которые вы можете просматривать по одному шагу за раз: сравнить H
до K
, затем K
до L
; или просто сравните H
с L
напрямую, чтобы увидеть все изменений, которые сделал Том.
Выполнение:
git merge origin/master
сообщает вашему Git: найти лучший общий, общий коммит . 1 Это коммит H
, очевидно: коммит H
включен master
, но также и yourbranch
, потому что J
ведет вернуться к I
, что приводит к H
. Точно так же, начиная с L
, мы go возвращаемся к K
, а затем H
. Так что H
- это shared , и идти дальше будет просто глупо, так что это также best shared commit.
Наличие основанного общего коммита - который Git вызывает базу слияния двух коммитов-подсказок J
и L
- Git, теперь сравнивает снимки в H
против J
и снова в H
vs L
:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
* Механизм слияния 1769 * теперь объединяет эти изменения, файл за файлом. Если Том изменил hello.cs
, а вы также изменили hello.cs
, Git объединяет два изменения, как показано в двух списках различий. 2
Если все идет хорошо, Git записывает полученный объединенный файл hello.cs
в два места: ваше рабочее дерево, которое содержит обычный файл, с которым вы можете работать, и с Git внутренним index aka область подготовки , где Git хранит копию файла, которую он передаст. 3
Если все идет хорошо с этим процессом объединения - если Git может объединить изменения и применить объединенные изменения к снимку, взятому из коммита H
- Git сделает новый коммит слияния M
:
I--J
/ \
...--G--H M <-- yourbranch (HEAD)
\ /
K--L origin/master
(В вашем собственном Git все еще есть master
, указывающий на коммит H
, но его сейчас слишком сложно нарисовать, поэтому я его пропустил на этот раз.)
Теперь вы можете использовать git push
, чтобы отправить успешно объединенный M
коммит, а также коммиты I
и J
в репозиторий Джона, дав ему какое-то имя. Возможно, вы назовете это чем-то отличным от yourbranch
; по-видимому, вы не будете пытаться назвать это master
, поскольку сам Джон - единственный, кто должен обновлять master
Джона. Чтобы назвать его yourbranch
, вы можете просто запустить:
git push origin yourbranch
(и вы можете захотеть git push -u
, чтобы ваш Git теперь установил origin/yourbranch
как восходящий поток из ваша ветка с именем yourbranch
).
1 Обратите внимание, что хотя origin/master
технически не является именем ветви - это remote- вместо этого отслеживая имя - оно работает здесь как имя ветки, где нас действительно интересует коммитов , а не ветвей. Git не так уж много о ветвях в конце концов. Речь идет о коммитах . Просто мы находим коммиты по именам веток или другим именам. В этом случае мы находим коммиты по именам удаленного слежения , таким как origin/master
тоже.
2 Git делает все это внутренне, без необходимости запуска git diff
напрямую. Эффект , как если бы Git запускал git diff
и читал списки различий, хотя.
3 Технически, индекс содержит ссылку на внутреннюю Git объект blob , а не копия самого файла. Однако вы можете думать об этом как о индексной / промежуточной области, содержащей копии каждого из файлов, взятых из каждого коммита.
Во время объединения индекс расширяется: вместо удержания one файл hello.cs
для фиксации, изначально взятый из текущего коммита, содержит три hello.cs
файла, один из базы слияния H
, один из текущего коммита J
и один из их коммитов L
. Если Git сможет объединить их изменения с вашими изменениями, Git сократит эту одну запись до готовой к объединению копии, соответствующей обновленной копии рабочего дерева. Если нет, Git оставит лишние копии в индексе, поэтому Git знает, что существует конфликт слияния. Это также оставит беспорядочное частичное слияние в файле рабочего дерева hello.cs
.
Перебазирование - это еще один вариант
Вместо слияния - превращение:
I--J <-- yourbranch (HEAD)
/
...--G--H <-- master
\
K--L origin/master
в то, что мы видели выше - вы можете, если хотите, копировать ваши коммиты I-J
в новые и улучшенные коммиты. Они будут как I
и J
, но:
- Копия
I
, которую мы назовем I'
, чтобы пометить ее как копию , начнется с файлов, которые находятся в L
, прежде чем мы сделаем I'
. I'
сделает до те файлы, которые I
сделал с файлами в H
. - Родитель из
I'
будет L
, а не H
. - Копия
J
, которую мы назовем J'
, также будет иметь I'
в качестве родителя и вносить те же изменения в снимок I'
, что J
сделал для I
.
Итак, мы нарисуем эта новая и улучшенная серия коммитов выглядит следующим образом:
I--J <-- yourbranch-old
/
...--G--H <-- master
\
K--L origin/master
\
I'-J' <-- yourbranch-new (HEAD)
Мы могли бы сделать это, создав новое имя ветви, указывающее на коммит L
, а затем скопировав каждый коммит— I
и J
- по одному. Команда Git, которая копирует коммит, подобный этой: git cherry-pick
.
Однако есть мощный инструмент, который сделает всю работу за нас. Этот электроинструмент git rebase
. Это:
- перечисляет некоторый набор коммитов для копирования;
- выполняет
git checkout
некоторого коммита, куда должны идти копии; - копирует каждый коммит,
- и, наконец, перемещает название ветви.
Имя ветви git rebase
перемещается - это текущее имя ветви. Коммиты, которые он копирует, - это коммиты, которые находятся в текущей ветви, за вычетом любых коммитов, которые делятся с местом, куда вы говорите, чтобы скопировать коммиты после.
В этом случае мы бы run:
git checkout yourbranch # if needed
git rebase origin/master
Поскольку origin/master
имена фиксируют L
, мы скопируем коммиты, которые имеют в yourbranch
, но не в origin/master
. Мы не будем копировать L
, ни K
, ни H
, ни что-либо до H
. Мы все равно не собирались копировать L
или K
, но это нормально! Это оставляет только J
и I
. Ребаз знает, как копировать их в правильном порядке - назад для Git, но вперед для людей: сначала I
, а затем J
. Так Git копирует их по одному.
Затем команда rebase
, сделав копии, берет имя ветви yourbranch
и заставляет его указывать на последний скопированный коммит:
I--J [abandoned]
/
...--G--H <-- master
\
K--L origin/master
\
I'-J' <-- yourbranch (HEAD)
Поскольку мы находим коммиты, начиная с имен веток и работая в обратном направлении - это то, что делает git log
- если мы посмотрим сейчас, это будет выглядеть так, как мы изменились фиксирует I
и J
в I'
и J'
. Мы не сделали. Это невозможно. Даже Git не может изменить эти коммиты.
Но похоже на , мы изменили коммиты, пока мы не обращаем никакого внимания на фактические идентификаторы ha sh. Пока мы никогда не отправляли сами фактические коммиты I-J
в какой-либо другой Git репозиторий, никто никогда даже не узнает, что мы это сделали.
Сделав этот ребаз, мы можем теперь git push origin yourbranch
Точно так же, как и раньше. (Возможно, вы захотите git push -u
, как и раньше.)
У Rebase есть своего рода сбой
Если «Джон» здесь представляет общий репозиторий с каким-то управлением доступом к ветвям - таким как GitHub-репозиторий - и вы git push
свои собственные ветки, а затем публикуете GitHub-стиль запросов , вам может понадобиться время от времени обновлять свои собственные PR.
Когда вы используете rebase, как мы видели выше, это копирует ваши коммиты новых и улучшенных коммитов. Сделав копии, вам может потребоваться обновить GitHub PR.
Ваш GitHub PR состоит из имени, указанного в репозитории GitHub. На самом деле это не имя ветки, но оно всегда работает как единица и всегда включает имя ветки где-то , возможно, в ветке GitHub. В любом случае имя ветви - будь то в основном репозитории GitHub или в форке - является именем ветви. В данном конкретном примере это имя yourbranch
.
Обновив свой собственный репозиторий Git:
...--K--L <-- origin/master
\
I'-J' <-- yourbranch (HEAD)
и запустив git push -u origin yourbranch
, GitHub установит их имя yourbranch
и вы получите origin/yourbranch
в вашем Git хранилище:
...--K--L <-- origin/master
\
I'-J' <-- yourbranch (HEAD), origin/yourbranch
Если есть некоторая задержка, возможно, origin/master
добавит новый коммит N
прежде чем ваша ветвь может быть объединена (с реальным или ускоренным слиянием), давая:
...--K--L--N <-- origin/master
\
I'-J' <-- yourbranch (HEAD), origin/yourbranch
Теперь вам необходимо выполнить повторный ребаз, выдав:
I"-J" <-- yourbranch (HEAD)
/
...--K--L--N <-- origin/master
\
I'-J' <-- origin/yourbranch
У них теперь есть ваши старые коммиты I'-J'
. * Вы должны сказать им отменить эти коммиты, заменить их моими новыми и улучшенными. Для этого требуется форма force-pu sh, например :
git push -f origin yourbranch
Если кто-то еще начал использовать вашу последовательность I'-J'
, это не очень дружелюбно.
Вы можете гарантировать, что они забудут только ваш I'-J'
, записанный их имя yourbranch
с использованием git push --force-with-lease
. У вас есть Git проверка с их Git, что их yourbranch
все еще указывает на J'
, прежде чем они обновят свое yourbranch
имя до обновленной J"
копии. Как правило, это хорошая идея, но она не мешает кому-то еще зависеть от вашей последовательности коммита I'-J'
. Единственное реальное лекарство - это заранее договориться со всеми другими пользователями Git, что yourbranch
может двигаться таким образом.
О git pull
git pull
Команда действительно просто удобство. Чтобы работать с коммитами других людей, вы должны:
- использовать
git fetch
до получить их - используйте другую, вторую Git команду -
git merge
или git rebase
- до , включающую их
и git pull
делает именно это: run git fetch
затем немедленно введите вторую команду Git, не давая вам возможности увидеть, что сделал git fetch
. Вы должны выбрать эту вторую Git команду заранее, даже не подозревая, может ли ваш курс действий перебазировать, объединить или что-то еще целиком.
Обычно я избегаю git pull
, потому что Мне нравится получать, затем посмотреть, что произошло , и только потом выбрать мою следующую команду Git. Я признаю, однако, что на мои предпочтения повлияла древняя привычка Git - все время разрушать git pull
. (В нем было несколько очень серьезных ошибок, более десяти лет подряд go, и я несколько раз обгорел.)
Выводы
Знайте, как коммиты Работа. Они содержат полные снимки всех зафиксированных файлов, а также метаданные. Здесь есть много тонкостей, ни одна из которых мы не рассмотрели выше, о коммитах, сделанных из того, что находится в Git s index aka staging area .
Знайте, что делает git merge
. Это не перезаписывает. Знайте, что команда командной строки git merge
иногда выполняет операцию fast-forward , которая не является фактическим слиянием. Зеленая кнопка «слияния» на GitHub (при использовании в режиме слияния; выпадающий список позволяет выбирать другие режимы) всегда выполняет реальное слияние.
Знайте, что делает git rebase
, и когда (и нужно ли) его использовать.
Старайтесь не использовать git push --force
, даже в его более безопасном --force-with-lease
варианте. Если вы никогда не сделаете ребазинг, вам это не понадобится. (Помните, что режим «перебазирования и слияния» в GitHub из-за его зеленой кнопки слияния вызывает перебазировку! Затем он выполняет быструю перемотку к перебазированным коммитам. Было бы неплохо, если бы была опция перемотки вперед, которая не rebase.)
Если вам нужно принудительно-pu sh, убедитесь, что все пользователи другого Git репозитория заранее договорились, что имена ветвей будут быть вынужденным. Они должны понимать, от каких имен и от каких коммитов они могут зависеть, а какие могут быть выдернуты из-за перебазирования.