Содержание
series
Хотя это легко просто использовать Mercurial Queues, используя минимум возможностей и некоторые менее часто используемые возможности MQ для работы в сложных условиях разработки.
В этой главе я буду использовать в качестве примера технику которую я использовал для управления процессом создания драйверов устройства infiniband для ядра linux. Драйвер в большом вопросе (по крайней мере как драйвер запускается), с 25000 строк кода, распространяемых через 35 исходных файлов. Он поддерживается небольшой группой разработчиков.
Хотя значительная часть материала в этой главе, специфична для linux, те же принципы применяются к любой кодовой базе, для которых вы не основной владелец, и в котором вам нужно сделать много.
Ядро linux меняется очень быстро, и никогда не было внутренне стабильным; разработчики часто делают резкие изменения между версиями. Это означает, что версия драйвера, который хорошо работает с определенной версией ядра не будет даже правильно собираться, как правило, на какой-нибудь другой версией.
Чтобы разрабатывать драйвер, мы должны иметь различные версий ядра linux в виду.
Первой целью является основная ветка разработки ядра linux. Разработка кода в этом случае частично совместима с другими разработчиками в сообществе ядра, которые делают «drive-by» модификации для драйверов по мере их развития и совершенствования подсистем ядра.
Мы также поддерживали некоторое количество «backports» для старых версий ядра linux, в целях удовлетворения потребностей клиентов, которые работали под управлением более старых дистрибутивов linux, которые не имеют наших драйверов. (Чтобы портировать кусок кода, чтобы модифицировать его работу для старой версии целевого окружения, чем версия для которой был разработана.)
Наконец, мы делаем выпусков программ по графику, не обязательно совпадающего с тем, который используется дистрибьюторами linux и разработчиками ядра, так что мы можем добавлять новые возможности для клиентов, не заставляя их обновлять всё их ядро и дистрибутив.
Есть два «стандартных» способа поддерживать часть программы, которая должна работать в самых разных условиях.
Во-первых, поддерживать ряд ветвей, каждая предназначена для единственной цели. Проблема такого подхода заключается в том, что необходимо поддерживать железную дисциплину в потоке изменений между хранилищами. Новые функции или исправления ошибок должны начинать жизнь в «первозданном» репозитории, а затем проходить в каждый бекпортированый репозиторий. backport изменения, более ограничены в ветках они должны передаваться по наследству; портирование применённые изменения в ветку, к которой оно не принадлежит, вероятно, вызовет остановку компиляции драйвера.
Вторая заключается в сохранении единого дерева исходников наполненных условными выражениями, которые в свою очередь включают или выключают блоки кода в зависимости от поставленной цели. Поскольку эти «ifdefs» не допускаются в дереве ядра linux, ручной или автоматический процесс должен последовательно вырезать их и давать на выходе чистое дерево. Код основной разработки таким образом быстро становится крысиным гнездом условных блоков, которые трудно понимать и поддерживать.
Ни один из этих подходов не подходит хорошо для ситуации, когда вы используете не «свою» каноническую копию исходного дерева. В случае драйверов linux, которые распространяются со стандартным ядром, дерево Линуса содержит копию кода, который будет рассматриваться в мире как канонический. Апстрим версия «моих» драйверов может быть изменена людьми, которых я не знаю, без меня, я даже узнаю об этом только после отображения изменений в дереве Линуса.
Эти подходы добавил слабое место, которое затрудняет создание хорошо сформированного патча для отправки в апстрим.
В принципе, Mercurial Queues кажется хорошим кандидатом для управления сценарием развития, таким как выше. Хотя это действительно так, MQ содержит несколько дополнительных функций, которые делают работу более приятной.
Возможно, лучший способом поддерживать здраво так много задач является возможность выбирать конкретные патчи применяемые для той или иной ситуации. MQ обеспечивает функция называющуюся «стражи» (guards) (которая берет свое начало с командой quilt's guards
), которая делает тоже самое. Для начала, давайте создадим простой репозиторий для экспериментов
$
hg qinit
$
hg qnew hello.patch
$
echo hello > hello
$
hg add hello
$
hg qrefresh
$
hg qnew goodbye.patch
$
echo goodbye > goodbye
$
hg add goodbye
$
hg qrefresh
Это дает нам крошечный репозиторий, содержащее два патча, не имеющие никаких зависимостей друг от друга, потому что они затрагивают разные файлы.
Идея применения условия, которое вы можете указать в «тэг» патча со стражем, который является простой текстовой строкой по вашему выбору, которая скажет MQ, выбрать конкретного стража используемого при применении исправлений. MQ потом либо применит, либо пропустить, охраняемый патч, в зависимости от охранника, которого вы выбрали.
Патч может иметь произвольное число охранников, и каждый из них положительный («применить этот патч, если выбран этот охранник») или отрицательным («пропустить этот патч, если выбран этот охранник»). Патч, без охранника всегда применяется.
Команда qguard позволяет определить, какая охрана должна распространяться на патч, или отобразит охранников, которые уже действуют. Без аргументов, он отображает охранников на текущий верхний патч.
$
hg qguard
goodbye.patch: unguarded
Чтобы установить положительного охранника на патч, используйте префикс «+
» перед именем стража.
$
hg qguard +foo
$
hg qguard
goodbye.patch: +foo
Чтобы установить положительного охранника на патч, используйте префикс «-
» перед именем стража.
$
hg qguard -- hello.patch -quux
$
hg qguard hello.patch
hello.patch: -quux
Notice that we prefixed the arguments to the hg qguard command with a --
here, so that Mercurial would not interpret the text -quux
as an option.
Mercurial сохраняет охранников магазинов в файле series
; том виде, в котором они хранятся их легко понять и править руками. (Другими словами, вы не должны использовать qguard команду, если вы не хотите, достаточно просто редактировать файл series
).
$
cat .hg/patches/series
hello.patch #-quux goodbye.patch #+foo
Команда qselect определяет, какие охранники активны в данный момент времени. Результатом этого является определение, какие патчи MQ будет применяться при следующем запуске qpush. Команда не имеет другого эффекта, в частности, она не делает ничего, с уже применёнными патчами.
Без аргументов qselect перечисляет охранников применённых в настоящее время, по одному на строку. Каждый аргумент рассматривается как имя применённого охранника.
$
hg qpop -a
popping goodbye.patch popping hello.patch patch queue now empty$
hg qselect
no active guards$
hg qselect foo
number of unguarded, unapplied patches has changed from 1 to 2$
hg qselect
foo
Если вам интересно применённые охранники хранятся в файле guards
.
$
cat .hg/patches/guards
foo
Эффект от выбранных охранников мы увидим когда запустим qpush.
$
hg qpush -a
applying hello.patch applying goodbye.patch now at: goodbye.patch
Охранник не может начинаться с символа «+
» или «-
». Имя охранника не должно содержать пробелов, но большинство других символов разрешены. Если вы пытаетесь использовать неверное имя сторожа, MQ будет ругаться:
$
hg qselect +foo
abort: guard '+foo' starts with invalid character: '+'
Изменение выбранных охранников изменяет применяемые патчи.
$
hg qselect quux
number of guarded, applied patches has changed from 0 to 2$
hg qpop -a
popping goodbye.patch popping hello.patch patch queue now empty$
hg qpush -a
skipping goodbye.patch - guarded by '+foo'
Вы можете увидеть в приведенном ниже примере, что негативные охранники имеют приоритет над положительными охранниками.
$
hg qselect foo bar
number of unguarded, unapplied patches has changed from 0 to 2$
hg qpop -a
no patches applied$
hg qpush -a
applying hello.patch applying goodbye.patch now at: goodbye.patch
Правила, которые использует MQ при решении вопроса о применении патча состоят в следующем.
Если патч имеет негативного охранника, который соответствует любому выбранному охраннику, патч будет пропущен.
Если патч имеет положительного охранника, который соответствует любому выбранному охраннику, патч будет применён.
Если патч имеет положительного или отрицательного охранников, но никто из них не выбран, патч будет пропущен.
В работе над драйвером о котором я говорил ранее, я не применять патчи для нормального дерева ядра linux. Вместо этого я использую репозиторий, который содержит только снимок из исходных файлов и заголовков, которые имеют отношение к разработке Infiniband. Этот репозиторий 1% от размера репозитория ядра, так легче работать.
Я тогда выбрал «основную» версию поверх которой применял патчи. Это снимок дерева ядра linux содержал выбранные мной ревизии. Когда я беру снимок, я записываю id ревизии из репозитория ядра в сообщении фиксации. Поскольку снимок сохраняет «форму» и содержание соответствующих разделов дерева ядра, и я могу применить мои патчи поверх и моего маленького репозитория, и нормальный дерева ядра.
Как правило, на вершине основного дерева, к которому применяются патчи, должен быть снимок совсем недавнего апстрим дерева. Это в наибольшей степени способствует разработке патчей, которые могут легко быть отправлены в апстрим с небольшим или вообще без изменений.
Я классифицировал патчи в файле series
на несколько логических групп. Каждый раздел патчей начинается с блока комментариев, которые описывают цель исправлении, которые за ним следуют.
Последовательность групп патчей я поддерживаю следующую. Порядок этих групп является важным, и я расскажу, почему после того, как опишу группы.
Группа «принято». Патчи, которые команда разработчиков представила управляющему подсистемы infiniband, и которые он принял, но которые ещё не применены к снимку на основе малого репозитория. Эти патчи доступны «только для чтение», присутствуют только в трансформированном дереве в таком же состоянии, как они представлены сопровождающему верхнего репозитория.
Группа «доработки». Патчи, которые я принял, но вышележащий апстрим сопровождающий попросил изменить, до того как он их примет.
Группа «ожидают решения». Патчи, которые я еще не представили вышестоящему сопровождающему, но они готовы к работе. Они будут на некоторое время доступны «только для чтения». Если верхний сопровождающий их примет, при представлении, я перемещу их окончательно в группы «принято». Если он попросит меня изменить какой-либо из них, я перемещу их сначала в группу «доработки».
Группа «в процессе разработки». Патчи, которые активно развиваются, и не должны быть представлены еще нигде.
Группа «backport». Патчи, которые адаптированные для дерева исходных текстов с более старыми версиями ядра.
Группа «Не распростронять». Патчи, которые почему-то никогда не должны быть представлены в апстриме. Например, один такой патч может изменить встроенный идентификатор драйвера, чтобы было легче различать, области, между версией драйвера вне дерева и версией распространяемой поставщиками.
Теперь вернемся к причинам сортировки групп патчей таким образом. Мы хотели бы, самые низкие патчи в стеке были бы максимально устойчивы, так чтоб не нужно было перерабатывать патчи выше в связи с изменениями в соответствующем контексте. Введение патчей, которые никогда не будут изменятся первыми в файле series
служит именно этой цели.
Мы хотели бы также чтоб патчи, которые мы знаем, что нужно изменить должны применяться в верхней части дерева исходных текстов, которая напоминает апстрим дерево, насколько это возможно. Именно поэтому мы постоянно принимаем патчи близко к верху.
В группах «backport» и «Не распростронять» патчи находятся в конце файла series
. backport патчи должны применяться поверх всех других исправлений, также патчи из группы «Не распростронять» лучше держать от греха подальше.
В своей работе я использую набор охранников, чтобы контролировать, какие патчи будут применяться.
accepted
защищает «принятые» патчи. Я включаю эту охрану большую часть времени. Когда я применяю патчи поверх дерева, где эти патчи уже есть, я отключаю эти патчи, и следующие за ними применяются чисто.
Патчи, которые «завершены», но пока не отправлены, не имеют охраны. Если я применяю стек патчей к копию апстрим дерева, мне не нужно включать какого либо из охранников для получения достаточно безопасного исходного дерева.
Те, патчи, которые требуют доработки, прежде чем вновь отправится охраняются с помощью rework
.
Для тех патчей, которые находятся в стадии разработки, я использую devel
.
backport патчи имеют различных охранников, по одному для каждой версии ядра, к которому они относятся. Например, backports патча у кода ядра 2.6.9 будет охранник 2.6.9
.
Такое разнообразие охранников дает мне значительную гибкость в определении вида дерева исходников, к которому я хочу в итоге применить патчи. В большинстве случаев, выбор соответствующих охранников автоматизирован в процессе сборки, но я могу вручную настроить охранников для использования в менее общих обстоятельствах.
Использование MQ, для написания backport патчей простой процесс. Все что нужно сделать в патче, это изменить фрагмент кода, который использует функции ядра которой нет в старой версии ядра, так что драйвер продолжает работать правильно в соответствии с этой старой версией.
Полезная цель при написании хорошего backport патча это сделать код выглядящим так, будто он была написан для старой версии ядра, на которую вы ориентируетесь. Чем менее навязчив патч, тем легче будет его понять и поддерживать. Если вы пишете коллекцию backport патчей, чтобы избежать эффекта «крысиного гнезда» — большого количества #ifdef
s (блоков исходного кода, которые будут использоваться только условно) в коде, не вводите зависимые от версии #ifdef
s в патчи. Вместо этого, напишите несколько патчей, каждый из которых дает безусловные изменения, а также контролируйте их примеением с помощью охранников.
Есть две причины выделить backport патчи в отдельную группу, подальше от «обычных» патчей, последствия которых они модифицируют. Во-первых, их смешение делает более сложным в использовании инструменты такие, как расширение patchbomb
для автоматизации процесса передачи патчей сопровождающим апстрима. Во-вторых, портированое исправление может нарушить контекст, в котором применяются обычные патчи, что делает невозможным применение очередного патча чисто, если ранее применился backport патч.
Если вы работаете на крупный проект с MQ, не трудно накопить большое количество патчей. Например, у меня есть репозиторий патчей, который содержит более 250 патчей.
Если вы можете сгруппировать эти патчи на отдельные логические категории, вы можете, если хотите хранить их в разных каталогах; MQ не имеет проблем с именами патчей, которые содержат разделители пути.
Если вы разрабатываете набор патчей в течение длительного времени, хорошая идея сохранить их в репозитории, о чем говорится в разделе Раздел 12.12, «Управление патчами в репозитории». Если вы это сделаете, вы быстро обнаружите, что использование команды hg diff, чтобы взглянуть на историю изменений патча обреченно на провал. Это отчасти потому, что вы смотрите на вторую производную от реального кода (список различий различий), но также и потому, MQ добавляет шуму в этот процесс, меняя временные метки и имена каталогов при обновлении патча.
Тем не менее, вы можете использовать расширение extdiff
, которое поставляется вместе с Mercurial, для просмотра различий двух версий патча в читабельном виде. Для этого вам понадобится сторонний пакет называющийся patchutils
[web:patchutils]. Он дает команду interdiff, которая показывает разницу между двумя diff-ами как diff. Используется на двух версиях одного и того же патча, она выдаёт diff, который представляет собой различия с первой по вторую версии.
Вы можете включить расширение extdiff
обычным способом, путем добавления строки в раздел extensions
вашего ~/.hgrc
.
[extensions] extdiff =
Команда interdiff ожидает два аргумента — два названия файлов, но расширение extdiff
передаёт в запускаемую программу пару каталогов, каждый из которых может содержать произвольное количество файлов. Таким образом, потребуется небольшая программа, которая будет запускать interdiff к каждой паре файлов в этих двух каталогах. Эта программа существует и называется hg-interdiff
в каталоге examples
репозитория исходного кода, прилагаемый к этой книге.
Запустить программу hg-interdiff
в в вашей консоли, вы можете запустить его следующим образом, внутри каталога патчей MQ:
hg extdiff -p hg-interdiff -r A:B my-change.patch
Так как вы, наверное, хотите, чтобы использовать эту многословную команду часто, вы можете получить hgext
чтобы сделать её доступной как обычную Mercurial команду, опять же, редактированием ~/.hgrc
.
[extdiff] cmd.interdiff = hg-interdiff
Эта строка приказывает hgext
сделать команду interdiff
доступной, поэтому теперь вы можете сократить предыдущую команду extdiff до чего-то чуть более простого.
hg interdiff -r A:B my-change.patch
Расширение extdiff
полезно для более просто улучшение отображения патча MQ. Чтобы узнать больше о ней, перейдите в раздел Раздел 14.2, «Гибкая поддержка diff с расширением extdiff
».