Глава 13. Расширенное использование Mercurial Queues

Содержание

13.1. Проблема множества целей
13.1.1. Соблазнительные подходы, которые работают не очень хорошо
13.2. Условное применение патчей с защитой
13.3. Управление защитой патча
13.4. Выбор используемых охранников
13.5. Правила применения патчей в MQ
13.6. Обрезка рабочего окружения
13.7. Разделение файла series
13.8. Поддержка серии патчей
13.8.1. Искусство писать backport патчи
13.9. Полезные советы для разработки с MQ
13.9.1. Организация патчей в каталогах
13.9.2. Просмотр истории патча

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

В этой главе я буду использовать в качестве примера технику которую я использовал для управления процессом создания драйверов устройства infiniband для ядра linux. Драйвер в большом вопросе (по крайней мере как драйвер запускается), с 25000 строк кода, распространяемых через 35 исходных файлов. Он поддерживается небольшой группой разработчиков.

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

13.1. Проблема множества целей

Ядро linux меняется очень быстро, и никогда не было внутренне стабильным; разработчики часто делают резкие изменения между версиями. Это означает, что версия драйвера, который хорошо работает с определенной версией ядра не будет даже правильно собираться, как правило, на какой-нибудь другой версией.

Чтобы разрабатывать драйвер, мы должны иметь различные версий ядра linux в виду.

  • Первой целью является основная ветка разработки ядра linux. Разработка кода в этом случае частично совместима с другими разработчиками в сообществе ядра, которые делают «drive-by» модификации для драйверов по мере их развития и совершенствования подсистем ядра.

  • Мы также поддерживали некоторое количество «backports» для старых версий ядра linux, в целях удовлетворения потребностей клиентов, которые работали под управлением более старых дистрибутивов linux, которые не имеют наших драйверов. (Чтобы портировать кусок кода, чтобы модифицировать его работу для старой версии целевого окружения, чем версия для которой был разработана.)

  • Наконец, мы делаем выпусков программ по графику, не обязательно совпадающего с тем, который используется дистрибьюторами linux и разработчиками ядра, так что мы можем добавлять новые возможности для клиентов, не заставляя их обновлять всё их ядро и дистрибутив.

13.1.1. Соблазнительные подходы, которые работают не очень хорошо

Есть два «стандартных» способа поддерживать часть программы, которая должна работать в самых разных условиях.

Во-первых, поддерживать ряд ветвей, каждая предназначена для единственной цели. Проблема такого подхода заключается в том, что необходимо поддерживать железную дисциплину в потоке изменений между хранилищами. Новые функции или исправления ошибок должны начинать жизнь в «первозданном» репозитории, а затем проходить в каждый бекпортированый репозиторий. backport изменения, более ограничены в ветках они должны передаваться по наследству; портирование применённые изменения в ветку, к которой оно не принадлежит, вероятно, вызовет остановку компиляции драйвера.

Вторая заключается в сохранении единого дерева исходников наполненных условными выражениями, которые в свою очередь включают или выключают блоки кода в зависимости от поставленной цели. Поскольку эти «ifdefs» не допускаются в дереве ядра linux, ручной или автоматический процесс должен последовательно вырезать их и давать на выходе чистое дерево. Код основной разработки таким образом быстро становится крысиным гнездом условных блоков, которые трудно понимать и поддерживать.

Ни один из этих подходов не подходит хорошо для ситуации, когда вы используете не «свою» каноническую копию исходного дерева. В случае драйверов linux, которые распространяются со стандартным ядром, дерево Линуса содержит копию кода, который будет рассматриваться в мире как канонический. Апстрим версия «моих» драйверов может быть изменена людьми, которых я не знаю, без меня, я даже узнаю об этом только после отображения изменений в дереве Линуса.

Эти подходы добавил слабое место, которое затрудняет создание хорошо сформированного патча для отправки в апстрим.

В принципе, Mercurial Queues кажется хорошим кандидатом для управления сценарием развития, таким как выше. Хотя это действительно так, MQ содержит несколько дополнительных функций, которые делают работу более приятной.

13.2. Условное применение патчей с защитой

Возможно, лучший способом поддерживать здраво так много задач является возможность выбирать конкретные патчи применяемые для той или иной ситуации. 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 потом либо применит, либо пропустить, охраняемый патч, в зависимости от охранника, которого вы выбрали.

Патч может иметь произвольное число охранников, и каждый из них положительный («применить этот патч, если выбран этот охранник») или отрицательным («пропустить этот патч, если выбран этот охранник»). Патч, без охранника всегда применяется.

13.3. Управление защитой патча

Команда 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.

[Примечание] Setting vs. modifying

Команда qguard устанавливает охранников на патч, но не изменять их. Это означает, что при запуске hg qguard +a +b на патч, а затем hg qguard +c на тот же патч, единственным охранником, который будет установлен на него будет +c.

Mercurial сохраняет охранников магазинов в файле series; том виде, в котором они хранятся их легко понять и править руками. (Другими словами, вы не должны использовать qguard команду, если вы не хотите, достаточно просто редактировать файл series).

$ cat .hg/patches/series
hello.patch #-quux
goodbye.patch #+foo

13.4. Выбор используемых охранников

Команда 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

13.5. Правила применения патчей в MQ

Правила, которые использует MQ при решении вопроса о применении патча состоят в следующем.

  • Патч, который не имеет охраны применяется всегда.

  • Если патч имеет негативного охранника, который соответствует любому выбранному охраннику, патч будет пропущен.

  • Если патч имеет положительного охранника, который соответствует любому выбранному охраннику, патч будет применён.

  • Если патч имеет положительного или отрицательного охранников, но никто из них не выбран, патч будет пропущен.

13.6. Обрезка рабочего окружения

В работе над драйвером о котором я говорил ранее, я не применять патчи для нормального дерева ядра linux. Вместо этого я использую репозиторий, который содержит только снимок из исходных файлов и заголовков, которые имеют отношение к разработке Infiniband. Этот репозиторий 1% от размера репозитория ядра, так легче работать.

Я тогда выбрал «основную» версию поверх которой применял патчи. Это снимок дерева ядра linux содержал выбранные мной ревизии. Когда я беру снимок, я записываю id ревизии из репозитория ядра в сообщении фиксации. Поскольку снимок сохраняет «форму» и содержание соответствующих разделов дерева ядра, и я могу применить мои патчи поверх и моего маленького репозитория, и нормальный дерева ядра.

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

13.7. Разделение файла series

Я классифицировал патчи в файле series на несколько логических групп. Каждый раздел патчей начинается с блока комментариев, которые описывают цель исправлении, которые за ним следуют.

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

  • Группа «принято». Патчи, которые команда разработчиков представила управляющему подсистемы infiniband, и которые он принял, но которые ещё не применены к снимку на основе малого репозитория. Эти патчи доступны «только для чтение», присутствуют только в трансформированном дереве в таком же состоянии, как они представлены сопровождающему верхнего репозитория.

  • Группа «доработки». Патчи, которые я принял, но вышележащий апстрим сопровождающий попросил изменить, до того как он их примет.

  • Группа «ожидают решения». Патчи, которые я еще не представили вышестоящему сопровождающему, но они готовы к работе. Они будут на некоторое время доступны «только для чтения». Если верхний сопровождающий их примет, при представлении, я перемещу их окончательно в группы «принято». Если он попросит меня изменить какой-либо из них, я перемещу их сначала в группу «доработки».

  • Группа «в процессе разработки». Патчи, которые активно развиваются, и не должны быть представлены еще нигде.

  • Группа «backport». Патчи, которые адаптированные для дерева исходных текстов с более старыми версиями ядра.

  • Группа «Не распростронять». Патчи, которые почему-то никогда не должны быть представлены в апстриме. Например, один такой патч может изменить встроенный идентификатор драйвера, чтобы было легче различать, области, между версией драйвера вне дерева и версией распространяемой поставщиками.

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

Мы хотели бы также чтоб патчи, которые мы знаем, что нужно изменить должны применяться в верхней части дерева исходных текстов, которая напоминает апстрим дерево, насколько это возможно. Именно поэтому мы постоянно принимаем патчи близко к верху.

В группах «backport» и «Не распростронять» патчи находятся в конце файла series. backport патчи должны применяться поверх всех других исправлений, также патчи из группы «Не распростронять» лучше держать от греха подальше.

13.8. Поддержка серии патчей

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

  • accepted защищает «принятые» патчи. Я включаю эту охрану большую часть времени. Когда я применяю патчи поверх дерева, где эти патчи уже есть, я отключаю эти патчи, и следующие за ними применяются чисто.

  • Патчи, которые «завершены», но пока не отправлены, не имеют охраны. Если я применяю стек патчей к копию апстрим дерева, мне не нужно включать какого либо из охранников для получения достаточно безопасного исходного дерева.

  • Те, патчи, которые требуют доработки, прежде чем вновь отправится охраняются с помощью rework.

  • Для тех патчей, которые находятся в стадии разработки, я использую devel.

  • backport патчи имеют различных охранников, по одному для каждой версии ядра, к которому они относятся. Например, backports патча у кода ядра 2.6.9 будет охранник 2.6.9.

Такое разнообразие охранников дает мне значительную гибкость в определении вида дерева исходников, к которому я хочу в итоге применить патчи. В большинстве случаев, выбор соответствующих охранников автоматизирован в процессе сборки, но я могу вручную настроить охранников для использования в менее общих обстоятельствах.

13.8.1. Искусство писать backport патчи

Использование MQ, для написания backport патчей простой процесс. Все что нужно сделать в патче, это изменить фрагмент кода, который использует функции ядра которой нет в старой версии ядра, так что драйвер продолжает работать правильно в соответствии с этой старой версией.

Полезная цель при написании хорошего backport патча это сделать код выглядящим так, будто он была написан для старой версии ядра, на которую вы ориентируетесь. Чем менее навязчив патч, тем легче будет его понять и поддерживать. Если вы пишете коллекцию backport патчей, чтобы избежать эффекта «крысиного гнезда» — большого количества #ifdefs (блоков исходного кода, которые будут использоваться только условно) в коде, не вводите зависимые от версии #ifdefs в патчи. Вместо этого, напишите несколько патчей, каждый из которых дает безусловные изменения, а также контролируйте их примеением с помощью охранников.

Есть две причины выделить backport патчи в отдельную группу, подальше от «обычных» патчей, последствия которых они модифицируют. Во-первых, их смешение делает более сложным в использовании инструменты такие, как расширение patchbomb для автоматизации процесса передачи патчей сопровождающим апстрима. Во-вторых, портированое исправление может нарушить контекст, в котором применяются обычные патчи, что делает невозможным применение очередного патча чисто, если ранее применился backport патч.

13.9. Полезные советы для разработки с MQ

13.9.1. Организация патчей в каталогах

Если вы работаете на крупный проект с MQ, не трудно накопить большое количество патчей. Например, у меня есть репозиторий патчей, который содержит более 250 патчей.

Если вы можете сгруппировать эти патчи на отдельные логические категории, вы можете, если хотите хранить их в разных каталогах; MQ не имеет проблем с именами патчей, которые содержат разделители пути.

13.9.2. Просмотр истории патча

Если вы разрабатываете набор патчей в течение длительного времени, хорошая идея сохранить их в репозитории, о чем говорится в разделе Раздел 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
[Примечание] Примечание

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

Расширение extdiff полезно для более просто улучшение отображения патча MQ. Чтобы узнать больше о ней, перейдите в раздел Раздел 14.2, «Гибкая поддержка diff с расширением extdiff».