Вы приписываете слишком много магии ветвям. : -)
Способ работы Git действительно удивительно прост. имя ветви - это просто имя для одного хеш-идентификатора коммита Git. (Я также советую вам забыть, что git pull
даже существует, но мы скоро увидим, что это такое и как его использовать.)
О коммитах, хэш-идентификаторах, именах веток и цепочках коммитов
Давайте немного поговорим об этих хеш-идентификаторах коммитов. Идентификатор хэша представляет собой большую некрасивую строку букв и цифр, например 0d0ac3826a3bbb9247e39e12623bbcfdd722f24c
. Это однозначно идентифицирует некоторый объект Git - обычно коммит, и когда мы работаем с именами веток, это всегда, безусловно, коммит. Каждая фиксация записывает хеш-идентификатор своей родительской или предшествующей фиксации. Это позволяет Git связывать коммиты в обратной цепочке.
Это означает, что мы можем нарисовать эти цепочки коммитов. Если мы позволим одной заглавной букве заменить большой некрасивый хеш-идентификатор, мы получим нечто, похожее на это:
... <-F <-G <-H <--master
Имя master
содержит действительный идентификатор хеша коммита H
. Это позволяет Git найти G
в море коммитов, плавающих внутри хранилища. Начиная с H
, Git может получить хеш-код G
, который является H
родителем. Так что теперь Git может найти G
. Используя G
, Git может найти F
и так далее в обратном направлении. Стрелки здесь можно прочитать как указывает на: master
указывает на H
, H
указывает на G
и т. Д.
Содержимое каждого коммита полностью, полностью, полностью заморожено / доступно только для чтения. Ничто внутри любого коммита не может измениться. Поэтому нам не нужно рисовать внутренние стрелки. Однако имена ветвей do меняются. Git добавляет новый коммит к master
, записывая объект коммита, сохраняя хеш-идентификатор H
в новом объекте вместе с новым снимком коммита и любым другим метаданные , такие как ваше имя, адрес электронной почты и сообщение в журнале. Это создает новый хеш, который мы назовем I
, а не пытаемся угадать его:
...--F--G--H--I
и теперь Git просто нужно записать хеш-код I
в имя master
, так что master
теперь указывает на I
:
...--F--G--H--I <-- master
Если у вас более одной ветви или если у вас есть несколько имен удаленного отслеживания , таких как origin/master
и origin/BranchA
, мы просто рисуем их все:
...--F--G--H <-- master, origin/master
\
I--J <-- origin/BranchA
(Мы поговорим подробнее об именах для удаленного отслеживания в ближайшее время. Они похожи на имена веток, но с изюминкой.)
Когда вы создаете новое имя ветки, все, что Git должен сделать, это заставить новое имя указывать на какой-то существующий коммит. Например, давайте создадим наш собственный BranchA
сейчас, используя git checkout BranchA
: 1
...--F--G--H <-- master, origin/master
\
I--J <-- BranchA, origin/BranchA
Теперь давайте также создадим testBranch
, также указывая на коммит J
:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch, BranchA, origin/BranchA
Если вы создаете новый коммит сейчас, ваш Git должен знать , какое имя ветки обновлять . Таким образом, у вашего Git есть это специальное имя, HEAD
, написанное прописными буквами вот так. 2 Git прикрепляет это имя к одному из имен вашей ветки:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch (HEAD), BranchA, origin/BranchA
, что означает, что testBranch
- это текущая ветвь и, следовательно, это имя, которое Git обновит , когда вы запустите git commit
, чтобы сделать новый коммит. git checkout
управляет этим HEAD-вложением.
1 Поскольку у вас нет a BranchA
, вы можете подумать: Как я могу это проверить? На самом деле, вы должен подумать так: это действительно хороший вопрос. Ответ в том, что ваш Git создаст ваш BranchA
из имени удаленного отслеживания . Вот почему вам пришлось git checkout -b testBranch
, но не git checkout -b BranchA
: флаг -b
говорит: create , и без него Git будет создавать, только если имя не существует и есть имя для удаленного отслеживания, которое существует , которое выглядит правильно. Это еще не все, но это хорошее начало.
2 Из-за этой странности обычно можно использовать строчные буквы head
в Windows и MacOS, но не в Unix-подобных системах, таких как Linux. Рекомендуется избегать этой привычки, поскольку она не будет работать в Linux: если вам не нравится вводить HEAD
во всех заглавных буквах, используйте @
, что является синонимом магического имени.
Имена удаленного слежения, или что происходит, когда кто-то делает коммиты в каком-то другом Git-репозитории?
В именах этих веток есть то, что они относятся к вашему репозиторию Git . Ваш master
является вашим master
. Ваш BranchA
ваш BranchA
и ваш testBranch
тоже ваш. Они не изменятся, если вы не измените их.
На самом деле, даже ваши имена для удаленного отслеживания - origin/master
и origin/BranchA
- тоже ваши, но их имена для удаленного отслеживания состоят в том, что ваш Git автоматически изменит их, чтобы запомнить то, что ваш Git видит в каком-то другом Git, всякий раз, когда ваш Git вызывает их Git и спрашивает их о их названиях ветвей. То есть, ваш Git имеет URL-адрес для какого-то другого Git-репозитория, указанный под remote name origin
: origin
- это короткое имя для какого-то длинного, возможно, трудного для ввода URL-адреса. Вы можете запустить:
git fetch origin
и ваш Git вызовет свой Git по URL-адресу, указанному в origin
, и спросит у своего Git о их филиалах. Они скажут: О, конечно, вот так: мой master
- это , а мой BranchA
- это . (Чтобы увидеть это, запустите git ls-remote origin
, что похоже на git fetch origin
за исключением того, что после получения списка удаленных имен и хэшей он просто распечатывает их.)
Имея этот список в руках, ваш Git продолжает спрашивать у своего Git о любых новых коммитах у них есть то, чего у вас нет. Так что, если они обновили свои BranchA
, вы получите их новые коммиты. Затем, независимо от того, что еще произошло, ваш Git теперь устанавливает все ваши имена для удаленного отслеживания , которые начинаются с origin/
. То есть предположим, что у них было два новых коммита. Ваш собственный репозиторий теперь выглядит так:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch (HEAD), BranchA
\
K--L <-- origin/BranchA
Ваши собственные BranchA
и testBranch
не переехали . Это ваши ветви, поэтому они двигаются только тогда, когда вы перемещаете их. Ваш origin/master
не двигался, потому что их master
не двигался, но ваш origin/BranchA
имеет перемещено, чтобы запомнить новый коммит L
, который вы только что получили от них, потому что их BranchA
действительно двигались и теперь указывают на тот же коммит L
.
(Помните, наши заглавные буквы обозначают настоящие большие уродливые уникальные хеш-идентификаторы. Если они сделали новые коммиты, и вы сделали новые коммиты, Git гарантирует, что их новые хеш-идентификаторы отличаются от каждый новый хэш коммита, который вы сделали! Вы можете видеть, что с активным репозиторием отдельные заглавные буквы будут слишком быстро заканчиваться и их будет слишком сложно сделать уникальными. Но их намного проще рисовать и чтобы нам было легче говорить о коммитах, поэтому я использую их здесь.)
Перемещение имен веток
Теперь, когда они обновили свои BranchA
, вы можете захотеть сделать свой собственный BranchA
ход. Здесь все может начать усложняться, но давайте рассмотрим простой способ сделать это.
Начнем с запуска git checkout BranchA
снова. Это прикрепит HEAD
к BranchA
, так что команды Git, которые используют текущую ветвь , используют BranchA
. Тогда мы будем использовать git merge
, который в данном случае фактически не выполняет слияние!
git checkout BranchA
git merge origin/BranchA
До git merge
у нас есть это в нашем хранилище:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch, BranchA (HEAD)
\
K--L <-- origin/BranchA
git merge
смотрит на origin/BranchA
и находит, что оно указывает на L
. Он смотрит на нашу текущую ветку - ту, к которой прикреплена HEAD
, и находит, что она указывает на J
. Он понимает, что, начиная с L
и работая в обратном направлении, он может перейти прямо к J
. Это означает, что название ветви BranchA
можно как бы «сдвинуть вперед» против направления внутренних, указывающих назад стрелок. Git называет эту операцию fast-forward . В контексте git merge
это больше похоже на git checkout
, который перемещает имя текущей ветви. То есть, коммит L
становится текущим коммитом , но он делает это с помощью , перемещая имя BranchA
. Результат:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch
\
K--L <-- BranchA (HEAD), origin/BranchA
Теперь у вас есть коммит L
в качестве вашего текущего коммита, а коммит L
заполняет index и work-tree . Пришло время немного поговорить об этих двух.
Указатель и дерево работы
Мы уже упоминали, что файлы, хранящиеся в коммитах, полностью, полностью, полностью заморожены / доступны только для чтения. Они хранятся в специальном сжатом формате Git-only. Это позволяет Git сэкономить много места и повторно использовать неизмененные файлы: если новый коммит имеет в основном те же файлы, что и предыдущий коммит, нет необходимости сохранять все файлы. Копии старого коммита замораживаются, поэтому новый коммит может просто поделиться ими. (Детали, с помощью которых этот процесс работает, здесь не имеют большого значения, но Git использует хеш-идентификаторы, которые Git называет объектами BLOB-объектов , чтобы добиться этого трюка.)
Это прекрасно для Git, но мы не можем использовать замороженные сжатые файлы только для Git, чтобы делать что-либо иначе . Поэтому Git должен разморозить и распаковать замороженные файлы в их обычную повседневную форму, чтобы мы и остальные программы на нашем компьютере могли использовать их.
Оттаявшие файлы попадают в рабочее дерево , которое называется так, потому что именно там мы работаем над ними. Здесь мы можем делать все что угодно с нашими файлами. Таким образом, для каждого файла в текущем коммите есть замороженная копия, а в рабочем дереве - размороженная копия. (Могут быть и замороженные копии в других коммитах, но один из них в текущем коммите является наиболее интересным, поскольку мы можем и будем часто сравнивать его с тем в рабочем дереве.)
index , также называемый областью подготовки или иногда cache , является своеобразной вещью, уникальной для Git. Другие системы управления версиями также имеют замороженные коммиты и оттаявшие рабочие деревья, но либо не имеют индекса, либо не хранят ничего похожего на индекс полностью, чтобы вам не нужно было об этом знать. Git, с другой стороны, будет время от времени бить вас по лицу указателем. Вы должны знать об этом, даже если вы не используете это для причудливых уловок.
Индекс содержит, по сути, копию каждого файла. То есть каждый файл в текущем коммите равен и в индексе. Индексная копия находится в специальном формате Git-only. В отличие от замороженной копии коммита, однако, она только полузамороженная, если хотите, немного слякотная. Вы можете заменить в любое время новой, другой, Git-ified и полузамороженной копией. Вот что делает git add
: он выполняет Git-копирование рабочей копии файла, сжимает ее в формат Git-only и заменяет предыдущую индексную копию. (Если новый соответствует любому старому, в любом замороженном коммите Git он снова использует этот старый: экономит место! В противном случае это новая копия Git-ized.)
Для создания нового коммита в Git просто необходимо мгновенно заморозить эти индексные копии. Все они уже готовы к этому, и это значительная часть того, почему git commit
намного быстрее, чем другие системы контроля версий. Но это также означает, что индекс можно описать как , что войдет в ваш следующий коммит . Git создает новые коммиты из индекса, а не из рабочего дерева.
Вам нужно рабочее дерево для работы с вашими файлами. Git нуждается и использует индекс для новых коммитов. Индекс и копии рабочего дерева могут отличаться; Это часть вашей работы, направленная на git add
копий рабочего дерева, для перезаписи индексных копий обновленными перед фиксацией.
Обновление вашего testBranch
После всего этого давайте теперь рассмотрим обновление вашего testBranch
. Помните, мы запустили git fetch
, чтобы обновить все наши origin/*
имена, затем git checkout BranchA
и git merge origin/BranchA
, чтобы обновить BranchA
, так что теперь у нас есть это:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch
\
K--L <-- BranchA (HEAD), origin/BranchA
Теперь нам нужно git checkout testBranch
, чтобы прикрепить HEAD
к нему. Тогда мы можем запустить git merge BranchA
или git merge origin/BranchA
:
git checkout testBranch
git merge <anything that identifies commit L>
Идея в том, чтобы Git посмотрел на commit L
. Затем команда слияния увидит, возможно ли выполнить ту же операцию ускоренной перемотки, что и для BranchA
. Ответ будет положительным: определенно можно перейти от коммита J
к коммиту L
. Так что по умолчанию Git сделает именно это, и вы получите следующее:
...--F--G--H <-- master, origin/master
\
I--J
\
K--L <-- testBranch, BranchA, origin/BranchA
Обратите внимание, что мы можем сделать это, даже если мы никогда не создадим нашу собственную BranchA
, потому что вместо git merge BranchA
мы можем запустить git merge origin/BranchA
. То есть, если мы имеем:
...--F--G--H <-- master, origin/master
\
I--J <-- testBranch (HEAD)
\
K--L <-- origin/BranchA
и запустите git merge origin/BranchA
, Git сделает то же самое ускоренное выполнение, что и с версией с именем BranchA
, указывающей на коммит L
. Здесь важны не имена ветвей, а коммиты. Ну, наши собственные имена ветвей, такие как testBranch
, имеют значение, так как нам нужно, чтобы они указывали, где они должны; но другие имена - имена для удаленного слежения - мы используем их только для поиска коммитов . Они просто более читабельны , чем хеш-идентификаторы, и наш Git автоматически обновит их на git fetch
.
Следовательно, предположим, что мы никогда не создавали BranchA
во-первых. Предположим, вместо этого мы сделали:
$ git clone <url>
$ cd <repository>
$ git checkout -b testBranch origin/BranchA
... wait until colleague updates origin/BranchA ...
$ git fetch # defaults to using origin
$ git merge origin/BranchA
тогда мы были бы готовы, без необходимости возиться с нашим BranchA
, который мы даже никогда не создавали.
Я собираюсь опустить то, что происходит здесь, если вы делаете свои собственные коммиты. В этом случае вы получите истинное слияние - git merge
увидит, что не можно просто перемотать вперед, запустит процесс слияния, а затем сделает коммит типа merge совершить . Вместо этого давайте обратимся к последнему кусочку головоломки: git pull
.
О git pull
(не используйте его!)
Мой совет для git pull
заключается в том, что как новичок вы должны старательно избегать этого. Тем не менее, другие люди и документация скажут вам использовать его, поэтому вы должны хотя бы знать, что он делает. Все, что git pull
есть и делает, это запускает две команды Git для вас. Это должно быть удобно. Проблема в том, что иногда это удобно, а иногда замечательно не -удобно. На мой взгляд, гораздо лучше сначала научиться использовать две базовые команды Git.
Первая команда Git, которую запускает git pull
, это просто git fetch
. Мы уже видели, что это так: он вызывает какой-то другой Git, получает из него список его названий ветвей (и имен тегов) и хеш-идентификаторов и вводит в ваш репозиторий все необходимые вам коммиты, так что ваш Git может обновить все ваши имена для удаленного слежения . Тогда это сделано: с вашим индексом и рабочим деревом ничего не произошло Безопасно запускать git fetch
в любое время , поскольку он просто добавляет новые коммиты и обновляет имена для удаленного отслеживания.
Команда second , которую запускает git pull
, запускает проблему. Вы можете выбрать , какая вторая команда, которую она запускает. Обычно это git merge
, что делает то, что мы видели выше. Но вы можете запустить его git rebase
, который мы здесь не рассмотрели.
В любом случае git pull
передает некоторые дополнительные аргументы команде git merge
или git rebase
. Эти дополнительные аргументы вызывают некоторые неудобства, потому что они отличаются от аргументов, которые вы, возможно, захотите использовать. В частности, если вы запустите:
git pull origin master
это имеет эффект запуска:
git fetch origin master
git merge -m "merge branch 'master' of $url" origin/master
Обратите внимание на косую черту в последнем аргументе - Git собирается объединить коммит, теперь идентифицируемый вашим origin/master
. -m
(сообщение) содержит URL-адрес, взятый из origin
, плюс имя master
, а не имя origin/master
, но эффект слияния, будь то быстрая перемотка вперед или реальная объединить - это то же самое, что объединить обновленное имя удаленного отслеживания, origin/master
. 3
Если вы используете отдельные команды git fetch
и git merge
, они имеют больше смысла. Когда вы используете git pull
, имя ветви, которое вы перечисляете, если вы его перечислите, это имя в другом Git , а не имя удаленного отслеживания в вашем Git.
То же самое верно, даже если у вас есть git pull
run git rebase
для вас. И, в последнем повороте того, чтобы быть не -удобным, решение о том, использовать ли слияние или перебазирование, вы должны сделать после запуска git fetch
. То есть вы должны посмотреть на что git fetch
выбирает , чтобы решить, какую вторую команду запустить. Но если вы используете git pull
, вы должны принять это решение до того, как запустите git fetch
, чтобы вы не могли смотреть.
Если вы некоторое время использовали Git и хорошо знакомы с git merge
и git rebase
, , тогда вы можете безопасно начать использовать git pull
. (Но я все еще в основном не знаю.)
3 Здесь есть еще одна проблема с довольно старыми версиями Git: до Git версии 1.8.4 git pull
не не обновляло имя удаленного слежения. Modern Git устраняет эту странную причуду, но некоторые системы все еще используют действительно старые версии Git, поэтому важно знать о них.