Кроме того, вы можете использоваться git apply, чтобы узнать, чисто ли накладывается патч, ещё до того, как вы будете применять его на самом деле — для этого выполните git apply --check, указав нужный патч:
$ git apply --check 0001-seeing-if-this-helps-the-gem.patch error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply
Если никакого вывода нет, то патч должен наложиться без ошибок. Если проверка прошла неудачно, то команда завершится с ненулевым статусом, так что вы можете использовать её при написании сценариев.
Применение патчей с помощью команды am
Если разработчик является достаточно хорошим пользователем Git и применил команду format-patch для создания своего патча, то ваша задача становится проще, так как такой патч содержит информацию об авторе и сообщение коммита. Если есть возможность, поощряйте участников проекта на использование команды format-patch вместо diff при генерировании патчей для вас. Команду git apply стоит использовать, только если нет другого выхода, и патчи уже созданы при помощи diff.
Чтобы применить патч, созданный при помощи format-patch, используйте команду git am. С технической точки зрения, git am читает mbox-файл, который является простым текстовым форматом для хранения одного или нескольких электронных писем в одном текстовом файле. Он выглядит примерно следующим образом:
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith
Это начало вывода команды format-patch, который мы уже видели в предыдущем разделе. Это одновременно и правильный mbox формат для e-mail. Если кто-то прислал вам по почте патч, правильно воспользовавшись для этого командой git send-email, и вы сохранили это сообщение в mbox-формате, тогда вы можете указать этот mbox-файл команде git am — в результате команда начнёт применять все патчи, которые найдёт. Если вы пользуетесь почтовым клиентом, способным сохранять несколько электронных писем в один mbox-файл, то можете сохранить всю серию патчей в один файл и затем использовать команду git am для применения всех патчей сразу.
Однако, если кто-нибудь загрузил патч, созданный через format-patch, в тикет-систему или что-либо подобное, вы можете сохранить файл локально и затем передать его команде git am, чтобы его наложить:
$ git am 0001-limit-log-function.patch Applying: add limit to log function
Как видите, патч был применён без ошибок и за вас автоматически создан новый коммит. Информация об авторе берётся из полей From и Date письма, а сообщение коммита извлекается из поля Subject и тела (до начала самого патча) электронного письма. Например, если применить патч из mbox-файла приведённого выше примера, то созданный для него коммит будет выглядеть следующим образом:
$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author: Jessica Smith
В поле Commit указан человек, применивший патч, а в CommitDate — время его применения. Информация Author определяет человека, создавшего патч изначально, и время его создания.
Однако возможна ситуация, когда патч не наложиться без ошибок. Возможно ваша основная ветка слишком далеко ушла вперёд относительно той, на которой патч был основан, или этот патч зависит от другого патча, который вы ещё не применили. В этом случае выполнение команды git am будет приостановлено, а у вас спросят, что вы хотите сделать:
$ git am 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply Patch failed at 0001. When you have resolved this problem run "git am --resolved". If you would prefer to skip this patch, instead run "git am --skip". To restore the original branch and stop patching run "git am --abort".
Эта команда выставляет отметки о конфликтах в каждый файл, с которым возникают проблемы, точно так же, как это происходит при операции слияния или перемещения с конфликтами. И разрешается данная ситуация тем же способом — отредактируйте файл, чтобы разрешить конфликт, добавьте новый файл в индекс, а затем выполните команду git am --resolved, чтобы перейти к следующему патчу:
$ (исправление файла) $ git add ticgit.gemspec $ git am --resolved Applying: seeing if this helps the gem
Если вы хотите, чтобы Git постарался разрешить конфликт более умно, воспользуйтесь опцией -3, при которой Git попытается выполнить трёхходовую операцию слияния. Эта опция не включена по умолчанию, так как она не работает в случае, если коммита, на котором был основан патч, нет в вашем репозитории. Если этот коммит всё же у вас есть — в случае, когда патч был основан на публичном коммите — то опция -3 как правило гораздо умнее в наложении конфликтных патчей:
$ git am -3 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... No changes -- Patch already applied.
В этом случае я пытался применить патч, который я уже применил. Без опции -3 это привело бы к конфликту.
При применении серии патчей из mbox-файла, вы также можете запустить команду am в интерактивном режиме — в этом случае команда останавливается на каждом найденном патче и спрашивает вас, хотите ли вы его применить:
$ git am -3 -i mbox Commit Body is: -------------------------- seeing if this helps the gem -------------------------- Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all
Это удобно, если у вас накопилось множество патчей, так как вы сможете сначала просмотреть патч, если вы забыли, что он из себя представляет, или отказаться применять патч, если он уже применён.
После того как вы примените все патчи по интересующей вас теме и сделаете для них коммиты в своей ветке, вы можете принять решение — интегрировать ли их в свои стабильные ветки и если да, то каким образом.
Проверка удалённых веток
Если к вам поступили наработки от человека, использующего Git и имеющего свой собственный репозиторий, в который он и отправил свои изменения, а вам он прислал ссылку на свой репозиторий и имя удалённой ветки, в которой находятся изменения, то вы можете добавить его репозиторий в качестве удалённого и выполнить слияния локально.
Например, если Джессика присылает вам письмо, в котором говорится, что у неё есть классная новая функция в ветке ruby-client в её репозитории, вы можете протестировать её, добавив её репозиторий в качестве удалённого для вашего проекта и выгрузив содержимое этой ветки в рабочий каталог:
$ git remote add jessica git://github.com/jessica/myproject.git $ git fetch jessica $ git checkout -b rubyclient jessica/ruby-client
Если она снова пришлёт вам письмо с другой веткой и с новой замечательной функцией, вы сможете сразу извлечь эти наработки и переключиться на эту ветку, так как её репозиторий уже прописан в ваших удалённых репозиториях.
Этот метод наиболее удобен, если вы работаете с человеком постоянно. Если кто-то изредка представляет вам по одному патчу, то менее затратно по времени будет принимать их по e-mail, чем заставлять всех иметь свои собственные репозитории и постоянно добавлять и удалять удалённые репозитории, чтобы получить пару патчей. Также вы скорее всего не захотите иметь у себя сотни удалённых репозиториев — для всех, кто предоставил вам один или два патча. Хотя сценарии и функции хостингов могут упростить эту ситуацию — всё зависит от того, как ведёте разработку вы и участники вашего проекта.
Другим преимуществом данного подхода является тот факт, что вы получаете не только патчи, но и историю коммитов. Если вы даже обнаружите проблемы со слиянием, то вы по крайней мере будете знать, на каком коммите в вашей истории основана их работа. Правильное трёхходовое слияние в этом случае используется по умолчанию, что лучше, чем передать -3 и надеяться, что патч был сгенерирован на основе публичного коммита, к которому у вас есть доступ.
Если вы не работаете с человеком постоянно, но всё же хотите принять его изменения таким способом, можете указать URL его удалённого репозитория команде git pull. Так вы получите нужные изменения, а URL не будет сохранён в списке удалённых репозиториев:
$ git pull git://github.com/onetimeguy/project.git From git://github.com/onetimeguy/project * branch HEAD -> FETCH_HEAD Merge made by recursive.
Определение вносимых изменений
Сейчас у вас есть тематическая ветка, содержащая наработки участников проекта. На этом этапе вы можете определить, что бы вы хотели с ними сделать. В этом разделе мы снова рассмотрим несколько команд, которые, как вы увидите, можно использовать для точного определения того, что вы собираетесь слить в свою основную ветку.
Часто полезно просмотреть все коммиты, которые есть в этой ветке, но нет в вашей ветке master. Исключить коммиты из ветки master можно добавив опцию --not перед именем ветки. Например, если участник вашего проекта прислал вам два патча, и вы создали ветку с именем contrib и применили эти патчи в ней, вы можете выполнить следующее:
$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon
Чтобы увидеть какие изменения вносит каждый коммит, если помните, можно передать опцию -p команде git log — к каждому коммиту будет добавлен его diff.
Чтобы посмотреть полный diff того, что добавится при слиянии вашей тематической ветки с другой веткой, вам может понадобиться использовать странный трюк, чтобы получить нужный результат. Вы, возможно, решите выполнить такую команду:
$ git diff master
Эта команда выведет вам diff, но результат может ввести вас в заблуждение. Если ваша ветка master была промотана вперёд с того момента, когда вы создали на её основе тематическую ветку, вы, наверняка, увидите странный результат. Это происходит по той причине, что Git напрямую сравнивает снимок состояния последнего коммита тематической ветки, на которой вы находитесь, и снимок последнего коммита ветки master. Например, если вы добавили строку в файл в ветке master, прямое сравнение снимков покажет, что изменения в тематической ветке собираются эту строку удалить.
Если master является прямым предком вашей тематической ветки, то проблем нет. Но если две линии истории разошлись, то diff будет выглядеть так, будто вы добавляете всё новое из вашей тематической ветки и удаляете всё уникальное в ветке master.
То, что вы действительно хотели бы видеть — это изменения, добавленные в тематической ветке, то есть те наработки, которые вы внесёте при слиянии этой ветки с веткой master. Это выполняется путём сравнения последнего коммита в вашей тематической ветке с первым общим с веткой master предком.
Технически, вы можете сделать это, выделив общего предка явным образом и выполнив затем команду diff:
$ git merge-base contrib master 36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 $ git diff 36c7db
Однако это не очень удобно, так что в Git есть отдельное сокращённое обозначение для выполнения того же самого — запись с тремя точками. В контексте команды diff, вы можете поставить три точки после названия одной из веток, чтобы увидеть дельту между последним коммитом ветки, на которой вы находитесь, и их общим предком с другой веткой:
$ git diff master...contrib
Эта команда покажет вам только те наработки в вашей текущей тематической ветке, которые были внесены после её ответвления от ветки master. Это очень удобный синтаксис и его надо запомнить.
Интегрирование чужих наработок
Когда все наработки в вашей тематической ветке готовы к интегрированию в более стабильную ветку, встаёт вопрос — как это сделать? Более того — какой рабочий процесс в целом вы хотите использовать, занимаясь поддержкой своего проекта? Есть множество вариантов, так что рассмотрим некоторые из них.
Процессы слияния
Один из простых рабочих процессов заключается в слиянии наработок в ветку master. В этом случае ваша ветка master содержит основную стабильную версию кода. Если у вас в тематической ветке находится работа, которую вы уже доделали, или полученные от кого-то наработки, которые вы уже проверили, вы сливаете её в свою ветку master, удаляете тематическую ветку, а затем продолжаете работу. Если в вашем репозитории наработки находятся в двух ветках, названия которых ruby_client и php_client (Рисунок 5-19), и вы выполняете слияние сначала для ветки ruby_client, в потом для php_client, то ваша история коммитов в итоге будет выглядеть, как показано на Рисунке 5-20.
Это, по всей видимости, наиболее простой рабочий процесс, но при работе с большими проектами здесь возникает ряд проблем.
Если ваш проект более крупный, или вы работаете с большим количеством разработчиков, вы, вероятно, будете применять по крайней мере двухэтапный цикл слияний. При этом сценарии у вас есть две долго живущие ветки, master и develop, и вы решили, что ветка master обновляется только тогда, когда выходит очень стабильный релиз, а весь новый код включается в ветку develop. Изменения в обеих этих ветках регулярно отправляются в публичный репозиторий. Каждый раз, когда у вас появляется новая тематическая ветка для слияния (Рисунок 5-21), вы сначала сливаете её в develop (Рисунок 5-22); затем, когда вы выпускаете релиз, вы делаете перемотку (fast-forward) ветки master на нужный стабильный коммит ветки develop (Рисунок 5-23).
При таком подходе, клонируя ваш репозиторий, люди могут либо выгрузить ветку master, чтобы получить последний стабильный релиз и легко поддерживать этот код обновлённым, либо переключиться на ветку develop, которая включает в себя всё самое свежее. Вы также можете развить данный подход, создав ветку для интегрирования, в которой будет происходить слияние всех наработок. И когда код на этой ветке станет стабилен и пройдёт все тесты, вы сольёте её в ветку develop; и если всё будет работать как надо в течение некоторого времени, вы выполните перемотку ветки master.
Рабочие процессы с крупными слияниями
Проект Git имеет четыре долго живущие ветки: master, next, pu (proposed updates) для новых наработок и maint для ретроподдержки (backports). Когда участники проекта подготавливают свои наработки, они собираются в тематических ветках в репозитории мейнтейнера проекта примерно так, как мы уже описывали (смотри Рисунок 5-24). На этом этапе проводится оценка проделанной работы — всё ли работает, как положено, стабилен ли код, или ему требуется доработка. Если всё в порядке, то тематические ветки сливаются в ветку next, которая отправляется на сервер, чтобы у каждого была возможность опробовать интегрированные воедино изменения из тематических веток.
Если тематические ветки требуют доработки, они сливаются в ветку pu. Когда будет установлено, что тематические ветки полностью стабильны, они переливаются в master, а ветки pu и next перестраиваются на основе тематических веток, находившихся в next, но ещё не дозревших до master. Это означает, что master практически всегда движется в прямом направлении, ветка next перемещается (rebase) иногда, а ветка pu перемещается чаще всех (смотри Рисунок 5-25).
Когда тематическая ветка была полностью слита в ветку master, она удаляется из репозитория. В проекте Git есть ещё ветка maint, которая ответвлена от последнего релиза и предоставляет backport-патчи, на случай если потребуется выпуск корректировочной версии. Таким образом, когда вы клонируете Git-репозиторий, вы получаете четыре ветки, переключаясь на которые вы можете оценить проект на разных стадиях разработки (в зависимости от того, насколько свежую версию вы хотите получить, или от того, каким образом вы хотите внести в проект свою работу); а мейнтейнер, в свою очередь, имеет структурированный рабочий процесс, который помогает ему изучать новые присланные патчи.
Рабочие процессы с перемещениями и отбором лучшего
Другие мейнтейнеры вместо слияния предпочитают выполнять перемещение или отбор лучших наработок участников проекта на верхушку своей ветки master, чтобы иметь практически линейную историю разработки. Когда у вас есть наработки в тематической ветке, которые вы хотите интегрировать в проект, вы переходите на эту ветку и запускаете команду rebase, которая перемещает изменения на верхушку вашей текущей ветки master (или develop, и т.п.). Если всё прошло хорошо, то можете выполнить перемотку ветки master, получив тем самым линейную историю работы над проектом.
Другой вариант перемещения сделанных наработок из одной ветки в другую — отбор лучшего (cherry-pick). Отбор лучшего в Git является чем-то наподобие перемещения для отдельных коммитов. Берётся патч, который был представлен в коммите, и делается попытка применить его на ветке, на которой вы сейчас находитесь. Это удобно в том случае, если у вас в тематической ветке находится несколько коммитов, а вы хотите включить в проект только один из них, или если у вас только один коммит в тематической ветке, но вы предпочитаете выполнять отбор лучшего вместо перемещения. Например, предположим, ваш проект выглядит так, как показано на Рисунке 5-26.
Если вы хотите вытащить коммит e43a6 в ветку master, выполните:
$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf Finished one cherry-pick. [master]: created a0a41a9: "More friendly message when locking the index fails." 3 files changed, 17 insertions(+), 3 deletions(-)
Эта команда включит в ветку master такие же изменения, которые были добавлены в e43a6, но вы получите новое значение SHA-1 для этого коммита, так как у него будет другая дата применения. Теперь ваша история коммитов выглядит, как показано на Рисунке 5-27.
Теперь вы можете удалить свою тематическую ветку и отбросить коммиты, которые вы не захотели включить в проект.
Отметка релизов
Если вы решили выпустить релиз, вы, вероятно, захотите присвоить ему метку, так чтобы вы потом смогли восстановить этот релиз в любой момент. Процесс создания новой метки обсуждался в Главе 2. Если вы решили подписать вашу метку как мейнтейнер, то процедура будет выглядеть примерно следующим образом:
$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon
Если вы подписываете свои метки, у вас может возникнуть проблема с распространением открытого PGP-ключа, используемого для подписи ваших меток. Мейнтейнер проекта Git решил эту проблему, добавив свой публичный ключ в виде блоба (blob) прямо в репозиторий и затем выставив метку, указывающую прямо на содержимое ключа. Чтобы сделать это, определите какой ключ вам нужен, выполнив gpg --list-keys:
$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid Scott Chacon
Затем вы можете напрямую импортировать ключ в базу данных Git'а, экспортировав его и передав по конвейеру команде git hash-object, которая создаст новый блоб с содержимым ключа и вернёт вам SHA-1 этого блоба:
$ gpg -a --export F721C45A | git hash-object -w --stdin 659ef797d181633c87ec71ac3f9ba29fe5775b92
Теперь, когда у вас в Git хранится ваш ключ, вы можете создать метку, напрямую указывающую на него, использовав значение SHA-1, возвращённое командой hash-object:
$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92
Если вы запустите команду git push --tags, то метка maintainer-pgp-pub станет доступна каждому. Если кто-нибудь захочет проверить какую-нибудь метку, он сможет напрямую импортировать ваш PGP-ключ, вытащив блоб прямо из базы данных и импортировав его в GPG:
$ git show maintainer-pgp-pub | gpg --import
Этот ключ может быть использован для проверки любых подписанных вами меток. Кроме того, если вы включите инструкции в сообщение метки, запуск git show <метка> позволит конечному пользователю получить инструкции по проверке меток.
Генерация номера сборки
Так как коммитам в Git не присваиваются монотонно возрастающие номера наподобие 'v123' или чего-то аналогичного, то в случае, если вы хотите присвоить коммиту имя удобное для восприятия, запустите команду git describe для этого коммита. Git вернёт вам имя ближайшей метки с числом коммитов сделанных поверх этой метки и частичное значения SHA-1 описываемого коммита:
$ git describe master v1.6.2-rc1-20-g8c5b85c
Таким образом при экспорте снимка состояния проекта или его сборки вы можете дать им имя понятное для людей. На самом деле, если вы собираете Git из исходного кода, склонированного из Git-репозитория, git --version вернёт вам что-то подобное. Если вы описываете коммит, которому вы напрямую присвоили метку, команда вернёт вам имя метки.
Команду git describe хорошо использовать с аннотированными метками (метками, созданными при помощи опций -a или -s), так что если вы используете git describe, то метки для релизов должны создаваться этим способом — в этом случае вы сможете удостовериться, что при описании коммиту было дано правильное имя. Вы также можете использовать эту строку в командах checkout и show для указания нужного коммита, однако в будущем она может перестать работать правильно в силу того, что в строке присутствует сокращённое значение SHA-1. Например, в ядре Linux недавно перешли от 8 к 10 символам необходимым для обеспечения уникальности SHA-1 объектов, и поэтому старые имена, сгенерированные командой git describe, стали недействительными.
Подготовка релиза
Теперь хотелось бы выпустить релиз сборки. Вероятно, вам захочется сделать архив последнего состояния вашего кода для тех бедолаг, которые не используют Git. Для этого используется команда git archive:
$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz $ ls *.tar.gz v1.6.2-rc1-20-g8c5b85c.tar.gz
Если кто-нибудь откроет этот tarball, он получит последний снимок состояния вашего проекта внутри каталога project. Таким же способом вы можете создать zip-архив, указав команде git archive опцию --format=zip:
$ git archive master --prefix='project/' --format=zip > `git describe master`.zip
Теперь у вас есть тарбол и zip-архив с релизом вашего проекта, которые вы можете загрузить на свой сайт или отправить людям по почте.
Команда shortlog
Пришло время написать письмо для списка рассылки, чтобы поделиться новостями проекта со всеми, кто им интересуется. При помощи команды git shortlog можно быстро получить что-то наподобие лога изменений (changelog), описывающего, что появилось нового в вашем проекте со времени последнего релиза или последнего письма в список рассылки. Лог изменений включает в себя все коммиты в указанном диапазоне; например, следующая команда вернёт вам сводку по всем коммитам, сделанным со времени прошлого релиза (если последний релиз имел метку v1.0.1):
$ git shortlog --no-merges master --not v1.0.1 Chris Wanstrath (8): Add support for annotated tags to Grit::Tag Add packed-refs annotated tag support. Add Grit::Commit#to_patch Update version and History.txt Remove stray `puts` Make ls_tree ignore nils Tom Preston-Werner (4): fix dates in history dynamic version method Version bump to 1.0.2 Regenerated gemspec for version 1.0.2
Мы получили аккуратную сводку по всем коммитам, начиная с метки v1.0.1, сгруппированным по авторам. Вывод этой команды можно послать в свой список рассылки.
Итоги
Вы должны чувствовать себя достаточно свободно, внося свой вклад в проект под управлением Git, а также занимаясь поддержкой своего собственного проекта или интегрированием наработок других пользователей. Поздравляем тебя, опытный Git-разработчик! В следующей главе вы познакомитесь с более мощными инструментами, а также получите советы по действию в сложных ситуациях, что сделает из вас настоящего мастера в Git.
Инструменты Git
К этому времени вы уже изучили большинство повседневных команд и способы организации рабочего процесса, необходимые для того, чтобы поддерживать Git-репозиторий для управления версиями вашего исходного кода. Вы выполнили основные задания связанные с добавлением файлов под версионный контроль и записью сделанных изменений, и вы вооружились мощью подготовительной области (staging area), легковесного ветвления и слияния.
Сейчас вы познакомитесь с множеством весьма сильных возможностей Git. Вы совсем не обязательно будете использовать их каждый день, но, возможно, в какой-то момент они вам понадобятся.
Выбор ревизии
Git позволяет вам указывать конкретные коммиты или их последовательности несколькими способами. Они не всегда очевидны, но иногда их полезно знать.
Одиночные ревизии
Вы можете просто сослаться на коммит по его SHA-1 хешу, но также существуют более понятные для человека способы ссылаться на коммиты. В этом разделе кратко описаны различные способы обратиться к одному определённому коммиту.
Сокращенный SHA
Git достаточно умён для того, чтобы понять какой коммит вы имеете в виду по первым нескольким символам (частичному хешу), конечно, если их не меньше четырёх и они однозначны, то есть если хеш только одного объекта в вашем репозитории начинается с этих символов.
Например, предположим, что вы хотите посмотреть содержимое какого-то конкретного коммита. Вы выполняете команду git log и находите этот коммит (например тот, в котором вы добавили какую-то функциональность):
$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon
В нашем случае, выберем коммит 1c002dd..... Если вы будете использовать git show, чтобы посмотреть содержимое этого коммита следующие команды эквивалентны (предполагая, что сокращенные версии однозначны):
$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d
Git может показать короткие, уникальные сокращения ваших SHA-1 хешей. Если вы передадите опцию --abbrev-commit команде git log, то её вывод будет использовать сокращённые значения, сохраняя их уникальными; по умолчанию будут использоваться семь символов, но при необходимости длина будет увеличена для сохранения однозначности хешей:
$ git log --abbrev-commit --pretty=oneline ca82a6d changed the version number 085bb3b removed unnecessary test code a11bef0 first commit
В общем случае, восемь-десять символов более чем достаточно для уникальности внутри проекта. В одном из самых больших проектов на Git, ядре Linux только начинает появляться необходимость использовать 12 символов из 40 возможных для сохранения уникальности.
Небольшое замечание о SHA-1
Многие люди интересуются, что произойдет, если они в какой-то момент, по некоторой случайности, получат два объекта в репозитории, которые будут иметь два одинаковых значения SHA-1 хеша. Что тогда?
Если вы вдруг закоммитите объект, SHA-1 хеш которого такой же, как у некоторого предыдущего объекта в вашем репозитории, Git обнаружит предыдущий объект в вашей базе данных Git, и посчитает, что он был уже записан. Если вы в какой-то момент попытаетесь получить этот объект опять, вы всегда будете получать данные первого объекта.
Однако, вы должны осознавать то, как смехотворно маловероятен этот сценарий. Длина SHA-1 составляет 20 байт или 160 бит. Количество случайно хешированных объектов, необходимое для того, чтобы получить 50% вероятность одиночного совпадения составляет порядка 280 (формула для определения вероятности совпадения: p = (n(n-1)/2) * (1/2^160))). 280 это 1.2 x 1024 или один миллион миллиарда миллиардов. Это в 1200 раз больше количества песчинок на земле.
Вот пример для того, чтобы вы поняли, что необходимо, чтобы получить SHA-1 коллизию. Если бы все 6.5 миллиардов людей на Земле программировали, и каждую секунду каждый из них производил количество кода, эквивалентное всей истории ядра Linux (1 миллион Git объектов) и отправлял его в один огромный Git-репозиторий, то потребовалось бы 5 лет для того, чтобы заполнить репозиторий достаточно для того, чтобы получить 50% вероятность единичной SHA-1 коллизии. Более вероятно, что каждый член вашей команды программистов будет атакован и убит волками в несвязанных друг с другом случаях в одну и ту же ночь.
Ссылки на ветки
Для самого прямого метода указать коммит необходимо, чтобы этот коммит имел ветку ссылающуюся на него. Тогда, вы можете использовать имя ветки в любой команде Git, которая ожидает коммит или значение SHA-1. Например, если вы хотите посмотреть последний коммит в ветке, следующие команды эквивалентны, предполагая, что ветка topic1 ссылается на ca82a6d:
$ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1
Чтобы посмотреть на какой именно SHA указывает ветка, или понять для какого-то из приведённых примеров к каким SHA он сводится, можно использовать служебную (plumbing) утилиту Git, которая называется rev-parse. Вы можете заглянуть в Главу 9 для получения большей информации о служебных утилитах; в основном rev-parse нужна для выполнения низкоуровневых операций и не предназначена для использования в повседневной работе. Однако, она может пригодиться, если вам необходимо разобраться, что происходит на самом деле. Сейчас вы можете попробовать применить rev-parse к своей ветке.
$ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949
RefLog-сокращения
Одна из вещей, которую Git делает в фоновом режиме, пока вы работаете, это запоминание ссылочного лога — лога того, где находились HEAD и ветки в течение последних нескольких месяцев.
Ссылочный лог можно просмотреть с помощью git reflog:
$ git reflog 734713b... HEAD@{0}: commit: fixed refs handling, added gc auto, updated d921970... HEAD@{1}: merge phedders/rdocs: Merge made by recursive. 1c002dd... HEAD@{2}: commit: added some blame and merge stuff 1c36188... HEAD@{3}: rebase -i (squash): updating HEAD 95df984... HEAD@{4}: commit: # This is a combination of two commits. 1c36188... HEAD@{5}: rebase -i (squash): updating HEAD 7e05da5... HEAD@{6}: rebase -i (pick): updating HEAD
Каждый раз, когда верхушка ветки обновляется по какой-либо причине, Git сохраняет эту информацию в эту временную историю. И вы можете использовать и эти данные, чтобы задать прошлый коммит. Если вы хотите посмотреть какое значение HEAD имел пять шагов назад для своего репозитория, вы можете использовать ссылку вида @{n}, как показано в выводе команды reflog:
$ git show HEAD@{5}
Также вы можете использовать эту команду, чтобы увидеть, где ветка была некоторое время назад. Например, чтобы увидеть, где была ветка master вчера, наберите
$ git show master@{yesterday}
Эта команда покажет, где верхушка ветки находилась вчера. Такой подход работает только для данных, которые всё ещё находятся в ссылочном логе. Так что вы не сможете использовать его для коммитов с давностью в несколько месяцев.
Чтобы просмотреть информацию ссылочного лога в таком же формате как вывод git log, можно выполнить git log -g:
$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon
Важно отметить, что информация в ссылочном логе строго локальная — это лог того, чем вы занимались со своим репозиторием. Ссылки не будут теми же самыми в чьей-то чужой копии репозитория; и после того как вы только что склонировали репозиторий, ссылочный лог будет пустым, так как вы ещё ничего не делали со своим репозиторием. Команда git show HEAD@{2.months.ago} сработает только если вы склонировали свой проект как минимум два месяца назад. Если вы склонировали его пять минут назад, то вы ничего не получите.
Ссылки на предков
Ещё один основной способ указать коммит — указать коммит через его предков. Если поставить ^ в конце ссылки, для Git это будет означать родителя этого коммита. Допустим история вашего проекта выглядит следующим образом:
$ git log --pretty=format:'%h %s' --graph * 734713b fixed refs handling, added gc auto, updated tests * d921970 Merge commit 'phedders/rdocs' |\ | * 35cfb2b Some rdoc changes * | 1c002dd added some blame and merge stuff |/ * 1c36188 ignore *.gem * 9b29157 add open3_detach to gemspec file list
В этом случае вы можете посмотреть предыдущий коммит указав HEAD^, что означает "родитель HEAD":
$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon
Вы также можете указать некоторое число после ^. Например, d921970^2 означает "второй родитель коммита d921970". Такой синтаксис полезен только для коммитов-слияний, которые имеют больше, чем одного родителя. Первый родитель это ветка, на которой вы находились во время слияния, а второй — коммит на ветке, которая была слита:
$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon
Другое основное обозначение для указания на предков это ~. Это тоже ссылка на первого родителя, поэтому HEAD~ и HEAD^ эквивалентны. Различия становятся очевидными, только когда вы указываете число. HEAD~2 означает первого родителя первого родителя HEAD или прародителя — это переход по первым родителям указанное количество раз. Например, для показанной выше истории, HEAD~3 будет
$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner
То же самое можно записать как HEAD^^^, что опять же означает первого родителя первого родителя первого родителя:
$ git show HEAD^^^
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner
Кроме того, можно комбинировать эти обозначения. Например, можно получить второго родителя для предыдущей ссылки (мы предполагаем, что это коммит-слияние) написав HEAD~3^2, ну и так далее.
Диапазон коммитов
Теперь, когда вы умеете задавать отдельные коммиты, разберёмся как указать диапазон коммитов. Это особенно полезно при управлении ветками — если у вас много веток, вы можете использовать обозначения диапазонов, чтобы ответить на вопросы типа "Какие в этой ветке есть коммиты, которые не были слиты в основную ветку?"
Две точки
Наиболее распространённый способ задать диапазон коммитов — это запись с двумя точками. По существу, таким образом вы просите Git взять набор коммитов достижимых из одного коммита, но не достижимых из другого. Например, пускай ваша история коммитов выглядит так как показано на Рисунке 6-1.
Допустим, вы хотите посмотреть что в вашей ветке experiment ещё не было слито в ветку master. Можно попросить Git показать вам лог только таких коммитов с помощью master..experiment — эта запись означает "все коммиты достижимые из experiment, которые недостижимы из master". Для краткости и большей понятности в примерах мы будем использовать буквы для обозначения коммитов на диаграмме вместо настоящего вывода лога в том порядке в каком они будут отображены:
$ git log master..experiment D C
С другой стороны, если вы хотите получить обратное — все коммиты в master, которых нет в experiment, можно переставить имена веток. Запись experiment..master покажет всё, что есть в master, но недостижимо из experiment:
$ git log experiment..master F E
Такое полезно если вы хотите, чтобы ветка experiment была обновлённой, и хотите посмотреть, что вы собираете в неё слить. Ещё один частый случай использования этого синтаксиса — посмотреть, что вы собираетесь отправить на удалённый сервер:
$ git log origin/master..HEAD
Эта команда покажет вам все коммиты в текущей ветке, которых нет в ветке master на сервере origin. Если бы вы выполнили git push, при условии, что текущая ветка отслеживает origin/master, то коммиты, которые перечислены в выводе git log origin/master..HEAD это те коммиты, которые были бы отправлены на сервер. Кроме того, можно опустить одну из сторон в такой записи — Git подставит туда HEAD. Например, вы можете получить такой же результат как и в предыдущем примере, набрав git log origin/master.. — Git подставит HEAD сам если одна из сторон отсутствует.
Множество вершин
Запись с двумя точками полезна как сокращение, но, возможно, вы захотите указать больше двух веток, чтобы указать нужную ревизию. Например, чтобы посмотреть, какие коммиты находятся в одной из нескольких веток, но не в текущей. Git позволяет сделать это с помощью использования либо символа ^, либо --not перед любыми ссылками, коммиты достижимые из которых вы не хотите видеть. Таким образом, следующие три команды эквивалентны:
$ git log refA..refB $ git log ^refA refB $ git log refB --not refA
Это удобно, потому что с помощью такого синтаксиса можно указать более двух ссылок в своём запросе, чего вы не сможете сделать с помощью двух точек. Например, если вы хотите увидеть все коммиты достижимые из refA или refB, но не из refC, можно набрать одну из таких команд:
$ git log refA refB ^refC $ git log refA refB --not refC
Всё это делает систему выбора ревизий очень мощной, что должно помочь вам определять, что содержится в ваших ветках.
Три точки
Последняя основная запись для выбора диапазона коммитов — это запись с тремя точками, которая означает те коммиты, которые достижимы по одной из двух ссылок, но не по обеим одновременно. Вернёмся к примеру истории коммитов на Рисунке 6-1. Если вы хотите увидеть, что находится в master или experiment, но не в обоих сразу, выполните
$ git log master...experiment F E D C
Повторимся, что это даст вам стандартный log-вывод, но покажет только информацию об этих четырёх коммитах упорядоченных по дате коммита как и обычно.
В этом случае вместе с командой log обычно используют параметр --left-right, который показывает, на какой стороне диапазона находится каждый коммит. Это помогает сделать данные полезнее:
$ git log --left-right master...experiment < F < E > D > C
С помощью этих инструментов, вы можете намного легче объяснить Git, какой коммит или коммиты вы хотите изучить.
Интерактивное индексирование
Вместе с Git поставляется пара сценариев (script), облегчающих выполнение некоторых задач в командной строке. Сейчас мы посмотрим на несколько интерактивных команд, которые помогут вам легко смастерить свои коммиты так, чтобы включить в них только определённые части файлов. Эти инструменты сильно помогают в случае, когда вы поменяли кучу файлов, а потом решили, что хотите, чтобы эти изменения были в нескольких сфокусированных коммитах, а не в одном большом путанном коммите. Так вы сможете убедиться, что ваши коммиты это логически разделённые наборы изменений, которые будет легко просматривать другим разработчикам работающими с вами. Если вы выполните git add с опцией -i или --interactive, Git перейдёт в режим интерактивной оболочки, и покажет что-то похожее на это:
$ git add -i staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now>
Как видите, эта команда показывает содержимое индекса, но в другом виде — по сути, ту же информацию вы получили бы при вызове git status, но здесь она в более сжатом и информативном виде. git add -i показывает проиндексированные изменения слева, а непроиндексированные — справа.
Затем идёт раздел Commands (команды). Тут можно сделать многие вещи, включая добавление файлов в индекс, удаление файлов из индекса, индексирование файлов частями, добавление неотслеживаемых файлов и просмотр дельт (diff) проиндексированных изменений.
Добавление и удаление файлов из индекса
Если набрать 2 или u в приглашении What now>, сценарий спросит какие файлы вы хотите добавить в индекс:
What now> 2 staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>>
Чтобы проиндексировать файлы TODO и index.html, нужно набрать их номера:
Update>> 1,2 staged unstaged path * 1: unchanged +0/-1 TODO * 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>>
Символ * рядом с каждым файлом означает, что файл выбран для индексирования. Если вы сейчас ничего не будете вводить, а нажмёте Enter в приглашении Update>>, то Git возьмёт всё, что уже выбрано, и добавит в индекс:
Update>> updated 2 paths *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb
Как видите, теперь файлы TODO и index.html проиндексированы (staged), а файл simplegit.rb всё ещё нет. Если в этот момент вы хотите удалить файл TODO из индекса, используйте опцию 3 или r (revert):
*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 3 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> 1 staged unstaged path * 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> [enter] reverted one path
Взглянув на статус снова, вы увидите, что файл TODO удалён из индекса:
*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: unchanged +0/-1 TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb
Чтобы посмотреть дельту для проиндексированных изменений, используйте команду 6 или d (diff). Она покажет вам список проиндексированных файлов, и вы можете выбрать те, для которых хотите посмотреть дельту. Это почти то же, что указать git diff --cached в командной строке:
*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 6 staged unstaged path 1: +1/-1 nothing index.html Review diff>> 1 diff --git a/index.html b/index.html index 4d07108..4335f49 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ Date Finder
...
- +