Оформить заказ в другой ветви, если в текущей ветви есть незафиксированные изменения

354

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

Однако иногда Git позволяет мне извлекать другую ветку без фиксации или сохранения этих изменений, и она переносит эти изменения в ветку, которую я извлекаю.

Какое правило здесь? Имеет ли значение, являются ли эти изменения поэтапными или нет? Перенос изменений в другую ветку не имеет никакого смысла для меня, почему git иногда допускает это? То есть это полезно в некоторых ситуациях?

Xufeng
источник

Ответы:

356

Предварительные заметки

Наблюдение здесь заключается в том, что после того, как вы начинаете работать 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документации в разделе «Слияние двух деревьев» .)

Торек
источник
3
... есть также git checkout -m, который объединяет ваше рабочее дерево и изменения индекса в новой проверке.
jthill
1
Спасибо за это отличное объяснение! Но где я могу найти информацию в официальных документах? Или они неполные? Если да, то какова официальная ссылка на git (надеюсь, кроме его исходного кода)?
макс
1
(1) вы не можете, и (2) исходный код. Основная проблема в том, что Git постоянно развивается. Например, прямо сейчас, есть большой толчок, чтобы увеличить или отбросить SHA-1 с или в пользу SHA-256. Эта конкретная часть Git была довольно стабильной в течение долгого времени, однако, и основной механизм прост: Git сравнивает текущий индекс с текущим и целевым коммитами и решает, какие файлы изменить (если они есть) на основе целевого коммита затем проверяет «чистоту» файлов рабочего дерева, если запись индекса нуждается в замене.
Торек
6
Короткий ответ: есть правило, но для обычного пользователя слишком тупо иметь какую-либо надежду на понимание, не говоря уже о запоминании, поэтому вместо того, чтобы полагаться на инструмент, чтобы вести себя разумно, вы должны вместо этого полагаться на дисциплинированное соглашение о проверке только тогда, когда текущая ветка совершенная и чистая. Я не понимаю, как это отвечает на вопрос о том, когда было бы полезно перенести выдающиеся изменения в другую ветку, но я, возможно, пропустил это, потому что изо всех сил пытаюсь понять это.
Нейтрино,
2
@HawkeyeParker: этот ответ подвергся многочисленным изменениям, и я не уверен, что кто-то из них значительно его улучшил, но я постараюсь добавить кое-что о том, что означает, что файл находится «в ветке». В конечном счете это будет шатким, потому что понятие «ветвь» здесь не определено должным образом, но это еще один элемент.
Торек
51

У вас есть два варианта: спрятать ваши изменения:

git stash

потом, чтобы вернуть их:

git stash apply

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

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

обкрадывать
источник
1
Не правильная команда git stash apply? здесь документы.
Thomas8
1
Как раз то, что я искал: временно переключаться на другие ветви, что-то искать и возвращаться к тому же состоянию ветви, над которой я работаю. Спасибо, Роб!
Найшта
1
Да, это правильный способ сделать это. Я ценю детали в принятом ответе, но это делает вещи сложнее, чем они должны быть.
Майкл Леонард
5
Кроме того, если вам не нужно хранить тайник, вы можете использовать git stash popего, и он удалит тайник из вашего списка, если он успешно применяется.
Майкл Леонард
1
лучше использовать git stash pop, если только вы не собираетесь хранить журнал тайников в своей истории репо
Дамилола Оловуокер
14

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

Пример:

$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "adding file.txt"

$ git checkout -b experiment
$ echo 'goodbye world' >> file.txt
$ git add file.txt
$ git commit -m "added text"
     # experiment now contains changes that master doesn't have
     # any future changes to this file will keep you from changing branches
     # until the changes are stashed or committed

$ echo "and we're back" >> file.txt  # making additional changes
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
    file.txt
Please, commit your changes or stash them before you can switch branches.
Aborting

Это касается как неотслеживаемых, так и отслеживаемых файлов. Вот пример для неотслеживаемого файла.

Пример:

$ git checkout -b experimental  # creates new branch 'experimental'
$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "added file.txt"

$ git checkout master # master does not have file.txt
$ echo 'goodbye world' > file.txt
$ git checkout experimental
error: The following untracked working tree files would be overwritten by checkout:
    file.txt
Please move or remove them before you can switch branches.
Aborting

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

$ echo 'experimental change' >> file.txt # change to existing tracked file
   # I want to save these, but not on master

$ git checkout -b experiment
M       file.txt
Switched to branch 'experiment'
$ git add file.txt
$ git commit -m "possible modification for file.txt"
Gordolio
источник
На самом деле я до сих пор не совсем понимаю. В вашем первом примере, после того, как вы добавили «и мы вернулись», говорится, что локальное изменение будет перезаписано, какое именно локальное изменение? "а мы вернулись"? Почему git просто не переносит это изменение в master, чтобы в master файл содержал «hello world» и «and we back»
Xufeng
В первом примере мастер имеет только «привет мир». В эксперименте был принят «Привет, мир!». Для изменения ветки, файл file.txt должен быть изменен, проблема в том, что есть незафиксированные изменения "привет мир \ ngoodbye мир \ n и мы вернулись".
Гордолио
1

Правильный ответ

git checkout -m origin/master

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

JD1731
источник
0

Если вы не хотите, чтобы эти изменения были зафиксированы, сделайте это git reset --hard.

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

Kacpero
источник