Предварительные заметки
Наблюдение здесь заключается в том, что после того, как вы начинаете работать branch1(забывая или не понимая, что было бы хорошо branch2сначала переключиться на другую ветку ), вы запускаете:
git checkout branch2
Иногда Git говорит: «Хорошо, ты сейчас на branch2!» Иногда Git говорит: «Я не могу этого сделать, я бы потерял некоторые из ваших изменений».
Если Git не позволит вам сделать это, вы должны зафиксировать свои изменения, чтобы сохранить их где-нибудь навсегда. Вы можете использовать их git stashдля сохранения; это одна из вещей, для которой он предназначен. Обратите внимание, что git stash saveили на git stash pushсамом деле означает «Зафиксируйте все изменения, но ни на одной ветке, а затем удалите их с того места, где я сейчас». Это позволяет переключаться: у вас нет текущих изменений. Вы можете затем git stash applyих после переключения.
Боковая панель: git stash saveстарый синтаксис; git stash pushбыла введена в Git версии 2.13, чтобы исправить некоторые проблемы с аргументами git stashи допустить новые опции. Оба делают то же самое, когда используются основными способами.
Вы можете перестать читать здесь, если хотите!
Если Git не позволит вам переключиться, у вас уже есть лекарство: используйте git stashили git commit; или, если ваши изменения тривиальны для воссоздания, используйте, git checkout -fчтобы форсировать его. Этот ответ полностью о том, когда Git позволит вам, git checkout branch2даже если вы начали вносить некоторые изменения. Почему это работает иногда , а не в другие времена?
Правило здесь простое с одной стороны и сложное / трудно объяснимое с другой:
Вы можете переключать ветки с незафиксированными изменениями в рабочем дереве, если и только если упомянутое переключение не требует сглаживания этих изменений.
То есть - и обратите внимание, что это все еще упрощено; Есть несколько очень сложных угловых случаев со ступенями git adds, git rms и тому подобными branch1. А git checkout branch2придется сделать это:
- Для каждого файла, который находится в,
branch1а не в branch2, 1 удалите этот файл.
- Для каждого файла, который находится в,
branch2а не в branch1, создайте этот файл (с соответствующим содержимым).
- Для каждого файла, который находится в обеих ветвях, если версия в
branch2отличается, обновите версию рабочего дерева.
Каждый из этих шагов может затормозить что-то в вашем рабочем дереве:
- Удаление файла является «безопасным», если версия в рабочем дереве совпадает с версией, зафиксированной в
branch1; «небезопасно», если вы внесли изменения.
- Создание файла в том виде, в котором оно выглядит,
branch2является «безопасным», если его сейчас нет. 2 «Небезопасно», если оно существует сейчас, но имеет «неправильное» содержание.
- И, конечно, замена версии файла рабочего дерева другой версией «безопасна», если версия рабочего дерева уже зафиксирована
branch1.
Создание нового branch ( git checkout -b newbranch) всегда считается «безопасным»: никакие файлы не будут добавлены, удалены или изменены в рабочем дереве как часть этого процесса, и область index / staging также не затронута. (Предостережение: это безопасно при создании новой ветки без изменения начальной точки новой ветки; но если вы добавите другой аргумент, например git checkout -b newbranch different-start-point, это, возможно, придется изменить вещи, чтобы перейти к different-start-point. Git затем будет применять правила безопасности проверки как обычно .)
1 Это требует, чтобы мы определили, что означает, что файл находится в ветви, что, в свою очередь, требует правильного определения слова ветвь . (Смотрите также Что именно мы подразумеваем под «ветвь»? ) Вот, что я на самом деле означает это коммит , к которому имя-ветви рассасывается: файл , чей путь является в случае производит хэш. Этот файл не в , если вы получите сообщение об ошибке. Существование пути в вашем индексе или рабочем дереве не имеет значения при ответе на этот конкретный вопрос. Таким образом, секрет здесь состоит в том, чтобы изучить результат на каждомP branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path, Это либо не удается, потому что файл находится "в" не более одной ветви, или дает нам два идентификатора хеша. Если два идентификатора хеша совпадают , файл одинаков в обеих ветвях. Никаких изменений не требуется. Если идентификаторы хеша различаются, файл отличается в двух ветвях и должен быть изменен для переключения ветвей.
Ключевым понятием здесь является то, что файлы в коммитах заморожены навсегда. Файлы, которые вы будете редактировать, явно не заморожены. Мы, по крайней мере изначально, смотрим только на несоответствия между двумя замороженными коммитами. К сожалению, мы или Git-же приходится иметь дело с файлами, которые не коммита вы собираетесь перейти от и находятся в коммит вы собираетесь перейти. Это приводит к остальным сложностям, поскольку файлы могут также существовать в индексе и / или в рабочем дереве, без необходимости существования этих двух конкретных замороженных коммитов, с которыми мы работаем.
2 Это может считаться «своего рода безопасным», если оно уже существует с «правильным содержимым», так что Git не должен его создавать. Я помню, по крайней мере, некоторые версии Git, позволяющие это сделать, но тестирование только сейчас показывает, что это считается «небезопасным» в Git 1.8.5.4. Тот же аргумент будет применяться к измененному файлу, который оказывается измененным в соответствии с ветвью, подлежащей переключению. Опять же, 1.8.5.4 просто говорит "будет перезаписано", хотя. См. Также конец технических заметок: моя память может быть неисправна, поскольку я не думаю, что правила дерева чтения изменились с тех пор, как я впервые начал использовать Git в версии 1.5.
Имеет ли значение, являются ли эти изменения поэтапными или нет?
Да, в некотором смысле. В частности, вы можете внести изменения, а затем «де-изменить» файл рабочего дерева. Вот файл в двух ветках, он отличается от branch1и branch2:
$ git show branch1:inboth
this file is in both branches
$ git show branch2:inboth
this file is in both branches
but it has more stuff in branch2 now
$ git checkout branch1
Switched to branch 'branch1'
$ echo 'but it has more stuff in branch2 now' >> inboth
На этом этапе рабочий файл дерева inbothсовпадает с файлом branch2, даже если мы включены branch1. Это изменение не предназначено для фиксации, что git status --shortпоказано здесь:
$ git status --short
M inboth
Пробел-то-М означает «измененный, но не поэтапный» (или, точнее, копия рабочего дерева отличается от поэтапной / индексной копии).
$ git checkout branch2
error: Your local changes ...
Хорошо, теперь давайте создадим копию рабочего дерева, которая, как мы уже знаем, также соответствует копии branch2.
$ git add inboth
$ git status --short
M inboth
$ git checkout branch2
Switched to branch 'branch2'
Здесь готовые и рабочие копии совпадали с тем, что было branch2, поэтому проверка была разрешена.
Давайте попробуем еще один шаг:
$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches
Внесенное мною изменение теперь потеряно из области подготовки (потому что касса записывает через область подготовки). Это немного угловой случай. Изменение не было, но тот факт , что я поставил его, как исчез.
Давайте создадим третий вариант файла, отличный от ветки-копии, а затем установим рабочую копию в соответствии с текущей версией ветки:
$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth
Два Ms здесь означают: промежуточный файл отличается от HEADфайла, а файл рабочего дерева отличается от промежуточного файла. Версия рабочего дерева соответствует branch1(ака HEAD) версии:
$ git diff HEAD
$
Но git checkoutне позволят оформить заказ:
$ git checkout branch2
error: Your local changes ...
Давайте установим branch2версию как рабочую версию:
$ git show branch2:inboth > inboth
$ git status --short
MM inboth
$ git diff HEAD
diff --git a/inboth b/inboth
index ecb07f7..aee20fb 100644
--- a/inboth
+++ b/inboth
@@ -1 +1,2 @@
this file is in both branches
+but it has more stuff in branch2 now
$ git diff branch2 -- inboth
$ git checkout branch2
error: Your local changes ...
Даже если текущая рабочая копия совпадает с текущей branch2, промежуточный файл не соответствует, поэтому a git checkoutпотеряет эту копию и git checkoutбудет отклонена.
Технические заметки - только для безумно любопытных :-)
Основным механизмом реализации всего этого является индекс Git . Индекс, также называемый «промежуточной областью», - это место, где вы строите следующий коммит: он начинается с текущего коммита, т. Е. Независимо от того, что вы извлекли сейчас, а затем каждый раз, когда вы git addфайл, вы заменяете версию индекса. с тем, что у вас есть в вашем рабочем дереве.
Помните, что дерево работы - это то, где вы работаете со своими файлами. Здесь они имеют свою обычную форму, а не какую-то специальную форму, полезную только для Git, как в коммитах и в индексе. Таким образом, вы извлекаете файл из коммита через индекс и затем в рабочее дерево. После того, как вы git addего измените, вы перейдете в индекс. Таким образом, на самом деле для каждого файла есть три места: текущий коммит, индекс и рабочее дерево.
Когда вы запускаете git checkout branch2, то, что делает Git под прикрытием, это сравнивает коммит с коммитомbranch2 с тем, что находится в текущем коммите и в индексе сейчас. Любой файл, который соответствует тому, что там сейчас, Git может оставить в покое. Это все нетронутым. Любой файл, который одинаков в обеих фиксациях , Git также может оставить в покое - и это те, которые позволяют вам переключать ветки.
Большая часть Git, включая переключение коммитов, является относительно быстрой из-за этого индекса. На самом деле в индексе находится не каждый файл, а хеш каждого файла . Копия самого файла хранится в хранилище как то, что Git называет объектом BLOB-объекта . Это похоже на то, как файлы хранятся в коммитах: коммиты на самом деле не содержат файлов , они просто приводят Git к хеш-идентификатору каждого файла. Таким образом, Git может сравнивать хэш-идентификаторы - в настоящее время строки длиной 160 битов - чтобы определить, имеют ли коммиты X и Y один и тот же файл или нет. Затем он может сравнить эти хеш-идентификаторы с хеш-идентификатором в индексе.
Это то, что приводит ко всем вышеперечисленным угловым случаям. У нас есть коммиты X и Y, которые оба имеют файл path/to/name.txt, и у нас есть индексная запись для path/to/name.txt. Возможно, все три хэша совпадают. Может быть, два из них совпадают, а один нет. Может быть, все три разные. И у нас также может быть another/file.txtэто только в X или только в Y и есть или нет в индексе сейчас. Каждый из этих различных случаев требует отдельного рассмотрения: нужно ли Git копировать файл из коммита в индекс или удалять его из индекса, чтобы переключиться с X на Y ? Если так, то это также должноскопируйте файл в рабочее дерево или удалите его из рабочего дерева. И если это так, то версии индекса и рабочего дерева должны лучше соответствовать по крайней мере одной из зафиксированных версий; в противном случае Git будет забивать некоторые данные.
(Полные правила для всего этого описаны не в git checkoutдокументации, как вы могли бы ожидать, а в git read-treeдокументации в разделе «Слияние двух деревьев» .)
git checkout -m, который объединяет ваше рабочее дерево и изменения индекса в новой проверке.