Распределённый контроль версий с Mercurial Copyright © 2006, 2007 Bryan O’Sullivan. Материал может распространяться только при условии соблюдения положений и условий, изложенных в версии 1.0 лицензии Open Publication License. Текст лицензии можно посмотреть в Приложении D. Английский оригинал книги собран из ревизии 028543f67bea (2008-08-20 15:27 -0700), с использованием Mercurial собранного из ревизии a58a611c320f. Введение Распределённые системы контроля версий — относительно новое явление, развивающееся благодаря стремлению людей ко всему новому и непознанному. Я пишу книгу о распределённых системах контроля версий (Version Control System — VCS или Revision Control System — RCS), так как считаю эту тему важной и заслуживающей хорошего руководства. Я решил написать о Mercurial потому, что это программное средство прекрасно подходит для учебных целей, и в то же время она справляется с задачами, которые не по силам многим другим системам контроля версий. 0.1 Работа над книгой ещё не закончена Я публикую незаконченную книгу, потому что надеюсь, она будет полезной для всех интересующихся этой темой. И ещё я надеюсь, читатели помогут мне в написании книги так, как сочтут нужным. 0.2 О примерах в этой книге В этой книге довольно необычные примеры. Каждый пример «живой» — фактически, это результат исполнения скрипта, который выполняет приведённые команды Mercurial. Каждый раз, когда книга собирается из исходников, все примеры автоматически исполняются и их текущие результаты сравниваются с ожидаемыми. Плюс такого подхода в том, что примеры всегда верны; они описывают поведение в точности той версии Mercurial, которая указана в начале книги. Если я обновлю версию Mercurial, которую я описываю, и вывод какой-нибудь команды изменится — сборка книги не выполнится. У этого подхода есть маленький недостаток, заключающийся в том, что даты и время, которые вы видите в примерах, слишком близки друг к другу, чего бы не происходило, если бы те же самые команды вбивал человек. Там, где человек может набрать не больше одной команды за несколько секунд, с соответствующим образом разнесёнными «отпечатками времени» (timestamps), мои автоматизированные примеры исполняют много команд за одну секунду. Как пример этого, несколько последовательных коммитов в примере могут выглядеть исполненными в одну и ту же секунду. Вы можете увидеть это в примере использования команды bisect (деление пополам) в разделе 9.5. Так что когда вы читаете примеры, не придавайте слишком большого значения датам или времени, которые вы увидите в результатах исполнения команд. Однако, будьте уверены, что поведение команд, которое вы видите, в целом верно и воспроизводимо. 0.3 Выходные данные — эта книга Свободна Эта книга выпущена под лицензией Open Publication License, и создана исключительно с использованием Свободного ПО. Набрана она в LATEX; иллюстрации нарисованы и сформированы с помощью Inkscape. Полный исходный код для этой книги выпущен в виде Mercurial хранилища и доступен по адресу http://hg.serpentine.com/mercurial/book. Глава 1 Введение 1.1 О контроле версий Контроль версий — это процесс управления множеством версий некой информации. В своём простейшем виде это то, что многие люди делают вручную: каждый раз, изменив файл, вы сохраняете его с новым именем, содержащим число, большее, чем число предыдущей версии. Управление множеством версий кода даже одного файла — это задача, чреватая ошибками, так что программные инструменты для автоматизации этого процесса были сделаны довольно давно. Самые первые системы контроля версий предназначались для помощи пользователю-одиночке в управлении ревизиями одиночных файлов. На протяжении последних десятилетий область применения инструментов управления версиями очень сильно расширилась. Сейчас они управляют множеством файлов и помогают многим людям работать вместе. Лучшие из современных средств управления версиями без проблем справляются с тысячами людей, работающими совместно над проектами из сотен тысяч файлов. 1.1.1 Зачем нужен контроль версий? Есть несколько причин, почему вы или ваша команда можете захотеть использовать автоматизированную систему управления версиями для проекта. * Она будет отслеживать историю и прогресс вашего проекта вместо вас. Для каждого изменения у вас будет запись о том, кто его сделал, почему он это сделал, когда он это сделал и в чем заключалось изменение. * Когда вы работаете с другими людьми, система контроля версий облегчает вам взаимодействие. Например, когда несколько человек делают потенциально конфликтные изменения одновременно, программа поможет вам определить и разрешить такие конфликты. * Она может помочь вам восстановиться после ошибок. Если вы сделаете изменение, которое потом окажется ошибкой, вы сможете вернуться к более ранней версии одного или нескольких файлов. На самом деле, действительно хорошая система контроля версий даже поможет вам легко найти тот момент времени, когда проблема впервые появилась (см. раздел 9.5). * Она поможет вам работать одновременно над несколькими версиями проекта и переключаться между ними. Все эти причины одинаково важны (по крайней мере, в теории) независимо от того, работаете ли вы над проектом в одиночку или вместе с сотней других людей. Ключевой вопрос относительно практичности системы контроля версий в таких разных средах («хакер-одиночка» и «огромная команда») в том, как преимущества соотносятся с «ценой» использования. Система, которую сложно понять или использовать, обойдётся дорого. Проект для пятисот человек скорее всего развалится под собственным весом практически сразу, если в нём не используется система контроля версий и нет процесса разработки. В подобном случае даже говорить о цене использования контроля версий малоосмысленно, так как без неё провал практически неизбежен. С другой стороны, проект «на коленке» для программиста-одиночки может показаться неудачным местом для использования системы контроля версий, потому как цена её использования наверняка будет сравнима с общей стоимостью всего проекта. Верно? Mercurial уникален тем, что поддерживает оба предела размеров проекта. Вы можете изучить основы всего за несколько минут и, благодаря минимальным накладным расходам, вы с лёгкостию сможете применять управление версиями к наималейшим из проектов. Его простота означает, что вам не придется удерживать в голове множество маловразумительных концепций или последовательностей команд для того, чтобы совершить желаемое. В то же время, высокая производительность Mercurial и его распределённая природа позволит вам безболезненно увеличивать масштаб для управления большими проектами. Никакая система контроля версий не поможет спасти скверно управляемый проект, но хорошие инструменты могут оказать значительное влияние на удобство работы над проектом. 1.1.2 Множество названий контроля версий Контроль версий — довольно многозначное понятие, из-за чего для его обозначения существует множество разных названий и акронимов. Вот некоторые из них: * Контроль ревизий (RCS, Revision control system) * Конфигурационное управление (SCM, Software configuration management) * Управление исходным кодом (Source code management) * Контроль исходного кода (Source code control) * Контроль версий (VCS, Version control system) Некоторые люди утверждают, что на самом деле у этих терминов разные значения, но на практике они настолько сильно пересекаются, что нет общепринятого или хотя бы полезного способа вполне разделить их. 1.2 Краткая история контроля версий Самая известная из старых утилит контроля версий — SCCS (Source Code Control System, система контроля исходного кода), которую написал Марк Рочкайнд (Marc Rochkind) из Bell Labs, в начале 70-х. SCCS оперировала отдельными файлами и требовала, чтобы каждый человек, работающий над проектом, имел доступ к общему рабочему пространству, существовавшему в единственном экземпляре. Только один человек мог одновременно редактировать файл в один момент времени; конфликты доступа к файлам разрешались блокировками. Обычной ситуацией было забывание снятия блокировки после редактирования, что запрещало доступ к файлу другим людям без помощи администратора. Вальтер Тичи (Walter Tichy) разработал свободную альтернативу SCCS в начале 1980-х; он назвал свою программу RCS (Revision Control System, система контроля ревизий). Подобно SCCS, RCS требовала от разработчиков как работы в едином разделяемом рабочем пространстве, так и блокировки файлов для предотвращения одновременного изменения файлов разными людьми. Позднее, в 1980-х же годах, Дик Грюн (Dick Grune) использовал RCS как основу для набора shell-скриптов, изначально названных cmt, а позднее переименованных в CVS (Concurrent Versions System, система одновременных версий). Крупное нововведение CVS заключалось в том, что она позволяла разработчикам работать одновременно и, в некоторой степени, независимо в их личных рабочих пространствах. Этими-то пространствами и предотвратилось беспрестанное наступание разработчиков друг другу на пятки, которое было обычным делом в SCCS и RCS. Каждый разработчик имел копию каждого файла проекта, разработчики могли модифицировать свои копии независимо. Им приходилось объединять собственные правки перед отсылкою изменений в центральное хранилище. Брайан Берлинер (Brian Berliner) взял первоначальные скрипты Грюна и переписал их на Си, выпустив в 1989 году код, который впоследствии развился в современную версию CVS. CVS в дальнейшем приобрела возможность работать по сети, обретя клиент-серверную архитектуру. Архитектура CVS является централизованной: только на сервере есть копия истории проекта. Клиентские рабочие копии содержали только экземпляры файлов последней версии и небольшие метаданные для определения местонахождения сервера. Система CVS достигла невероятного успеха: вероятно, она является самой широко используемой системой контроля версий в мире. В начале 1990-х годов Sun Microsystems разработала раннюю распределённую систему контроля версий, называвшуюся TeamWare. Каждая рабочая копия TeamWare содержала полную копию истории изменений проекта. Понятие центрального репозитория в TeamWare отсутствовало как таковое. (Подобно CVS, использовавшей RCS для хранения истории, TeamWare использовала SCCS.) Шли 1990-ые, росла осведомлённость о нескольких проблемах CVS. Система записывает одновременные изменения нескольких файлов раздельно, а не группирует их в одну логически атомарную операцию. Способ управления файловой иерархией не очень хорош: нетрудно устроить в репозитории беспорядок, переименовывая файлы и каталоги. Более того, исходные коды CVS непросто понимать и поддерживать, что сделало практически непреодолимым «болевой порог» исправления этих архитектурных проблем. В 2001 году Джим Бланди (Jim Blandy) и Карл Фогель (Karl Fogel) — два разработчика, прежде работавшие над CVS — начали проект по её замене таким средством, которое имело бы архитектуру получше и код почище. Результат, Subversion, не отошёл от централизованной клиент-серверной модели CVS, но добавил атомарные коммиты нескольких файлов, лучшее управление пространствами имён и другие возможности, которые сделали Subversion более удобным средством работы, нежели CVS. Со времени выхода первой версии Subversion быстро обретал популярность. Более или менее одновременно, Грейдон Хоар (Graydon Hoare) начал работать над амбициозной системой контроля версий, которую назвал Monotone. Эта система не только устраняет множество проблем внутреннего устройства CVS и имеет распределённую архитектуру, но и идёт далее нескольких прежних (и последующих) систем контроля версий в некоторых своих нововведениях. Monotone использует криптографические хеши в качестве идентификаторов и имеет неотъемлемое представление о «доверии» коду из различных источников. Жизнь Mercurial началась в 2005 году. В то время как некоторые аспекты его архитектуры были созданы под влиянием Monotone, Mercurial сосредоточен на простоте использования, высокой производительности и масштабируемости до очень больших проектов. 1.3 Тенденции в контроле версий За последние четыре десятилетия в разработке и в использовании систем контроля версий, в то время как люди осваивают возможности своих инструментов и сталкиваются с их ограничениями, безошибочно явствует некая тенденция. Первое поколение начинало с управления одиночными файлами на индивидуальных компьютерах. Хотя эти инструменты имели большое преимущество над специализированным ручным управлением версиями, их блокирующая модель и зависимость от одного компьютера позволяли применять их лишь в тесных маленьких командах. Второе поколение ослабило эти ограничения переходом на сетевые архитектуры и комплексное управление проектами. По мере роста проектов появлялись новые проблемы. Когда клиентам нужно было очень часто взаимодействовать с серверами, масштабирование сервера становилось сложной задачей на больших проектах. Ненадёжное соединение с сетью могло вообще не давать отдалённым пользователям общаться с сервером. Когда проекты с открытым кодом стали давать всем анонимный доступ только на чтение, люди, не имеющие достаточных прав для внесения изменений, обнаружили, что не могут использовать инструменты для взаимодействия с проектом естественным образом, так как не могут записать свои правки. Современное поколение инструментов контроля версий — по природе децентрализованное (peer-to-peer). Все эти системы устранили зависимость от одного центрального сервера и позволили людям передавать их данные контроля версий туда, где действительно есть в них нужда. Сотрудничество через Интернет, прежде ограниченное технологией, ныне стало вопросом выбора и согласия. Современные инструменты могут действовать и без сети, неограниченно и автономно, испытывая нужду в сетевом соединении только при синхронизации изменений с другим репозиторием. 1.4 Некоторые преимущества распределённого контроля версий Хотя инструменты распределённого контроля версий уже несколько лет так же надёжны и удобны, как и их аналоги предыдущего поколения, люди, использующие старые инструменты, ещё не осознали преимущества новых. Во многих отношениях распределённые инструменты блистают по сравнению с централизованными. Для отдельного разработчика распределённые инструменты практически всегда намного быстрее централизованных. Этому есть простое объяснение: централизованная утилита для многих обыденных операций должна общаться по сети, поскольку большинство метаданных хранятся в единственном экземпляре на центральном сервере. Распределённый инструмент хранит все свои метаданные локально. При прочих равных условиях общение по сети увеличивает накладные расходы использования централизованного инструмента. Не недооценивайте значимость шустрого, быстро реагирующего инструмента: вам доведётся провести массу времени, взаимодействуя с системою контроля версий. Распределённые инструменты безразличны к причудам вашей серверной инфраструктуры, опять же потому, что они создают дубликаты метаданных во множестве мест. Если вы используете централизованную систему, а ваш сервер воспламенится, то останется надеяться на то, что резервные копии надёжны, и что последнее создание их прошло успешно и не очень давно. С распределённым инструментом вам доступно множество резервных копий — на компьютере у каждого разработчика. Надёжность вашего сетевого соединения будет влиять на распределённые системы значительно меньше, чем на централизованные. А использовать централизованную утилиту без сетевого соединения у вас даже не получится, за исключением нескольких сильно ограниченных команд. С распределённой системой отключение сетевого соединения во время работы вообще может пройти незамеченным. Единственное, что будет невозможным — запросы к репозиториям на других компьютерах, что происходит не так уж и часто по сравнению с другими операциями. Если вы состоите в группе разработчиков, находящихся на большом расстоянии друг от друга, это может быть важным. 1.4.1 Преимущества для проектов с открытым исходным кодом Если вы нашли открытый проект, над которым вам хотелось бы поработать, и проект использует распределённую систему контроля версий, вы находитесь на одной ступеньке с людьми, которые являются "ядром" проекта. Если они публикуют свои репозитории, вы можете незамедлительно копировать историю разработки, делать изменения и записывать их точно так же, как это делают полноправные участники проекта. Централизованную систему, напротив, придётся использовать в режиме «только чтение», если только кто-нибудь не даст вам достаточно прав для фиксирования изменений на центральном сервере. До тех пор у вас не будет никакой возможности фиксировать изменения и они будут под риском искажения каждый раз при обновлении рабочей копии репозитория. Ветвления - не проблема Есть мнение, что распределённые системы контроля версий добавляют риска проектам с открытым исходным кодом, поскольку делают простым ветвление разработки проекта. Это случается, когда существуют разногласия во взглядах или отношениях между группами разработчиков, которые ведут к принятию ими решения о невозможности работать вместе. Тогда каждая сторона берёт более или менее полную копию исходного кода проекта и идёт в своём собственном направлении. Иногда стороны решают объединиться и согласовать изменения. С централизованной системой процесс слияния изменений технически очень сложен и в основном должен быть произведён вручную. Вам придётся решать, чья история "выиграет", и каким-то образом прививать изменения другой команды. Обычно при этом теряется история изменений одной или обеих сторон. Распределённые системы поступают с ветвлением очень просто — они объявляют его единственным путём развития проекта. Каждое изменение, которое вы делаете, потенциально является точкой ответвления. Силой такого подхода является то, что инструмент должен быть действительно хорош в объединении веток, потому что ветки крайне важны: они всё время создаются. Если каждый кусочек работы, делаемой всеми, всегда оформляется в терминах ответвления и слияния, тогда то, что мир открытого ПО называет "ветвлением", становится исключительно социальной проблемой. Как бы то ни было, распределённые системы понижают вероятность ветвления: * Они убирают социальное разделение, которое привнесли централизованные системы — между инсайдерами (теми, кто может вносить изменения) и аутсайдерами (теми, кто не может). * Они упрощают воссоединение после социального ветвления, так как с точки зрения контроля версий это ничем не отличается от обычного слияния. Некоторые люди сопротивляются использованию распределённого контроля версий, потому что хотят сохранить за собой строгий контроль над своими проектами, и думают, что централизованные системы дадут им его. Тем не менее, если вы придерживаетесь таких убеждений, и при этом разрешаете публичный доступ к своему CVS/Subversion репозиторию, то знайте, что существует множество инструментов, позволяющих вытащить полную историю вашего проекта (пусть даже и медленно) и пересоздать её в таком месте, которое вы не сможете контролировать. Таким образом получается, что ваш контроль в этом случае иллюзорен, и в то же время вы потеряли возможность гибко сотрудничать с теми людьми, которые почувствовали себя вынужденными продублировать вашу историю или ответвиться от неё. 1.4.2 Преимущества для коммерческих проектов Команды многих коммерческих проектов зачастую разбросаны по всему земному шару. Территориально удалённые от главного сервера разработчики могут сталкиваться с такими проблемами, как замедленная реакция на выполнение команд и, возможно, перерывы в доступности системы. Коммерческие системы контроля версий предлагают решение данной проблемы в виде расширений для удалённой репликации данных. Такие расширения как правило довольно дороги и сложны для администрирования. Распределённые системы изначально лишены подобных недостатков. Более того, можно установить несколько проектных серверов, скажем один в каждом офисе, для сокращения объёма избыточного трафика между репозиториями через дорогие каналы связи. Централизованные системы контроля версий как правило обладают ограниченной масштабируемостью. Падение дорогой централизованной системы под нагрузкой, вызванной одновременным обращением всего пары дюжин пользователей, не является чем-то необычным. Повторяясь, наиболее типичным решением проблемы будет дорогой и тяжелый механизм репликации. Так как нагрузка на главный сервер — даже если он единственный — для распределённого инструмента контроля версий во много раз меньше (потому что все данные реплицируются повсюду), один недорогой сервер может удовлетворять потребности гораздо более многочисленной команды разработчиков и для репликации с целью распределения нагрузки нужно лишь написать несложные скрипты. Если некоторые члены вашей команды работают "в поле", разрешая проблемы на площадке заказчика, они также получают определённые преимущества от использования распределённой системы контроля версий. Инструмент позволит им строить кастомные сборки, пробовать различные исправления изолированно друг от друга, а также проводить эффективный поиск причины ошибки в среде заказчика с использованием истории версий. Всё это может быть выполнено без необходимости в постоянном соединении с внутренней сетью компании. 1.5 Почему следует остановить выбор на Mercurial? Mercurial обладает уникальным набором свойств, позволяющим выбрать его в качестве наиболее подходящей системы контроля версий. * Прост в изучении и в использовании * Легковесный * Превосходно масштабируется * Легко настраивается под конкретные нужды Если вы обладаете опытом в использовании систем контроля версий, вам потребуется меньше пяти минут, чтобы начать работать с Mercurial. Если же вы новичок, процесс знакомства не должен занять больше десяти минут. Mercurial предоставляет единообразную и последовательную систему команд и функций, что позволяет руководствоваться небольшим набором общих правил вместо того, чтобы учить массу исключений. В небольших проектах вы можете начать работу с Mercurial в считанные минуты. Создание новых веток и изменений, распространение изменений (как локально, так и по сети), операции с историей и статусом — всё это работает быстро. Mercurial старается быть незаметным и не путаться под вашими ногами, не требует от вас больших умственных усилий и совершает свои операции невероятно быстро. Mercurial применяется не только в маленьких проектах, его используют и в проектах с сотнями и тысячами разработчиков, проектах, которые содержат десятки тысяч файлов и сотни мегабайт исходного кода. Если вам не хватает базовой функциональности Mercurial, то её легко расширить. Mercurial хорошо подходит для задач скриптинга, его понятное устройство и реализация на языке Python позволяет легко добавлять новые возможности в виде расширений. Существует большое количество популярных и полезных расширений, охватывающих спектр задач от помощи в нахождении ошибок до улучшения производительности. 1.6 Mercurial в сравнении с другими системами контроля версий Прежде чем вы продолжите чтение, вам следует уяснить, что этот раздел отражает мой опыт, интересы и (да, я осмелюсь сказать это) мои наклонности. Я использовал каждую из перечисленных ниже систем контроля версий в большинстве случаев в течение нескольких лет. 1.6.1 Subversion Subversion — популярная система контроля версий, разработанная с целью заменить CVS. Subversion имеет централизованную клиент/серверную архитектуру. Subversion и Mercurial имеют похожие команды для одних и тех же операций, так что если вы хорошо знаете одну из этих систем, вам легко будет научиться пользоваться другой. Обе системы портированы на все популярные операционные системы. У Subversion отсутствует возможность слияния с учетом истории изменений, что заставляет пользователей вручную отслеживать какие из ревизий были слиты между ветками. Если пользователи этого не делают или допускают ошибки, они сталкиваются с необходимостью ручного разрешения необязательных конфликтов при слиянии. Subversion также не может слить изменения если файл или каталог был переименован. Плохая поддержка слияния - единственный крупный недостаток Subversion. Mercurial обладает большей производительностью, чем Subversion, в каждой операции, производительность которой я измерял. Скорость больше в 2-6 раз, когда речь идет о локальном репозитории Subversion 1.4.3 (самый быстрый метод доступа). При более реалистичном варианте использования - сетевой репозиторий, Subversion находится в существенно худшем положении. В силу того, что команды Subversion должны взаимодействовать с сервером и при этом Subversion не имеет полезных средств репликации, производительность сервера и пропускная способность сети становятся узкими местами даже для некрупных проектов. Кроме того, Subversion требует дополнительное дисковое пространство для того, чтобы избежать сетевых запросов при выполнении некоторых операций: поиск модифицированных файлов (status) и отображение изменений (diff). В результате рабочая копия Subversion такого же размера (а то и больше) как репозиторий Mercurial и рабочий каталог вместе взятые, хотя репозиторий Mercurial содержит полную историю проекта. Subversion имеет широкую поддержку инструментария сторонних производителей. В этом отношении у Mercurial сейчас существенное отставание. Хотя разрыв сокращается, и некоторые GUI-утилиты для Mercurial превосходят свои аналоги для Subversion. Как и Mercurial, Subversion располагает отличным руководством пользователя. Из-за того, что Subversion не хранит историю изменений на клиенте, она хорошо подходит для управления проектами, содержащими большое количество двоичных файлов. Если вы внесете в несжимаемый десятимегабайтный файл 50 изменений, то дисковое пространство, использованное Subversion останется неизменным. Пространство, используемое любой из распределенных систем контроля версий, будет быстро увеличиваться пропорционально количеству изменений, потому что различия между правками большие. Кроме того, обычно трудно, а чаще невозможно слить разные версии двоичного файла. Subversion позволяет пользователю заблокировать файл, в результате пользователь на время получает эксклюзивные права на внесение изменений в него. Это может быть значительным преимуществом для проекта, в котором широко используются двоичные файлы. Mercurial может импортировать историю изменений из репозитория Subversion. Возможен и обратный процесс. Это делает возможным прощупать почву и использовать Mercurial и Subversion одновременно, прежде чем решить, осуществлять переход или нет. Преобразование истории — пошаговый процесс, так что вы можете осуществить начальное преобразование, а потом вносить новые изменения. 1.6.2 Git Git — распределенная система контроля версий, которая была разработана для управления исходниками ядра Linux. Как и у Mercurial, на начальный дизайн системы оказал влияние Monotone. Git предоставляет большой список команд, число которых в версии 1.5.0 достигает 139 уникальных единиц. Он имеет репутацию инструмента, сложного для изучения. В сравнении с Git, Mercurial делает упор на простоту. Что касается производительности — Git очень быстр. В некоторых случаях он быстрее, чем Mercurial (по крайней мере под Linux), а в других быстрее оказывается Mercurial. Однако под Windows как производительность, так и общий уровень поддержки, во время написания этой книги у Git гораздо хуже, чем у Mercurial. В то время как репозиторий Mercurial не требует операций по техническому обслуживанию, репозиторий Git требует частых ручных "перепаковок" собственных метаданных. Если этого не делать, производительность начинает падать, наряду с увеличением объёма занимаемого дискового пространства. Дисковый массив сервера, содержащего несколько Git репозиториев, по отношению к которым не выполняется строгое правило частой "перепаковки", рано или поздно забивается под завязку, в результате чего процесс ежедневного резервного копирования легко может занимать более 24 часов. Только что "запакованный" репозиторий Git занимает немного меньше места, чем репозиторий Mercurial, но объём не перепакованного репозитория будет на несколько порядков больше. Ядро Git написано на языке С. Многие команды Git реализованы в виде Shell скриптов или скриптов на языке Perl и уровень качества данных скриптов сильно разнится. Я встречал несколько установок, в которых скрипты тупо продолжали выполнение, несмотря на наличие фатальных ошибок. Mercurial предоставляет возможность импорта истории версий из репозитория Git. 1.6.3. CVS CVS, наверное, самая широко распространённая система контроля версий в мире. Благодаря почтенному возрасту, а также бардаку, царящему внутри, он очень слабо поддерживается уже много лет. CVS основан на централизованной, клиент/серверной архитектуре. Он не выполняет группировку файловых изменений в атомарные коммиты, тем самым позволяя людям легко "сломать билд": один человек может успешно внести часть изменений в репозиторий, а затем оказаться заблокированным из-за необходимости выполнения слияния. Это приведёт к ситуации, когда остальные участники увидят только часть из тех изменений, которые они должны были увидеть. Данная особенность также влияет на то, как вы будете работать с историей изменений. Если вы хотите получить все изменения, которые один из членов команды внёс для решения определённой задачи, вам необходимо вручную исследовать описания и дату внесения изменений, произведённых для каждого затрагиваемого файла (если вы вообще знаете, какие файлы были затронуты). CVS оперирует довольно запутанными понятиями веток и меток, которые я даже не буду пытаться описать в данной книге. Он не поддерживает переименование как файлов, так и папок, благодаря чему репозиторий может быть достаточно легко повреждён. Так как внутренние механизмы контроля целостности практически отсутствуют, зачастую даже невозможно точно утверждать, повреждён ли репозиторий, и если да, то каким образом. Таким образом я бы не стал рекомендовать CVS для использования в любом из существующих или новых проектов. Mercurial предоставляет возможность импорта истории версий CVS. Тем не менее здесь есть несколько подводных камней, с которыми также сталкиваются любые другие инструменты иморта из CVS. Отсутствие атомарных изменений и версионирования иерархических данных файловой системы приводит к невозможности абсолютно точного реконструирования истории изменений CVS, поэтому в некоторых случаях используются допущения, а переименования обычно не отображаются. Так как множество задач по администрированию CVS должны выполняться вручную, что повышает риск ошибок, обычна ситуация, когда средство для импорта из CVS возвращает множество ошибок целостности репозитория (абсолютно нереальные даты изменения версий и файлы, которые остаются заблокированными на протяжении последнего десятка лет — это лишь пара из наименее интересных проблем, которые я могу вспомнить из собственного опыта). Mercurial предоставляет возможность импорта истории версий из репозитория CVS. 1.6.4. Коммерческий инструментарий Perforce основан на централизованной, клиент/серверной архитектуре, при этом данные не кэшируются на клиентской стороне. В отличие от современных средств контроля версий, Perforce требует от пользователя запуска специальной команды, информирующей сервер о каждом файле, который человек собирается редактировать. Производительность Perforce вполне достаточна для небольших команд, но стремительно падает, если количество пользователей переваливает за пару дюжин. Умеренно большие установки Perforce требуют развёртывания прокси-серверов для распределения нагрузки, генерируемой пользователями. 1.6.5. Выбор системы контроля версий За исключением CVS, все инструменты, перечисленные выше, имеют уникальные свойства, которые делают их подходящими для определённых стилей ведения проектов. Не существует инструмента, который мог бы быть использован в любой ситуации. Например, Subversion является хорошим выбором для работы с часто изменяющимися бинарными файлами, благодаря его централизованной архитектуре и поддержке блокировок на уровне файлов. Если вы не перевариваете работу из командной строки, данный инструмент в настоящее время имеет лучший GUI, чем остальные системы контроля версий. Однако слабые возможности по слиянию ставят под вопрос его использование в интенсивно развивающихся проектах с параллельной разработкой. Лично я нахожу такие качества Mercurial, как простота, производительность и хорошая поддержка слияния, превосходным набором, который служит мне уже несколько лет. 1.7 Переход с других систем контроля версий Mercurial поставляется с расширением под названием convert, которое пошагово импортирует историю изменений из некоторых систем контроля версий. Под словом "пошагово" я подразумеваю, что вы за один раз конвертируете историю проекта до определенной даты, а позже запускаете преобразование еще раз, чтобы получить изменения, произошедшие после начального преобразования. Поддерживаются преобразование из следующих систем контроля версий: * Subversion * CVS * Git * Darcs Кроме того, "convert" может экспортировать изменения из Mercurial в Subversion. Это позволяет использовать Subversion и Mercurial параллельно, без риска потери данных. Команда “hg convert” проста в использовании. Просто укажите путь или URL исходного репозитория и имя целевого репозитория (необязательно), и она начнет работу. После начального преобразования, запустите ту же самую команду для получения новых изменений. Глава 2 Обзор Mercurial: основы 2.1 Установка Mercurial на вашей системе Прекомпилированные пакеты Mercurial доступны для каждой популярной операционной системы. Это позволяет вам начать использование Mercurial на вашем компьютере немедленно. 2.1.1 Linux В силу того, что каждый дистрибутив Linux использует свои собственные менеджеры пакетов, стратегии и темпы разработки, трудно дать подробную инструкции по установке Mercurial. Версия Mercurial, которую вы получите очень зависит от того, насколько активен тот, кто занимается созданием пакетов для вашего дистрибутива. Чтобы не усложнять процесс, я сфокусируюсь на установке Mercurial из командной строки в наиболее популярных дистрибутивах Linux. Большинство из них располагают менеждерами пакетов с графическим интерфейсом, что позволит вам установить Mercurial нажатием одной кнопки. Пакет для установки называется mercurial. * Debian apt-get install mercurial * Fedora Core yum install mercurial * Gentoo emerge mercurial * OpenSUSE yum install mercurial * Ubuntu Пакет Mercurial для Ubuntu основан на пакете для Debian. Чтобы установить его, выполните следующую команду. apt-get install mercurial Пакет Mercurial для Ubuntu выпускается с существенной задержкой по сравнению с пакетом для Debian (на время написания книги она составляет 7 месяцев), что означает, что на Ubuntu вы можете столкнуться с проблемами, которые уже решены в Debian. 2.1.2 Solaris XXX. 2.1.3 Mac OS X Lee Cantey выложил инсталлятор Mercurial для Mac по адресу http://mercurial.berkwood.com. Он предназначен для Маков и с процессорами Intel и с PowerPC. Прежде чем использовать его, вам потребуется установить соответствующую версию Universal MacPython [BI]. Это просто, просто следуйте инструкциям на его сайте. 2.1.4 Windows Lee Cantey также выложил инсталлятор и для Windows (http://mercurial.berkwood.com). У этого инсталлятора нет зависимостей, он просто работает. Примечание: Windows-версия Mercurial не осуществляет автоматическое преобразование симолов конца строки между стандартами Windows и Unix. Если вы хотите работать параллельно с пользователями Unix, то потребуется небольшая работа по конфигурированию Mercurial. 2.2 Начало работы Для начала выполним команду “hg version”, чтобы удостовериться, что Mercurial установлен правильно. Какая версия на самом деле — неважно, главное, что она вообще что-то выводит. $ hg version Mercurial Distributed SCM (version a58a611c320f) 3 Copyright (C) 2005-2008 Matt Mackall and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 2.2.1 Встроенная справка У Mercurial есть встроенная справка. Очень сложно переоценить ее наличие, особенно когда ваша работа остановилась из-за того, что вы не можете вспомнить как выполнить какую-то команду. Если Вы застопорились, просто выполните “hg help”, и на экран выведется краткий список команд с описанием назначения каждой из них. Если же вы еще и укажите конкретную команду (см. ниже), выведется подробная информация именно о ней. $ hg help init hg init [-e CMD] [--remotecmd CMD] [DEST] 3 Создает новый репозиторий в указанной директории 5 Инициализирует новый репозиторий в указанной директории. Если указанная директория не существует, то она будет создана 8 Если не указать директорию, то будет использована текущая директория 10 Поддерживается ссылки типа ssh:// в качестве целевой директории Для дополнительной информации по этой возможности смотри см. справку 'hg help urls' 14 options: 16 -e --ssh specify ssh command to use --remotecmd specify hg command to run on the remote side 19 use "hg -v help init" to show global options Для большей детализации справочной информации выполните “hg help -v”. Опция -v - сокращение от --verbose, заставит Mercurial выводить больше информации, чем обычно. 2.3 Работа с репозиторием В Mercurial все происходит внутри репозитория. Репозиторий проекта содержит все файлы, которые относятся к проекту, а также историю изменений этих файлов. В репозитории нет никакой магии, это просто каталог в файловой системе, который Mercurial обрабатывает особым образом. Вы можете переименовать или удалить репозиторий в любое время через командную строку или вашим собственным файловым менеджером. 2.3.1 Создание локальной копии репозитория Копирование репозитория кое-чем отличается. Хотя вы можете скопировать репозиторий как обычный каталог, лучше использовать встроенную команду Mercurial. Она называется “hg clone”, потому что создает идентичную копию существующего репозитория. $ hg clone http://hg.serpentine.com/tutorial/hello destination directory: hello requesting all changes adding changesets adding manifests adding file changes added 5 changesets with 5 changes to 2 files updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Если клонирование прошло успешно, то у вас должен появится каталог под названием "hello". В нем должны быть какие-то файлы. $ ls -l total 4 drwxrwxr-x 3 bos bos 4096 Aug 21 18:22 hello $ ls hello Makefile hello.c У файлов в нашем репозитории то же самое содержимое и история, как и в исходном. Каждый репозиторий Mercurial полон, самодостаточнен и независим. Он содержит свою собственную копию файлов проекта и их историю. Склонированный репозиторий помнит, откуда он был склонирован, но не общается с тем репозиторием, да и ни с каким другим тоже, до тех пор пока вы ему не скажете. Это означает, что вы можете свободно экспериментировать с вашим репозиторием. Это безопасно, потому что ваш репозиторий — "закрытая песочница", изменения в котором не повлияют ни на что, кроме него. 2.3.2 Что такое репозиторий? Когда мы более пристально присмотримся к репозиторию, мы увидим, что он содержит каталог под названием .hg. Это место, где Mercurial хранит все метаданные репозитория. $ cd hello $ ls -a . .. .hg Makefile hello.c Содержимое каталога .hg и его подкаталоги принадлежат Mercurial. Все остальные файлы и каталоги — ваши, делайте с ними что угодно. Строго говоря, каталог .hg — это и есть "настоящий" репозиторий, а все остальные файлы и каталоги рядом с ним называются рабочим каталогом. Разницу запомнить довольно просто — репозиторий содержит всю историю вашего проекта, в то время как рабочий каталог содержит "слепок" вашего проекта в определенной точке истории. 2.4 Путешествие по истории Самое первое, что вы захотите сделать с новым, неизвестным репозиторием — изучить его историю. Команда "hg log" предназначена как раз для этого. $ hg log changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments. 7 changeset: 3:0272e0d5a517 user: Bryan O'Sullivan date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. 12 changeset: 2:fef857204a0c user: Bryan O'Sullivan date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c. 17 changeset: 1:82e55d328c8c user: mpm@selenic.com date: Fri Aug 26 01:21:28 2005 -0700 summary: Create a makefile 22 changeset: 0:0a04b987be5a user: mpm@selenic.com date: Fri Aug 26 01:20:50 2005 -0700 summary: Create a standard "hello, world" program 27 По умолчанию, эта команда выводит краткую информацию о каждом изменении в проекте, которое было зафиксировано. В терминологии Mercurial мы называем эти зафиксированные события ревизией (changeset). "hg log" выводит записи со следующими полями: * changeset (ревизия). Она состоит из десятичного числа, двоеточия и строки шестнадцатеричных цифр. Это идентификаторы ревизии, а два их, потому что число короче и его проще набирать чем строку. * user (пользователь) Идентификатор человека, создавшего ревизию. Там может находится все, что угодно, но чаще это имя человека и адрес электронной почты. * date. Дата и время, когда была создана ревизия, а также часовой пояс, в котором она была создана. (Дата и время приведены относительно этого часового пояса, они указывают сколько времени было для того, кто создал ревизию) * summary. Первая строка комментария к ревизии, который оставил её автор. По умолчанию "hg log" выводит очень общие сведения, с отсутствием множества деталей. Рисунок 2.1 содержит графическое представление истории репозитория "hello", что слегка облегчает понимание того, в каком направлении развивается его история. Мы будем возвращаться к этому рисунку в этой и следующей главах. тут должна быть картинка Рисунок 2.1: Графическое представление истории репозитория "hello" 2.4.1 Изменения, ревизии и общение с другими людьми Английский язык печально известен своей небрежностью, а компьютерная наука имеет обширную историю неразберихи в терминах (зачем один термин, если можно использовать четыре). В контроле версий есть множество слов и фраз, означающих одно и то же. Если вы разговариваете об истории в Mercurial, вы обратите внимание, что слово “changeset” (набор изменений), обычно сокращается до “change”, и , при письме, до “cset”, а еще “changeset” называют “revision” (ревизия) или “rev”. Не важно, как вы называете концепцию ревизии. Идентификатор, по которому вы ссылаетесь на определенную ревизию имеет гораздо большее значение. Вспомните, что ревизия в выводе команды “hg log” идентифицируется номером и шестнадцатеричной строкой. * номер ревизии действителен только в пределах своего репозитория * в то время как шестнадцатеричная строка - постоянный, незменный идентификатор, который всегда идентифицирует одну и ту же ревизию в каждой копии репозитория. Это различие очень важно. Если вы отправите кому-нибудь письмо с упоминанием "ревизии 33", существует большая вероятность, что их ревизия 33 будет совсем другой. Причиной этого является то, что номер ревизии зависит от порядка, в котором ревизии попадают в репозиторий, и нет никакой гарантии, что одни и те же изменения произойдут в одинаковом порядке в различных репозиториях. Три изменения А, Б, В запросто могут появится в одном репозитории в порядке 0, 1, 2, а в другом — 1, 0, 2. Mercurial использует номера ревизий исключительно для удобства. Если вам нужно обсудить с кем-то конкретную ревизию, или по какой-то другой причине ссылаться на нее (в багтрекере, например), используйте шестнадцатеричный идентификатор. 2.4.2 Просмотр определенных ревизий Чтобы ограничить вывод команды “hg log” до одной ревизии, используйте опцию -r (или --rev). Вы можете использовать или номер ревизии или ее идентификатор, а также запросить ревизий столько, сколько вам захочется. $ hg log -r 3 changeset: 3:0272e0d5a517 user: Bryan O'Sullivan date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. 6 $ hg log -r 0272e0d5a517 changeset: 3:0272e0d5a517 user: Bryan O'Sullivan date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. 12 $ hg log -r 1 -r 4 changeset: 1:82e55d328c8c user: mpm@selenic.com date: Fri Aug 26 01:21:28 2005 -0700 summary: Create a makefile 18 changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments. 24 Если вы хотите увидеть историю нескольких ревизий, но не хотите перечислять их все, можно указать диапазон, как бы говоря "мне нужны все ревизии от А до Б, включительно". $ hg log -r 2:4 changeset: 2:fef857204a0c user: Bryan O'Sullivan date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c. 6 changeset: 3:0272e0d5a517 user: Bryan O'Sullivan date: Sat Aug 16 22:08:02 2008 +0200 summary: Get make to generate the final binary from a .o file. 11 changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan date: Sat Aug 16 22:16:53 2008 +0200 summary: Trim comments. 17 Mercurial учитывает порядок, в котором ревизии были указаны, так что “hg log -r 2:4” выводит ревизии 2,3,4, а “hg log -r 4:2” - 4,3,2. 2.4.3 Подробности В то время как информация, которую выводит “hg log” полезна, если вы знаете что ищете, вам может понадобиться полное описание изменений или список измененных файлов, если вы хотите узнать та ли это ревизия, что вам нужна. Команда “hg log” с аргументом -v (--verbose) предоставит вам такую возможность. $ hg log -v -r 3 changeset: 3:0272e0d5a517 user: Bryan O'Sullivan date: Sat Aug 16 22:08:02 2008 +0200 files: Makefile description: Get make to generate the final binary from a .o file. 8 9 Чтобы увидеть одновременно и описание, и изменения, используйте аргумент -p (--patch). Изменения выведутся в виде диффа (если вы не знаете, что это такое, посмотрите раздел 12.4) $ hg log -v -p -r 2 changeset: 2:fef857204a0c user: Bryan O'Sullivan date: Sat Aug 16 22:05:04 2008 +0200 files: hello.c description: Introduce a typo into hello.c. 8 9 diff -r 82e55d328c8c -r fef857204a0c hello.c --- a/hello.c Fri Aug 26 01:21:28 2005 -0700 +++ b/hello.c Sat Aug 16 22:05:04 2008 +0200 @@ -11,6 +11,6 @@ 14 int main(int argc, char ⋆⋆argv) { - printf("hello, world!∖n"); + printf("hello, world!∖"); return 0; } 21 О опциях комманд. Давайте сделаем перерыв в изучении команд Mercurial и обсудим шаблоны работы с ними; это будет вам полезно, когда мы продолжим наше турне. Mercurial имеет простой и последовательный подход в работе с опциями, которые вы можете передавать командам. Это следует из соглашений по опциям, которые являются общими для современных Linux и Unix систем. * Каждая опция имеет длинное имя. К примеру, как мы уже видели, команда "hg log" принимает параметр --rev * Также большинство опций имеют короткие имена. Вместо --rev можно использовать -r.(Причина того, что не все опции имеют короткие имена в том, что неторые из них редко используются) * Длинные опции начинаются с двух тире (как --rev), а короткие начинаются с одного (например,-r). * Имена опций и их применение в командах согласовано. Например, все команды, позволяющие указывать ID или номер ревизии принимают оба аргумента -r и --rev. В примерах в этой книге я использую короткую запись аргументов, а не длинную. Это всего лишь дело моего вкуса, так что не ищите в этом скрытого смысла. Большинство выводящих какую-то информацию команд выдадут большее количество информации, если им передать опцию -v (или --verbose), или меньшее, если передать опцию -q (или --quiet). 2.6 Изменения и просмотр изменений Так как мы уже умеем просматривать историю в Mercurial, можно заняться внесением изменений и их изучением. Первое, что необходимо сделать - изолировать эксперимент в его собственном репозитории. Для этого используется команда "hg clone", но так как у нас уже есть копия репозитория локально, то мы можем клонировать её вместо клонирования по сети. Это действие значительно быстрее, а кроме того, в большинстве случаев использует меньше места на диске. $ cd .. $ hg clone hello my-hello updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd my-hello Кстати говоря, хорошей идеей является хранение нетронутой копии удалённого репозитория, который можно использовать для создания временных клонов для получения "песочниц" для каждой задачи, над которой вам хочется поработать. Это позволяет вам работать над множеством задач одновременно, изолированных друг от друга до завершения и вашей готовности их интегрировать. Потому как локальные клоны настолько дёшевы, что их создание и уничтожение в любое удобное время практически не несёт накладных расходов. В нашем репозитории "my-hello" есть файл hello.c, который содержит классическую программу "Привет, мир". Давайте используем почтенную утилиту sed для того, чтобы добавить вторую строку к выводу программы. (sed здесь используется лишь потому, что легко записать скрипт для изменения файла таким образом. Так как у вас нету такой необходимости, вы спокойно можете использовать свой любимый текстовый редактор) $ sed -i '/printf/a∖∖tprintf("hello again!∖∖n");' hello.c Команда Mercurial "hg status" покажет, что Mercurial знает о файлах в репозитории. $ ls Makefile hello.c $ hg status M hello.c "hg status" выводит информацию не обо всех файлах, а только об изменённых: это строка, начинающаяся с буквы "M" для hello.c. Пока вы не укажете это специально, "hg status" не будет выводить информацию про файлы, которые не изменились. "M" показывает, что Mercurial оповещен о модификации hello.c. Мы не уведомляли Mercurial о том, что изменили файл ни перед, ни после окончания работы, он способен самостоятельно находить изменения. Недостаточно знать только о факте изменения файла, поэтому можно просматривать точный список изменений командой "hg diff" $ hg diff diff -r 2278160e78d4 hello.c --- a/hello.c Sat Aug 16 22:16:53 2008 +0200 +++ b/hello.c Thu Aug 21 18:22:27 2008 +0000 @@ -8,5 +8,6 @@ int main(int argc, char ⋆⋆argv) { printf("hello, world!∖"); + printf("hello again!∖n"); return 0; } 2.7 Запись изменений в новую ревизию Мы можем изменять файлы, тестировать их и использовать команды "hg status", "hg diff" для просмотра изменений, пока мы не будем удовлетворены ими и не достигнем естественной точки, когда захочется записать проделанную работу в новый набор изменений. Команда "hg commit" позволяет создать новый набор изменений, мы будем называть этот процесс "сделать коммит" или "закоммитить". 2.7.1 Установка имени пользователя В первый раз выполнение команды "hg commit" может пройти неудачно. Mercurial записывает ваше имя и адрес в каждый набор изменений, чтобы вы или другие пользователи могли связаться с автором каждого изменения. Mercurial пытается найти наиболее разумное имя пользователя для коммита. Поиск происходит в следующем порядке: 1. Если в команде "hg commit" вы указали опцию -u, с последующим именем пользователя, то оно будет обладать наивысшим приоритетом. 2. Если у вас установлена переменная окружения HGUSER, то она будет проверена. 3. Если вы создали в своей домашней директории файл .hgrc, и в нём есть директива username - она будет использована. Чтобы узнать, как должен выглядеть содержимое этого файла, смотрите главу 2.7.1 далее. 4. Если у вас установлена переменная окружения EMAIL, то будет использована она. 5. Mercurial использует локальное имя пользователя и хоста в системе, чтобы создать конечное имя. Так как часто полученное имя малополезно, то будет выведено предупреждение. Если все варианты поиска завершились неудачно, Mercurial выведет сообщение об ошибке и не позволит создать коммит, пока вы не укажете имя пользователя. Переменная HGUSER и опция -u в команде "hg commit" должны использоватся для изменения стандартного способа выбора имени. Для нормальной эксплуатации наиболее простой и надежный путь: установить имя в файле .hgrc. Подробности смотри ниже. Создание файла конфигурации Mercurial Чтобы установить имя пользователя используйте свой любимый текстовый редактор для создания файла .hgrc в домашней директории. Mercurial будет использовать этот файл для поиска персональных настроек. Первоначальное содержание этого файла должно выглядить примерно так. # This is a Mercurial configuration file. [ui] username = Firstname Lastname Строка "[ui]" обьявляет секцию конфигурационного файла. Вы можете прочитать строку "username = ..." как "установить значение переменной username в секции ui". Секции продолжаются до начала новых секций. Пустые строки и строки, начинаюшиеся с "#" игнорируются. Выбор имени пользователя. Вы можете использовать какой хотите текст в качестве имени пользователя, так как эта информация предназначена для других людей, а не для интерпретации Mercurial'ом. В примере выше для этого использовалось распространенное соглашение: комбинация имени и адреса email. Замечание: встроенный веб-сервер Mercurial обфусцирует адреса электронной почты для затруднения работы утилит сбора адресов, которые используют спамеры. Это уменьшит вероятность того, что вы станете получать больше спама, опубликовав в сети репозиторий Mercurial. 2.7.2 Описание набора изменений Когда мы публикуем изменения, Mercurial переводит нас в текстовый редактор, чтобы ввести сообщение, описывающее модификации, которые мы внесли в этом наборе изменений. Такое описание называется сообщением об изменениях (описанием изменений, описанием ревизии). Это будет записью для читателей о том, что мы сделали и почему, и будет выводиться при выполнении команды «hg log» после того, как мы закончим публикацию ревизии. $ hg commit Комментарий, который откроется в редакторе при выполнении команды "hg commit", будет содержать пустую строку и несколько строк, начинающихся с "HG:". пустая строка HG: changed hello.c Mercurial игнорирует строки, начинающиеся с «HG:»; он использует их только для того, чтобы сообщить нам, изменения в каких файлах он запишет. Редактирование или удаление этих строк ни на что не повлияет. 2.7.3 Хорошее описание набора изменений. Так как «hg log» по умолчанию выводит только первую строку описания изменений, лучше всего его написать так, что первая строка будет отделена. Вот настоящий пример сообщения об изменениях, который не следует этим указаниям, а следовательно, плохо написан. changeset: 73:584af0e231be user: Censored Person date: Tue Sep 26 21:37:07 2006 -0700 summary: include buildmeister/commondefs. Add an exports and install Для оставшейся части описания ревизии нет жестких правил. Сам Mercurial не обрабатывает и не заботится о содержимом сообщения об изменениях, хотя в вашем проекте могут быть правила, предписывающие определённое форматирование. Моё личное предпочтение — короткие, но содержательные описания набора изменений, которые сообщают мне что-либо, что я не могу выяснить при беглом взгляде на вывод команды «hg log --patch». 2.7.4 Прерывание публикации ревизии Если вы передумаете публиковать изменения во время редактирования описания ревизии, просто выйдите из своего редактора без сохранения изменяемого им файла. Это не вызовет изменений ни в репозитории, ни в рабочей директории. Выполнение команды «hg commit» без аргументов запишет все изменения, которые мы сделали и которые можно увидеть при выполнении «hg status» и «hg diff». 2.7.5 Полюбуемся на наше творение Закончив публикацию ревизии, мы можем воспользоваться командой «hg tip» для показа только что созданного набора изменений. Вывод этой команды похож на вывод команды «hg log», но отображает только последнюю версию в репозитории. $ hg tip -vp changeset: 5:9f2d6836accf tag: tip user: Брайан О'Салливан date: Thu Aug 21 18:22:27 2008 +0000 files: hello.c description: Added an extra line of output 9 10 diff -r 2278160e78d4 -r 9f2d6836accf hello.c --- a/hello.c Sat Aug 16 22:16:53 2008 +0200 +++ b/hello.c Thu Aug 21 18:22:27 2008 +0000 @@ -8,5 +8,6 @@ int main(int argc, char ⋆⋆argv) { printf("hello, world!∖"); + printf("hello again!∖n"); return 0; } 21 Мы называем последнюю ревизию конечной (или верхней) ревизией или просто «вершиной» (tip). 2.8 Распространение изменений Ранее мы упоминали, что репозитории в Mercurial самодостаточны. Это значит, что только что созданный набор изменений существует лишь в нашем репозитории my-hello. Давайте рассмотрим несколько способов, которыми мы можем распространить это изменение в другие репозитории. 2.8.1 Получение(вытягивание) изменений из другого репозитория Для начала клонируем наш исходный репозиторий hello, в котором нет последнего изменения, только что нами опубликованного. Назовём наш временный репозиторий hello-pull. $ cd .. $ hg clone hello hello-pull updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Мы будем использовать команду «hg pull», для получения изменений из my-hello в hello-pull. Однако вытягивание вслепую неизвестных изменений в репозиторий может быть пугающей перспективой. Mercurial позволяет узнать с помощью команды «hg incoming», какие изменения команда «hg pull» вытянет в репозиторий без реального применения изменений. cd hello-pull $ hg incoming ../my-hello comparing with ../my-hello searching for changes changeset: 5:9f2d6836accf tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Добавлен вывод ещё одной строки 10 (Разумеется, кто-нибудь мог успеть добавить дополнительные изменения в репозиторий, для которого мы выполнили команду «hg incoming», перед тем как нам предоставится возможность выполнить «hg pull». В таком случае мы получим изменения, которыx не ожидали.) Получение изменений в репозиторий означает лишь выполнение команды «hg pull» и указание ей, из какого репозитория следует вытягивать. $ hg tip changeset: 4:2278160e78d4 tag: tip user: Bryan O'Sullivan date: Sat Aug 16 22:16:53 2008 +0200 summary: Сокращены комментарии. 7 $ hg pull ../my-hello pulling from ../my-hello searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (run 'hg update' to get a working copy) $ hg tip changeset: 5:9f2d6836accf tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Добавлен вывод ещё одной строки 22 Как видно из вывода команды «hg tip» до и после, мы успешно вытянули изменения в наш репозиторий. Остается один шаг до того, как мы увидим изменения в рабочем каталоге. 2.8.2 Обновление рабочего каталога До сих пор мы объясняли связь между репозиторием и его рабочим каталогом. Команда «hg pull», которую мы выполняли в секции 2.8.1 получала изменения в репозиторий, но если проверить, в рабочей директории нет ни следа этих изменений. Это потому что «hg pull» не трогает (по умолчанию) рабочий каталог. Чтобы это произошло, мы используем команду «hg update». $ grep printf hello.c printf("hello, world!∖"); $ hg update tip 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ grep printf hello.c printf("hello, world!∖"); printf("hello again!∖n"); Может показаться странным то, что «hg pull» не обновляет рабочий каталог автоматически. На самом деле этому есть веская причина: вы можете использовать «hg update», для обновления рабочей директории до состояния, в котором она была в любой ревизии в истории репозитория. Если вы обновили рабочий каталог до старой версии — например, чтобы отыскать причину ошибки, — а затем выполнили бы «hg pull», который обновил рабочий каталог автоматически до новой версии, вы были бы ужасно недовольны этим. Однако так как вытягивание с последующим обновлением является распространенным явлением, Mercurial позволяет вам совместить их передачей ключа -u команде «hg pull». hg pull -u Если вы снова посмотрите на вывод команды «hg pull» в секции 2.8.1, когда мы выполнили её без -u, вы увидите, что в её выводе было полезное напоминание о том, что нам необходимо явное действие для обновления рабочего каталога. (run 'hg update' to get a working copy) Чтобы узнать, какая ревизия у рабочего каталога, используйте команду «hg parents». $ hg parents changeset: 5:9f2d6836accf tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Added an extra line of output 7 Если вы снова взглянете на рисунок 2.1, вы увидите стрелки, соединяющие между собой каждую последующую ревизию. Вершина, из которой в каждом случае ведёт стрелка, — родитель, а та вершина, куда стрелка ведёт, — потомок. Аналогично, у рабочего каталога есть родитель; это набор изменений, который содержится в данный момент в рабочем каталоге. Чтобы обновить рабочий каталог до конкретной ревизии, передайте номер ревизии или идентификатор набора изменений команде «hg update». $ hg update 2 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg parents changeset: 2:fef857204a0c user: Bryan O'Sullivan date: Sat Aug 16 22:05:04 2008 +0200 summary: Introduce a typo into hello.c. 8 $ hg update 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Если вы опустите явное указание ревизии, «hg update» обновит до верхней ревизии, как видно в примере выше при втором вызове «hg update». 2.8.3 Передача изменений в другой репозиторий Mercurial позволяет передать изменения в другой репозиторий из репозитория, в котором мы в данный момент находимся. Как с примером «hg pull» выше, создадим временный репозиторий для передачи в него наших изменений. $ cd .. $ hg clone hello hello-push updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved Команда «hg outgoing» сообщит нам об изменениях, которые будут переданы в другой репозиторий. $ cd my-hello $ hg outgoing ../hello-push comparing with ../hello-push searching for changes changeset: 5:9f2d6836accf tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Added an extra line of output 10 И команда «hg push» вызывает настоящую передачу. $ hg push ../hello-push 2 pushing to ../hello-push 3 searching for changes 4 adding changesets 5 adding manifests 6 adding file changes 7 added 1 changesets with 1 changes to 1 files Как и «hg pull», команда «hg push» не обновляет рабочую директорию репозитория, в который передаются изменения. (В отличие от «hg pull», у «hg push» нет ключа -u, который обновлял бы рабочую директорию другого репозитория). Что же произдойдёт, если мы попробуем получить или передать изменения, а в принимающем репозитории они уже есть? Ничего особенного. $ hg push ../hello-push pushing to ../hello-push searching for changes no changes found 2.8.4 Распространение изменений по сети Команды, которые мы затронули в нескольких предыдущих разделах, не ограничиваются работой с локальными репозиториями. Любая из них работает таким же способом и через сеть; просто передайте команде URL вместо локального пути. $ hg outgoing http://hg.serpentine.com/tutorial/hello comparing with http://hg.serpentine.com/tutorial/hello searching for changes changeset: 5:9f2d6836accf tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Added an extra line of output 9 В следующем примере мы можем увидеть, какие изменения мы могли бы передать в удалённый репозиторий, но в репозитории, очевидно, не разрешён приём от анонимных пользователей. $ hg push http://hg.serpentine.com/tutorial/hello pushing to http://hg.serpentine.com/tutorial/hello searching for changes ssl required Глава 3 Экскурсия по Mercurial: слияние результатов работы К этому моменту мы рассмотрели клонирование репозитория, внесение изменений в репозиторий и получение или передачу изменений из одного репозитория в другой. Следующим нашим шагом будет слияние изменений из независимых репозиториев. 3.1 Слияние потоков работы Слияние — это основная часть работы с инструментами распределённого контроля версий. * Элис и Боб имеют собственные репозитории проекта, над которым они вместе работают. Элис пофиксила ошибку в своем репозитории, а Боб добавил новую фичу в свой. Они хотят чтобы общий репозиторий содержал и багфикс и новую фичу. * Я часто работаю в одном проекте одновременно над несколькими разными задачами, каждая из которых изолирована в собственном репозитории. Работая таким образом частенько приходится производить слияние одной части моей работы с другой. Поскольку слияние очень частая операция, Mercurial имеет простые средства её осуществления. Давайте рассмотрим процесс слияния. Начнем с еще одного клонирования репозитория (заметили насколько часто мы это делаем?) и внесения изменений в него. $ cd .. $ hg clone hello my-new-hello updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd my-new-hello $ sed -i '/printf/i∖∖tprintf("once more, hello.∖∖n");' hello.c $ hg commit -m 'A new hello for a new day.' Теперь у нас есть две копии hello.c c разным содержимым. История изменения этих двух репозиториев тоже различается, как показано на рисунке 3.1 $ cat hello.c /* ⋆ Placed in the public domain by Bryan O'Sullivan. This program is ⋆ not covered by patents in the United States or other countries. ⋆/ 6 #include 8 int main(int argc, char ⋆⋆argv) { printf("once more, hello.∖n"); printf("hello, world!∖"); return 0; } $ cat ../my-hello/hello.c /⋆ ⋆ Placed in the public domain by Bryan O'Sullivan. This program is ⋆ not covered by patents in the United States or other countries. ⋆/ 20 #include 22 int main(int argc, char ⋆⋆argv) { printf("hello, world!∖"); printf("hello again!∖n"); return 0; } Рисунок Рисунок 3.1: Расхождение последних изменений в репозиториях my-hello и my-new-hello Мы уже знаем, что получение изменений из репозитория my-hello не изменит состояния рабочего каталога. $ hg pull ../my-hello pulling from ../my-hello searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) При этом команда "hg pull" говорит что-то о "heads". 3.1.1 Голова набора изменений Голова — это ревизия, у которой нет потомков или т.н. детей. Вершина — это голова, потому что у новейшей ревизии в репозитории нет потомков, но в репозитории может быть несколько голов. Рисунок Рисунок 3.2: Содержимое хранилища my-new-hello после получения изменений из my-hello На рисунке 3.2, показан результат получения изменений из my-hello в my-new-hello. История, уже имеющаяся в my-new-hello не затронута, но добавлена новая ревизия. По сравнению с рисунком 3.1, видно, что ID ревизии остался прежним, а номер ревизии изменён. (Это, кстати, хороший пример почему не надёжно использование номеров ревизий при обсуждении наборов изменений.) Посмотреть головы в хранилище позволяет команда "hg heads". $ hg heads changeset: 6:9f2d6836accf tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Added an extra line of output 8 changeset: 5:b332ec0ddfba user: Bryan O'Sullivan date: Thu Aug 21 18:22:29 2008 +0000 summary: A new hello for a new day. 13 3.1.2 Выполнение слияния Что произойдёт, если мы попытаемся выполнить обычную команду "hg update" для обновления до новой вершины. $ hg update abort: crosses branches (use 'hg merge' or 'hg update -C') Mercurial рассказывает нам, что команда "hg update" не выполняет слияния; она думает, что мы ожидаем слияния, и не обновит рабочий каталог, если мы не принудим её к этому. Вместо этого, мы воспользуемся командой "hg merge", чтобы выполнить слияние двух голов. $ hg merge merging hello.c 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) Рисунок Рисунок 3.3: Рабочий каталог и хранилище в процессе слияния и последующего коммита. Она обновляет рабочий каталог так, что он содержит изменения из обоих голов, что отражает вывод "hg parents" и содержимое hello.c. $ hg parents changeset: 5:b332ec0ddfba user: Bryan O'Sullivan date: Thu Aug 21 18:22:29 2008 +0000 summary: A new hello for a new day. 6 changeset: 6:9f2d6836accf tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan date: Thu Aug 21 18:22:27 2008 +0000 summary: Added an extra line of output 13 $ cat hello.c /⋆ ⋆ Placed in the public domain by Bryan O'Sullivan. This program is ⋆ not covered by patents in the United States or other countries. ⋆/ 19 #include 21 int main(int argc, char ⋆⋆argv) { printf("once more, hello.∖n"); printf("hello, world!∖"); printf("hello again!∖n"); return 0; } 3.1.3 Коммит результатов слияния Каждый раз, когда мы делаем слияние, "hg parents" показывает двух родителей пока мы не закрепим результат командой "hg commit". $ hg commit -m 'Слияние изменений' Теперь у нас есть новая главная ревизия; заметим, что обе бывшие головы теперь родители. Это те же ревизии, что раньше отображались командой "hg parents". $ hg tip changeset: 7:c35a687200ec tag: tip parent: 5:b332ec0ddfba parent: 6:9f2d6836accf user: Bryan O'Sullivan date: Thu Aug 21 18:22:29 2008 +0000 summary: Слияние изменений 9 На рисунке 3.3, вы можете увидеть представление того, что происходит с рабочим каталогом при слиянии, и как это влияет на хранилище, когда происходит коммит. Во время слияния, рабочий каталог состоял из двух родительских ревизий, и они стали родителями новой ревизии. 3.2 Слияние конфликтующих изменений Большая часть слияний проста, но иногда при слиянии возможны конфликты, когда участники изменяют одинаковые части одного и того же файла. Если оба изменения не идентичны, то произойдет конфликт и вам придется решать, как согласовать изменения во что-то связное. Рисунок Рисунок 3.4: Конфликт изменений в документе Рисунок 3.4 показывает пример конфликта. Мы начали с одной версии файла, затем сделали несколько изменений, в то время, как кто-то другой также изменял этот текст. Наша задача в разрешении конфликта изменений — решить, как должен выглядеть окончательный вариант файла. Mercurial не содержит встроенных средств обработки конфликтов. Вместо этого, он запускает внешнюю программу hgmerge. Это сценарий оболочки, связанный с Mercurial; вы можете изменить его под свои нужды. Что он делает по умолчанию, так это пытается найти один из инструментов слияния, которые могут быть установлены в вашей системе. Вначале делается попытка слияния с помощью нескольких полностью автоматических инструментов; если это не удаётся (разрешить конфликт может только человек) или нет подходящего инструмента, сценарий пытается запустить один из графических инструментов слияния. Также возможно чтобы Mercurial запускал другую программу или сценарий вместо hgmerge. Для этого нужно задать переменную окружения HGMERGE со значением имени необходимой программы. 3.2.1 Использование графического инструмента слияния Мой любимый графический инструмент слияния это kdiff3, и его я буду использовать для описания возможностей, которые являются общими для графических инструментов слияния. На рисунке 3.5 показан снимок экрана kdiff3 в работе. Выполняемое таким образом слияние называется тройственным (three-way), потому что есть три различные версии файла, интересующие нас. В инструменте сравнения верхняя часть окна поделена на три панели: Слева базовая версия файла, т.е. самая последняя версия, после которой произошло разделение на те две версии, которые мы пытаемся объединить. Посередине "наша" версия файла, содержащая наши изменения. * Справа "их" версия файла, т.е. версия из ревизии, с которой мы производим слияние. На панели снизу располагается текущий результат слияния. Наша задача — заменить весь красный текст, означающий неразрешенные конфликты, на осмысленный результат слияния "нашей" и "их" версий файла. Все четыре панели связаны друг с другом; если мы начнем прокручивать любую из них по вертикали или по горизонтали, остальные панели тоже изменятся и будут показывать соответствующие разделов файлов. Рисунок Рисунок 3.5: Использование kdiff3 для слияния разных версий файла. Для каждого конфликтующего участка файла можно выбрать для разрешения конфликта любое сочетание текстов из базовой, нашей и их версий. Мы также можем вручную отредактировать результирующий файл в любое время, если нужны дополнительные изменения. Существует множество инструментов слияния файлов, слишком много, чтобы их здесь описать. Они различаются доступностью для разных платформ и имеют свои слабые и сильные стороны. Большинство предназначены для слияния файлов, содержащих простой текст, но некоторые — для специализированных форматов (обычно XML). 3.2.2 Рабочий пример В этом примере мы воспроизведем историю модификации файла с рис. 3.4. выше. Давайте начнем с создания пустого репозитория с базовой версией нашего документа. $ cat > letter.txt < Greetings! > I am Mariam Abacha, the wife of former > Nigerian dictator Sani Abacha. > EOF $ hg add letter.txt $ hg commit -m '419 scam, first draft' Мы клонируем репозиторий и изменим файл. $ cd .. $ hg clone scam scam-cousin updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd scam-cousin $ cat > letter.txt < Greetings! > I am Shehu Musa Abacha, cousin to the former > Nigerian dictator Sani Abacha. > EOF hg commit -m '419 scam, with cousin' Добавим еще одну копию и съимитируем, будто кто-то еще сделал изменение этого файла. (Это намёк, что объединять свои же изменения — обычное дело, особенно когда вы разносите задачи по отдельным хранилищам, и вам нужно находить и разрешать конфликты между ними.) $ cd .. $ hg clone scam scam-son updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd scam-son $ cat > letter.txt < Greetings! > I am Alhaji Abba Abacha, son of the former > Nigerian dictator Sani Abacha. > EOF $ hg commit -m '419 scam, with son' Создав две разных версии файла, мы создали окружение, в котором можно будет произвести наше объединение. $ cd .. $ hg clone scam-cousin scam-merge updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd scam-merge $ hg pull -u ../scam-son pulling from ../scam-son searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) not updating, since new heads added (run 'hg heads' to see heads, 'hg merge' to merge) В данном случае я не хочу использовать стандартную программу слияния hgmerge, потому что она нарушит мою прекрасную методику автоматизированного выполнения примеров графическим интерфейсом. Вместо этого я устаановлю такое значение HGMERGE, чтобы Mercurial использовал консольную команду слияния. Она встроена во многие Unix-подобные системы. Если вы выполняете этот пример на своём компьютере, то можете себя этим не утруждать. $ export HGMERGE=merge $ hg merge merging letter.txt merge: warning: conflicts during merge merging letter.txt failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges $ cat letter.txt Greetings! <<<<<<< /tmp/tour-merge-conflictyaYI6c/scam-merge/letter.txt I am Shehu Musa Abacha, cousin to the former ======= I am Alhaji Abba Abacha, son of the former >>>>>>> /tmp/letter.txt~other.n_yttm Nigerian dictator Sani Abacha. Так как merge не может самостоятельно выбрать правильное из противоречащих изменений, она оставляет маркеры слияния в файле с конфликтами, обозначая наши и их строки, содержащие противоречие. Mercurial может определить, как завершилась merge, и если слияние не удалось, то он говорит, какие команды надо запустить, чтобы выполнить слияние по новой. Это может быть полезно, если мы запустили графическую утилиту объединения и вышли из нее, если что-то оказалось непонятно, или мы сделали ошибку. Если автоматическое или ручное объединение не удалось, то ничто не мешает нам самим поправить пострадавшие файлы и закоммитить результаты слияния: $ cat > letter.txt < Greetings! > I am Bryan O'Sullivan, no relation of the former > Nigerian dictator Sani Abacha. > EOF $ hg resolve -m letter.txt $ hg commit -m 'Send me your money' $ hg tip changeset: 3:b18b296054b6 tag: tip parent: 1:53ce87220c6a parent: 2:84ea9064200c user: Bryan O'Sullivan date: Thu Aug 21 18:22:28 2008 +0000 summary: Send me your money 16 3.3 Упрощение последовательности pull-merge-commit Процесс слияния изменений, как говорилось выше, прост, но требует выполнения последовательности из трёх команд. 1 hg pull 2 hg merge 3 hg commit -m 'Merged remote changes' В случае финального коммита вам также необходимо ввести сообщение о коммите, которое в большинстве случаев - кусок неинтересного стереотипного текста. Хорошо было бы, по возможности, сократить количество шагов. И действительно, Mercurial поставляется с расширением "fetch", которое делает именно это. Mercurial имеет гибкий механизм расширений, который позволяет расширять функциональность, оставляя ядро Mercurial небольшим и легким для использования. Некоторые расширения добавляют новые команды, которые вы можете использовать из командной строки, другие работают "за кулисами", например, расширение добавляющее возможности сервера. Расширение "fetch" добавляет новую команду, названную "hg fetch". Это расширение - комбинация команд "hg pull", "hg update" и "hg merge". Расширение начинает проталкивать изменения из необходимого репозитория в текущий. Если находятся изменения, добавляющие новую голову в репозиторий, то начинается слияние, затем коммит результатов с автоматической генерацией сообщения о закреплении. Если новые головы не были добавлены, расширение обновляет рабочую директорию с новой вершиной изменений. Подключить расширение fetch легко. Отредактируйте свой .hgrc, и либо пойдите в секцию [extensions], либо создайте секцию [extensions] . Добавьте строку, которая так и читается: “fetch”. [extensions] fetch = (Обычно, с правой стороны от “=” указывается местоположение расширения, но так как расширение fetch входит в стандартный пакет установки, Mercurial знает, где его искать.) Глава 4 За кулисами В отличии от многочисленных систем контроля версий, концепция, на которой строится Mercurial, достаточно проста, чтобы понять как в действительности работает ПО. Эти знания, безусловно, не являются необходимыми, но я считаю, что полезно иметь "мысленную модель" того, что происходит. Это понимание дает мне уверенность, что Mercurial была аккуратно разработана для безопасности и эффективности одновременно. И, что немаловажно, если для меня просто сохранить хорошее представление о том, что делает программа когда я выполняю задачу по контролю ревизии, я меньше буду удивлен её поведением. В этой главе мы сначала займемся внутренними понятиями проекта Mercurial, а затем обсудим интересные детали их реализации. 4.1 Запись истории в Mercurial 4.1.1 Отслеживание истории одного файла Когда Mercurial отслеживает изменения файла, он сохраняет историю этого файла в объекте метаданных называемом filelog. Каждая запись в filelog содержит достаточно информации чтобы восстановить одну ревизию отслеженного файла. Filelog'и хранятся в виде файлов в папке .hg/store/data. Они содержат два вида информации: данные о ревизиях и индексы, помогающие Mercurial эффективно искать ревизии. Для большого или имеющего длинную историю файла filelog хранится раздельно в файлах с данными (расширение ".d") и с индексом (расширение ".i"). Для маленьких файлов с небольшой историей ревизионные данные и индекс хранятся в едином файле с расширением ".i". Связь между файлом в рабочей директории и filelog'ом, который отслеживает его историю в хранилище, показана на рисунке 4.1. Рисунок Рисунок 4.1 Связь между файлами в рабочей директории и filelog'ом в репозитории 4.1.2 Управление отслеживаемыми файлами Для сбора и хранения информации об отслеживаемых файлах Mercurial использует структуру называемую манифест. Каждый раздел в манифесте содержит информацию о файлах, входящих в один набор изменений. В разделе записано, какие файлы присутствуют в наборе изменений, ревизия каждого файла и некоторые другие части метаданных файла. 4.1.3 Запись информации о наборах изменений Журнал изменений содержит информацию о каждом наборе изменений. Каждая ревизия записывает, кто передал изменение, комментарий набора изменений, другую связанную с этим набором информацию и манифест ревизии для дальнейшего использования 4.1.4 Зависимости между ревизиями Внутри журнала изменений, манифеста или filelog'а каждая ревизия хранит указатель на своего непосредственного родителя (или на двух родителей, если эта ревизия получена при слиянии). Как я упоминал выше, есть также отношения между ревизиями через эти структуры, и они являются иерархическими по природе. Каждому набору изменений в репозитории соответствует строго одна ревизия, хранящаяся в журнале изменений. Каждая ревизия в журнале изменений указывает на единственную ревизию в манифесте. Ревизия в манифесте указывает на единственную ревизию каждого filelog'а, отслеживающего, когда этот набор изменений был создан. Эти взаимосвязи отражены на рис. 4.2. Рисунок Рисунок 4.2: Взаимосвязь метаданных Как показывает рисунок, между ревизиями в журнале изменений, манифесте и filelog'е нет отношений вида один к одному. Если манифест не изменился между двумя наборами изменений, записи журнала изменений для этих наборов укажут на ту же самую ревизию манифеста. Если файл, который Mercurial отслеживает не изменился между двумя наборами, разделы для этого файла в двух ревизиях манифеста укажут на одну и ту же ревизию его filelog'а 4.2 Безопасное и эффективное хранилище Взаимосвязи журналов изменений, манифестов и filelog'ов обеспечены единой структурой, названной журнал ревизий (revlog). 4.2.1 Эффективное хранилище Журнал ревизий эффективно хранит ревизии используя дельта-механизм. Вместо хранения полной копии файла для каждой ревизии он содержит изменения необходимые для преобразования из старой ревизии в новую. Для многих типов файлов данных эти дельты составляют менее одного процента от размера полной копии файла. Некоторые устаревшие системы контроля ревизий могут работать только с дельтами текстовых файлов. Они должны хранить бинарные файлы как полные копии или перекодировать их в текстовое представление, но оба этих подхода слишком расточительны. Mercurial эффективно обращается с дельтами файлов с произвольным бинарным содержимым; ему не нужно рассматривать текст как нечто особое. 4.2.2 Безопасность работы Mercurial только дописывает данные в конец revlog-файла. Он никогда не изменяет секцию файла после того, как файл был записан. Это более здравый и эффективный подход чем схемы, которые должны изменить или переписать данные. Кроме того, каждое обращение Mercurial'а пишется как часть транзакции, которая может охватить много файлов. Транзакция является атомарной: или вся транзакция проходит, и все ее эффекты сразу видны читателям, или же вся транзакция не происходит. Эта гарантия атомарности означает, что, если Вы управляете двумя копиями Mercurial'а, где один читает данные и один пишет их, читатель никогда не будет видеть частично записанный результат, который мог бы запутать его. Тот факт что Mercurial оперирует только с файлами делает гарантированное выполнение транзакции более простым. Чем проще он выполняет подобные действия, тем вы можете более уверенными, что все выполнено корректно. 4.2.3 Быстрый поиск Mercurial грамотно обходит скользкое место, обычное для всех более ранних систем контроля версий - проблему неэффективного восстановления версий. Большинство систем контроля версий хранят содержимое ревизий как возрастающие серии модификаций по сравнению с моментальным снимком. Чтобы восстановить определенную ревизию, вам необходимо прочитать моментальный снимок, а затем все ревизии между ним и интересующий вас. Чем более длительную историю имеет файл, тем больше ревизий вам придется прочитать, следовательно, больше времени уйдет на восстановление отдельной ревизии. рисунок Рисунок 4.3: Моментальный снимок журнала изменений с возрастающими дельтами Нововведение в Mercurial решает эту проблему просто, но эффективно. Когда суммарное количество информации в дельте по сравнению с последним снимком превышает заданный порог, Mercurial сохраняет новый снимок (сжатый, конечно), вместо следующей дельты. Это позволяет быстро восстановить любую ревизию файла. Такой подход работает настолько хорошо, что уже был скопирован несколькими другими системами контроля версий. Рисунок 4.3 иллюстрирует идею. В разделе журнала изменений индексированного файла Mercurial сохраняет список разделов из файла с данными, который необходимо прочитать для восстановления определенной ревизии. Отступление: влияние сжатия видео Если вы знакомы со сжатием видео или когда-нибудь смотрели кабельное или спутниковое ТВ, то знаете, что в большинстве схем сжатия видео каждый кадр видео хранится как Δ от предыдущего кадра. Кроме того эти схемы используют сжатие с потерями для увеличения коэффициента сжатия, поэтому ошибки отображения накапливаются в зависимости от числа межкадровых Δ. Поскольку в видеопотоке иногда "выпадают" фреймы из-за проблем с сигналом, а также для уменьшения артефактов, внесённых процессом сжатия с потерями, кодирующие видеоустройства периодически вставляют полный кадр (называемый "ключевым кадром") в видеопоток; следующая Δ образуется уже от этого кадра. Это означает, что, если видеосигнал пропал, то он возобновится, как только будет получен следующий ключевой кадр. Кроме того, накопление ошибок кодирования перезапускается с нуля с каждым ключевым кадром. 4.2.4 Идентификация и надежная целостность Вместе с дельтами или снимками информации, журнал изменений содержит криптографический хеш представленных данных. Это затрудняет подделку содержимого ревизии и облегчает обнаружение случайной порчи данных. Хеши нужны не только для защиты от искажений - они используются и как идентификаторы ревизий. Хеши идентифицирующие наборы изменений, которые Вы видите как конечный пользователь, это хеши от ревизий журнала изменений. Хотя filelog'и и манифесты также используют хеши, Mercurial задействует их только во внутренних процессах. Mercurial проверяет правильность хешей при восстановлении ревизий файлов и при перемещении изменений из другого репозитория. При обнаружении проблемы целостности, он пожалуется и остановит текущие действия. В дополнение к эффективности поиска, использование Mercurial’ом периодических снимков делает его более устойчивым против частичного нарушения целостности данных. Если журнал ревизий частично поврежден из-за аппаратной или системной ошибки, часто возможно восстановить некоторые или большинство ревизий от неповрежденных секций журнала ревизий и перед, и после поврежденной секции. Это не было бы возможно с моделью хранения только дельты. 4.3 История ревизий, ветвление и слияние Каждый раздел в журнале ревизий Mercurial точно соотносится со своей непосредственной ревизией-предком, обычно называемой родителем. Фактически, ревизия содержит место не для одного родителя, а для двух. Mercurial использует специальный хеш, называемый “нулевым идентификатором” для обозначения “нет здесь никакого родителя”. Этот хеш - просто строка нулей. На рисунке 4.4 вы можете видеть пример общего представления структуры журнала ревизий. Filelog`и, манифесты и журналы изменений имеют такую же структуру, они отличаются только видом данных, хранящихся в дельтах и снимках. Первая ревизия в журнале (изображена внизу рисунка) имеет нулевые идентификаторы для обоих родителей. Для нормальной ревизии в слоте для одного родителя указан идентификатор ревизии-родителя, а второй слот содержит нулевой идентификатор, показывающий что у ревизии только один реальный родитель. Любые две ревизии с одинаковыми идентификаторами родителей называются ветвями. Ревизия, представляющая собой слияние между ветками имеет два нормальных идентификатора ревизий в родительских слотах. рисунок Рисунок 4.4: Общий вид структуры журнала ревизий 4.4 Рабочий каталог В рабочем каталоге Mercurial хранит точную копию файлов из репозитория в соответствии с одной из ревизий. Рабочий каталог "знает" какую из ревизий он содержит. Когда вы обновляете рабочий каталог до определенной ревизии, Mercurial просматривает соответствующую ревизию манифеста и находит там, какие файлы он отслеживал в то время, когда была создана ревизия, и какая из ревизий каждого из этих файлов была текущей. Потом он пересоздает копию каждого из этих файлов с содержимым на момент фиксации ревизии. В файле dirstate хранятся данные о рабочем каталоге - какой ревизии соответствует текущее состояние каталога и файлов, за которыми следит Mercurial. Так же, как и у ревизии в revlog есть место для двух родителей (обычная ревизия - с одним родителем, и результат слияния двух ревизий - с двумя родителями), у dirstate тоже есть слоты для двух родителей. Когда вы выполняете команду “hg update”, ревизия, которую вы обновляете сохраняется в слоте "первого родителя", а во второй слот помещается нулевое значение. Когда вы выполняете “hg merge” с другой ревизией, первый родитель остается неизменным, а вторым родителем становится ревизия с которой вы осуществляете слияние. Команда “hg parents” показывает родителей текущего состояния каталога. 4.4.1 Что происходит, когда вы фиксируете изменения (commit) Dirstate хранит информацию о родительских ревизиях не только для своих целей. Mercurial сохраняет родительские ревизии состояния рабочего каталога как родителей новой ревизии во время коммита. КАРТИНКА Рисунок 4.5: Рабочий каталог может иметь две родительские ревизии На рисунке 4.5 показано нормальное состояние рабочего каталога, с одной ревизией в качестве родителя. Эта ревизия - окончание ветки (tip) - самая последняя ревизия в репозитории, у которой нет детей. КАРТИНКА Рисунок 4.6: после коммита у рабочего каталога появляются новые родители Полезно думать о рабочем каталоге, как о "ревизии, которую я сейчас зафиксирую". Любая операция с файлами (добавление, удаление, переименование, копирование), о которой вы сообщили Mercurial, будет отражена в этой ревизии, так же как и изменение в тех файлах, состояние которых он уже отслеживает. У новой ревизии будут те же родители, что у рабочего каталога. После фиксации Mercurial обновит родителей рабочего каталога таким образом, что первым родителем будет идентификатор новой ревизии, а вторым - нулевой идентификатор. Это показано на рисунке 4.6. Mercurial не изменяет файлы в рабочем каталоге при фиксации изменений, он всего лишь изменяет dirstate, чтобы запомнить новых родителей. 4.4.2 Создание новой головы (head) Абсолютно нормально обновлять текущий каталог до ревизии, которая не является последней. Например, вы хотите знать, как выглядел ваш проект в прошлый вторник, или вы просматриваете ревизии, чтобы найти ту, в которой появилась ошибка. В подобных случаях обычное дело обновить рабочий каталог до интересующей ревизии и потом просматривать содержимое файлов в нём в том состоянии, в каком они были во время фиксации ревизии. Результат этих действий показан на рисунке 4.7 РИСУНОК Рисунок 4.7: Рабочий каталог, обновленный до ранней ревизии Что произойдет, если мы сделаем изменения в то время, как рабочий каталог обновлен до более старой ревизии, а потом зафиксируем их? Mercurial поступит точно так, как я и говорил раньше. Родители рабочего каталога станут родителями новой ревизии. У новой ревизии не будет детей, так что она станет конечной ревизией. И с этого момента в репозитории будет две ревизии без детей, мы называем их головами (head). Вы можете посмотреть на то, что получилось на рисунке 4.8 РИСУНОК Рисунок 4.8. После фиксации, сделанной в то время, как рабочий каталог был обновлен до ранней ревизии. Примечание: если вы новичок в Mercurial, то вам следует помнить о распространенной "ошибке" - использовании команды “hg pull” без аргументов. По умолчанию, “hg pull” не обновляет рабочий каталог, так что вы получаете новые ревизии в репозиторий, но каталог соответствует той же ревизии, что и перед выполнением команды. Если вы сделаете изменения и зафиксируете их, то вы создадите новую голову, потому что рабочий каталог не соответствует ни одной концевой ревизии. Я написал "ошибка" в кавычках, потому что все что вам нужно сделать, чтобы исправить эту ситуацию - это выполнить “hg merge”, а потом “hg commit”. Другими словами, такая ситуация никогда не приводит к негативным последствиям, она просто приводит людей в замешательство. Позже мы обсудим пути обхода этой ситуации, а также почему Mercurial по умолчанию ведет себя таким образом. 4.4.3 Слияние голов Когда вы выполняете команду “hg merge”, Mercurial оставляет первого родителя рабочего каталога неизменным, а вторым родителем назначает ревизию, с которой вы осуществляете слияние (см. рисунок 4.9) РИСУНОК Рисунок 4.9: Слияние двух голов Mercurial также изменяет рабочий каталог, чтобы осуществить слияние файлов из двух ревизий. Немного упрощенно процесс слияния проходит следующим образом - для каждого файла в манифестах обеих ревизий: - Если ни одна из ревизий не изменяла файл, то ничего с ним не делать - Если одна ревизия изменила файл, а другая - нет, то создать измененную копию файла в рабочем каталоге - Если одна ревизия удалила файл, а другая - нет (или тоже удалила его), то удалить файл из рабочего каталога - Если одна ревизия удалила файл, а другая изменила его, то спросить пользователя что делать - оставить измененный файл или удалить его? - Если обе ревизии изменили файл, то вызвать внешнюю программу для слияния, чтобы определить содержимое слитого файла. Эта операция может потребовать взаимодействия с пользователем. - Если одна ревизия изменила файл, а другая переименовала или скопировала файл, то удостовериться, что изменения будут перенесены в файл с новым именем На самом деле все сложнее - у слияния есть множество подводных камней, но вышеперечисленное - это основные решения, которые нужно принимать при слиянии. Как видите, большая часть из них полностью автоматизирована, и таким образом, большая часть слияний завершается автоматически, без вмешательства пользователя для разрешения конфликтов. Когда вы думаете о том, что произойдет, когда вы зафиксируете изменения после слияния, опять вспомните правило "рабочий каталог - это ревизия, которую я сейчас зафиксирую". После завершения команды “hg merge” у рабочего каталога будет два родителя, они же и станут родителями новой ревизии. Mercurial допускает проведение множественных слияний, но вы должны фиксировать результаты каждого слияния в отдельности. Это необходимо, потому что Mercurial отслеживает только двух родителей и для ревизий и для рабочего каталога. Технически возможно осуществить слияние нескольких ревизий одновременно, но велика вероятность того, что пользователь сам запутается и внесет неразбериху в результат слияния. 4.5 Другие интересные особенности дизайна В предыдущих разделах я попытался осветить наиболее важные аспекты дизайна Mercurial, чтобы продемонстрировать, что он уделяет особое внимание надежности и производительности. Хотя этим внимание к деталям не ограничивается. Есть множество других аспектов, которые лично я нахожу интересными. Я перечислю их здесь, отдельно от "крупногабаритных" особенностей, перечисленных выше. Так что если вам интересно, вы можете получить представление о объеме работ, необходимым для создания хорошо спроектированной системы. 4.5.1 Умное сжатие Когда нужно, Mercurial хранит и основные файлы и дельты в сжатой форме. Для этого он всегда пытается сжать файл или дельту, но хранит сжатую версию только в том случае, если она меньше оригинальной. Это означает, что Mercurial правильно обрабатывает ситуацию, когда сохраняется файл, который уже сжат, например, zip-архив или JPEG-изображение. Когда подобные файлы пережимаются второй раз, они обычно больше оригинала, так что Mercurial сохраняет оригинальный zip или JPEG. Дельты между ревизиями сжатых файлов часто больше, чем сами файлы, и тут Mercurial тоже поступает правильно. Если он видит, что размер такой дельты превышает размер файла, то он сохраняет сам файл, что опять же дает экономию дискового пространства по сравнению хранением только дельты файлов. Сжатие при передаче по сети При сохранении ревизий на диск, Mercurial использует алгоритм сжатия "deflate" (такой же, как и в популярном формате архивов ZIP), который сочетает хорошую скорость и приличный уровень сжатия. Однако, при передаче данных по сети, Mercurial распаковывает сжатые данные. Если при передаче данных используется протокол HTTP, Mercurial переупаковывает весь поток данных целиком, используя алгоритм сжатия, который дает более высокую степень сжатия (алгоритм Burrows-Wheeler из архиватора bzip2). Комбинация из алгоритма и упаковки потока целиком (вместо сжатия каждой ревизии по отдельности) в значительной степени сокращает количество передаваемых данных, что в результате даёт лучшую производительность практически на любых видах сетевых подключений. (если используется протокол ssh, то Mercurial не сжимает поток, потому что ssh делает это сам). 4.5.2 Порядок чтения/записи и атомартность Добавление к файлам - ещё недостаточное условие, чтобы гарантировать, что читатель не увидит частичнозаписанные данные. Если Вы ещё раз посмотрите на рисунок 4.2, ревизии в журнале изменений указывают на ревизии в манифесте, а ревизии в манифесте - на ревизии в filelog`ах. Эта иерархия является преднамеренной. При записи транзакция начинается с записи данных filelog`а и манифеста, а в журнал изменений ничего не записывается до окончания работы с этими данными. Чтение же начинается с журнала изменений, потом читается данные манифеста, а потом - данные filelog`а. С момента завершения записи в filelog и манифест и до записи в журнал изменений невозможно чтение ссылок на частично записанную ревизию манифеста из журнала изменений и на частичную ревизию filelog`а из манифеста. 4.5.3 Конкурентный доступ Порядок чтения/записи и гарантии атомарности подразумевают, что Mercurial не нуждается в блокировке репозитория при чтении данных даже если паралельно с чтением происходит запись. Это оказывает большой эффект на масштабируемость; у Вас может быть произвольное число процессов Mercurial'а, безопасно читающих данные из репозитория одновременно, независимо от того записываются ли они него в или нет. Неблокирующее чтение означает, что при использовании репозитория в многопользовательской системе вам не нужно наделять других локальных пользователей правами записи в ваш репозиторий для клонирования или вытягивания изменений из него; им будет достаточно прав чтения. (Это не общая черта среди систем контроля версий, так что не принимайте её, как очевидное! Большинство требует, чтобы читатели были в состоянии блокировать репозиторий для безопасного доступа к нему, а это требует прав записи по крайней мере для одного каталога, что, конечно, способствует всем видам противной и раздражающей безопасности и административным проблемам.) Mercurial использует блокировки, чтобы гарантировать, что только один процесс может писать в репозиторий за один раз (механизм блокировки безопасен даже для файловых систем, которые известны своей враждебностью к блокировкам, такими как NFS). Если репозиторий будет блокирован, то программа записи будет ждать некоторое время, чтобы повторить попытку, если репозиторий будет разблокирован, но если репозиторий останется блокированным слишком долго, то процесс, пытающийся написать, будет остановлен по таймауту через некоторое время. Это означает, что Ваши ежедневные автоматизированные скрипты не будут застревать навсегда и накапливаться, если в системе незамеченный сбой, например. (Да, время ожидания настраивается, от нуля до бесконечности.) Безопасный доступ к файлу dirstate Как и в случае с ревизией данных, Mercurial не блокирует файл dirstate для чтения; файл блокируются только для записи. Чтобы избежать чтения частично записанной копии файла dirstate, Mercurial пишет в файл с уникальным именем в том же каталоге, что dirstate, а затем автоматически переименовывает временный файл в dirstate. Файл с именем dirstate, таким образом, гарантированно будет полным, а не частично сохраненным. 4.5.4 Предотвращение поиска секторов Критичным для производительности Mercurial является предотвращение поиска сектора головкой диска, поскольку любой поиск требует гораздо больше накладных расходов, чем длительная операция чтения Вот почему, например, dirstate сохраняется в едином файле. Если бы этот файл сохранялся в каждом подкаталоге структуры, обрабатываемой Mercurial, операцию поиска пришлось бы делать по разу на каждый подкаталог. Вместо этого Mercurial читает единственный цельный файл dirstate за один шаг. Mercurial также использует схему "копирование при записи", когда клонирует репозитарий в локальное хранилище. Вместо копирования каждого файла revlog из старого репозитария в новый создается "жесткая ссылка", которая является стенографическим способом сказать: "эти два имени указывают на один и тот же файл". Когда Mercurial готовится изменять один из файлов revlog, он проверяет, нет ли у этого файла нескольких имен. Если есть, - это значит, что больше одного репозитария используют данный файл, и Mercurial создает новую копию этого файла, приватную для данного репозитария. Некоторые разработчики revision control отмечают, что эта идея по созданию полной частной копии файла не слишком эффективна с точки зрения использования свободного места. Хотя это и правда, хранение дешево, и этот метод дает наилучшую производительность вычислений на большинстве операционных систем. Альтернативные схемы, вероятнее всего, уменьшат производительность и увеличат сложность приложения, а обе эти характеристики весьма важны для "ощущений" от повседневного использования. 4.5.5 Другое содержимое dirstate Поскольку Mercurial не требует от Вас сообщать ему, когда Вы изменили файл, он использует dirstate для сохранения некоторой дополнительной информации, помогающей ему эффективно определять, что файл изменен. Для каждого файла в рабочем каталоге сохранятся время последнего изменения, и размер файла в этот момент. Когда вы явным образом производите операции “hg add”, “hg remove”, “hg rename” или “hg copy” с файлами, Mercurial обновляет dirstate и таким образом знает, что необходимо делать с этими файлами, когда Вы фиксируете изменения (commit). Когда Mercurial проверяет статус файлов в рабочем каталоге, в первую очередь проверяется время изменения файла. Если этот параметр не изменился, файл не должен быть изменен. Если поменялся размер файла - значит, файл должен быть изменен. Если поменялось время модификации, а размер не изменился - только тогда Mercurial должен будет прочесть реальное содержимое файла, чтобы посмотреть, не изменилось ли оно. Сохранение этих небольших фрагментов информации поразительно уменьшает объем данных, которые Mercurial должен читать, что приводит к большому увеличению производительности по сравнению с другими системами контроля версий. Глава 5 Повседневное использование Mercurial 5.1 Указание Mercurial, какие файлы необходимо отслеживать Mercurial не работает с файлами в хранилище, пока вы не скажете ему, чтобы он управлял ими. Команда "hg status" покажет о каких файлах Mercurial не знает, он использует "?" для отображения таких файлов. Чтобы сказать Mercurial отслеживать файлы, используйте команду "hg add". После того, как вы добавили файл, запись в результатах "hg status" для этого файла изменится с "?" на "A". $ hg init add-example $ cd add-example $ echo a > a $ hg status ? a $ hg add a $ hg status A a $ hg commit -m 'Added one file' $ hg status После запуска “hg commit” файлы, которые вы добавили перед фиксацией не будут отображаться в выводе “hg status”. Дело в том, что “hg status” по умолчанию сообщает только о "интересных" файлах - о тех, что вы модифицировали, либо указали Mercurial'у сделать что-либо с ними. Если ваш репозиторий содержит тысячи файлов, то вам редко понадобится информация обо всех не измененных файлах, которые отслеживает Mercurial. (Вы можете узнать и о них; мы вернемся к этому позже.) Когда вы добавляете файл, Mercurial сразу ничего с ним не делает. Только после фиксации Mercurial сделает снимок состояния файла. Он будет продолжать отслеживать изменения каждый раз после фиксации, до тех пор пока файл не будет удален. 5.1.1 Явное и неявное именование файлов При выполнении любой команды, если вы не указываете имя файла, Mercurial понимает это как "Я собираюсь работать со всеми файлами в этом каталоге и его подкаталогах". $ mkdir b $ echo b > b/b $ echo c > b/c $ mkdir b/d $ echo d > b/d/d $ hg add b 7 adding b/b 8 adding b/c 9 adding b/d/d $ hg commit -m 'Added all files in subdirectory' Обратите внимание, что в данном примере в отличие от предыдущего Mercurial вывел имена добавленных файлов. Когда, как в предыдущем случае, мы явно указываем имя добавляемого файла в командной строке, Mercurial делает предположение, что вы знаете, что делаете, и не выводит ничего. Однако, когда мы подразумеваем файлы, указывая только имя каталога, Mercurial дополнительно выводит имя каждого обработанного файла. Это делает процесс более прозрачным и уменьшает вероятность незаметных неприятных сюрпризов. Такое поведение свойственно большинству команд Mercurial. 5.1.2 Mercurial отслеживает файлы, а не каталоги Mercurial не отслеживает информацию о каталогах. Вместо этого он отслеживает путь к файлу. Перед созданием файла он создает недостающие каталоги пути. После удаления - удаляет все пустые каталоги, которые присутствовали в пути файла. Кажется в этом нет ничего особенного, но имеется незначительное следствие: в Mercurial невозможно содержать пустой каталог. Пустые каталоги нужны не часто, и есть несколько вариантов обойти ограничение, чтобы достичь желаемого эффекта. Разработчики Mercurial полагали, что усложнения, которые потребуются для поддержки управления пустыми каталогами, не стоят незначительных преимуществ данной опции. Если вам требуется пустой каталог в репозитории, есть несколько путей сделать это. Первый - создать каталог и добавить в него "hg add" скрытый ("hidden") файл. В Unix-подобных системах любой файл, имя которого начинается с точки ("."), рассматривается как скрытый большинством команд и инструментами GUI. Этот метод представлен на рисунке 5.1. $ hg init hidden-example $ cd hidden-example $ mkdir empty $ touch empty/.hidden $ hg add empty/.hidden $ hg commit -m 'Manage an empty-looking directory' $ ls empty $ cd .. $ hg clone hidden-example tmp updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ ls tmp empty $ ls tmp/empty Рисунок 5.1. Имитация пустого каталога с помощью скрытого файла Другой вариант удовлетворить потребность в пустой директории - просто создать необходимый каталог автоматически с помощью скриптов. 5.2 Как прекратить отслеживание файла Когда файл больше не нужен в репозитории, используйте команду "hg remove", которая удаляет файл и указывает Mercurial прекратить его отслеживание. Удаленный файл в выводе “hg status” отображается буквой "R". $ hg init remove-example $ cd remove-example $ echo a > a $ mkdir b $ echo b > b/b $ hg add a b adding b/b $ hg commit -m 'Small example for file removal' $ hg remove a $ hg status R a $ hg remove b removing b/b После выполнения “hg remove” над файлом Mercurial больше не отслеживает его изменения даже если вы пересоздадите файл с таким же именем в этом каталоге. Если вы создали одноименный файл и хотите, чтобы Mercurial отслеживал новый файл, просто выполните “hg add” с ним. Mercurial будет знать что вновь добавленный файл никак не связан со старым одноименным файлом. 5.2.1. Удаление файла не влияет на его историю Важно понимать, что удаление файла, имеет только два результата. * Удаляется текущая версия файла из рабочего каталога. *Mercurial прекращает отслеживать изменения над файлом со следующей фиксации (commit'а) Удаление файла в любом случае не меняет историю его изменений. Если вы обновите рабочий каталог до версии, в которой удаленный файл еще отслеживался, то он появится в каталоге и будет содержать данные, которые в нем были на момент фиксации версии. Если вы обновите каталог до более поздней версии, где данный файл уже был удален, Mercurial снова удалит файл из каталога. 5.2.2 Отсутствующие файлы Mercurial считает потерянными файлы, которые вы удалили не используя “hg remove”. Отсутствующие файлы в выводе “hg status” отображаются с “!”. Команды Mercurial как правило ничего не сделают с потерянными файлами. $ hg init missing-example $ cd missing-example $ echo a > a $ hg add a $ hg commit -m 'File about to be missing' $ rm a $ hg status ! a Если в вашем репозитории есть файл, который “hg status” отображает как потерянный, и вы хотите его действительно удалить, вы можете это сделать командой “hg remove --after”. $ hg remove --after a $ hg status R a С другой стороны если вы случайно удалили файл, используйте команду “hg revert filename” чтобы его восстановить. Файл будет восстановлен в неизмененной форме. $ hg revert a $ cat a a $ hg status 5.2.3 Замечание: почему в Mercurial явно указывается удаление файла? Возможно вы удивитесь, что Mercurial требует явно указывать удаление файла. Раньше при разработке Mercurial можно было удалять файл, когда угодно. Mercurial замечал отсутствие файла автоматически при следующем запуске “hg commit” и прекращал отслеживание файла. На практике выяснилось, что это приводит к случайному незаметному удалению файлов. 5.2.4 Полезное сокращение - добавление и удаление файлов в один прием Mercurial предоставляет комбинированную команду “hg addremove”, которая добавляет неотслеживаемые файлы и помечает отсутствующие файлы как удаленные. $ hg init addremove-example $ cd addremove-example $ echo a > a $ echo b > b $ hg addremove adding a adding b Команда “hg commit”также имеет опцию -A, которая выполняет то же самое добавление-и-удаление, за которыми сразу же следует фиксация. $ echo c > c $ hg commit -A -m 'Commit with addremove' adding c 5.3 Копирование файлов Для создания копии файла Mercurial предоставляет команду “hg copy”. Когда вы копируете файл с помощью этой команды, Mercurial создает запись о том, что новый файл является копией исходного файла. Он использует такие скопированные файлы, когда вы объединяете свою работу с чьей-либо еще. 5.3.1 Поведение копии при слиянии При объединении получается, что изменения "преследуют" копии. Чтобы лучше продемонстрировать эту мысль, рассмотрим пример. Для начала возьмем обычный скромный репозиторий с одним файлом. $ hg init my-copy $ cd my-copy $ echo line > file $ hg add file $ hg commit -m 'Добавлен файл' Нам необходимо работать параллельно, и затем объединить. Поэтому давайте клонируем наш репозиторий. $ cd .. $ hg clone my-copy your-copy updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Возвращаясь к исходному репозиторию, выполним команду “hg copy”, чтобы сделать копию первого созданного файла. $ cd my-copy $ hg copy file new-file Если посмотрим вывод команды “hg status”, увидим, что скопированный файл отображается как обычный добавленный файл. $ hg status A new-file Но если мы укажем опцию -C в команде “hg status”, в выводе будет еще одна строка: файл, копия которого была сделана. $ hg status -C A new-file file $ hg commit -m 'Copied file' Теперь, вернувшись к клонированному репозиторию, сделаем параллельные изменения. Добавим строку в исходный файл. $ cd ../your-copy $ echo 'new contents' >> file $ hg commit -m 'Файл изменен' Теперь мы имеем измененный файл в этом репозитории. Когда мы подтягиваем изменения из первого репозитория и объединяем две последние ревизии (head), Mercurial переносит изменения, которые были сделаны в файле file в его копию new-file. $ hg pull ../my-copy pulling from ../my-copy searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg merge merging file and new-file to new-file 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cat new-file line new contents 5.3.2 Почему изменения следует переносить в копии? Перенос изменений исходного файла в копии может показаться понятным лишь посвященным, но в большинстве случаев это крайне желательно. Прежде всего напомним, что подобный перенос происходит только при объединении. И если вы делаете “hg copy” файла и последовательно изменяете оригинал в процессе работы, с копией ничего не происходит. Во-вторых, надо понимать, что изменения будут вноситься в копию до тех пор пока репозиторий, из которого подтягиваются изменения, не знает о существовании копии. Mercurial делает это по следующей причине. Представим, что я исправляю серьезный баг в исходнике, и фиксирую изменения. А в то же время вы решаете сделать “hg copy” файла в вашем репозитории, при этом вы ничего не знаете о баге и не видите последней фиксации, и вы начинаете колдовать со своей копией файла. Когда вы подтянули и объединили мои изменения, и Mercurial не перенес изменения в копии, ваш исходник будет содержать баг, и пока вы не вспомните и не перенесете исправление вручную, баг останется не исправленным в вашей копии файла. Благодаря автоматическому переносу зафиксированных изменений от исходного файла к копиям, Mercurial предотвращает подобные проблемы. Насколько мне известно, Mercurial единственная система контроля версий, которая подобным образом переносит изменения в копии. Когда в истории изменений есть запись о появлении копии и последующего объединения, обычно не требуется последующий перенос изменений в исходном файле в копию, и поэтому Mercurial переносит только те изменения в копию, которые были до этой точки, а не дальнейшие. 5.3.3 Как сделать, чтобы изменения не переходили в копию Если по каким-то причинам вы решили, что автоматический перенос изменений в копии не для Вас, то просто используйте обычную системную команду копирования (в Unix-подобных системах, это cp), а затем добавьте файл вручную с помощью “hg add”. Перед тем как это сделать, перечитайте раздел 5.3.2, и убедитесь, что в вашем специфическом случае это действительно не нужно. 5.3.4 Поведение команды “hg copy” При выполнении команды “hg copy” Mercurial делает копии каждого исходного файла в таком виде, в каком на текущий момент он находится в рабочем каталоге. Это означает, что если вы вносите изменения в файл, а затем делаете его копию без предварительной фиксации изменений, то копия будет содержать изменения, внесенные до момента копирования. (Мне кажется такое поведение нелогичным, поэтому я обращаю внимание здесь.) Действие команды “hg copy” подобно команде cp в Unix (для удобства вы можете использовать алиас “hg cp”). Последний аргумент - адрес назначения, все предыдущие - источники. Если вы указываете единственный файл как источник и файл назначения не существует, будет создан новый файл с этим именем. $ mkdir k $ hg copy a k $ ls k a Если в качестве адреса назначения указан каталог, Mercurial сделает копии источников в этот каталог $ mkdir d $ hg copy a b d $ ls d a b Копирование каталогов рекурсивно, и сохраняет структуру каталогов источника. $ hg copy c e copying c/a/c to e/a/c Если и источник, и назначение - каталоги, то дерево каталогов источника будет также создано в каталоге назначения. $ hg copy c d copying c/a/c to d/c/a/c Также как с командой “hg rename”, если вы создали копии вручную и хотите, чтобы Mercurial знал, что это копии, просто используйте опцию --after в команде “hg copy”. $ cp a z $ hg copy --after a z 5.4 Переименование файлов Переименование файлов обычно используется чаще, чем копирование. Я рассмотрел команду “hg copy” до команды переименования, так как по существу Mercurial воспринимает переименование так же, как копирование. Следовательно, понимание того, как ведет себя Mercurial с копиями, объяснит, чего ожидать от переименования файлов. При выполнении команды “hg rename” Mercurial копирует исходные файлы, затем удаляет их и помечает как удаленные. $ hg rename a b Команда “hg status” показывает, что новые файлы были добавлены, а файлы, с которых они были скопированы, удалены. $ hg status A b R a Так же как с результатами “hg copy” мы должны использовать опцию - C команды “hg status”, чтобы увидеть, что добавленный файл действительно отслеживается Mercurial, как исходный, уже удаленный, файл. $ hg status -C A b a R a Так же как и для команд “hg remove” и “hg copy”, вы можете указать Mercurial о переименовании после выполнения, используя опцию --after. В большинстве других случаев, поведение команды “hg rename” и ее поддерживаемых опций подобно поведению команды “hg copy”. 5.4.1 Переименование файлов и объединение изменений Поскольку переименование в Mercurial реализовано как копирование и удаление, после переименования происходит такой же перенос изменений при объединении (merge), как и после копирования. Если я изменю файл, а вы переименовали его, а затем мы объединяем наши изменения, мои модификации в файле с первоначальным именем будут перенесены в файл с новым именем. (Это кажется достаточно простым действием, однако не во всех системах контроля версий оно работает.) Если по поводу следования изменений за копией вы возможно кивнете и скажете: "Да, это может быть полезно", то, что касается следования их при переименовании, абсолютно ясно, что это действительно нужно. Без этой возможности изменения легко бы оставались осиротевшими при переименовании файлов. 5.4.2 Расходящиеся переименования и слияние Расходящиеся переименования происходят, когда два разработчика берутся за один файл - назовем его foo - в своих репозиториях. $ hg clone orig anne updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg clone orig bob updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Анна переименовывает файл в bar. $ cd anne $ hg mv foo bar $ hg ci -m 'Rename foo to bar' Тем временем Bob переименовывает его в quux. $ cd ../bob $ hg mv foo quux $ hg ci -m 'Rename foo to quux' Я расцениваю это как конфликт, потому что каждый разработчик выразил свое мнение о том, как данный файл должен называться. Как вы считаете, что должно произойти когда они объединят работу? На самом деле Mercurial всегда сохраняет оба имени при слиянии изменений, которые содержат расходящиеся переименования. # См. http://www.selenic.com/mercurial/bts/issue455 $ cd ../orig $ hg pull -u ../anne pulling from ../anne searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 1 files removed, 0 files unresolved $ hg pull ../bob pulling from ../bob searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg merge warning: detected divergent renames of foo to: bar quux 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ ls bar quux Обратите внимание, что Mercurial предупредил о наличии расходящихся переименований, но оставил на ваше усмотрение, как с ними поступить после слияния. 5.4.3 Сходящиеся переименования и слияние Другой вариант конфликта возникает, когда два человека переименовывают два разных файла и дают им одно и то же имя. В таком случае Mercurial выполняет стандартное слияние и предоставляет вам управлять им для нахождения подходящего решения. 5.4.4 Другие проблемы с именованием У Mercurial есть давнишний баг, который дает ошибку при выполнении слияния, если с одной стороны имеется некоторый файл, а с другой стороны - каталог с таким же именем. Баг задокументирован как Mercurial баг №29. $ hg init issue29 $ cd issue29 $ echo a > a $ hg ci -Ama adding a $ echo b > b $ hg ci -Amb adding b $ hg up 0 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ mkdir b $ echo b > b/b $ hg ci -Amc adding b/b created new head $ hg merge abort: Is a directory: /tmp/issue29H-RU71/issue29/b 5.5 Избавление от ошибок У Mercurial есть несколько полезных команд для восстановления после некоторых распространенных ошибок. Команда “hg revert” позволяет отменить изменения, сделанные в рабочем каталоге. Например, если вы случайно выполнили "hg add", просто запустите “hg revert”, указав имя добавленного файла, и файл больше не будет считаться добавленным для отслеживания в Mercurial. “hg revert” можно использовать и для отмены от ошибочных изменений в файле. Необходимо помнить, что команда “hg revert” действует только на изменения, которые не были фиксированы. Если вы зафиксировали изменение, но поняли, что произошла ошибка, вы по-прежнему можете ее исправить, хотя возможности будут более ограничены. Дополнительная информация о команде “hg revert” и о том, что можно сделать с зафиксированными изменениями, приведена в главе 9. Глава 6 Взаимодействие с людьми Mercurial, как полностью децентрализованный инструмент, не навязывает никакой политики взаимодействия людей друг с другом. Однако, если вы новичок в работе с распределенным контролем версий, будет полезно иметь некоторые инструменты и примеры в голове, обдумывая возможные модели рабочего процесса. 6.1 Веб-интерфейс Mercurial Mercurial имеет мощный веб-интерфейс, обеспечивающий несколько полезных возможностей. В плане интерактивного использования интерфейс позволяет просматривать один или несколько репозиториев. Вы можете просматривать историю репозитория, изменения (комментарии и различия), а также содержимое каждого каталога и файла. Также для использования человеком web-интерфейс обеспечивает RSS-канал для изменений в репозитарии. Это позволяет вам "подписаться" на репозитарий, используя вашу любимую программу для чтения новостей, и автоматически получать сообщения об активности в данном репозитарии, как только что-то произойдет. Я считаю эту возможность гораздо более удобной, чем модель подписки на почтовый список рассылки, с помощью которого будут рассылаться сообщения, поскольку это не требует дополнительной настройки со стороны владельца репозитария. Web-интерфейс также позволяет удаленным пользователям клонировать репозиторий, получать с него изменения и в случае, если сервер настроен для внесения изменений, возвращать изменения обратно. Тунельный HTTP протокол Mercurial хорошо сжимает данные и это позволяет работать даже на низкоскоростных сетевых соединенияx. Простейший способ начать использовать Web-интерфейс - использовать ваш web-браузер для посещения существующего репозитария, например, такого, как основной репозитарий Mercurial, расположенный по адресу http://www.selenic.com/repo/hg?style=gitweb Если Вам интересно обеспечить Web-интерфейс к своему собственному репозитарию, Mercurial обеспечивает два пути. Первый - использовать команду “hg serve”, что лучше всего подходит для "кратковременного" обеспечения доступа. Смотри раздел 6.4 ниже для подробного описания этой команды. Если у вас долгоживущий репозитарий, который вы хотите сделать постоянно доступным, в Mercurial встроена поддержка стандарта CGI (Common Gateway Interface), что поддерживается всеми серверами. Смотри раздел 6.6 для подробной информации по конфигурированию CGI. 6.2 Модель сотрудничества Если есть достаточно гибкий инструмент, принятие решений по поводу рабочего процесса становится задачей скорее социальной инженерии, чем технической. Mercurial накладывает немного ограничений на то, как Вы можете структурировать работу над проектом, таким образом, Вам и Вашей группе возможно создать модель, удовлетворяющую Вашим особым нуждам, и жить по этой модели. 6.2.1 Факторы, которые необходимо иметь в виду Наиболее важным аспектом любой модели, который Вы должны иметь в виду, является то, как эта модель соответствует потребностям и возможностям людей, которые будут ее использовать. Это может показаться абсолютно ясным, но даже если это так, вы все равно не можете себе позволить забыть об этом хотя бы на время. Я как-то создал модель рабочего процесса, которая казалась, была идеальной для меня, но которая вызвала значительные потрясения и беспорядки в моей команде разработчиков. Несмотря на мои попытки объяснить, почему нам необходим набор различных ветвей репозитария, и каким образом изменения должны проходить между ними, несколько членов команды восстали. Несмотря на то, что они были умные люди, они не хотели обращать внимание на ограничения, в соответствии с которыми мы действовали, или столкнуться с последствиями таких ограничений в деталях той модели, которую я защищал. Не заметайте поддающиеся предвидению социальные или технические проблемы под ковер. Какую бы схему Вы ни внедряли, вы должны планировать ошибки и проблемные сценарии. Рассмотрите добавление автоматизированных механизмов для предотвращения или быстрого восстановления после проблем, которые Вы можете предвидеть. Например, если Вы хотите иметь ветвь, в которой хранятся изменения не-для-релиза, Вы должны заранее хорошо подумать над тем, что кто-то может случайно слить эти изменения с релизом. Вы могли бы избежать этой конкретной проблемы, написав перехватчик, который будет мешать изменениям, вносимым в неподходящую ветвь кода. Неформальный подход Я не стал бы рекомендовать подход "сойдет все что угодно", как нечто продолжительное, однако он крайне прост для восприятия, и отлично работает в некоторых нестандартных ситуациях. К примеру, множество проектов представляет собой группу слабо взаимодействующих между собой участников, которые крайне редко встречаются лично. Некоторые группы стараются преодолеть возникающую в результате удаленной работы изоляцию, устраивая "спринты". Во время "спринта", участники собираются вместе в назначенном месте - конференц-зале компании или отеля - и проводят несколько дней в неотрывной разработке, разбираясь со сложными местами проектов. "Спринт" - отличное место для применения команды "hg serve", поскольку эта команда не требует никакой сложной серверной инфраструктуры. Вы можете приступить к использованию "hg serve" моментально, прочитав раздел 6.4. Вы можете просто сообщить соседу, что Вы запустили сервер, передать ему ссылку любым удобным способом, и у вас уже есть отличное средство для совместной работы. Ваш сосед может открыть полученный URL своим браузером и ознакомиться с внесенными Вами изменениями, он может воспользоваться сделанными Вами исправлениями, а может клонировать ветвь, содержащую новые возможности, и опробовать ее. Одновременно положительной и отрицательной стороной такого варианта взаимодействия является то, что только те люди, которые знают о внесенных вами изменениях, могут их увидеть. Неформальный подход просто невозможно использовать в больших коллективах, поскольку каждый участник должен отслеживать изменения в n репозиториях, чтобы получить их. Единый центральный репозиторий Для маленьких проектов, мигрирующих с централизованных систем контроля версий, возможно самым легким путем будет использование одного центрального репозитория. Это наиболее частый "кирпич" для создания более сложных структур. Каждый участник разработки начинает работу с создания локальной копии центрального репозитория. Он может получать изменения из него тогда, когда ему понадобится. В то же время некоторые (а возможно и все) разработчики имеют привилегии на добавление в репозиторий готовых к публикации изменений. В рамках этого подхода также остается возможным обмен изменениями напрямую между разработчиками, без добавления их в центральный репозиторий. К примеру, я исправил ошибку, однако я не могу гарантировать, что будучи опубликованным в центральном репозитории, мое исправление не нарушит работу кода других разработчиков, которые получат это исправление. Чтобы снизить риск возможного вреда, я могу попросить вас клонировать мой репозиторий в ваш собственный временный репозиторий, и проверить работоспособность. Это позволит нам избежать публикации потенциально небезопасных изменений до тех пор, пока они не пройдут небольшого тестирования. При таком типе работы, разработчики обычно используют протокол ssh для безопасного добавления изменений в центральный репозиторий, как это описано в разделе 6.5. Также, часто используется возможность публикации доступной только для чтения копии репозитория с помощью HTTP-сервера, используя CGI, как показано в секции 6.6. Публикация с помощью HTTP удовлетворяет потребностям людей, которые не имеют доступа на запись, и которые хотят использовать web-браузеры для просмотра истории репозитория. 6.2.4 Работа с несколькими ветвями Работа над проектами более-менее значительного размера, как правило, идет сразу на нескольких фронтах. В течение жизненного цикла, проект переживает периодические официальные релизы. После этого релиз может на некоторое время после выпуска перейти в "режим поддержки" - когда в программное обеспечение вносятся только исправления ошибок, не добавляя новых возможностей. Параллельно с этими релизами, один или несколько будущих релизов находятся в разработке. Для обозначения подобных направлений в разработке, используется термин "ветвь". Mercurial исключительно хорошо подходит для ведения нескольких похожих, но не одинаковых ветвей. Каждое "направление разработки" может храниться в своем собственном центральном репозитории, и вы можете добавлять изменения из одного в другой, когда появляется такая необходимость. Поскольку репозитории являются независимыми, нестабильные изменения в разрабатываемой ветви не повлияют на стабильную ветвь, покуда кто-нибудь не захочет объединить их. Вот как это работает на практике: Допустим, у вас есть одна "главная ветвь" на центральном сервере. $hg init main $cd main $ echo 'This is a boring feature.' > myfile $ hg commit -A -m 'We have reached an important milestone!' adding myfile Остальные участники клонируют его, делают изменения, проверяют их, и добавляют в репозиторий. Когда главная ветвь достигает состояния релиза, вы можете использовать команду "hg tag", чтобы дать постоянное имя этой ревизии. $hg tag v1.0 $hg tip changeset: 1:e5451c80cb58 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:15 2008 +0000 summary: Added tag v1.0 for changeset 66e429afbffd 8 $hg tags tip 1:e5451c80cb58 v1.0 0:66e429afbffd Теперь, скажем, произошли изменения в главной ветви. $ cd ../main $ echo 'This is exciting and new!' >> myfile $ hg commit -m 'Add a new feature' $ cat myfile This is a boring feature. This is exciting and new! Используя тег для пометки релиза, участник, клонирующий репозиторий, в любое время в последующем может воспользоваться командой “hg update” для получения точной копии рабочей папки по состоянию на момент релиза. $ cd .. $ hg clone -U main main-old $ cd main-old $ hg update v1.0 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cat myfile This is a boring feature. В дополнение к этому, сразу же после того, как основная ветка будет тегирована, кто-либо может клонировать основную ветку на сервере в новую "стабильную" ветку, также находящуюся на сервере. $ cd .. $ hg clone -rv1.0 main stable requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved Любой, кому нужно сделать изменения в стабильной ветке, может клонировать этот репозиторий, вполнить изменения, сделать коммит и передать изменения сюда. $ hg clone stable stable-fix updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd stable-fix $ echo 'This is a fix to a boring feature.' > myfile $ hg commit -m 'Fix a bug' $ hg push pushing to /tmp/branchingvNSzCm/stable searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files Поскольку репозитории Mercurial независимы, и поскольку Mercurial не осуществляет автоматически изменения, стабильная и основная ветки изолированы друг от друга. Изменения, сделанные вами в основной ветке не “просачиваются” в стабильную ветку, и обратно. Часто у вас будет возникать желание, что бы багфиксы из стабильной ветки применялись и к основной. Вместо простого переписывания исправления в основную ветку, вы можете просто выполнить pull и merge измениний стабильной ветки в основную. И Mercurial перенесет вам эти багфиксы $ cd ../main $ hg pull ../stable pulling from ../stable searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg merge merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg commit -m 'Bring in bugfix from stable branch' $ cat myfile This is a fix to a boring feature. This is exciting and new! Основная ветка все так же содержит изменения, которых нет в стабильной ветке, но она еще и содержит все исправления из стабильной ветки. А стабильная ветка продолжает оставаться незатронутой этими изменениями. 6.2.5 Ветви для новых функций Для больших проектов эффективным способом управлять изменениями будет разбиение команды на несколько меньших групп. Каждая из которых будет использовать свою собственную ветку клонированную из единой "главной" ветки, используемой для всего проекта. Люди работающие над отдельными ветками обычно хорошо изолированны от изменений в других ветках. Изображение Рисунок 6.1: Ветви для новых функций Когда отдельная функция приобретает удобоваримую форму, кто-то из команды, работавшей над данной функцией затягивает изменения из главной ветки в ветку функции и выполняет слияние, а затем заливает изменения назад в главную ветку. 6.2.6 Релиз по расписанию Некоторые проекты организованы по принципу "поезда": выпуск новой версии планируется каждые несколько месяцев, и все функции, которые завершены к "отправлению поезда", включаются в релиз. Эта модель имеет много общего с моделью "ветви для новых функций". Отличие следующее: в случае, если функция не вошла в релиз (опоздала на поезд), один из членов команды-разработчика функции вытягивает вошедшие в релиз изменения в ветку функции и производит слияние, после чего команда продолжает работать "поверх" нового релиза и, таким образом, сможет включить функцию в новый релиз. 6.2.7 Модель ядра Linux В разработке ядра Linux есть неглубокая иерархическая структура, окруженная очевидным облаком хаоса. Поскольку большинство разработчиков Linux используют git, распределенный инструмент управления версиями, подобный Mercurial, полезно описать рабочий процесс в этом окружении. Если вам понравятся идеи, подход легко переносится на другие инструменты. В центре сообщества сидит Линус Торвальдс (Linus Torvalds), создатель Linux. Он публикует единственный исходный архив, который считается "исходным" текущим деревом всего сообщества разработчиков. Любой может клонировать дерево Линуса, но он очень разборчив в выборе деревьев с которых можно вливать изменения. У Линуса есть несколько "доверенных лейтенантов". Как правило он помещает любые изменения, которые они издают, в большинстве случае даже не рассматривая их изменения. Некоторые из лейтенантов вообще являются "мейнтейнерами", отвечающими за отдельные подсистемы в пределах ядра. Если случайный разработчик ядра хочет сделать изменения в подсистему, которые они хотят внести в дерево Линуса, они должны узнать кто является мейнтейнером подсистемы и попросить его внести изменения. Если мейнтейнер рассмотрит их изменения и согласиться их взять, то он передаст их Линусу должны образом. У индивидуальных лейтенантов имеются свои собственные подходы к рассмотрению, принятию и публикации изменений, и для того чтобы решить когда передать их Линусу. В дополнение, существует несколько хорошо известных веток которые люди используют для различных целей. Например, некоторые люди обслуживают "stable" ("стабильную ветку") репозитория с несколько устаревшей версией ядра, внося критические исправления если они необходимы. Некоторые мейнтейнеры публикуют несколько деревьев: одно для экспериментальных изменений, одно для изменений которые они собираются внести в стабильные и так далее. Другие просто публикуют отдельные деревья. У этой модели есть две примечательные особенности. Первая это "только внесение". Вы должны попросить, убедить или умолять другого разработчика взять изменения у вас, потому что практически нет веток, куда могут помещать свой код больше чем один человек, и не существует способа помещать код в ветки, контролируемые кем-то еще. Вторая базируется на репутации и доверии. Если вы никому не известны, Линус вероятно проигнорирует ваши изменения и даже оставит вас без ответа. Но майнтейнер подсистемы вероятно рассмотрит их, если они пройдут его критерии пригодности. Чем более хорошие изменения вы вносите, тем более вероятно что они будут доверять вашему суждению и принимать ваши изменения. Если вы хорошо известны и поддерживаете долгое время какую-либо возможность в ветке, которые Линус еще не принял, люди с похожими интересами могут вносить ваши изменения регулярно, для того чтобы не отставать от вашей работы. Репутация и признание не распространяется на другие подсистемы или людей. Если вы будете уважаемым, но специализированным разработчиком по файловым хранилищам, и попытаетесь исправить баг в сетевой подсистеме, это скорее всего, будет рассматриваться майнтейнером этой подсистемы как исправление от незнакомца. Людям, пришедшим с более строгих проектов, и сравнивающих процесс разработки ядра Linux с места откуда они пришли, процесс разработки кажется полностью безумным. Разработка подчиняется прихотям людей; люди делают большие изменения всякий раз, как считают это нужным; и темп эволюционирования потрясает. Но все же Linux - это успешная и хорошо оцениваемая часть программного обеспечения. 6.2.8 Втягивающее против совместно-вносимого сотрудничества Постоянным источником жара в open source сообществе является вопрос, которая из двух моделей разработки лучше: та, в которой люди всегда берут изменения у других, или та, в которой множество людей вносят изменения в общий репозиторий. Обычно, сторонники модели совместного вноса используют инструменты принуждающие использовать этот метод. Если вы используете инструмент централизованного контроля версий вроде Subversion, нет возможности выбрать модель: инструмент даёт вам только совместный внос, и если вы захотите что-то ещё, вам придётся прикрутить свой собственный метод сверху (как например наложение патча вручную). Хорошая распределенная версионная система, такая как Mercurial, поддерживает обе модели. И вы и ваши коллеги можете организовывать совместную работу, основываясь на ваших нуждах и предпочтениях, а не так, как принуждают вас ваши инструменты. 6.2.9 Когда разработка сталкивается с управлением ветвлениями Единожды создав несколько распределенных репозиториев и начав распространять изменения между локальным и общим репозиториями, вы с вашей командой начнете становиться связанными, но столкнетесь с немного другой проблемой: управлением направлениями, в которых ваша команда может разом двинуться. Хотя эта тема тесно связана с тем, как ваша команда взаимодействует, она довольно глубока, чтобы заслужить собственное рассмотрение в главе 8. 6.3 Техническая сторона совместного использования Остаток от этой главы посвящен вопросу обмена данными с вашими сотрудниками. 6.4 Неофициальное распределение с “hg serve” Команда "hg serve" чудесно подходит для маленьких, тесно связанных и быстро продвигающихся групп (разработчиков). Она также предоставляет отличную возможность пощупать использование команд Mercurial через сеть. Запустите "hg serve" внутри репозитория, и через секунду она (команда) поднимет специальный HTTP сервер, принимающий подключения от любого клиента и предоставляющий данные этого репозитория, пока вы не отключите его. Любой, кто знает URL только что запущенного сервера, и способный подключиться к вашему компьютеру через сеть, может использовать браузер или Mercurial для чтения дынных этого репозитория. URL запущенного экземпляра "hg serve" на ноутбуке вероятно будет выглядеть приблизительно как http://my-laptop.local:8000/. Команда “hg serve” это не полноценный web сервер. Вы можете делать с ним только две вещи: * Разрешать людям просматривать репозиторий через обычный web браузер * Использовать родной протокол Mercurial, так что люди смогут сделать “hg clone” или “hg pull” с вашего репозитория. На практике команда “hg serve” не позволяет удаленным пользователям модифицировать ваш репозиторий. Она предназначена для использования в рамках "только чтение". Если вы начинаете знакомство с Mercurial, нет ничего чтобы препядствовало вам сделать “hg serve” чтобы дать общий доступ к вашему локальному репозиторию и использовать команды вроде “hg clone”, “hg incoming” и так далее, так, будто репозиторий находится удаленно. Это может помочь вам быстро ознакомиться с использованием команд для репозиториев, расположеных в сети. 6.4.1 Несколько важных замечаний Поскольку "hg serve" предоставляет доступ на чтение всем клиентам без идентификации, эту команду следует использовать только в окружении, где вы либо не имеете причин для беспокойства, либо имеете возможность определять, кто может подключиться к вашему хранилищу и получить из него данные. Команда "hg serve" ничего не знает о брандмауэрах, которые могут быть установлены в вашей системе или сети. Она не обнаруживает и не управляет вашими брандмауэрами. Если сторонний человек не может обратиться к запущенному экземпляру "hg serve", второе, что нужно проверить (после того, как убедитесь, что он использует правильный URL) — это конфигурация брандмауэра. По умолчанию, "hg serve" принимает входящие соединения на 8000 порт. Если другой процесс уже прослушивает порт, который вы хотите использовать, вы можете указать другой порт с помощью ключа -p. Обычно, когда "hg serve" запущена, она ничего не выводит, что может быть немного странным. Если вы хотите убедиться, что она действительно работает, и вы можете передать URL своим сотрудникам, запускайте её с ключом -v. 6.5 Использование протокола Secure Shell (ssh) Используя протокол Secure Shell (ssh), Вы можете безопасно получать и записывать изменения через сетевое соединение. Для того, что бы воспользоваться этим, Вы должны немного изменить конфигурацию на стороне клиента или сервера. Если Вы не знакомы с ssh, то это протокол, который позволяет Вам безопасно осуществлять соединение с другим компьютером. Чтобы использовать его с Mercurial, Вам нужно настроить одну или несколько пользовательских учетных записей на сервере так, чтобы удаленные пользователи могли входить в систему и исполнять команды. (Если Вы знакомы с ssh, приведенный ниже материал наверняка покажется Вам элементарным.) 6.5.1 Как читать и записывать, используя ssh URL-ы Обычно, ssh URL выглядит подобным образом: 1 ssh://bos@hg.serpentine.com:22/hg/hgbook 1. Часть “ssh://” указывает Mercurial, что нужно использовать ssh протокол. Часть “bos” представляет собой имя пользователя для подключения к серверу. Вы можете не указывать эту часть, если имя пользователя на удалённом сервере совпадает с Вашим локальным именем пользователя. 3. “hg.serpentine.com” представляет собой имя хоста для подключения. 4. “:22” указывает номер порта для подключения к серверу. По умолчанию используется порт 22, поэтому Вам нужно указывать эту часть только если используется не стандартный 22 порт. 5. Остальная часть URL представляет собой локальный путь к хранилищу на серевере. Существует множество неразберихи по прочтению ssh-URL, так как не существует стандарта его интерпретации. Некоторые программы ведут себя одним образом, другие - иначе, когда речь идет о таких путях. Это не идеальная ситуация, и она вряд ли изменится. Пожалуйста, прочтите внимательно следующие абзацы. Mercurial рассматривает путь к хранилищу на сервере относительно домашней директории удаленного пользователя. Например, если домашняя директория пользователя foo на сервере /home/foo, тогда ssh-URL, который содержит компонент пути bar (ssh://foo@myserver/bar), в действительности ссылается на директорию /home/foo/bar. Если Вы хотите указать путь относительно домашней директории другого пользователя, можете использовать путь, который начинается с тильды, за которой следует имя пользователя(назовем его otheruser), к примеру. 1 ssh://server/~otheruser/hg/repo Если Вы действительно хотите указать абсолютный путь на сервере, укажите в начале два слэша, как в примере. 1 ssh://server//absolute/path 6.5.2 Выбор ssh-клиента для Вашей системы Почти все Unix-подобные системы поставляются с предустановленным OpenSSH. Если Вы используете такую систему, запустите which ssh, что бы узнать, установлен ли ssh (он, как правило, находится в /usr/bin). В маловероятном случае, если его нету, обратитесь к документации на вашу систему, чтобы выяснить как его установить. В Windows Вам сначала придётся скачать и установить подходящий SSH-клиент. Есть несколько вариантов. * Пакет PuTTY [Tat] (автор Simon Tatham) обеспечивает полный набор команд ssh-клиента. *Если у Вас достаточно терпения, Вы можете использовать Cygwin порт OpenSSH. В любом случае Вам нужно исправить Ваш файл Mercurial.ini, что бы указать Mercurial, где искать ssh-клиент. Например, если Вы используете PuTTY, то нужно использовать команду plink для запуска ssh-клиента. 1 [ui] 2 ssh = C:\path\to\plink.exe -ssh -i "C:\path\to\my\private\key" Обратите внимание: Путь к plink не должен содержать пробелов, иначе есть вероятность, что Mercurial не сможет его запустить (поэтому сохранять в С:\ Program Files, очевидно, не лучшая идея). 6.5.3 Генерация криптографической пары (открытого и секретного ключей) Чтобы не вводить пароли каждый раз при использовании ssh-клиента, рекомендую сгенерировать криптографическую пару. На Unix-подобных системах это можно сделать командой ssh-keygen. Если Вы используете PuTTY на Windows, команда puttygen -- то, что Вам нужно. При генерации криптографической пары весьма целесообразно защитить секретный ключ с помощью парольной фразы (единственный случай, когда Вам не захочется этого делать - это когда вы используете ssh протокол для автоматизации задач) Одной только генерации пары ключей не достаточно. Вам также необходимо добавить открытый ключ к множеству авторизованных ключей пользователя, который подключается удаленно. Для серверов, которые используют OpenSSH (подавляющее большинство), это означает, что нужно добавить публичный ключ к списку в файле под названием authorized_keys в директории .ssh пользователя. На Unix-подобных системах, Ваш открытый ключ будет иметь расширение ".pub". Если Вы используете puttygen на Windows, то Вы можете сохранить открытый ключ в файл или вставить из окна puttygen в файл authorized_keys. 6.5.4 Использование агента аутентификации Аутентификационный агент - демон, который хранит парольную фразу в памяти (и она будет утеряна, когда Вы выйдете из системы). Ключевая фраза передаётся ssh-клиенту, если он запущен и опрашивает агента для получения парольной фразы. Если агент аутентификации не запущен, или не хранит необходимой парольной фразы, то пароль будет запрашиваться вский раз, когда Mercurial попробует установить соединение с сервером от Вашего имени (при отправке или получении Вами изменений). Недостатком хранения парольных фраз в агенте является возможность их восстановления хорошо подготовленными злоумышленниками. Вам решать, насколько это приемлемый для Вас риск. С другой стороны, это избавляет от многократного повторного ввода. На Unix-подобных системах агент называется ssh-agent, который запускается автоматически когда вы входите в систему. Для того, что бы агент запомнил парольную фразу в хранилище, нужно использовать команду ssh-add. Если Вы используете PuTTY на Windows, команда pageant выступает в качестве агента. Он добавляет иконку в системный трей, с помощью которой, Вы можете управлять сохраненными парольными фразами. 6.5.5 Правильная настройка сервера. Настройка ssh кропотливая, и если Вы новичек, то есть ряд вещей, которые могут пойти не так. Добавим сверху ещё и Mercurial, и появится ещё больше причин почесать затылок. Большинство потенциальных проблем возникают не на стороне клиента, а на стороне сервера. Хорошая новость заключается в том, что как только Вы получили рабочую конфигурацию, то, как правило, она будет работать неограниченное время. Перед тем, как пытаться использовать Mercurial с доступом через ssh сервер, лучше для начала убедиться в том, что вы можете нормально использовать ssh или команду putty для доступа к серверу. Если вы столкнетесь с проблемами непосредственно при использовании этих команд, то наверняка Mercurial также не будет работать. Более того, он будет скрывать проблему. Когда Вы захотите настроить работу Mercurial по ssh, то сначала убедитесь, что подключения клиента по ssh работают, и только потом беспокойтесь о том, что это проблема Mercurial. Первое, что нужно сделать, что бы убедиться в работоспособности серверной стороны - попробовать войти в систему с другой машины. Если вы не можете войти с помощью ssh или putty, полученное Вами сообщение об ошибке может дать несколько подсказок, что именно идет не так. Наиболее распространенными проблемами являются следующие: * Если получаете сообщение “connection refused” - либо не запущен демон SSH на стороне сервера, либо он не доступен из-за конфигурации межсетевого экрана, либо есть ещё какие-либо ограничения (например, в hosts.allow). * Если Вы получаете сообщение об ошибке "no route to host" - Вы ввели неправильный адрес сервера или серьёзно заблокированы межсетевым экраном, который вообще не признаёт существование этого сервера. * Если Вы получаете ошибку “permission denied” - Вы ошиблись при вводе логина на сервере, опечатались в ключевой фразе или пароле пользователя. В итоге, если у Вас возникают проблемы подключения к даемону ssh сервера, первое в чем нужно убедиться - запущен ли он вообще. На многих системах он установлен, но по-умолчанию отключен. После того, как закончили этот шаг, Вы должны убедиться в том, что брандмауэр на стороне сервера разрешает входящие соединения к порту, на котором слушает ssh даемон (обычно 22). Не беспокойтесь о других возможных ошибках, пока не проверите эти две. Если Вы используете агента аутентификации на клиентской стороне для хранения Ваших парольных фраз ключей, Вы можете подключаться к серверу без ввода парольной фразы или пароля. Если Вас спрашивают парольную фразу, есть несколько возможных причин: *Возможно, Вы забыли использовать ssh-add или pageant для сохранения парольной фразы. *Возможно, Вы ввели парольную фразу не для того ключа. Если Вас спрашивают пароль для аккаунта на сервере, есть несколько возможных причин: * Права доступа к пользовательской домашней директории или поддиректории .ssh имеет лишние разрешения. В результате этого ssh даемон не доверяет такому authorized_keys файлу. Например, право на запись для группы в домашнюю или .ssh директорию часто является причиной такого поведения. * Файл authorized_keys пользователя тоже может быть причиной проблем. Если кроме пользователя ещё кто-либо может в него писать, то ssh даемон не станет доверять такому файлу и не будет его использовать. В идеальном случае, Вы должны успешно выполнить следующую команду и получить её вывод: текущую дату и время на сервере. ssh myserver date или, для Windows: plink.exe myserver date Если на Вашем сервере при входе выполняются скрипты, выводящие баннеры и прочий мусор, даже при запуске неинтерактивных сессий, вроде приведённой выше - Вы должны избавиться от них или настроить сервер, чтобы это безобразие появлялось только при интерактивном входе. Иначе этот мусор будет, по-крайней мере, загромождать вывод Mercurial. Также он создаёт потенциальные проблемы для удалённого выполнения команд Mercurial. Mercurial пытается определять и игнорировать баннеры в неинтерактивных ssh сессиях, но не всегда успешно. (Если Вы изменяли логин-скрипты на сервере, то чтобы убедититься в их работоспособности в интерактивной сессии - проверьте код, возвращаемый командой tty -s.) Как только Вы проверите простой ssh-доступ на сервер, следующим шагом к победе будет проверка выполнения Mercurial на сервере. Следующая команда должна успешно исполниться: ssh myserver hg version или plink.exe myserver hg version и выдать что-то наподобие: "Mercurial Distributed SCM (version 1.3) Copyright (C) 2005-2009 Matt Mackall and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." Если Вы получили ошибку вместо нормального вывода команды "hg version", то это, как правило, из-за неустановленного Mercurial в /usr/bin. Не беспокойтесь, если это так, то вам не нужно этого делать. Но Вы должны проверить несколько возможных проблем. * Mercurial вообще действительно установлен на сервере? Я знаю, что звучит банально, но это стоит проверить! * Возможно в вашей командной оболочке неверно указаны пути поиска исполняемых файлов (как правило определяются переменной окружения PATH). * Может быть переменная окружения PATH у вас правильно устанавливается только при интерактивном входе в систему. Это может случиться, если вы установили путь в неверном скрипте входа. Смотрите документацию по своей командной оболочке для подробностей. * Переменная среды PYTHONPATH может потребоваться для определения пути к модулям Mercurial. Может быть она вообще не установлена; содержит неверное значение; или устанавливается только при интерактивной сессии оболочки. Если Вы успешно выполнили “hg version” по ssh соединению, то всё готово. Ваш сервер и клиент настроены. Вы можете начинать использовать Mercurial для доступа к хранилищам этого пользователя на этом сервере. При возникновении проблем на этом этапе, попробуйте использовать опцию --debug для более полного представления о происходящем. 6.5.6 Использование сжатия по ssh Mercurial не сжимает данные, передаваемые по ssh, поскольку ssh протокол сам может прозрачно это делать. Однако, по-умолчанию, ssh-клиенты не используют компрессию передаваемых данных. При работе по сети, отличной от высокоскоростной LAN, (даже по беспроводной сети), использование сжатия значительно ускоряет выполнение сетевых операций Mercurial'а. Например, кто-то измерил, что сжатие уменьшило время на клонирование через WAN достаточно большого хранилища с 51 минуты до 17 минут. Оба клиента, и ssh, и plink, понимают опцию "-C", которая включает сжатие передаваемых данных. Вы легко можете разрешить компрессию по ssh для Mercurial, отредактировав Ваш hgrc. [ui] ssh = ssh -C Если Вы используете ssh, то можете настроить постоянное использование сжатия при работе с Вашим сервером. Для этого поправьте файл .ssh/config (может не существовать), как приведено ниже. Host hg Compression yes HostName hg.example.com Здесь определён синоним, hg. При его использовании в командной строке ssh или в ssh-URL Mercurial'а, выполняется подключение к хосту hg.example.com с использованием сжатия. Это даёт Вам сразу и короткое имя для ввода, и включает компрессию, что хорошо и по-отдельности, и вместе. 6.6 Работа по HTTP с использованием CGI В зависимости от ваших целей, конфигурирование интерфейса CGI системы Mercurial может занять от нескольких мгновений до нескольких часов Мы начнем с самого простого примера и далее перейдем к более сложной конфигурации. Даже в обычных случаях вам непременно потребуется читать и корректировать конфигурацию вашего веб-сервера. Примечание: Конфигурирование веб-сервера это сложная, кропотливая деятельность, сильно зависящая от всей системы. Я не могу дать указания для всех случаев, с которыми вы столкнетесь. Пожалуйста, будьте внимательны и рассудительны в следующей части работы. Будьте готовы совершить множество ошибок и провести бессоные ночи, читая логи ошибок сервера. 6.6.1 Список для проверки конфигурации веб-сервера Прежде чем вы продолжите, обязательно уделите пару минут для проверки некоторых настроек вашей системы. 1. Имеется ли в вашей системе установленный web-сервер? Mac OS X поставляется с Apache, но во многих других операционных системах веб-серверы могут быть не установлены. 2. Если web-сервер установлен, запущен ли он? В большинстве систем, даже если он есть, он может быть отключен по умолчанию. 3. Позволяет ли конфигурация вашего сервера выполнять программы CGI в директории, где вы планируете это делать? Большинство серверов по умолчанию явно запрещают запуск CGI программ. Если ваш web-сервер не установлен или у вас нет достаточного опыта конфигурирования Apache, вам следует использовать web-сервер lighttpd вместо Apache. Apache известен за причудливость и запутаность конфигурации. Хотя lighttpd менее функционален, чем Apache, большинство из его недостатков не относятся к обслуживанию хранилищ Mercurial. И с lighttpd несомненно легче начать работу, чем c Apache. 6.6.2 Базовая конфигурация CGI Обычно в Unix-системах поддиректория web-контента пользователя имеет имя public_html и расположена в домашней директории. Файл с именем foo в этой директории будет доступен по ссылке http://www.example.com/~username/foo. Для начала работы найдите скрипт hgweb.cgi, который должен быть в вашей копии Mercurial. Если вы не можете быстро найти его локальную копию в вашей системе, просто скачайте ее с основного хранилища Mercurial по http://www.selenic.com/repo/hg/raw-file/tip/hgweb.cgi. Вам следует скопировать данный скрипт в вашу директорию public_html и задать права на исполнение. cp .../hgweb.cgi ~/public_html chmod 755 ~/public_html/hgweb.cgi Аргумент 755 команды chmod - установка прав на выполнение скрипта; он описывает, что скрипт может читаться и выполняться всеми пользователями, а прав на запись у группы и других пользователей нет. Если бы Вы оставили всем права на запись, то модуль suexec сервера Apache скорее всего отказался бы выполнять скрипт. Suexec так же требует, чтобы директория, в которой находится скрипт, не была доступна для записи другими пользователями. 1 chmod 755 ~/public_html Где могут возникнуть проблемы? Как только вы скопировали CGI скрипт, обратитесь к браузеру и попробуйте открыть ссылку http://myhostname/∼myuser/hgweb.cgi, но не падайте духом перед неудачными попытками. Вероятней всего вы не сможете открыть ссылку, и на это есть много причин. На самом деле вы спотыкаетесь на одной из ошибок, приведенных ниже, поэтому, пожалуйста, прочтите внимательно. Это те ошибки, с которыми я сталкивался в системе при запуске Fedora 7 с только что установленным Apache и пользовательским аккаунтом, который я специально создал для этого. Ваш веб-сервер может иметь настройки, запрещающие пользовательский веб-контент. При использовании Apache найдите в его конфигурационном файле директиву UserDir. Если она отсутствует, то отображение пользовательских директорий запрещено. В противном случае следующее за UserDir строковое значение определяет имя директории в хомяке пользователя, контент из которой будет отдаваться Apache'м. Например, public_html. Ваши настройки безопасности могут быть слишком ограничивающими. Web сервер должен иметь возможность доступа к вашей домашней директории и вложенные в public_html директории, а также файлам в них. Вот рецепт, чтобы помочь вам сделать соотвествующие настройки безопасности. chmod 755 ~ find ~/public_html -type d -print0 | xargs -0r chmod 755 find ~/public_html -type f -print0 | xargs -0r chmod 644 Другая возможная проблема с правами состоит в том, что вы можете получить пустое окно, когда пытаетесь загрузить скрипт. В этом случае очень похоже что ваши настройки безопасности также слишком ограничивают выполнение подсистемой запуска скриптов Apache скриптов у которых, к примеру, стоят права записи для группы или всех пользователей. Ваш web сервер может быть сконфигурирован с отключенными опциями выполнения CGI программ. Вот файл пользовательской конфигурации по умолчанию из моей ОС Fodora: AllowOverride FileInfo AuthConfig Limit Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec Order allow,deny Order allow,deny Order deny,allow Deny from all Если вы обнаружите похожую на эту группу Directory в вашей конфигурации Apache, добавьте ExecCGI в конец директивы Options, если она отсуствует и перезапустите web сервер. Если при открытии через web браузер Ваш Apache выдаёт исходный текст CGI скрипта, вместо результатов его выполнения, то Вам надо добавить или раскомментировать (если уже есть) такую директиву: 1 AddHandler cgi-script .cgi Следующая возможная проблема, которую вам придется решить, может быть красочный вывод трассировки Python, который будет говорить о том что он не может импортировать связанный с mercurial модуль. Это уже прогресс! Теперь сервер пытается выполнить CGI программу. Это ошибка вероятно произойдет, если вы проведете инсталяцию Mercurial для себя, вместо версии установки в систему.Помните что ваш web сервер запускает ваши CGI программы без каких-либо переменных среды, которые присуствуют в интерактивном сеансе. Если случилась такая ошибка, отредактируйте ваш hgweb.cgi файл и следйуте указаниям как установить внутри него правильную переменную среды PYTHONPATH. В конце-концов вы увидите другую трассировку Python, которая будет жаловаться что не может найти /path/to/repository. Отредактируйте ваш hgweb.cgi скрипт и замените в нем строку /path/to/repository на полный путь к репозиторию, который вы хотите отобразить в web сервере. В этом месте, когда вы попробуете перезагрузить web страничку, вы должны увидеть простой HTML интерфейс истории вашего репозитория. Фуф! Настройка lighttpd Чтобы быть исчерпывающим в моих экспериментах, я попробовал настроить всё более популярный lighttpd веб сервер для обслуживания того же репозитория, который я выше описывал с Apache. Я уже преодолел все проблемы выделенные с Apache, многие из которых не связаны с сервером. В результате я справедливо убедился в правильности разрешений на мои файлы и директории, и в том, что мой скрипт hgweb.cgi был надлежаще исправлен. Раз у меня работал Apache, я смог быстро заставить lighttpd обслуживать репозиторий (другими словами, даже если вы попробуете использовать lighttpd, вам придётся читать раздел про Apache). Для начала я отредактировал секцию access в его конфигурационном файле, чтобы включить модули cgi и userdir, которые по умолчанию были отключены в моей системе. Затем я добавил несколько строк в конце конфигурационного файла, чтобы настроить эти модули. 1 userdir.path = "public_html" 2 cgi.assign = ( ".cgi" => "" ) После этого lighttpd отлично запустился. Если бы я настраивал lighttpd до Apache, то почти наверняка столкнулся бы с теми же проблемами конфигурации системы, что и в Apache. Однако, я нашел, что lighttpd заметно проще конфигурировать чем Apache, несмотря на то, что я уже использовал Apache больше десяти лет, и это был мой первый опыт работы с lighttpd. 6.6.3 Настройка доступа к нескольким хранилищам с помощью одного CGI-скрипта Скрипт hgweb.cgi позволяет Вам опубликовать одно хранилище, что является досадным ограничением. Если Вы хотите опубликовать больше одного, использовав несколько копий этого скрипта с разными именами без раздражения себя, лучшим выбором будет использовать скрипт hgwebdir.cgi. Процедура настройки hgwebdir.cgi несколько сложнее чем hgweb.cgi. Для начала, Вы должны получить копию скрипта. Если у Вас его нет под рукой, можете загрузить из хранилища Mercurial по ссылке http://www.selenic.com/repo/hg/raw-file/tip/hgwebdir.cgi Вы должны копировать этот скрипт в каталог public_html и убедиться в том, что он исполняемый. cp .../hgwebdir.cgi ~/public_html chmod 755 ~/public_html ~/public_html/hgwebdir.cgi После базовой настроки попробуйте открыть http://myhostname/∼myuser/hgwebdir.cgi в Вашем браузере. Должен отображаться пустой список хранилищ. Если Вы получаете пустое окно или сообщение об ошибке, попробуйте просмотреть список потенциальных проблем в разделе 6.6.2 Скрипт hgwebdir.cgi зависит от внешнего файла конфигурации. По-умолчанию он ищет файл hgweb.config в этом же каталоге. Вы должны создать его и сделать общедоступным. Формат этого файла похож на формат "ini" файлов в Windows, который распознается в Python модулем ConfigParser [Pyt]. Самый простой способ настроить hgwebdir.cgi - использовать раздел, который называется collections. Это позволит автоматически опубликовать все хранилища в каталоге, который Вы укажите. Этот раздел должен выглядеть следующим образом: [collections] /my/root = /my/root Mercurial воспринимает это так: смотрит имя директории с правой стороны от знака "="; ищет репозитории внутри этой директории; и использует текст с левой стороны чтобы обрезать совпадающий текст в именах, фактически отображающихся в веб интерфейсе. Оставшаяся часть после обрезания пути называется "виртуальный путь". В упомянутом выше примере, если мы имеем репозиторий, чей локальный путь "/my/root/this/repo", CGI скрипт обрежет начальную часть "/my/root" в имени и опубликует репозиторий с виртуальным путём "this/repo". Если базовый URL нашего CGI скрипта "http://myhostname/∼myuser/hgwebdir.cgi", тогда полный путь для этого репозитория будет "http://myhostname/∼myuser/hgwebdir.cgi/this/repo". Если мы заменим /my/root с левой стороны этого примера на /my, тогда hgwebdir.cgi будет просто обрезать /my из имени репозитория и будет предоставлять виртуальный путь root/this/repo вместо this/repo. Скрипт hgwebdir.cgi будет рекурсивно искать в каждой из перечисленных в секции коллекций конфигурационного файла директории, но не будет рекурсивно искать в найденых репозиториях. Механизм коллекций позволяет легко публиковать множество репозиториев в манере "выстрелил и забыл". Вам нужно только один раз установить CGI скрипт и конфигурационный файл. Впоследствии вы можете публиковать и прятать репозиторий в любое время, просто помещая внутрь или убирая из дерктории, за которой наблюдает hgwebdir.cgi. Явное определение публикуемых репозиториев В дополнение в механизму коллекций, скрипт hgwebdir.cgi позволяет вам публиковать специфичный лист репозиториев. Для того сделать это создайте секцию [paths] с содержимым подобным этому: [paths] repo1 = /my/path/to/some/repo repo2 = /some/path/to/another В этом случае, виртуальный путь (показывающаяся в URL часть) расположен слева в каждом определении, тогда как путь к репозиторию находиться справа. Обратите внимание, что нет никакой зависимости между выбранным вами виртуальным путём и положением ропозитория в вашей файловой системе. Если вы хотите, то можете использовать коллекции и механизм путей одновременно в одном конфигурационном файле. Замечание: если несколько репозиториев будут иметь один и тот же виртуальный путь, hgwebdir.cgi не будет сообщать об ошибке. Вместо этого он будет вести себя непредсказуемо. 6.6.4 Загрузка исходных архивов Web интерфейс Mercurial позволяет пользователям скачивать архив любой ревизии. Этот архив будет содержать снимок рабочей директории, но не будет содержать копию данных репозитория. По умолчанию эта возможность отключена. Если вы хотите включить её, вам нужно будет добавить элемент allow_archive в секцию [web] вашего hgrc файла. 6.6.5 Опции настройки веб интерфейса Web интерфейс Mercurial (команда "hg serve", hgweb.cgi и скрипты hgwebdir.cgi) имеют несколько опций, которые вы можете настроить. Они располагаются в секции [web]. * allow_archive Определяет который (если нужно) из методов загрузки архивов поддерживает Mercurial. Если вы включите эту опцию, пользователи веб интерфейса смогут скачать архив любой версии репозитория, который они просматривают. Чтобы включить возможность загрузки архивов, этот элемент должен содержать последовательность слов, перечисленных ниже. o bz2 Архив tar, сжатый по алгоритму bzip2. Имеет наилучшую степень сжатия, но использует больше процессорного времени на сервере. o gz Архив tar, сжатый по алгоритму gzip. o zip Архив zip, сжатый используя алгоритм LZW. Этот формат имеет наихудшее сжатие, но широко используется в мире Windows. Если у вас будет пустой список или не будет записи allow_archive вообще, то эта возможность будет отключена. Ниже дан пример как включить все три поддерживаемых формата. [web] allow_archive = bz2 gz zip * allowpull Boolean. Определяет позволяет ли web интерфейс удаленным пользователям использовать команды “hg pull” и “hg clone” через HTTP. Если эта опция установлена в no или false, то только "пользовательская" часть web интерфейса доступна. * contact String. Строка свободного формата (но предпочтительно короткая) отождествляющая человека или группу создавшую репозиторий. Она обычно содежит имя и email адрес человека или листа почтовой рассылки. Эту запись обычно располагают в собственном .hg/hgrc репозитория, но так же можно расположить в глобальном hgrc, если каждый репозиторий имеет единственного мейнтейнера. * maxchanges Integer. Максимальное количество наборов изменений отображаемое на одной странице по умолчанию. * maxfiles Integer. Максимальное количество модифицированных файлов отображаемое на одной странице по умолчанию. * stripes Integer. Если web интерфейс отображает разноцветные полосы, облегчающие визуальное восприятие выводимых строк, то значение этого параметра будет означать количество строк текста в каждой цветной строке. * style. Управляет шаблоном Mercurial, использующимся для отображения web интерфейса. Mercurial поставляется с двумя web шаблонами, с именами "default" и "gitweb" (последний намного более привлекателен визуально). Вы можете также определить свой шаблон; более детально эта возможность обсуждается в главе 11. Ниже вы можете видеть как включить стиль gitweb. [web] style = gitweb * templates Path. Path содержит имя каталога в котором ищутся файлы шаблонов. По умолчанию Mercurial ищет их в директории где установлен. Если вы используете hgwebdir.cgi, вы можете поместить для удобства некоторые элементы конфигурации в секцию [web] файла hgweb.config, вместо файла hgrc. Это элементы - motd и style. Опции, специфичные для индивидуального репозитория Некоторые элементы раздела [web] должны быть помещены в локальный файл .hg/hgrc, вместо помещения их в глобальный hgrc для пользователя. * description String. Строка в свободной форме (предпочтительно небольшая), которая описывает сущьность репозитория или его содержимое. * name String. Строка с именем репозитория для использования в web интерфейсе. Она имеет больший приоритет (переопределяет) нежели имя по умолчанию, которое является последним компонентом пути репозитория. Опции, специфичные для команды “hg serve” Некоторые элементы секции [web] файла hgrc используются только с командой “hg serve”. * accesslog Path. Имя файла для аудита доступа. По умолчанию команда “hg serve” пишет свою информацию в stdout, а не в файл. Элементы лог файла пишутся в стандартном "объединенном" формате файлов, который используется почти всеми web серверами. * address String. Локальный адрес, который должен слушать web сервер в ожидании подключений к нему. По умолчанию сервер слушает все адреса. * errorlog Path. Имя файла в который будут выводиться сообщения об ошибках. По умолчанию команда “hg serve”записывает эту информацию в stdout, а не в файл. * ipv6 Boolean. Определяет использовать или нет IPv6 протокол при работе сервера. По умолчанию IPv6 не используется. * port Integer. Определяет номер порта (число) который будет слушать web сервер. По умолчанию значение равно 8000. Выбор правильного файла hgrc для добавления элементов в секцию [web]. Важно помнить, что веб сервера вроде Apache или lighttpd запускаются под пользовательским аккаунтом (UID-ом), который отличается от того, под которым работаете вы. Скрипты CGI, запускаемые вашим сервером, такие как hgweb.cgi, обычно запускаются под тем же пользователем (UID-ом). Если вы добавляете элементы в секцию [web] вашего персонального hgrc, то CGI скрипты не станут читать этот файл. Произведенные там настройки влияют только на поведение команды “hg serve”, когда вы ее используете. Чтобы заставить CGI скрипты видеть ваши параметры настроек создайте файл hgrc в домашнем каталоге пользователя под кем запускается web сервис, или добавьте эти параметры к основному системному файлу hgrc. Глава 7 Имена файлов и шаблоны совпадений Mercurial предоставляет механизмы, которые позволят вам работать с именами файлов единообразным и выразительным образом. 7.1 Простое именование файлов Mercurial использует единообразную технику "скрытую под капотом" для обработки имен файлов. Каждая команда ведет себе одинаково относительно имен файлов. Путь по которому команды работают с именами файламов следующий. Если вы явно указываете имена реальных файлов, Mercurial работает именно с теми файлами, которые вы ожидаете. $ hg add COPYING README examples/simple.py Когда вы указываете имя директории, Mercurial интерпретирует это как "сделай что либо со всеми файлами в этой директории и во вложенных директориях". Mercurial обрабатывает файлы и поддиректории в указанной директории в алфавитном порядке. Когда Mercurial дойдет до каталога, то он рекурсивно спустится в него и произведет теже действия что и выше, перед продолжением обработки в текущем каталоге. $ hg status src ? src/main.py ? src/watcher/_watcher.c ? src/watcher/watcher.py ? src/xyzzy.txt 7.2 Запуск команд без указания имен файлов Команды Mercurial, которые работают с именами файлов имеют полезные, заданные по умолчанию поведения, когда вы вызываете их без параметров имен файлов или шаблонов имен. Ожидаемое поведение зависит от того, что делает команда. Вот несколько небольших правил которые вы можете использовать, чтобы предсказать то, что вероятно сделает команда, если вы не укажите имена с которой ей работать. * Большинство команд работают со всей рабочей директорией. Так например работает команда “hg add”. * Если результат команды нельзя или очень сложно отменить, то она вынудит вас указать как минимум одно имя или шаблон (см. ниже). Например, это защитит вас от случайного удаления файлов запуском команды “hg remove” без аргументов. Легко обойти стандартное поведение если оно вас не устраивает. Если команда как правило работает с целым рабочим каталогом, вы можете ссылаться на него только в текущем каталоге или его подкаталогах, давая ему имя "." $ cd src $ hg add -n adding ../MANIFEST.in adding ../examples/performant.py adding ../setup.py adding main.py adding watcher/_watcher.c adding watcher/watcher.py adding xyzzy.txt $ hg add -n . adding main.py adding watcher/_watcher.c adding watcher/watcher.py adding xyzzy.txt Как видно выше, некоторые команды обычно выводят относительные имена от корня репозитория, даже если вы выполняете их в подкаталоге. Этаже команда напечатает имена файлов относительно вашего подкаталога, если вы дадите ей явные имена. Давайте выполним “hg status” из подкаталога и заставим ее работать со всем рабочим каталогом, печатая имена файлов относительно нашего подкаталога, передав ей вывод команды “hg root”. $ hg status A COPYING A README A examples/simple.py ? MANIFEST.in ? examples/performant.py ? setup.py ? src/main.py ? src/watcher/_watcher.c ? src/watcher/watcher.py ? src/xyzzy.txt $ hg status ‘hg root‘ A ../COPYING A ../README A ../examples/simple.py ? ../MANIFEST.in ? ../examples/performant.py ? ../setup.py ? main.py ? watcher/_watcher.c ? watcher/watcher.py ? xyzzy.txt 7.3 Информация "что произошло" К примеру команда “hg add”, использованная в предыдущем разделе илюстрирует нечто большее, чем просто помощь по командам Mercurial. Если команда воздействует на файл, который вы явно не задали в командной строке, она обычно выведет имя файла, чтобы вас не удивляли изменения. Принцип очень прост: если вы явно именуете файл в командной строке, нет никакого смысла в повторении его снова вам. Если же Mercurial работает с файлом неявно, потому что вы не указали имени, или директории, или шаблона (см. ниже), безопаснее сказать вам что произошло. Для команд, которые ведут себя так есть простой способ заставить их замочать, используя опцию -q. Вы также можете заставить их печатать имена файлов, даже если они явно заданы вами, используя опцию -v. 7.4 Использование шаблонов для указания файлов В дополнение к работе с именами файлов и директорий, Mercurial предоставляет вам возможность использования шаблонов. Шаблоны Mercurial достаточно выразительны. В Unix-подобных системах (Linux, MaxOS, и т.д.), работу по сопоставлению имен файлов шаблонам обычно выполняет интерпретатор (shell). В этих системах вы должны явно указать Mercurial, что указанное имя является шаблоном. В системе Windows, интерпретатор не раскрывает шаблоны, так что Mercurial автоматически идентифицирует имена как шаблоны, и раскрывает их для вас. Для указания шаблона вместо обычного имени в командной строке используется следующий синтаксис: 1 syntax:patternbody Таким образом, шаблон идентифицируется короткой строкой, которая говорит какой тип шаблона, следующий двоеточия, используется. Mercurial поддерживает два типа синтаксиса шаблонов. Наиболее часто употребляется синтаксис glob. Это тот же самый вид сопоставления, используемый интерпретатором Unix и также должен быть знаком пользователям командной строки Windows. Когда Mercurial выполняет автоматическое сопоставление с шаблоном в Windows, она использует glob синтаксис. Вы также можете опустить префикс "glob:" в Windows, но более безопасно включать его в опции команды тоже. Синтаксис "re" более могущественный. Он предполагает что вы указали регулярное выражение (regexps) как шаблон. Между прочим, в примерах далее обратите внимание что я делаю все возможное обрамляя все мои шаблоны в кавычки, чтобы они не раскрывались интерпретатором командной строки прежде чем их обработает Mercurial. 7.4.1 glob-шаблоны в стиле shell Этот раздел посвящен краткому обзору видов шаблонов, которые вы можете использовать в glob шаблонах. Символ "*" (звездочка) соотвествует любой строке в пределах одного каталога. $ hg add 'glob:⋆.py' adding main.py Шаблон "**" (две звездочки) соответствует любой строке, включает субкаталоги. Это не стандартный токен Unix, но он используется в некоторых популярных интерпретаторах командной строки Unix и очень полезен. 1 $ cd .. $ hg status 'glob:⋆⋆.py' A examples/simple.py A src/main.py ? examples/performant.py ? setup.py ? src/watcher/watcher.py Шаблон "?" соотвествует любому одиному символу. $ hg status 'glob:⋆⋆.?' ? src/watcher/_watcher.c Символ "[" открывает класс символов. Этот шаблон соотвествует любому одному символу в указанном классе. Класс оканчивается символом "]". Класс может содержать массивы в форме "a-f", которые раскрываются в "abcdef". $ hg status 'glob:⋆⋆[nr-t]' ? MANIFEST.in ? src/xyzzy.txt Если первый символ после "[" в классе символов является "!", это инвертирует класс, делая его соотвествующим любому символу не из класса. Символ "{" начинает группу подшаблонов, где вся группа соответствует если любой из шаблонов в ней совпадает. Символ "," разделяет подшаблоны, а символ "}" закрывает группу. $ hg status 'glob:⋆.{in,py}' ? MANIFEST.in ? setup.py Внимание ! Не забывайте что если вы хотите соответствия в каком-либо каталоге вы не должны использовать "*<любой токен>", т.к. "*" работает только с одной директорией. Вместо него используйте "**" (две звездочки). Ниже пример иллюстрирующий различия. $ hg status 'glob:*.py' ? setup.py $ hg status 'glob:**.py' A examples/simple.py A src/main.py ? examples/performant.py ? setup.py ? src/watcher/watcher.py 7.4.2 Шаблоны регулярных выражений Mercurial поддерживает теже регулярные выражения, что и язык программирования Python (используется движок regexp из Python-а). Синтаксис базируется на регулярных выражениях Perl, которые являются наиболее популярных диалектом (например он используется в Java). Я не стану обсуждать диалект regexp в Mercurial т.к. он не часто используется. В любом случае регулярные выражения Perl хорошо документированы на множестве веб сайтов и в большом количестве книг. Вместо этого я сфокусируюсь на некоторых вещах, которые вы должны знать если хотите использовать регулярные выражения в Mercurial. Регулярные выражения применяются к полному пути файла, относительно корня репозитория. Другими словами, даже если вы уже в поддиректории foo и хотите работать с файлами в поддиректории вы должны начать паттерн с "foo/". Стоит отметить одну вещь: если вы знакомы с регулярными выражениями Perl - регулярные выражения Mercurial применяются с начала строки. Таким образом, regexp ищет совпадения с начала строки и не ищет совпадения где-нибудь внутри строки. Для поиска везде в строке вы должны начинать ваше регулярное выражение с ".*" 7.5 Фильтрация файлов Mercurial не только позволяет вам указывать разными способами нужные файлы, он также дает вам возможность отсеивать ненужные файлы. Команды, работающие с именами принимают две опции для фильтрования. * -I, или --include, позволяет указать шаблон имен файлов, которые должны быть обработаны. * -X, или --exclude, дает возможность указать шаблон имен для исключения из обработки. Вы можете указывать многократно и ту и другую опции в командной строке и смешивать их так, как вам нужно. Mercurial интерпретирует шаблоны используя glob синтаксис по умолчанию (но вы можете указать префикс "re:" для использования синтаксиса regexp). Вы можете считать опцию -I filter как "обрабатывать только те файлы, которые совпадают с этим фильтром" $ hg status -I '⋆.in' ? MANIFEST.in Опцию -X filter проще всего понять как "обрабатывать только те файлы, которые не попадают под фильтр". $ hg status -X '⋆⋆.py' src ? src/watcher/_watcher.c ? src/xyzzy.txt 7.6 Игнорирование ненужных файлов и директорий XXX. 7.7 Регистрозависимость Если Вы работаете в гетерогенной среде, которая включает unix-подобные системы и Mac'и или Windows, Вы должны запомнить, что смешение регистров в именах файлов - путь к гарантированному нервному расстройству. Операционные и файловые системы отличаются по способам работы с регистром символов в именах файлов и директорий. Существует три основных модели работы с именами. * Полностью без учета регистра. Буквы в верхнем и нижнем регистрах считаются идентичными как при создании файла, так и при последующей работе с ним. Это поведение типично для файловых системы выросших из DOS. * Сохранение регистра, без его учета в дальнейшем. Когда создается файл или каталог его имя созраняется так как было введено. Оно также зачитывается операционной системой в сохраненном регистре. Когда производится работа с файлом, то регистр не учитывается. Это стандартное поведение в Windows и MaxOS. Например имена foo и FoO идентичны с этой точки зрения. Подобная обработка прописных и строчных букв как взаимозаменяемых также известна как сворачивание регистра. * С учетом регистра. В этом случае имя файла существенно всегда. Имена foo и FoO идентифицируют разные файлы. Это стандартно для Linux и Unix систем и в них это используется по умолчанию. В Unix-подобных системах возможно иметь любой или даже все вышеупомянутые способы обработки имен сразу. Например, если вы используете USB накопитель с файловой системой FAT32 в Linux. то имена файлов в этой файловой системе будут сохранять регистр, но его учет не будет производиться. 7.7.1 Безопасное и переносимое хранилище Система хранилищ Mercurial регистронезависима. Она переводит имена файлов так, что они могут быть безопасно сохранены и на регистрозависимых, и на регистронезависимых файловых системах. Это означает, что вы можете использовать обычное копирование файлов, например, на USB-флешку и спокойно переносить на ней хранилище между компьютерами, использующими MacOS, Windows и Linux. 7.7.2 Определение конфликтов регистра символов Когда операционная система работает в директории, Mercurial соблюдает политику обозначений в файловой системе где расположена директория. Если файловая система позволяет сохранять регистр, но не учитывает его, Mercurial считает имена, различные по регистру, равнозначными. Важным аспектом этого поведения является то что возможно сделать commit набора изменений в системе с учетом регистра в файловой системе (в Linux или Unix), которые вызовут проблемы у пользователей с другим типом файловой системы (Windows или MacOS). Если Linux-пользователь создаст файлы myfile.c и MyFile.C, оба будут успешно сохранены в репозитории и с ними можно будет работать на других Linux-машинах. Если пользователь Windows или Mac зальет в свой репозиторий подобные изменения, то он не получит каких-то проблем, т.к. механизм хранения в репозитории Mercurial безопасен к регистру символов. В тоже время, как только он попытается сделать “hg update” в рабочем каталоге до этого набора изменений, или “hg merge” с ним, Mercurial определит конфликт имен файлов, которые файловая система трактует как идентичные и запретит обновление или слияние. 7.7.3 Исправление конфликта регистра символов Если вы используете Windows или Mac в среде, где кто-то использует Linux или Unix, и Mercurial сообщает о проблемах с регистром, когда вы пытаетесь сделать “hg update” или “hg merge”, процедура исправления этого очень проста. Просто найдите ближайшую машину с Linux или Unix, клонируйте на нее репозиторий и используйте команду "hg rename" для изменения имени на какое-то другое, так чтобы конфликта не было. Сохраните свои изменения ("hg commit"), сделайте “hg pull” или “hg push” в ваш Windows или MacOS репозитарий и “hg update” на ревизию без конфликта. Список изменений с конфликтом регистра останется в истории вашего проекта и вы все еще будете не в состоянии выполнить “hg update” вашей рабочей директории к этому набору изменений в Windows или MacOS, но вы сможете продолжить разработку. Замечание: до версии 0.9.3 Mercurial не использует безопасный к регистру механизм хранения репозитория и не обнаруживает конфликты имен. Если вы используете старую версию Mercurial на Windows или MacOS, я настоятельно рекомендую произвести обновление. Глава 8 Управление релизами и ветками Mercurial предоставляет вам несколько механизмов для управления проектом, которые работают на множестве фронтов одновременно. Для понимания этих механизмов сперва кратко рассмотрим довольно распространенные примеры структуры программных проектов. Многие програмные проекты выпускают переодические "major" релизы, которые содержат некоторые новые существенные возможности. В тоже время они выпускают множество "minor" релизов. Они, как правило, идентичны основным релизам на которых они основаны, но исправляют ошибки. В этой главе вы поговорим о том как вести учет вех проекта, таких как релизы. Затем мы продолжим разговор о рабочем процессе между двумя разными фазами проекта и увидим как Mercurial может помочь вам изолировать и управлять этим процессом. 8.1 Задание постоянного имени для ревизии Как только вы решаете что хотели бы сделать определенную ревизию релизом - хорошая идея сделать идентифицирующую запись об этом. Это позволит вам позднее получить релиз в любое время позднее (воспроизвести баг, портировать на новую платформу и т.д.). $ hg init mytag $ cd mytag $ echo hello > myfile $ hg commit -A -m 'Initial commit' adding myfile Mercurial дает вам возможность указать постоянное имя любой ревизии используя команду “hg tag”. Не удивительно что эти имена называют "тегами". $ hg tag v1.0 Тег это ничто иное как "символическое имя" для ревизии. Теги существуют просто для вашего удобства, чтобы у вас был простой способ обратиться к ревизии. Mercurial никак не обрабатывает имена тегов, которые вы указываете. Mercurial не устанавливает ограничения на имена тегов, кроме некоторых, чтобы гарантировать что тег может быть проанализирован однозначно. Имя тега не может содержать ни один из следующих символов: * Двоеточие (ASCII 58, “:”) * Возрат каретки (ASCII 13, “\r”) * Новая строка (ASCII 10, “\n”) Вы можете использовать “hg tags” для показа тегов в вашем репозитории. В выводе каждая тегированая ревизия идентифицируется сначала по имени, затем по номеру ревизии, и потом по уникальному хешу ревизии. $ hg tags tip 1:eaa5c1c54989 v1.0 0:973bfbbf1341 Обратите внимание, что в выводе "hg tags" показан тег tip (окончание ветки). Тег tip это специальный "плавающий" тег, который всегда указывает на новейшую ревизию в репозитории. В выводе команды “hg tags” теги показаны в обратном (по номеру ревизии) порядке. Обычно это значит, что более новые теги будут показаны перед более старыми. Также это означает, что tip всегда будет показан первым в выводе "hg tags". Когда вы запускаете “hg log”, если она показывает номер ревизии, которая имеет ассоциацию с тегами, то печатаются и эти теги: $ hg log changeset: 1:eaa5c1c54989 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:24 2008 +0000 summary: Added tag v1.0 for changeset 973bfbbf1341 7 changeset: 0:973bfbbf1341 tag: v1.0 user: Bryan O'Sullivan date: Thu Aug 21 18:22:24 2008 +0000 summary: Initial commit 13 Всегда, когда вам необходимо подставить идентификатор ревизии в команду Mercurial, можно подставлять имя соответствующего тега. Внутри себя Mercurial переводит имя тега в идентификатор ревизии. $ echo goodbye > myfile2 $ hg commit -A -m 'Second commit' adding myfile2 $ hg log -r v1.0 changeset: 0:973bfbbf1341 tag: v1.0 user: Bryan O'Sullivan date: Thu Aug 21 18:22:24 2008 +0000 summary: Initial commit 10 Для отдельной ревизии или всего репозитория не существует лимита на количество тегов, которые вы можете использовать. На практике это означает что "слишком много тегов" (число которое будет сильно изменяться от проекта к проекту) не очень хорошая идея, просто потому что теги, как предполагается, помогают вам находить ревизии. Если же у вас много тегов, простота их использования очень сильно уменьшается. К примеру, если ваш проект будет создавать вехи каждые пару дней совершенно разумно помечать их тегами. Но если вы используете систему непрерывной интеграции, которая гарантирует чистоту сборки каждой ревизии, тегирование каждой успешной сборки внесет неясности. Взамен вы можете помечать тегом ревизии, чья сборка завершилась неудачей (при условии что они редки!), или просто не использовать теги для отслеживания процесса сборки. Если вы хотите удалить более не используемый тег используйте команду “hg tag --remove”. $ hg tag --remove v1.0 $ hg tags tip 3:7dc55273d032 Вы также можете изменять тег в любое время, т.о. для идентифицирования разных ревизий выполните новую команду “hg tag”. Вы также можете использовать опцию -f, чтобы указать что вы действительно хотите изменить тег. $ hg tag -r 1 v1.1 $ hg tags tip 4:109e92ac2f1e v1.1 1:eaa5c1c54989 $ hg tag -r 2 v1.1 abort: tag 'v1.1' already exists (use -f to force) $ hg tag -f -r 2 v1.1 $ hg tags tip 5:44c13e2e85a5 v1.1 2:e0f1ae893f90 В выводе все еще присутствуют старые записи тегов, но Mercurial не будет использовать их в дальнейшем.Таким образом нет никакой беды, если вы установили тег для неверной ревизии - все что нужно изменить неверный тег и скорректировать ревизию как только вы обнаружите ошибку. Mercurial хранит теги как обычный файл с контролем ревизий в вашем репозитории. Если вы создаете любые теги, вы сможете найти их все в файле .hgtags. Когда вы запускаете “hg tag”, Mercurial модифицирует этот файл, а затем автоматически комитит изменения в нем. Это означает что на каждый запуск “hg tag” вы можете найти соответствующий набор изменений в выводе команды “hg log”. $ hg tip changeset: 5:44c13e2e85a5 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:25 2008 +0000 summary: Added tag v1.1 for changeset e0f1ae893f90 7 8.1.1 Обработка конфликтов слияния Вы не должны особо заботиться о файле .hgtags, но иногда он дает о себе знать при слиянии изменений. Формат файла очень прост: он состоит из строк. Каждая строка начинается с хеша набора изменений, за который через пробел следует имя тега. Если вы исправляете конфликт в файле .hgtags, есть одно замечание про его модификацию: когда Mercurial читает теги в репозитории, он никогда не читает рабочую копию файла .hgtags. Вместо этого он читает наиболее старшую ревизию этого файла. Неудачное последствие такого дизайна состоит в том, что вы не можете проверить правильность вашего файла .hgtags после слияния до тех пор, пока вы не закомитите изменения. Таким образом, если вы решаете конфликт в .hgtags во время слияния убедитесь что вы запустили “hg tags” после слияния. Если эта команда найдет ошибку в файле .hgtags, она сообщит о месте ее возникновения. Эта информация поможет вам в исправлении ошибки и повторном комите. Вы должны запустить “hg tags” вновь, как только будете уверены в правильности формата. 8.1.2 Теги и клонирование Возможно вы обратили внимание, что команда “hg clone” имеет опцию -r, которая позволяет клонировать точную копию репозитория как частичные наборы изменений. Новый клон не будет содержать истории проекта, которая появилась позже указанной вами ревизии. Это относится и к тегам, что может стать неожиданностью для неосторожных. Вспомните что тег сохранен как ревизия в файле .hgtags, так что когда вы создаете тег, набору изменений в котором он записан нужно обратиться к старшему набору изменений. Когда вы запускаете команду “hg clone -r foo” для клонирования репозитория по имени тега foo, новый клон не будет содержать истории создания клона. Результатом этого станет то, что вы получите полное подмножество истории проекта в новом репозитории, а не тег, который вы, возможно, ожидали. 8.1.3 Когда тегов становится слишком много Итак, так как теги в Mercurial подвержены версионности и связаны с историей проекта, любой кто работает с ними может видеть созданые вами теги. Именованая ревизия 4237e45506ee будет доступна по простому тегу v2.0.2. Если же вы пытаетесь отыскать сложноуловимую ошибку, вы возможно захотите чтобы тег напоминал вам нечто вроде "Анна видела признаки ошибки в этой ревизии" (речь тут идет о "пометках на полях для себя", прим. переводчика). Для подобных целей вы можете захотеть использовать локальные теги. Вы можете создать локальный тег используя опцию -l команды “hg tag”. Это позволит записать тег в файл .hg/localtags, вместо .hgtags. Версия файла .hg/localtags не отслеживается. Любые теги созданые с опцией -l остаются локальными для вашего репозитория. 8.2 Поток изменений - "большая картинка" против "маленькой" Вернёмся к схеме, которую я нарисовал в начале главы и в то же время подумаем о проекте как о множественных конкурирующих кусках работы под разработкой. Это может быть толчком для нового "основного" релиза; нового "небольшого" релиза с исправленными багами для последнего "основного" релиза; и неожиданного "горячего" изменения для старого релиза, который сейчас ещё поддерживается. Обычно люди называют разные конкурирующие направления "ветками". Однако мы уже видели несколько раз, что Mercurial обрабатывает всё историю как серию "веток" и "слияний". На самом дела у нас есть 2 идеи, которые плохо связаны, но имеют одинаковые названия. "Большие картинки" веток представляют собой вариацию проекта; люди дают им имена и говорят о них. "Маленькие картинки" веток являются артефактами каждодневной разработки и слияния изменений. Они показывают, как код разрабатывался. 8.3 Управление ветками "больших картинок" в репозитории (хранилище) Самый простой путь изолировать "большую картинку" веток в Mercurial это отдельное хранилище. Если у вас уже есть созданное общее хранилище - скажем, с именем "myproject" - которое достигло контрольной точки "1.0", вы можете начинать подготовку для будующих "основных" релизов сверху версии "1.0" добавляя ревизию, где вы готовы для релиза "1.0". $ cd myproject $ hg tag v1.0 Затем вы можете клонировать новое хранилище проекта в myproject-1.0.1. $ cd .. $ hg clone myproject myproject-1.0.1 updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved После этого, если кому-то будет нужно работать для устранения багов к предстоящему "1.0.1" релизу, то можно клонировать хранилище myproject-1.0.1, сделать нужные изменения и вернуть их обратно. $ hg clone myproject-1.0.1 my-1.0.1-bugfix updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd my-1.0.1-bugfix $ echo 'I fixed a bug using only echo!' >> myfile $ hg commit -m 'Important fix for 1.0.1' $ hg push pushing to /tmp/branch-repot8wz0J/myproject-1.0.1 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files Тем временем разработчики следующего "большого" релиза могут продолжать работать, изолированные в хранилище myproject. $ cd .. $ hg clone myproject my-feature updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd my-feature $ echo 'This sure is an exciting new feature!' > mynewfile $ hg commit -A -m 'New feature' adding mynewfile $ hg push pushing to /tmp/branch-repot8wz0J/myproject searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 8.4 Не повтряйте сами себя: слияния между "ветками" Во многих случаях если вы должны исправить баг в "основной" ветке, есть большие шансы, что этот баг есть в ваших остальных ветках. И девелопер должен исправить это много раз. Давайте рассмотрим несколько вариантов, когда Mercurial может помочь вам исправить баги без дублирования вашей работы. В простейшнм случае всё, что вам нужно сделать - это извлечь изменения с "основной" ветки в вашу локальную копию этой ветки. $ cd .. $ hg clone myproject myproject-merge updating working directory 3 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd myproject-merge $ hg pull ../myproject-1.0.1 pulling from ../myproject-1.0.1 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) Затем вам нужно соединить заголовки 2-х веток и вернуться к "основной" ветке. $ hg merge 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg commit -m 'Merge bugfix from 1.0.1 branch' $ hg push pushing to /tmp/branch-repot8wz0J/myproject searching for changes adding changesets adding manifests adding file changes added 2 changesets with 1 changes to 1 files 8.5 Наименование веток в одном репозитории(хранилище) Во многих случаях изоляция веток в хранилищах - это хорошее решение. Это просто понять; и также сложно сделать ошибку. Это отношения один-к-одному между ветками, с которыми вы работаете и папками(директориями) в вашей системе. Тогда вы можете использовать нормальные(не как Mercurial) программы, чтобы работать с файлами в ветке/хранилище. Если вы (как и ваши коллеги) более "сильный пользователь" , то вы можете рассматривать другой способ хранения веток. Я уже упоминал разницу человеческого уровня между "маленькой картинкой" и "большой картинкой" веток. Пока Mercurial всё время работает с несколькими "маленькими картинками" в хранилище (например, когда вы отослали изменения, но ещё не соединили), Mercurial так же может работать с несколькими ветками "больших картинок". Ключ к работе в этом направлении в том, что Mercurial позволяет вам дать постоянное имя ветке. И всегда существует ветка, названная по-умолчанию. Даже перед тем, как вы переименуете ветку сами, вы можете найти историю ветки по-умолчанию, если поищите. И как пример, когда вы запускаете команду “hg commit”, и вы попадаете в редактор, где вы можете ввести commit, посмотрите на верхнюю линию, которая содержит текст “HG: branch default”. Это говорит вам о том, что ваш commit случится с веткой, названной по-умолчанию. Чтобы начать работу с именованными ветками используйте команду “hg branches”. Эта команда покажет вам список именованных веток, которые есть в хранилище, расскажет, какая ветка есть изменение другой. $ hg tip changeset: 0:34b3e0e21c40 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:16 2008 +0000 summary: Initial commit 7 $ hg branches default 0:34b3e0e21c40 Пока вы не создали ни одной именованной ветки, существует только одна, названная по-умолчанию. Найти, какая "текущая" ветка сейчас, запустите команду “hg branch” без аргументов. Вы узнаете, какая ветка является "родительской" для "текущей". $ hg branch default Чтобы создать новую ветку запустите команду “hg branch” снова. В этот раз с аргументом: именем ветки, которую вы создаёте. $ hg branch foo marked working directory as branch foo $ hg branch foo После создания новой ветки вы спросите, что сделала команда “hg branch”? Что показывают команды “hg status” и “hg tip”? $ hg status $ hg tip changeset: 0:34b3e0e21c40 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:16 2008 +0000 summary: Initial commit 8 Ничего не изменилось в рабочей папке и не было создано новой истории. Запуск команды “hg branch” не производит постоянного эффекта; она только говорит Mercurial, ветку с каким именем использовать для последующих фиксаций изменений (commit). Когда вы подтверждаете изменения, Mercurial записывает имя ветки, которую вы изменяете. Один раз переключив ветку по-умолчанию на друную и подтвердив, вы увидите имя новой ветки в результатах “hg log”, “hg tip”, и других комманд, которые показывают эту информацию. $ echo 'hello again' >> myfile $ hg commit -m 'Second commit' $ hg tip changeset: 1:63d1d92e1e30 branch: foo tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:16 2008 +0000 summary: Second commit 10 Команды как “hg log” напечатают имя ветки для каждого изменения, которое не в ветке по-умолчанию. Как результат, если вы никогда не именовали ветки - вы никогда не увидите эту информацию. Один раз дав имя ветке и подтвердив изменение с этим именем каждое последующее изменение будет иметь то же имя ветки. Вы можете сменить имя ветки в любое время, используя команду “hg branch”. 1 $ hg branch 2 foo 3 $ hg branch bar 4 marked working directory as branch bar 5 $ echo new file > newfile 6 $ hg commit -A -m 'Third commit' 7 adding newfile 8 $ hg tip 9 changeset: 2:e2e47096252f 10 branch: bar 11 tag: tip 12 user: Bryan O'Sullivan 13 date: Thu Aug 21 18:22:16 2008 +0000 14 summary: Third commit 15 На практике это то, что вы не должны делать часто, имена веток имеют тенденцию существовать долгое время(это не правило, лишь наблюдение). 8.6 Работа с несколькими поименованными ветками в хранилище. Если у вас болльше чем 1 ветка с именем в хранилище, Mercurial будет помнить ветку вашей рабочей папки когда вы запустите такую команду, как hg update” или “hg pull -u”. Это обновит рабочую папку с этой веткой. Обновить ветку с другим именем вы можете использовать опцию -C с командой “hg update”. Посмотрим это на практике. Сперва вспомните, какая ветка сейчас "текущая" и какие ветки есть в нашем хранилище. 1 $ hg parents 2 changeset: 2:e2e47096252f 3 branch: bar 4 tag: tip 5 user: Bryan O'Sullivan 6 date: Thu Aug 21 18:22:16 2008 +0000 7 summary: Third commit 8 9 $ hg branches 10 bar 2:e2e47096252f 11 foo 1:63d1d92e1e30 (inactive) 12 default 0:34b3e0e21c40 (inactive) Мы в "bar" ветке, но так же существует старая ветка “hg foo”. Мы можем использовать “hg update” назад и далее между foo и bar ветками без нужды использовать опцию -С, потому-что это содержит прошлое и пройдёт сквозь историю наших изменений. 1 $ hg update foo 2 0 files updated, 0 files merged, 1 files removed, 0 files unresolved 3 $ hg parents 4 changeset: 1:63d1d92e1e30 5 branch: foo 6 user: Bryan O'Sullivan 7 date: Thu Aug 21 18:22:16 2008 +0000 8 summary: Second commit 9 10 $ hg update bar 11 1 files updated, 0 files merged, 0 files removed, 0 files unresolved 12 $ hg parents 13 changeset: 2:e2e47096252f 14 branch: bar 15 tag: tip 16 user: Bryan O'Sullivan 17 date: Thu Aug 21 18:22:16 2008 +0000 18 summary: Third commit 19 Если мы вернёмся к ветке foo и затем запустим “hg update”, мы останемся в foo, не перемещаясь к вершине bar. 1 $ hg update foo 2 0 files updated, 0 files merged, 1 files removed, 0 files unresolved 3 $ hg update 4 0 files updated, 0 files merged, 0 files removed, 0 files unresolved Внесение нового изменения в ветку foo создаст новую голову. 1 $ echo something > somefile 2 $ hg commit -A -m 'New file' 3 adding somefile 4 created new head 5 $ hg heads 6 changeset: 3:e2b5f29a6ec7 7 branch: foo 8 tag: tip 9 parent: 1:63d1d92e1e30 10 user: Bryan O'Sullivan 11 date: Thu Aug 21 18:22:17 2008 +0000 12 summary: New file 13 14 changeset: 2:e2e47096252f 15 branch: bar 16 user: Bryan O'Sullivan 17 date: Thu Aug 21 18:22:16 2008 +0000 18 summary: Third commit 19 8.7 Имена веток и слияние Как вам вероятно уже известно, слияния в Mercurial не симметричны. Давайте представим, что у нашего репозитория две головы: 17 и 23. Тогда если я запущу “hg update” для 17, и затем “hg merge” с 23, Mercurial запишет 17 в качестве первого родителя слияния, и 23 в качестве второго. Тогда как если я запущу “hg update” для 23 и затем “hg merge” с 17, это запишет 23 первым родителем, а 17 - вторым. Это влияет на выбор имени ветки Mercurial'ом во время слияния. После слияния, Mercurial оставит ветке имя первого родителя, когда вы закрепите результат слияния. Если имя первой родительской ветки foo, и вы совершаете слияние с bar, после слияния имя ветки останется foo. Это не обязательно, что хранилище будет хранить 2 "головные" ветки с одним и тем же именем. Скажем, я работаю с веткой foo, как и вы. Мы сохраняем разные изменения; я забираю ваши изменения; у меня сейчас 2 "головные" ветки foo. Результатом слияния будет 1 "головная" ветка foo. Но если я работаю с веткой bar, то результатом слияния bar и foo будет bar ветка. 1 $ hg branch 2 bar 3 $ hg merge foo 4 1 files updated, 0 files merged, 0 files removed, 0 files unresolved 5 (branch merge, don't forget to commit) 6 $ hg commit -m 'Merge' 7 $ hg tip 8 changeset: 4:e825b593b263 9 branch: bar 10 tag: tip 11 parent: 2:e2e47096252f 12 parent: 3:e2b5f29a6ec7 13 user: Bryan O'Sullivan 14 date: Thu Aug 21 18:22:17 2008 +0000 15 summary: Merge 16 Более конкретный пример: если я работаю с тестовой веткой и хочу внести в неё последние из стабильной ветки, то Mercurial выберет имя "правой" (тестовой) ветки, когда сделаю вытащу и объединю изменения из стабильной ветки. 8.8 Называть ветки очень полезно. Вы не должны думать об именовании веток только когда несколько "устойчивых" веток находятся в одном хранилище. Это так же очень полезно даже для одной ветки в одном хранилище. В простом случае, если вы дадите имя каждой ветке, вы будете знать, какая ветка изменена. У вас будет больше контекста, когда вы попытаетесь проследить историю изменений. Если вы работаете с хранилищем в общем доступе, то вы можете задать pretxnchangegroup перехватчик, который будет блокировать все приходящие изменения с "неправильным" именем ветки. Это простой, но эффективный способ против случайного слияния изменений с "нестабильной" ветки в "стабильную". Так перехватчик может выглядеть внутри общедоступного repo’s hgrc. 1 [hooks] 2 pretxnchangegroup.branch = hg heads --template 'branches ' | grep mybranch Глава 9 Поиск и исправление ваших ошибок Человек может ошибиться, но высококлассная система управления версиями берется исправить последствия. В этой главе мы расскажем о некоторых способах поиска проблем, закравшихся в ваш проект. Mercurial предоставляет высокоэффективные возможности, помогающие отделить источник проблем и соответствующим образом обработать. 9.1 Удаление локальной истории 9.1.1 Случайная фиксация Не часто, но постоянно у меня возникает проблема, что я печатаю быстрее, чем думаю, что иногда приводит к тому, что зафиксированные изменения либо не законченные, либо просто ошибочные. Что касается меня, стандартный тип не завершенных изменений в том ,что я создал новый исходник, но забыл добавить (“hg add”) его. "Просто ошибочные" изменения не так часты, но не менее досаждающие. 9.1.2 Откат транзакции В разделе 4.2.2 я упоминал, что Mercurial рассматривает каждую модификацию хранилища как транзакцию. Каждый раз, когда вы фиксируете изменения или подтягиваете изменения из другого хранилища, Mercurial запоминает, что вы сделали. Вы можете отменить, или откатить, только одно из этих действий с помощью команды “hg rollback”. (См. раздел 9.1.4 о важном предупреждении о использовании данной команды.) Приведу пример частой собственной ошибки: фиксация изменений, в которых я создал новый файл, но забыл выполнить “hg add” для него. $ hg status M a $ echo b > b $ hg commit -m 'Add file b' Вывод команды “hg status” после фиксации подтверждает ошибку. $ hg status ? b $ hg tip changeset: 1:2cff0ede6a97 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:24 2008 +0000 summary: Add file b 9 Были зафиксированы изменения для файла "a", но не для нового файла "b". Если бы я отправил эти изменения в общее с коллегами хранилище, то велик шанс того, что что-то в "a" ссылается на "b", отсутствующий в репозиториях коллег после того, как они получат мои изменения. В таком случае я мог бы стать объектом некоторого негодования. Однако, мне повезло — я заметил ошибку прежде, чем отослал изменения. Я воспользовался командой «hg rollback», и Mercurial убрал последние измения. $ hg rollback rolling back last transaction $ hg tip changeset: 0:949883cff8a4 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:24 2008 +0000 summary: First commit 9 $ hg status M a ? b Имейте в виду, что изменение отсутствует в истории хранилища, и снова считается, что файл в рабочей директории изменён. Commit и rollback оставили рабочую директорию в том же состоянии, в котором она была перед commit; изменения полностью уничтожены. Теперь я могу спокойно «hg add» файл «b», и снова запустить «commit». $ hg add b $ hg commit -m 'Add file b, this time for real' 9.1.3 Ошибочное вытягивания Стандартной методикой работы с «mercurial» является поддержка разработки отдельных веток проекта в отдельных хранилищах. Ваша команда разработчиков может себе позволить разрабатывать версию 0.9 в одном хранилище, а версию 1.0 — в другом, с другими изменениями. В этом случае, неприятными могут быть последствия непредумышленного вытягивания изменений из общего хранилища с "1.0" в локальное с "0.9". В худшем случае, вы заплатите за невнимательность втолкнув те самые изменения в общее "0.9" дерево, запутав остальных разработчиков. (Не волнуйтесь, к этому ужасному сценарию мы вернёмся позже). Впрочем, вероятнее всего, Вы заметите это сразу же, поскольку Mercurial покажет вам URL из которого происходит вытягивание. Или Вы обратите внимание на подозрительно большое число изменений в хранилище. Для подчистки этих, только что втянутых изменений отлично подходит команда “hg rollback”. Mercurial группирует всё сделанное одной командой “hg pull” в одну трансакцию , и, таким образом, всё, что Вам нужно для исправления ошибки это одна команда “hg rollback”. 9.1.4 “hg rollback” бесполезен если изменения уже внесены. Ценность команды “hg rollback” падает до нуля, как только Вы втолкнете изменения в другое хранилище. Конечно, эта команда исправит ошибку, но только в том хранилище к которому Вы её применяете. И нет способа остановить распространение ошибочного изменения между хранилищами Если Вы втолкнули изменение в общее хранилище, оно по сути "вырвалось на свободу" и Вам придётся исправлять ошибку другим способом. Что будет, если Вы втолкнули набор изменений куда-либо, подтёрли их у себя и затем втянули их же обратно? Этот набор изменений опять появится в Вашем хранилище. Если Вы абсолютно точно знаете, что те изменения, которые Вы собираетесь откатить, самые свежие в том самом хранилище, и Вы знаете, что никто еще не успел их вытянуть из этого хранилища, то Вы можете подтереть и там тоже. Но рассчитывайте на то, что на самом деле Вы не сможете рассчитывать, на то, что сможет всё так точно рассчитать. Рано или поздно какие нибудь изменения в неподконтрольном Вам хранилище будут сделаны и это приведет к неприятностям. 9.1.5 Вы можете отменить только последнее изменение Mercurial хранит в своем логе только одну транзакцию, которая была последней выполнена в данном хранилище. Это значит, что вы можете отменить только одну транзакцию. Если вы попытаетесь отменить еще одну транзацию, предшествующую последней, то вам не удастся этого сделать. $ hg rollback rolling back last transaction $ hg rollback no rollback information available Если вы произведете отмену транзакции, вы не сможете выполнять данное действие до тех пор, пока не произведете другие операции commit или pull. 9.2 Отмена ошибочных изменений Если вы изменили файл, но потом решили, что вообще не хотите его изменять, и если вы ещё не зафиксировали изменения, то команда "hg revert" -- то, что Вам нужно. Она смотрит на предыдущую ревизию рабочей директории и восстанавливает содержимое файла к состоянию из этой ревизии. (Это занудный способ сказать, что обычно эта команда отменяет ваши изменения.) Давайте посмотрим, как работает команда "hg revert". В этом маленьком примере мы начнем с изменения файла, о котором Mercurial уже знает. $ cat file original content $ echo unwanted change >> file $ hg diff file diff -r 6a049adacc62 file --- a/file Thu Aug 21 18:22:19 2008 +0000 +++ b/file Thu Aug 21 18:22:19 2008 +0000 @@ -1,1 +1,2 @@ original content +unwanted change Нам не нужно это изменение, поэтому мы вызываем "hg revert" для этого файла. $ hg status M file $ hg revert file $ cat file original content Команда "hg revert" дополнительно заботится о безопасности данных, сохраняя наш измененный файл с расширением .orig. $hg status ? file.orig $ cat file.orig original content unwanted change Ниже приведены все случаи, в которых может быть полезна команда “hg revert”. В следующей секции будет детальное описание каждого из них. * Если вы изменяете файл, она восстановит его до предыдущего состояния. * Если вы используете “hg add”, она отменит “добавленное” состояние файла, но оставит сам файл неизменным. * Если вы удаляете файл без использования команд Mercurial, она восстановит файл с его предыдущим содержанием. * Если вы удаляете файл командой “hg remove”, то “hg remove” восстановит файл с его предыдущим содержанием и отменит у этого файла состояние “removed” 9.2.1 Ошибки управления файлами Команда "hg revert" полезна не только для измененных файлов. Она позволяет отменить результат любой команды управления файлами - "hg add", "hg remove" и др. Если вы используете “hg add” с файлом, но потом понимаете, что не хотите, чтобы Mercurial отслеживал его, используйте “hg revert” для отмены добавления. Не волнуйтесь: Mercurial не изменит файл ни в коем случае. Mercurial просто “вычеркнет” файл. $ echo oops > oops $ hg add oops $ hg status oops A oops $ hg revert oops $ hg status ? oops Аналогично, если Вы применили к файлу “hg remove”, то можете с помощью “hg revert” восстановить его с предыдущим содержимым. $ hg remove file $ hg status R file $ hg revert file $ hg status $ ls file file Это также сработает если файл был удалён "в ручную", без помощи Mercurial. Напомним, что в терминологии Mercurial такие файлы называются “missing”. $ rm file $ hg status ! file $ hg revert file $ ls file file Если вы откатываете операцию "hg copy", целевой файл все равно остается в вашей рабочей директории, однако изменения в нем не отслеживаются. Поскольку операция копирования в любом случае не затрагивает исходный файл, Меркуриал никак не изменяет исходный файл. $ hg copy file new-file $ hg revert new-file $ hg status ? new-file Немного более специфичный случай: откат переименования Если вы переименовываете файл с помощью "hg rename", нужно помнить одну небольшую деталь. Когда вы откатываете переименование с помощью "hg revert", недостаточно указать просто имя получившегося файла, как показано здесь. $ hg rename file new-file $ hg revert new-file $ hg status ? new-file Как вы можете видеть из вывода "hg status", полученный файл более не идентифицирован как добавленный, однако исходный файл все еще удален! Это однюдь не интуитивно понятно (по крайней мере для меня), но, все же, с этим не сложно разобраться. $ hg revert file no changes needed to file $ hg status ? new-file Итак, запомните: для отката команды "hg rename" необходимо указать имена и исходного и целевого файлов. (К слову, если вы переименовываете файл, после чего изменяете полученный файл, а потом откатываете оба этапа переименования, когда Меркуриал восстановит файл, который был удален в ходе операции переименования, он не будет содержать изменений. Если вам необходимо сохранить изменения из переименованного файла и перенести их в исходный, не забудте скопировать их) Эта морока с откатом переименования возможно является ошибкой в Меркуриал. 9.3 Работа с зафиксированными изменениями Рассмотрим случай, когда вы зафиксировали изменение "a" и другое изменение "b" поверх него. Затем вы обнаружили, что изменение "a" было некорректным. Mercurial позволяет вам автоматически "откатить" (back out) изменение целиком, и создаёт блоки, которые позволяют вам отменить часть изменения вручную. Перед прочтением этой части руководства, вы должны четко представлять себе следующее: команда “hg backout” отменяет изменения добавляя новые записи к истории, но ни в коем случае не редактируя и не удаляя уже существующую в истории информацию. Эта утилита хорошо подходит для исправления небольших багов, но не для отмены больших изменений, приведших к серьезным проблемам. Чтобы разобраться с такими проблемами, смотрите секцию 9.4. 9.3.1 Отзыв набора измений Команда “hg backout” позволяет вам автоматически "отменить" всю ревизию. Т.к. Меркуриал не позволяет изменять уже существующую историю, а только лишь добавлять в неё новые записи, данная команда не может просто удалить ревизию, которую вы хотите отменить. Вместо этого она создает новую ревизию, которая отражает состояние репозитория, если бы в него не была добавлена удаляемая ревизия. Действия выполняемые командой “hg backout” на первый взгляд могут показаться несколько запутанными, поэтому продемонстрируем их на примере. Для начала создадим репозиторий с несколькими простыми изменениями. $ hg init myrepo $ cd myrepo $ echo first change >> myfile $ hg add myfile $ hg commit -m 'first change' $ echo second change >> myfile $ hg commit -m 'second change' В качестве единственного параметра команда “hg backout” принимает ID удаляемой ревизии. Обычно “hg backout” перебрасывает вас в текстовый редактор, где можно создать комментарий, объясняющий причину отмены изменений. В этом же примере мы добавили комментарий прямо в командной строке при помощи параметра "-m". 9.3.2 Отзыв последнего набора изменений В данном разделе мы попробуем отозвать последний внесенный набор изменений. $ hg backout -m 'back out second change' tip reverting myfile changeset 2:c2bb30cbc56d backs out changeset 1:306625be151f $ cat myfile first change Как видите, в моём файле второй строки уже нет. Посмотрев на вывод команды “hg log” можно понять, что “hg backout” сработала. $ hg log --style compact 2[tip] c2bb30cbc56d 2008-08-21 18:22 +0000 bos back out second change 4 1 306625be151f 2008-08-21 18:22 +0000 bos second change 7 0 1e15c6cd5067 2008-08-21 18:22 +0000 bos first change 10 Заметим, что новый набор изменений, который был создан командой “hg backout” является "потомком" набора который был отозван. Это легко увидеть на рисунке 9.1, на котором показана история изменений в графическом виде. Она демонстрирует приятную линейность. ?? рис 9.1: Возврат изменений командой “hg backout” 9.3.3 Отзыв ревизии, не являющейся последней Если Вы хотите отозвать не последний набор изменений добавляйте к “hg backout” опцию "--merge" $ cd .. $ hg clone -r1 myrepo non-tip-repo requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd non-tip-repo Это позволяет делать отзыв любого набора изменений за одно действие, что обычно проще и быстрее. $ echo third change >> myfile $ hg commit -m 'third change' $ hg backout --merge -m 'back out second change' 1 reverting myfile created new head changeset 3:c2bb30cbc56d backs out changeset 1:306625be151f merging with changeset 3:c2bb30cbc56d merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) Если Вы посмотрите на содержимое "myfile" после завершения отзыва, то увидите, что присутствует первый и третий набор изменений. А второй отсутствует. $ cat myfile first change third change Как иллюстрирует изображение истории на рисунке 9.2, Mercurial коммитит два изменения в этой ситуации (прямоугольники — это узлы, которые Mercurial коммитит автоматически). Перед тем, как Mercurial начнёт процесс отзыва, он запомнит текущего родителя рабочей директории. Затем он отзывает указанную ревизию и производит коммит новой ревизии. И наконец, он выполняет слияние с предыдущим родителем рабочей директории, после чего осуществляется коммит результата слияния. Здесь должна быть картинка 9.2: Автоматический возврат не последнего набора изменений с помощью команды “hg backout” В результате вы оказываетесь там же, где и были лишь с небольшим увеличением истории, в которой отражён откат. Всегда используйте опцию --merge Фактически, поскольку опция --merge делает то что надо, независимо от того, является ли отменяемый набор изменений вершиной, или нет (т.е. не пытается зафиксировать возврат набора изменений, являющегося вершиной, поскольку в этом нет необходимости), вы должны всегда использовать эту опцию, когда используете команду "hg backout". 9.3.4 Получение большего контроля над процессом возврата До сих пор я рекомендовал вам всегда использовать опцию "--merge", когда вы возвращаете изменение, однако команда "hg backout" позволяет вам выбрать, каким образом произвести фиксацию возврата изменения. Получение полного контроля над на тем, что происходит в процессе возврата - это то, что вам понадобится достаточно редко, однако полезно понимать, что именно команда "hg backout" делает в автоматическом режиме. Чтобы проиллюстрировать этот процесс, создадим копию репозитория, исключив возвращенные изменения, которые он содержит. $ cd .. $ hg clone -r1 myrepo newrepo requesting all changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd newrepo Как и в предыдущем примере, Мы создадим третью ревизию, затем вернём в изначальное состояние, и посмотри что произошло. $ echo third change >> myfile $ hg commit -m 'third change' $ hg backout -m 'back out second change' 1 reverting myfile created new head changeset 3:98814fbdec28 backs out changeset 1:306625be151f the backout changeset is a new head - do not forget to merge (use "backout --merge" if you want to auto-merge) Наш новый набор изменений опять является потомком набора изменений, который мы возвращаем, а не того, который являлся вершиной. Команда "hg backout" вполне явно сообщает нам об этом. $ hg log --style compact 3[tip]:1 98814fbdec28 2008-08-21 18:22 +0000 bos back out second change 4 2 27f505fcad4a 2008-08-21 18:22 +0000 bos third change 7 1 306625be151f 2008-08-21 18:22 +0000 bos second change 10 0 1e15c6cd5067 2008-08-21 18:22 +0000 bos first change 13 И снова чтобы увидеть что произошло, проще взглянуть на граф истории ревизий на рис. 9.3. Он явно показывает, что когда мы использовали "hg backout" для возврата изменений, не являющихся вершиной, Меркуриал добавляет новый "head" в репозиторий (изменение, которое было зафиксировано, обозначено квадратом). Здесь должна быть картинка 9.3: Возврат изменений с помощью команды "hg backout" После выполнения команды "hg backout", новый "backout" набор изменений становится родителем рабочего каталога. $ hg parents changeset: 2:27f505fcad4a user: Bryan O'Sullivan date: Thu Aug 21 18:22:14 2008 +0000 summary: third change 6 Теперь у нас есть два отдельных набора изменений. $ hg heads changeset: 3:98814fbdec28 tag: tip parent: 1:306625be151f user: Bryan O'Sullivan date: Thu Aug 21 18:22:15 2008 +0000 summary: back out second change 8 changeset: 2:27f505fcad4a user: Bryan O'Sullivan date: Thu Aug 21 18:22:14 2008 +0000 summary: third change 13 Давайте подумаем о том, что мы ожидаем увидеть, в содержимом MyFile сейчас. Первое изменение, должно присутствовать, потому что мы никогда не возвращали его. Второе изменение должно пропасть, как изменение, которое мы вернули. Поскольку граф истории показывает третье изменение в качестве отдельной головы, мы не ожидаем увидеть третье изменение в MyFile. $ cat myfile first change second change third change Чтобы получить третье изменение обратно в файл, мы просто делаем нормальное слияние двух наших голов. $ hg merge merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg commit -m 'merged backout with previous tip' $ cat myfile first change third change После этого графическая история нашего хранилища показана на рисуноке 9.4. Здесь должна быть картинка 9.4: Ручное слияние возвращённых изменений 9.3.5 Почему команда “hg backout” работает именно так Вот краткое описание как работает команда “hg backout”. 1. Она гарантирует, что рабочий каталог будет "чистым", т.е., вывод команды “hg status” будет пустым. 2. Она запоминает текущего родителя рабочего каталога. Назовем эту ревизию оригинальной 3. Она не эквивалентна команде "hg update", синхронизирующей текущий каталог с ревизией к которой вы хотите вернуться Она находит родителя ревизии, которую Вы хотите откатить. Назовем ее ревизия-родитель. Для всех файлов, измененных в откатываемой ревизии, делается эквивалент команды hg revert -r parent, для восстановления их до содержимого которое они имели перед этой ревизией. Результат сохраняется как новая ревизия. Эта ревизия имеет родителем откатываемую ревизию. Если Вы указали ключ --merge в коммандной строке, делается слияние с оригинальной ревизией и результат слияния сохраняется как новая ревизия Альтернативный способ реализации "hg backout" команда "hg export" применённую к откатываемому набору изменений как diff-у, а затем использовать опцию --reverse для команды patch для ликвидации последствий изменения без возни с рабочей директорией. Это звучит гораздо проще, но работает это не так хорошо. Причина, по которой hg backout делает обновление, фиксацию, слияние, и ещё одну фиксацию используя механизм слияния в том, что это лучший шанс сделать работу хорошо, когда речь идет об сохранении всех изменения между ревизией которую мы откатываем, и текущей ревизией. Если вы отказываясь ревизии, которая была 100 ревизий назад в истории вашего проекта, вероятность того, что команда patch сможет применить обратный diff аккуратно не велика, потому что вмешательство последующих ревизий возможно "нарушило контекст", который использует patch, чтобы определить, может ли она применить патч (если это звучит для вас как бред, смотрите раздел под названием "Понимание патчей" для обсуждения строк команды patch). Кроме того, механизм слияние Mercurial будет работать с файлами и каталогами, который был переименованы, изменениями прав, и изменения в двоичных файлах, ни с одним из этих вариантов patch не может работать. 9.4 Изменения, которых быть не должно В большинстве случаев команда "hg backout" именно то, что нужно, если вы хотите откатить последствия изменения. Она оставляет постоянную запись о том, что вы сделали, в обоих случаях когда фиксируется основная ревизия и когда вы очищаете после этого. Хотя в некоторых случаях вы можете обнаружить, что закоммитили изменения, которым совершенно не место в репозитории. Например, если вместе с исходниками вы закоммитили объектные файлы вашего языка программирования (.obj или .o файлы), то это обычно считается ошибкой. Объектные файлы практически не несут пользы, и они огромные, поэтому сильно увеличивают размер репозитория, а также резко возрастает время выполнения операций клонирования, изменения, поиска. Прежде чем я расскажу возможные пути исправления "корявых" коммитов (на столько корявых, что вам захочется кидаться вещами), разрешите мне сначала обсудить некоторые подходы, которые вероятно не сработают. Т.к. Меркуриал трактует историю как накопляемую - каждое изменение основывается на всех предыдущих - вы, в принципе, не можете очистить её от корявых изменений. Единственный случай, когда это возможно, - это когда вы только-только закоммитили изменение, и оно ещё не было отправлено (или забрано) в другой репозиторий. Здесь вы можете безопасно использовать команду "hg rollback", как я рассказывал в разделе 9.1.2. После того, как вы отправили плохое изменение в другой репозиторий, вы ещё можете воспользоваться "hg rollback", чтобы очистить свой локальный репозиторий, но последствия будут не теми, которые вам нужны. Корявое изменение останется в удалённом репозитории, поэтому оно снова появится в вашем локальном репозитории после команды "pull". Если ситуация подобная этой появится, и вы будете знать все репозитории, в которые попал корявый код, вы можете попытаться избавится от него в каждом из них. Это, естественно, не удовлетворительное решение: если вы пропустите хотя бы один репозиторий, изменение пуститься "по ветру" и распространится дальше. А если вы закоммитите поверх корявого изменения еще несколько, то путей решения станет ещё меньше. Меркуриал не предоставляет возможности сделать "дыру в истории", оставив все чейнджсеты нетронутыми. Это должно быть заполнено. Скрипт "hg-replay" в папке с примерами работает, но не обрабатывает чейнджсеты со слияниями (merge). Довольно важное упущение. 9.4.1 Защити себя от "сбежавших" изменений Если вы закоммитили какие-то изменения в свой локальный репозиторий и они были залиты в другие места, это не обязательно такая уж большая катастрофа. Вы можете защититься от некоторых видов плохих изменений ещё до дедлайна. Это довольно просто если ваша команда обычно забирает новые изменения из центрального репозитория. Настроив на этом репозитории некоторые хуки для валидации (проверки) входящих чейнджсетов (см. главу 10), вы сможете автоматически полностью предотвратить заливание некоторых типов плохих чейнджсетов. С подобной структурой репозиториев некоторые виды плохих чейнджсетов будут сами по себе "склоняться к суициду", потому что они не могут попасть в центральный репозиторий. Даже лучше, - это будет происходить без какого-либо специально вмешательства. Например, хук на все входящие чейнджсеты, проверяющий код на компилябельность, сможет предотвратить заливание изменений, которые "ломают билд". 9.5 Поиск источника ошибки Конечно, возможность откатить изменение, которое внесло ошибку, это хорошо, но сначала нужно узнать, какой именно набор изменений, нужно откатить. Mercurial предоставляет неоценимую команду "hg bisect", которая поможет вам эффективно автоматизировать этот процесс. Идея "hg bisect" в том, что ревизии вводили некоторые изменения в поведении, которые можно определить с помощью некоторых простых бинарных испытаний. Вы не знаете, какая часть кода внесла изменения, но вы знаете, как проверить его на наличие ошибок. Команда "hg bisect" использует ваш тест для прямого поиска ревизии, которая содержит код, вызывающий ошибку. Вот несколько сценариев, которые помогут вам понять, как можно применить эту команду. * Самая последняя версия программного обеспечения имеет ошибку, и Вы помните, что ее не было несколько недель назад, но не знаете, когда она появилась. Тут-то, ваш бинарный тест проверяет [ревизии] на наличие этой ошибки. * Вы исправили ошибку в спешке, и теперь пришло время закрыть запись об ошибке в багтрекере вашей команды. Багтрекер данных требует ID ревизии, когда вы закрываете записи, но вы не помните, в какой ревизии Вы исправили ошибку. Снова, ваш бинарный тест проверяет на наличие ошибки. * Ваше программное обеспечение работает правильно, но работает на 15% медленнее, чем в прошлый раз. Вы хотите знать, какие ревизии внесли уменьшение производительности. В этом случае ваш бинарный тест измеряет производительность вашего программного обеспечения, чтобы показать, что "быстрее" или "медленнее". * Размеры компонент проекта, который Вы ведете внезапно раздулись, и вы подозреваете, что что-то изменилось во время построения проекта. Из этих примеров должно быть ясно, что команда "hg bisect" полезна не только для нахождения источников ошибок. С ее помощью можно найти любое "непредвиденное свойство" кодов. То, которое не удаётся найти простым текстовым поиском, но можно отловить бинарным тестом. Введем немного терминологии, просто чтобы понять, какая часть процесса поиска ложится на Вас и какая на Mercurial. "Тест" это то, что выбирает ревизию, когда вы запускаете "hg bisect" . Проверка - это то, что "hg bisect" запускает чтобы сказать, какая ревизия хороша. Наконец, мы будем использовать слово "bisect", и как существительное (бисектриса) и глагол (делить пополам), во фразе "поиск с использованием команды "hg bisect". Прямолинейный способ автоматизации процесса поиска - просто проверять каждый Changeset. Однако масштабы этого ненормальны. Если тестирование одной ревизии занимает десять минут то полный просмотр 10000 ревизий в вашем хранилище, потребует в среднем 35 дней. Даже если бы вы знали, что ошибка была внесена в одной из последних 500 ревизий и ограничили поиск этим, вы по-прежнему будете искать более чем 40 часов, чтобы найти эту ревизию. "hg bisect" использует свои знания "формы" истории вашего проекта и чтобы выполнить поиск по времени пропорционально логарифму числа проверяемых ревизий (вид выполняемого поиска называется дихотомический поиск). При таком подходе, поиск по 10000 ревизий займет менее трех часов, даже при десяти минутах на одно испытание (поиск потребует около 14 проверок). Ограничьте поиск последними ста ревизиями, и это займет всего около часа (примерно семь тестов). Команде известно о "ветвистом" характере истории проекта в Mercurial, поэтому у него нет проблем, связанных с ветвлениями, слияниями, или несколькими головами в репозитории. Он может обрезать все ветви истории одной проверкой, который, как она работает так эффективно. 9.5.1 Использование команды "hg bisect" Вот пример "hg bisect" в действии. Примечание: В версии 0.9.5 и более ранних, "hg bisect" не является встроенной командой: она распространялясь с Mercurial как расширение. В этом разделе описана встроенная команды, а не старое расширение. Теперь давайте создадим репозитарий, так что мы сможем попробовать команду "hg bisect" отдельно. $ hg init mybug extension 'hbisect' overrides commands: bisect $ cd mybug Мы будем моделировать проект, который содержит ошибку простым образом: создать незначительные изменения в цикле, и назначить одно конкретное изменение, которое будет иметь "ошибку". Этот цикл создает 35 ревизий, каждое добавление одного файла в хранилище. Мы представляем нашу "ошибку" с файлом, который содержит текст "У меня есть губы". 1 $ buggy_change=22 2 $ for (( i = 0; i < 35; i++ )); do 3 > if [[ $i = $buggy_change ]]; then 4 > echo 'i have a gub' > myfile$i 5 > hg commit -q -A -m 'buggy changeset' 6 > else 7 > echo 'nothing to see here, move along' > myfile$i 8 > hg commit -q -A -m 'normal changeset' 9 > fi 10 > done 11 extension 'hbisect' overrides commands: bisect 12 extension 'hbisect' overrides commands: bisect 13 xtension 'hbisect' overrides commands: bisect 14 extension 'hbisect' overrides commands: bisect 15 extension 'hbisect' overrides commands: bisect 16 extension 'hbisect' overrides commands: bisect 17 extension 'hbisect' overrides commands: bisect 18 extension 'hbisect' overrides commands: bisect 19 extension 'hbisect' overrides commands: bisect 20 extension 'hbisect' overrides commands: bisect 21 extension 'hbisect' overrides commands: bisect 22 extension 'hbisect' overrides commands: bisect 23 extension 'hbisect' overrides commands: bisect 24 extension 'hbisect' overrides commands: bisect 25 extension 'hbisect' overrides commands: bisect 26 extension 'hbisect' overrides commands: bisect 27 extension 'hbisect' overrides commands: bisect 28 extension 'hbisect' overrides commands: bisect 29 extension 'hbisect' overrides commands: bisect 30 extension 'hbisect' overrides commands: bisect 31 extension 'hbisect' overrides commands: bisect 32 extension 'hbisect' overrides commands: bisect 33 extension 'hbisect' overrides commands: bisect 34 extension 'hbisect' overrides commands: bisect 35 extension 'hbisect' overrides commands: bisect 36 extension 'hbisect' overrides commands: bisect 37 extension 'hbisect' overrides commands: bisect 38 extension 'hbisect' overrides commands: bisect 39 extension 'hbisect' overrides commands: bisect 40 extension 'hbisect' overrides commands: bisect 41 extension 'hbisect' overrides commands: bisect 42 extension 'hbisect' overrides commands: bisect 43 extension 'hbisect' overrides commands: bisect 44 extension 'hbisect' overrides commands: bisect 45 extension 'hbisect' overrides commands: bisect Следующее, что мы хотели бы сделать, это понять, как использовать команду "hg bisect". Для этого мы можем использовать встроенная справочная механизмом Mercurial. 1 $ hg help bisect 2 расширение 'hbisect' заменяет команду bisect 3 hg bisect [help|init|reset|next|good|bad] 4 5 Дихотомическим поиск в дереве (направленном ациклическом графе) ревизий 6 7 Это расширение помогает находить ревизию, в которой возникла проблема 8 Для использования, отметьте самую раннюю ревизию, в которой вы познакомлись с проблемой как плохую (bad), 9 затем пометьте последнюю ревизию, свободную от проблеммы как хорошую (good) 10 bisect будет "обновлять" ваш рабочий каталог до ревизии для тестирования 11 После выполнения теста текущий каталог помечается как хороший или плохой 12 и bisect будет либо "обновлять" папку до другой ревизии-кандидата 13 либо объявит, что она нашла плохую ревизию 14 15 Примечание: bisect ожидает, что плохая ревизия будет потомком хорошей ревизии 16 Если вы ищете точку, в которой проблема была исправлена, то сделайте 17 безпроблемную ревизию плохой, а проблемную - хорошей. 18 19 Для подкоманд см. "hg bisect help" 20 21 используйте "hg -v help bisect" для отображения всех опций Команда "hg bisect" работает по шагам. Каждый шаг происходит следующим образом. 1. Вы запускаете Ваш бинарный тест. * Если тест успешен, Вы говорите об этом запуская команду "hg bisect --good". * Если неуспешен, запускаете команду "hg bisect --bad" 2. Mercurial использует вашу информацию, чтобы решить, какая ревизия для тестирования следующая 3. Он обновляет рабочий каталог до этой ревизии и процесс повторяется сначала Процесс заканчивается, когда "hg bisect" идентифицирует уникальный набор изменений, который знаменует собой точку, где Ваш тест перешол из "успешног" в "неуспешный". Чтобы начать поиск, должны запустить команду “hg bisect --reset”. 1 $ hg bisect --init 2 extension 'hbisect' overrides commands: bisect 3 hg bisect: option --init not recognized 4 hg bisect [help|init|reset|next|good|bad] 5 6 Dichotomic search in the DAG of changesets 7 8 This extension helps to find changesets which cause problems. 9 To use, mark the earliest changeset you know introduces the problem 10 as bad, then mark the latest changeset which is free from the problem 11 as good. Bisect will update your working directory to a revision for 12 testing. Once you have performed tests, mark the working directory 13 as bad or good and bisect will either update to another candidate 14 changeset or announce that it has found the bad revision. 15 16 Note: bisect expects bad revisions to be descendants of good revisions. 17 If you are looking for the point at which a problem was fixed, then make 18 the problem-free state "bad" and the problematic state "good." 19 20 For subcommands see "hg bisect help" 21 22 use "hg -v help bisect" to show global options В нашем случае мы используем очень простой бинарный тест: мы проверим, содержит ли какой-то файл в хранилище строку "У меня есть жук". Если это так, эта ревизия содержит изменения, которые "вызвали ошибку". По соглашению, ревизия, которая обладает свойством "плохо", а не ту, которая не является "хорошим". Чаще всего, ревизия с которой синхронизирован рабочий каталог (как правило, 'typ' - последняя голова), уже столкнулись с проблемой внесенной бажной правкой, поэтому мы помечаем его как "плохое". 1 $ hg bisect --bad 2 extension 'hbisect' overrides commands: bisect 3 hg bisect: option --bad not recognized 4 hg bisect [help|init|reset|next|good|bad] 5 6 Dichotomic search in the DAG of changesets 7 8 This extension helps to find changesets which cause problems. 9 To use, mark the earliest changeset you know introduces the problem 10 as bad, then mark the latest changeset which is free from the problem 11 as good. Bisect will update your working directory to a revision for 12 testing. Once you have performed tests, mark the working directory 13 as bad or good and bisect will either update to another candidate 14 changeset or announce that it has found the bad revision. 15 16 Note: bisect expects bad revisions to be descendants of good revisions. 17 If you are looking for the point at which a problem was fixed, then make 18 the problem-free state "bad" and the problematic state "good." 19 20 For subcommands see "hg bisect help" 21 22 use "hg -v help bisect" to show global options Нашей следующей задачей является назначить ревизию, про которую известно, что она не содержит искомую ошибку; Команда"hg bisect" ограничивает свой поиск между первой парой "хорошая - плохая" ревизиия. В нашем случае, мы знаем, что 10-я ревизия не имела ошибок. (Я расскажу несколько слов о выборе первой "хорошей" ревизии позднее). 1 $ hg bisect --good 10 2 extension 'hbisect' overrides commands: bisect 3 hg bisect: option --good not recognized 4 hg bisect [help|init|reset|next|good|bad] 5 6 Dichotomic search in the DAG of changesets 7 8 This extension helps to find changesets which cause problems. 9 To use, mark the earliest changeset you know introduces the problem 10 as bad, then mark the latest changeset which is free from the problem 11 as good. Bisect will update your working directory to a revision for 12 testing. Once you have performed tests, mark the working directory 13 as bad or good and bisect will either update to another candidate 14 changeset or announce that it has found the bad revision. 15 16 Note: bisect expects bad revisions to be descendants of good revisions. 17 If you are looking for the point at which a problem was fixed, then make 18 the problem-free state "bad" and the problematic state "good." 19 20 For subcommands see "hg bisect help" 21 22 use "hg -v help bisect" to show global options Обратите внимание, эта команда что-то выводит [на экран] * Он рассказал нам, как много ревизий он должен рассматривать прежде чем он сможет определить, где введена одна ошибка, и как много тестов, потребуется. * Он обновлил рабочий каталог до следующей ревизии для тестирования, и рассказал нам, какую ревизию будем тестировать. Теперь запустим наш тест в рабочием каталоге. Мы используем команду grep, чтобы увидеть, находится ли наш "плохой" файл в рабочей директории. Если это так, эта ревизия является плохой, если нет - хорошей. 1 $ if grep -q 'i have a gub' * 2 > then 3 > result=bad 4 > else 5 > result=good 6 > fi 7 $ echo this revision is $result 8 this revision is bad 9 $ hg bisect --$result 10 extension 'hbisect' overrides commands: bisect 11 hg bisect: option --bad not recognized 12 hg bisect [help|init|reset|next|good|bad] 13 14 Dichotomic search in the DAG of changesets 15 16 This extension helps to find changesets which cause problems. 17 To use, mark the earliest changeset you know introduces the problem 18 as bad, then mark the latest changeset which is free from the problem 19 as good. Bisect will update your working directory to a revision for 20 testing. Once you have performed tests, mark the working directory 21 as bad or good and bisect will either update to another candidate 22 changeset or announce that it has found the bad revision. 23 24 Note: bisect expects bad revisions to be descendants of good revisions. 25 If you are looking for the point at which a problem was fixed, then make 26 the problem-free state "bad" and the problematic state "good." 27 28 For subcommands see "hg bisect help" 29 30 use "hg -v help bisect" to show global options Этот тест выглядит как идеальный кандидат для автоматизации, так что давайте превратим его в функцию shell. 1 $ mytest() { 2 > if grep -q 'i have a gub' * 3 > then 4 > result=bad 5 > else 6 > result=good 7 > fi 8 > echo this revision is $result 9 > hg bisect --$result 10 > } Теперь мы можем запустить весь шаг тестирования с одной командой, mytest. 1 $ mytest 2 this revision is bad 3 extension 'hbisect' overrides commands: bisect hg bisect: опция --bad не распознана hg bisect [help|init|reset|next|good|bad] 6 7 Dichotomic search in the DAG of changesets 8 9 This extension helps to find changesets which cause problems. 10 To use, mark the earliest changeset you know introduces the problem 11 as bad, then mark the latest changeset which is free from the problem 12 as good. Bisect will update your working directory to a revision for 13 testing. Once you have performed tests, mark the working directory 14 as bad or good and bisect will either update to another candidate 15 changeset or announce that it has found the bad revision. 16 17 Note: bisect expects bad revisions to be descendants of good revisions. Если вы ищете точку, в которой проблемы были исправлены, то сделайте 19 the problem-free state "bad" and the problematic state "good." 20 21 For subcommands see "hg bisect help" 22 используйте "hg -v help bisect", чтобы увидеть глобальные опции Еще несколько вызовов нашей команды "шаг испытаний", и мы закончили. 1 $ mytest эта ревизия плоха 3 extension 'hbisect' overrides commands: bisect hg bisect: опция --bad не распознана hg bisect [help|init|reset|next|good|bad] 6 7 Dichotomic search in the DAG of changesets 8 9 This extension helps to find changesets which cause problems. 10 To use, mark the earliest changeset you know introduces the problem 11 as bad, then mark the latest changeset which is free from the problem 12 as good. Bisect will update your working directory to a revision for 13 testing. Once you have performed tests, mark the working directory 14 as bad or good and bisect will either update to another candidate 15 changeset or announce that it has found the bad revision. 16 17 Note: bisect expects bad revisions to be descendants of good revisions. 18 If you are looking for the point at which a problem was fixed, then make 19 the problem-free state "bad" and the problematic state "good." 20 21 For subcommands see "hg bisect help" 22 23 use "hg -v help bisect" to show global options 24 $ mytest 25 this revision is bad 26 extension 'hbisect' overrides commands: bisect 27 hg bisect: option --bad not recognized 28 hg bisect [help|init|reset|next|good|bad] 29 30 Dichotomic search in the DAG of changesets 31 32 This extension helps to find changesets which cause problems. 33 To use, mark the earliest changeset you know introduces the problem 34 as bad, then mark the latest changeset which is free from the problem 35 as good. Bisect will update your working directory to a revision for 36 testing. Once you have performed tests, mark the working directory 37 as bad or good and bisect will either update to another candidate 38 changeset or announce that it has found the bad revision. 39 40 Note: bisect expects bad revisions to be descendants of good revisions. 41 If you are looking for the point at which a problem was fixed, then make 42 the problem-free state "bad" and the problematic state "good." 43 44 For subcommands see "hg bisect help" 45 46 use "hg -v help bisect" to show global options 47 $ mytest 48 this revision is bad 49 extension 'hbisect' overrides commands: bisect 50 hg bisect: option --bad not recognized 51 hg bisect [help|init|reset|next|good|bad] 52 53 Dichotomic search in the DAG of changesets 54 55 This extension helps to find changesets which cause problems. 56 To use, mark the earliest changeset you know introduces the problem 57 as bad, then mark the latest changeset which is free from the problem 58 as good. Bisect will update your working directory to a revision for 59 testing. Once you have performed tests, mark the working directory 60 as bad or good and bisect will either update to another candidate 61 changeset or announce that it has found the bad revision. 62 63 Note: bisect expects bad revisions to be descendants of good revisions. 64 If you are looking for the point at which a problem was fixed, then make 65 the problem-free state "bad" and the problematic state "good." 66 67 For subcommands see "hg bisect help" 68 69 use "hg -v help bisect" to show global options Хотя у нас было 40 ревизий для поиска, "hg bisect" сумел найти "ошибку" с пяти испытаний. Поскольку количество тестов, для "hg bisect" растет логарифмически с числом ревизий для поиска, то преимущество перед "методом грубой силы" увеличивается с каждой добавленной ревизией. 9.5.2 Очистка после поиска Когда вы закончили работать с "hg bisect", вы можете использовать "hg bisect --reset" чтобы сбросить информацию, которую он использовал для проведения вашего поиска. Команда не использует много места, поэтому не страшно, если вы забыли запустить эту команду. Тем не менее, "hg bisect" не позволит вам начать новый поиск в этом хранилище, пока вы cделаете "hg bisect --reset". 1 $ hg bisect --reset 2 extension 'hbisect' overrides commands: bisect 3 hg bisect: option --reset not recognized 4 hg bisect [help|init|reset|next|good|bad] 5 6 Dichotomic search in the DAG of changesets 7 8 This extension helps to find changesets which cause problems. 9 To use, mark the earliest changeset you know introduces the problem 10 as bad, then mark the latest changeset which is free from the problem 11 as good. Bisect will update your working directory to a revision for 12 testing. Once you have performed tests, mark the working directory 13 as bad or good and bisect will either update to another candidate 14 changeset or announce that it has found the bad revision. 15 16 Note: bisect expects bad revisions to be descendants of good revisions. 17 If you are looking for the point at which a problem was fixed, then make 18 the problem-free state "bad" and the problematic state "good." 19 20 For subcommands see "hg bisect help" 21 22 use "hg -v help bisect" to show global options 9.6 Советы для эффективного поиска ошибок 9.6.1 Давайте согласованный ввод Команда "hg bisect" требует, чтобы вы правильно сообщали о результатах каждого теста, который вы выполняете. Если вы скажете, что это испытание не прошло, когда это действительно удалось, иногда можно обнаружить несоответствие. Если удалось определить несоответствия в Ваших "отчетах", Меркуриал сообщит Вам, что некоторая ревизия и хорошая и плохая. Однако, он не может делать это всегда, чаще он укажет на неправильную ревизию в качестве источника ошибки. 9.6.2 Автоматизируйте как можно больше When I started using the “hg bisect” command, I tried a few times to run my tests by hand, on the command line. This is an approach that I, at least, am not suited to. After a few tries, I found that I was making enough mistakes that I was having to restart my searches several times before finally getting correct results. My initial problems with driving the “hg bisect” command by hand occurred even with simple searches on small repositories; if the problem you’re looking for is more subtle, or the number of tests that “hg bisect” must perform increases, the likelihood of operator error ruining the search is much higher. Once I started automating my tests, I had much better results. The key to automated testing is twofold: * always test for the same symptom, and * always feed consistent input to the “hg bisect” command. In my tutorial example above, the grep command tests for the symptom, and the if statement takes the result of this check and ensures that we always feed the same input to the “hg bisect” command. The mytest function marries these together in a reproducible way, so that every test is uniform and consistent. 9.6.3 Check your results Because the output of a “hg bisect” search is only as good as the input you give it, don’t take the changeset it reports as the absolute truth. A simple way to cross-check its report is to manually run your test at each of the following changesets: * The changeset that it reports as the first bad revision. Your test should still report this as bad. * The parent of that changeset (either parent, if it’s a merge). Your test should report this changeset as good. * A child of that changeset. Your test should report this changeset as bad. 9.6.4 Beware interference between bugs It’s possible that your search for one bug could be disrupted by the presence of another. For example, let’s say your software crashes at revision 100, and worked correctly at revision 50. Unknown to you, someone else introduced a different crashing bug at revision 60, and fixed it at revision 80. This could distort your results in one of several ways. It is possible that this other bug completely “masks” yours, which is to say that it occurs before your bug has a chance to manifest itself. If you can’t avoid that other bug (for example, it prevents your project from building), and so can’t tell whether your bug is present in a particular changeset, the “hg bisect” command cannot help you directly. Instead, you can mark a changeset as untested by running “hg bisect --skip”. A different problem could arise if your test for a bug’s presence is not specific enough. If you check for “my program crashes”, then both your crashing bug and an unrelated crashing bug that masks it will look like the same thing, and mislead “hg bisect”. Another useful situation in which to use “hg bisect --skip” is if you can’t test a revision because your project was in a broken and hence untestable state at that revision, perhaps because someone checked in a change that prevented the project from building. 9.6.5 Bracket your search lazily Choosing the first “good” and “bad” changesets that will mark the end points of your search is often easy, but it bears a little discussion nevertheless. From the perspective of “hg bisect”, the “newest” changeset is conventionally “bad”, and the older changeset is “good”. If you’re having trouble remembering when a suitable “good” change was, so that you can tell “hg bisect”, you could do worse than testing changesets at random. Just remember to eliminate contenders that can’t possibly exhibit the bug (perhaps because the feature with the bug isn’t present yet) and those where another problem masks the bug (as I discussed above). Even if you end up “early” by thousands of changesets or months of history, you will only add a handful of tests to the total number that “hg bisect” must perform, thanks to its logarithmic behaviour. Глава 10 Обработка событий в хранилище с помощью ловушек Mercurial предлагает мощный механизм, позволяющий автоматизировать действия при возникновении в хранилище каких-либо событий. В некоторых случаях Вы даже можете управлять реакцией Mercurial на эти события. Mercurial использует для этих действий название ловушка (hook). Ловушки в некоторых системах управления версиями называются "триггерами", но оба этих названия относятся к одной и той же идее. 10.1 Обзор ловушек Mercurial Это краткий список ловушек, поддерживаемых Mercurial. Мы подробно опишем каждую из ловушек позднее, в разделе 10.8. * changegroup: Выполняется после группы изменений, внесённых в хранилище извне. * commit: Выполняется после создания нового набора изменений в локальном хранилище. * incoming: Однократно выполняется для каждого нового набора изменений, внесённого в хранилище извне. Примечание: отличается от changegroup тем, что выполняется однократно перед внесением группы изменений. * outgoing: Выполняется после передачи группы изменений из этого хранилища. * prechangegroup: Выполняемся перед началом приёма группы изменений в хранилище. * precommit: Управляющая. Выполняется перед началом коммита. * preoutgoing: Управляющая. Выполняется перед началом передачи группы изменений из этого хранилища. * pretag: Управляющая. Выполняется перед созданием tag'а. * pretxnchangegroup: Управляющая. Выполняется после группы изменений, принятых в локальное хранилище от других, но до окончательной обработки транзакции в хранилище, которая сделает изменения постоянными. * pretxncommit: Управляющая. Выполняется после создания нового набора изменений в локальном хранилище, но перед окончательным постоянным внесением. * preupdate: Управляющая. Выполняется перед началом update или merge рабочей директории. * tag: Выполняется после создания tag'а. * update: Выполняется после завершения update или merge рабочей директории. Каждая из ловушек, описанная как "Управляющая", определяет - продолжать ли действие. Если ловушка отработала успешно, действие продолжается. Иначе действие не разрешается или не завершается, в зависимости от ловушки. 10.2 Ловушки и безопасность 10.2.1 Ловушки выполняются с Вашими привелегиями При запуске Вами команд Mercurial в хранилище и команд, вызывающих выполнение ловушек, эти ловушки выполняются на Вашей системе, в Вашем аккаунте и с Вашим уровнем доступа. Ловушки частично состоят из исполняемого кода, так что относитесь к ним с достаточным подозрением. Не устанавливайте ловушку, если не знаете Кто и Зачем её написал и Что она делает. В некоторых случаях Вы можете столкнуться с ловушками, которые Вы не устанавливали себе. Если вы работаете на незнакомой системе, Mercurial может выполнять ловушки, определённые в системном глобальном hgrc файле. Если вы работаете с хранилищем, принадлежащем другому пользователю, Mercurial может запускать ловушки, определённые в пользовательском хранилище, но он будет работать от "Вас". Например, если Вы выполняете "hg pull" из этого хранилища, а его .hg/hgrc определяет локальные outgoing ловушки, то ловушка будет работать под вашей учетной записью пользователя, даже если вы не владелец хранилища. Примечание: Это применимо только если Вы забираете изменения из хранилища в локальной или сетевой файловой системе. Если Вы забираете изменения по http или ssh, то любая outgoing ловушка будет запущена под аккаунтом, из-под которого выполняется серверный процесс, на сервере. Для просмотра ловушек, определённых в хранилище, используйте команду "hg showconfig hooks". Если Вы работаете в одном хранилище, но общаетесь с другим, которым не владеете (например, используете "hg pull" или "hg incoming"), запомните что это ловушки другого хранилища, не Ваши, и Вы должны их проверить. 10.2.2. Ловушки не распространяются В Mercurial ловушки не попадают под контроль ревизий и не распространяются, когда Вы клонируете хранилище, или забираете изменения из хранилища. Причина этого проста: ловушка это произвольный кусок исполняевого кода. Она работает от Вашей учётной записи, с Вашим уровнем привелегий, на Вашей машине. В любой распределенной системе контроля версий было бы крайне безрассудно реализовывать версионноконтролируемые ловушки, так как это даёт легкий способ компроментации аккаунтов пользователей системы управления версиями. Mercurial не распространяет ловушки, и, при работе с другими людьми над совместным проектом, Вы не должны полагать, что они используют те же ловушки Mercurial, что и Вы, или что они их правильно настроили. Вы должны документировать ловушки, если ожидаете их использование другими. В корпоративной интрасети это несколько легче контролировать, так как Вы можете, например, создать "стандартную" установку Mercurial на файловой системе NFS и использовать общесистемный hgrc файл для определения ловушек для всех пользователей. Тем не менее, здесь тоже есть ограничения; см. ниже. 10.2.3. Возможно переопределение ловушек Mercurial позволяет изменить ловушку, переопределив её. Вы можете отключить ловушку, установив значение равным пустой строке, или по своему желанию изменить её поведение. Вы должны понимать при распространении общесистемного или общесайтового hgrc файла, определяющего какие-либо ловушки, что пользователи могут отключать или переопределять эти ловушки. 10.2.4. Обеспечение выполнения критических ловушек Иногда Вам может понадобится соблюдение политики, и Вы не хотите, чтобы другие могли её обойти. Например, требование, что каждое изменение должно проходить строгий набор тестов. Определение этого требования с помощью ловушки в общесайтовом hgrc не будет работать для удаленных пользователей с ноутбуками, и, конечно, локальные пользователи также могут её обойти, переопределив ловушку. Вместо этого, Вы можете настроить политику использования Mercurial так, чтобы люди распространяли изменения через хорошо известный "канонический" сервер, который защищён и должным образом настроен. Один из способов осуществления этого - сочетание социальной инженерии и технологии. Настройте ограниченный доступ к учетной записи: пользователи могут отправлять изменения по сети в хранилище этой учетной записи, но не могут входить в аккаунт и запускать команды оболочки. В этом случае пользователи смогут отправлять изменения, содержащие любое любимое ими старьё. Когда кто-то отправляет изменения на сервер, с которого все их забирают, то сервер проверит набор изменений, прежде чем они будут приняты как постоянные, и отвергнет его, если набор изменений не пройдёт испытаний. Если человек только забирает изменения с этого фильтрующего сервера, то у него будет уверенность, что все им забираемые изменения были автоматически проверены. 10.3 Внимательность с pretxn ловушками в общедоступном хранилище Если вы хотите использовать ловушки для выполнения различной автоматизированной работы с хранилищем, к которому имеют открытый доступ несколько людей, вы должны быть внимательны делая это. Mercurial only locks a repository when it is writing to the repository, and only the parts of Mercurial that write to the repository pay attention to locks. Write locks are necessary to prevent multiple simultaneous writers from scribbling on each other’s work, corrupting the repository. Because Mercurial is careful with the order in which it reads and writes data, it does not need to acquire a lock when it wants to read data from the repository. The parts of Mercurial that read from the repository never pay attention to locks. This lockless reading scheme greatly increases performance and concurrency. With great performance comes a trade-off, though, one which has the potential to cause you trouble unless you’re aware of it. To describe this requires a little detail about how Mercurial adds changesets to a repository and reads those changes. When Mercurial writes metadata, it writes it straight into the destination file. It writes file data first, then manifest data (which contains pointers to the new file data), then changelog data (which contains pointers to the new manifest data). Before the first write to each file, it stores a record of where the end of the file was in its transaction log. If the transaction must be rolled back, Mercurial simply truncates each file back to the size it was before the transaction began. Когда Mercurial читает метаданные, сперва она читает журнал изменений, а затем все остальное. Поскольку у читателя будет доступ только к частям манифеста или файла метаданных, которые можно увидеть в журнале, он никогда не сможет увидеть частично записанные данные. Некоторые контролирующие ловушки (pretxncommit и pretxnchangegroup) запускаются, когда транзакция почти завершена. Все метаданные записаны, но Mercurial все равно может откатить назад и новые записанные данные исчезнут. Если одна из этих ловушек выполняется достаточно продолжительное время, то открывается "временнОе окно", через которое можно прочитать метаданные для набора изменений, которые ещё не приняты как постоянные и не должны рассматриваться как внесённые, но выглядят как настоящие. Чем дольше выполняется такая ловушка, тем дольше доступно такое окно. 10.3.1. Описание проблемы "временного окна" In principle, a good use for the pretxnchangegroup hook would be to automatically build and test incoming changes before they are accepted into a central repository. This could let you guarantee that nobody can push changes to this repository that “break the build”. But if a client can pull changes while they’re being tested, the usefulness of the test is zero; an unsuspecting someone can pull untested changes, potentially breaking their build. The safest technological answer to this challenge is to set up such a “gatekeeper” repository as unidirectional. Let it take changes pushed in from the outside, but do not allow anyone to pull changes from it (use the preoutgoing hook to lock it down). Configure a changegroup hook so that if a build or test succeeds, the hook will push the new changes out to another repository that people can pull from. In practice, putting a centralised bottleneck like this in place is not often a good idea, and transaction visibility has nothing to do with the problem. As the size of a project—and the time it takes to build and test—grows, you rapidly run into a wall with this “try before you buy” approach, where you have more changesets to test than time in which to deal with them. The inevitable result is frustration on the part of all involved. An approach that scales better is to get people to build and test before they push, then run automated builds and tests centrally after a push, to be sure all is well. The advantage of this approach is that it does not impose a limit on the rate at which the repository can accept changes. 10.4. Краткое руководство по использованию ловушек Напишем простую ловушку Mercurial. Начнём с ловушки, которая запускается при завершении “hg commit” и просто выводит хэш созданного Вами набора изменений. Ловушка называется commit. $ hg init hook-test $ cd hook-test $ echo '[hooks]' >> .hg/hgrc $ echo 'commit = echo committed $HG_NODE' >> .hg/hgrc $ cat .hg/hgrc [hooks] commit = echo committed $HG_NODE $ echo a > a $ hg add a $ hg commit -m 'testing commit hook' committed f03f7812cc0335ddc6daf5f5a760341cf420596d 10.1: Простая ловушка, выполняемая при передаче изменений Все ловушки следуют образцу в примере 10.1. Вы добавляете запись в секцию [hooks] вашего .hg/hgrc, слева имя триггера, справа выполняемое действие. Как вы видете, вы можете запустить произвольные команды оболочки в ловушке. Mercuria использует дополнительную информацию в ловушке использую переменные окружения. (Ищите HGt4ht@95xNODE в примере). 10.4.1 Performing multiple actions per event Quite often, you will want to define more than one hook for a particular kind of event, as shown in example 10.2. Mercurial lets you do this by adding an extension to the end of a hook’s name. You extend a hook’s name by giving the name of the hook, followed by a full stop (the “.” character), followed by some more text of your choosing. For example, Mercurial will run both commit.foo and commit.bar when the commit event occurs. $ echo 'commit.when = echo -n "date of commit: "; date' >> .hg/hgrc $ echo a >> a $ hg commit -m 'i have two hooks' committed 8f07c0fd923177a17a05a4f8d529db63fc643de9 date of commit: Thu Aug 21 18:22:21 GMT 2008 Figure 10.2: Defining a second commit hook To give a well-defined order of execution when there are multiple hooks defined for an event, Mercurial sorts hooks by extension, and executes the hook commands in this sorted order. In the above example, it will execute commit.bar before commit.foo, and commit before both. It is a good idea to use a somewhat descriptive extension when you define a new hook. This will help you to remember what the hook was for. If the hook fails, you’ll get an error message that contains the hook name and extension, so using a descriptive extension could give you an immediate hint as to why the hook failed (see section 10.4.2 for an example). 10.4.2 Controlling whether an activity can proceed In our earlier examples, we used the commit hook, which is run after a commit has completed. This is one of several Mercurial hooks that run after an activity finishes. Such hooks have no way of influencing the activity itself. Mercurial defines a number of events that occur before an activity starts; or after it starts, but before it finishes. Hooks that trigger on these events have the added ability to choose whether the activity can continue, or will abort. The pretxncommit hook runs after a commit has all but completed. In other words, the metadata representing the changeset has been written out to disk, but the transaction has not yet been allowed to complete. The pretxncommit hook has the ability to decide whether the transaction can complete, or must be rolled back. If the pretxncommit hook exits with a status code of zero, the transaction is allowed to complete; the commit finishes; and the commit hook is run. If the pretxncommit hook exits with a non-zero status code, the transaction is rolled back; the metadata representing the changeset is erased; and the commit hook is not run. $ cat check_bug_id #!/bin/sh # check that a commit comment mentions a numeric bug id hg log -r $1 --template {desc} | grep -q "\> .hg/hgrc $ echo a >> a $ hg commit -m 'i am not mentioning a bug id' transaction abort! rollback completed abort: pretxncommit.bug_id_required hook exited with status 1 $ hg commit -m 'i refer you to bug 666' committed 7f5326347c52aaed12d7fa424cab7716ffcd71d8 date of commit: Thu Aug 21 18:22:21 GMT 2008 10.3: Использование pretxncommit ловушки для контроля изменений Хук в примере 10.3 проверяет содержит ли комментарий в изменении ошибку ID. Если это произойдёт то изменение выполнится. Если нет, произойдёт откат изменения. 10.5 Написание собственных ловушек Когда вы запишете ловушку, вам может быть полезно запускать Mercurial с опцией -v, или указать в конфиге значение переменной verbose "true". Сделав это, Mercurial будет выводить сообщение перед каждым вызовом ловушки. 10.5.1 Choosing how your hook should run You can write a hook either as a normal program—typically a shell script—or as a Python function that is executed within the Mercurial process. Writing a hook as an external program has the advantage that it requires no knowledge of Mercurial’s internals. You can call normal Mercurial commands to get any added information you need. The trade-off is that external hooks are slower than in-process hooks. An in-process Python hook has complete access to the Mercurial API, and does not “shell out” to another process, so it is inherently faster than an external hook. It is also easier to obtain much of the information that a hook requires by using the Mercurial API than by running Mercurial commands. If you are comfortable with Python, or require high performance, writing your hooks in Python may be a good choice. However, when you have a straightforward hook to write and you don’t need to care about performance (probably the majority of hooks), a shell script is perfectly fine. 10.5.2 Hook parameters Mercurial calls each hook with a set of well-defined parameters. In Python, a parameter is passed as a keyword argument to your hook function. For an external program, a parameter is passed as an environment variable. Whether your hook is written in Python or as a shell script, the hook-specific parameter names and values will be the same. A boolean parameter will be represented as a boolean value in Python, but as the number 1 (for “true”) or 0 (for “false”) as an environment variable for an external hook. If a hook parameter is named foo, the keyword argument for a Python hook will also be named foo, while the environment variable for an external hook will be named HGt4ht@95xFOO. 10.5.3 Hook return values and activity control A hook that executes successfully must exit with a status of zero if external, or return boolean “false” if in-process. Failure is indicated with a non-zero exit status from an external hook, or an in-process hook returning boolean “true”. If an in-process hook raises an exception, the hook is considered to have failed. For a hook that controls whether an activity can proceed, zero/false means “allow”, while non-zero/true/exception means “deny”. 10.5.4. Написание внешних ловушек При определении внешней ловушки в hgrc и её запуске, значение ловушки передается Вашей командной оболочке, которая интерпретирует его. Это означает, что Вы можете использовать нормальные командные конструкции в теле ловушки. Для выполняемой ловушки всегда выставляется в качестве рабочей директории - корневая директория хранилища. Каждый параметр ловушки передаётся как переменная окружения. Имя приводится к верхнему регистру и к нему добавляется префикс "HG_". За исключением параметров ловушки, Mercurial не устанавливает и не изменяет переменные окружения при запуске ловушки. Это полезно помнить, если Вы пишете общесайтовые ловушки, которые могут быть запущены различными пользователями в различном программном окружении. При тестировании ловушки в многопользовательском режиме Вам не следует полаться на переменные окружения, существующие в Вашем окружении. 10.5.5 Telling Mercurial to use an in-process hook The hgrc syntax for defining an in-process hook is slightly different than for an executable hook. The value of the hook must start with the text “python:”, and continue with the fully-qualified name of a callable object to use as the hook’s value. The module in which a hook lives is automatically imported when a hook is run. So long as you have the module name and PYTHONPATH right, it should “just work”. The following hgrc example snippet illustrates the syntax and meaning of the notions we just described. 1 [hooks] 2 commit.example = python:mymodule.submodule.myhook When Mercurial runs the commit.example hook, it imports mymodule.submodule, looks for the callable object named myhook, and calls it. 10.5.6 Writing an in-process hook The simplest in-process hook does nothing, but illustrates the basic shape of the hook API: 1 def myhook(ui, repo, ⋆⋆kwargs): 2 pass The first argument to a Python hook is always a mercurial.ui.ui object. The second is a repository object; at the moment, it is always an instance of mercurial.localrepo.localrepository. Following these two arguments are other keyword arguments. Which ones are passed in depends on the hook being called, but a hook can ignore arguments it doesn’t care about by dropping them into a keyword argument dict, as with ⋆⋆kwargs above. 10.6 Пример некоторых хуков 10.6.1 Writing meaningful commit messages It’s hard to imagine a useful commit message being very short. The simple pretxncommit hook of figure 10.4 will prevent you from committing a changeset with a message that is less than ten bytes long. $ cat .hg/hgrc [hooks] pretxncommit.msglen = test ‘hg tip --template {desc} | wc -c‘ -ge 10 $ echo a > a $ hg add a $ hg commit -A -m 'too short' transaction abort! rollback completed abort: pretxncommit.msglen hook exited with status 1 $ hg commit -A -m 'long enough' Figure 10.4: A hook that forbids overly short commit messages 10.6.2 Checking for trailing whitespace An interesting use of a commit-related hook is to help you to write cleaner code. A simple example of “cleaner code” is the dictum that a change should not add any new lines of text that contain “trailing whitespace”. Trailing whitespace is a series of space and tab characters at the end of a line of text. In most cases, trailing whitespace is unnecessary, invisible noise, but it is occasionally problematic, and people often prefer to get rid of it. You can use either the precommit or pretxncommit hook to tell whether you have a trailing whitespace problem. If you use the precommit hook, the hook will not know which files you are committing, so it will have to check every modified file in the repository for trailing white space. If you want to commit a change to just the file foo, but the file bar contains trailing whitespace, doing a check in the precommit hook will prevent you from committing foo due to the problem with bar. This doesn’t seem right. Should you choose the pretxncommit hook, the check won’t occur until just before the transaction for the commit completes. This will allow you to check for problems only the exact files that are being committed. However, if you entered the commit message interactively and the hook fails, the transaction will roll back; you’ll have to re-enter the commit message after you fix the trailing whitespace and run “hg commit” again. 1 $ cat .hg/hgrc 2 [hooks] 3 pretxncommit.whitespace = hg export tip | (! egrep -q '̂∖+.⋆[ ∖t]$') 4 $ echo 'a ' > a 5 $ hg commit -A -m 'test with trailing whitespace' 6 adding a 7 transaction abort! 8 rollback completed 9 abort: pretxncommit.whitespace hook exited with status 1 10 $ echo 'a' > a 11 $ hg commit -A -m 'drop trailing whitespace and try again' Figure 10.5: A simple hook that checks for trailing whitespace Figure 10.5 introduces a simple pretxncommit hook that checks for trailing whitespace. This hook is short, but not very helpful. It exits with an error status if a change adds a line with trailing whitespace to any file, but does not print any information that might help us to identify the offending file or line. It also has the nice property of not paying attention to unmodified lines; only lines that introduce new trailing whitespace cause problems. 1 $ cat .hg/hgrc 2 [hooks] 3 pretxncommit.whitespace = .hg/check_whitespace.py 4 $ echo 'a ' >> a 5 $ hg commit -A -m 'add new line with trailing whitespace' 6 a, line 2: trailing whitespace added 7 commit message saved to .hg/commit.save 8 transaction abort! 9 rollback completed 10 abort: pretxncommit.whitespace hook exited with status 1 11 $ sed -i 's, ⋆$,,' a 12 $ hg commit -A -m 'trimmed trailing whitespace' 13 a, line 2: trailing whitespace added 14 commit message saved to .hg/commit.save 15 transaction abort! 16 rollback completed 17 abort: pretxncommit.whitespace hook exited with status 1 Figure 10.6: A better trailing whitespace hook The example of figure 10.6 is much more complex, but also more useful. It parses a unified diff to see if any lines add trailing whitespace, and prints the name of the file and the line number of each such occurrence. Even better, if the change adds trailing whitespace, this hook saves the commit comment and prints the name of the save file before exiting and telling Mercurial to roll the transaction back, so you can use “hg commit -l filename” to reuse the saved commit message once you’ve corrected the problem. As a final aside, note in figure 10.6 the use of perl’s in-place editing feature to get rid of trailing whitespace from a file. This is concise and useful enough that I will reproduce it here. 1 perl -pi -e 's, s+$,,' filename 10.7. Встроенные ловушки Mercurial ships with several bundled hooks. You can find them in the hgext directory of a Mercurial source tree. If you are using a Mercurial binary package, the hooks will be located in the hgext directory of wherever your package installer put Mercurial. 10.7.1 acl—access control for parts of a repository The acl extension lets you control which remote users are allowed to push changesets to a networked server. You can protect any portion of a repository (including the entire repo), so that a specific remote user can push changes that do not affect the protected portion. This extension implements access control based on the identity of the user performing a push, not on who committed the changesets they’re pushing. It makes sense to use this hook only if you have a locked-down server environment that authenticates remote users, and you want to be sure that only specific users are allowed to push changes to that server. Configuring the acl hook In order to manage incoming changesets, the acl hook must be used as a pretxnchangegroup hook. This lets it see which files are modified by each incoming changeset, and roll back a group of changesets if they modify “forbidden” files. Example: 1 [hooks] 2 pretxnchangegroup.acl = python:hgext.acl.hook The acl extension is configured using three sections. The [acl] section has only one entry, sources, which lists the sources of incoming changesets that the hook should pay attention to. You don’t normally need to configure this section. * serve Control incoming changesets that are arriving from a remote repository over http or ssh. This is the default value of sources, and usually the only setting you’ll need for this configuration item. * pull Control incoming changesets that are arriving via a pull from a local repository. * push Control incoming changesets that are arriving via a push from a local repository. * bundle Control incoming changesets that are arriving from another repository via a bundle. The [acl.allow] section controls the users that are allowed to add changesets to the repository. If this section is not present, all users that are not explicitly denied are allowed. If this section is present, all users that are not explicitly allowed are denied (so an empty section means that all users are denied). The [acl.deny] section determines which users are denied from adding changesets to the repository. If this section is not present or is empty, no users are denied. The syntaxes for the [acl.allow] and [acl.deny] sections are identical. On the left of each entry is a glob pattern that matches files or directories, relative to the root of the repository; on the right, a user name. In the following example, the user docwriter can only push changes to the docs subtree of the repository, while intern can push changes to any file or directory except source/sensitive. 1 [acl.allow] 2 docs/⋆⋆ = docwriter 3 4 [acl.deny] 5 source/sensitive/⋆⋆ = intern Testing and troubleshooting If you want to test the acl hook, run it with Mercurial’s debugging output enabled. Since you’ll probably be running it on a server where it’s not convenient (or sometimes possible) to pass in the --debug option, don’t forget that you can enable debugging output in your hgrc: 1 [ui] 2 debug = true With this enabled, the acl hook will print enough information to let you figure out why it is allowing or forbidding pushes from specific users. 10.7.2 bugzilla—integration with Bugzilla The bugzilla extension adds a comment to a Bugzilla bug whenever it finds a reference to that bug ID in a commit comment. You can install this hook on a shared server, so that any time a remote user pushes changes to this server, the hook gets run. It adds a comment to the bug that looks like this (you can configure the contents of the comment—see below): 1 Changeset aad8b264143a, made by Joe User in 2 the frobnitz repository, refers to this bug. 3 4 For complete details, see 5 http://hg.domain.com/frobnitz?cmd=changeset;node=aad8b264143a 6 7 Changeset description: 8 Fix bug 10483 by guarding against some NULL pointers The value of this hook is that it automates the process of updating a bug any time a changeset refers to it. If you configure the hook properly, it makes it easy for people to browse straight from a Bugzilla bug to a changeset that refers to that bug. You can use the code in this hook as a starting point for some more exotic Bugzilla integration recipes. Here are a few possibilities: * Require that every changeset pushed to the server have a valid bug ID in its commit comment. In this case, you’d want to configure the hook as a pretxncommit hook. This would allow the hook to reject changes that didn’t contain bug IDs. * Allow incoming changesets to automatically modify the state of a bug, as well as simply adding a comment. For example, the hook could recognise the string “fixed bug 31337” as indicating that it should update the state of bug 31337 to “requires testing”. Configuring the bugzilla hook You should configure this hook in your server’s hgrc as an incoming hook, for example as follows: 1 [hooks] 2 incoming.bugzilla = python:hgext.bugzilla.hook Because of the specialised nature of this hook, and because Bugzilla was not written with this kind of integration in mind, configuring this hook is a somewhat involved process. Before you begin, you must install the MySQL bindings for Python on the host(s) where you’ll be running the hook. If this is not available as a binary package for your system, you can download it from [Dus]. Configuration information for this hook lives in the [bugzilla] section of your hgrc. * version The version of Bugzilla installed on the server. The database schema that Bugzilla uses changes occasionally, so this hook has to know exactly which schema to use. At the moment, the only version supported is 2.16. * host The hostname of the MySQL server that stores your Bugzilla data. The database must be configured to allow connections from whatever host you are running the bugzilla hook on. * user The username with which to connect to the MySQL server. The database must be configured to allow this user to connect from whatever host you are running the bugzilla hook on. This user must be able to access and modify Bugzilla tables. The default value of this item is bugs, which is the standard name of the Bugzilla user in a MySQL database. * password The MySQL password for the user you configured above. This is stored as plain text, so you should make sure that unauthorised users cannot read the hgrc file where you store this information. * db The name of the Bugzilla database on the MySQL server. The default value of this item is bugs, which is the standard name of the MySQL database where Bugzilla stores its data. * notify If you want Bugzilla to send out a notification email to subscribers after this hook has added a comment to a bug, you will need this hook to run a command whenever it updates the database. The command to run depends on where you have installed Bugzilla, but it will typically look something like this, if you have Bugzilla installed in /var/www/html/bugzilla: 1 cd /var/www/html/bugzilla && ./processmail %s nobody@nowhere.com The Bugzilla processmail program expects to be given a bug ID (the hook replaces “%s” with the bug ID) and an email address. It also expects to be able to write to some files in the directory that it runs in. If Bugzilla and this hook are not installed on the same machine, you will need to find a way to run processmail on the server where Bugzilla is installed. Mapping committer names to Bugzilla user names By default, the bugzilla hook tries to use the email address of a changeset’s committer as the Bugzilla user name with which to update a bug. If this does not suit your needs, you can map committer email addresses to Bugzilla user names using a [usermap] section. Each item in the [usermap] section contains an email address on the left, and a Bugzilla user name on the right. 1 [usermap] 2 jane.user@example.com = jane You can either keep the [usermap] data in a normal hgrc, or tell the bugzilla hook to read the information from an external usermap file. In the latter case, you can store usermap data by itself in (for example) a user-modifiable repository. This makes it possible to let your users maintain their own usermap entries. The main hgrc file might look like this: 1 # regular hgrc file refers to external usermap file 2 [bugzilla] 3 usermap = /home/hg/repos/userdata/bugzilla-usermap.conf While the usermap file that it refers to might look like this: 1 # bugzilla-usermap.conf - inside a hg repository 2 [usermap] 3 stephanie@example.com = steph Configuring the text that gets added to a bug You can configure the text that this hook adds as a comment; you specify it in the form of a Mercurial template. Several hgrc entries (still in the [bugzilla] section) control this behaviour. * strip The number of leading path elements to strip from a repository’s path name to construct a partial path for a URL. For example, if the repositories on your server live under /home/hg/repos, and you have a repository whose path is /home/hg/repos/app/tests, then setting strip to 4 will give a partial path of app/tests. The hook will make this partial path available when expanding a template, as webroot. * template The text of the template to use. In addition to the usual changeset-related variables, this template can use hgweb (the value of the hgweb configuration item above) and webroot (the path constructed using strip above). In addition, you can add a baseurl item to the [web] section of your hgrc. The bugzilla hook will make this available when expanding a template, as the base string to use when constructing a URL that will let users browse from a Bugzilla comment to view a changeset. Example: 1 [web] 2 baseurl = http://hg.domain.com/ Here is an example set of bugzilla hook config information. 1 [bugzilla] 2 host = bugzilla.example.com 3 password = mypassword 4 version = 2.16 5 # server-side repos live in /home/hg/repos, so strip 4 leading 6 # separators 7 strip = 4 8 hgweb = http://hg.example.com/ 9 usermap = /home/hg/repos/notify/bugzilla.conf 10 template = Changeset {node|short}, made by {author} in the {webroot} 11 repo, refers to this bug. nFor complete details, see 12 {hgweb}{webroot}?cmd=changeset;node={node|short} nChangeset 13 description: n t{desc|tabindent} Testing and troubleshooting The most common problems with configuring the bugzilla hook relate to running Bugzilla’s processmail script and mapping committer names to user names. Recall from section 10.7.2 above that the user that runs the Mercurial process on the server is also the one that will run the processmail script. The processmail script sometimes causes Bugzilla to write to files in its configuration directory, and Bugzilla’s configuration files are usually owned by the user that your web server runs under. You can cause processmail to be run with the suitable user’s identity using the sudo command. Here is an example entry for a sudoers file. 1 hg_user = (httpd_user) NOPASSWD: /var/www/html/bugzilla/processmail-wrapper %s This allows the hgt4ht@95xuser user to run a processmail-wrapper program under the identity of httpdt4ht@95xuser. This indirection through a wrapper script is necessary, because processmail expects to be run with its current directory set to wherever you installed Bugzilla; you can’t specify that kind of constraint in a sudoers file. The contents of the wrapper script are simple: 1 #!/bin/sh 2 cd ‘dirname $0‘ && ./processmail "$1" nobody@example.com It doesn’t seem to matter what email address you pass to processmail. If your [usermap] is not set up correctly, users will see an error message from the bugzilla hook when they push changes to the server. The error message will look like this: 1 cannot find bugzilla user id for john.q.public@example.com What this means is that the committer’s address, john.q.public@example.com, is not a valid Bugzilla user name, nor does it have an entry in your [usermap] that maps it to a valid Bugzilla user name. 10.7.3 notify—send email notifications Although Mercurial’s built-in web server provides RSS feeds of changes in every repository, many people prefer to receive change notifications via email. The notify hook lets you send out notifications to a set of email addresses whenever changesets arrive that those subscribers are interested in. As with the bugzilla hook, the notify hook is template-driven, so you can customise the contents of the notification messages that it sends. By default, the notify hook includes a diff of every changeset that it sends out; you can limit the size of the diff, or turn this feature off entirely. It is useful for letting subscribers review changes immediately, rather than clicking to follow a URL. Configuring the notify hook You can set up the notify hook to send one email message per incoming changeset, or one per incoming group of changesets (all those that arrived in a single pull or push). [hooks] # отправлять одно электронное письмо для каждой группы изменений changegroup.notify = python:hgext.notify.hook # отправлять одно электронное письмо для каждого изменения incoming.notify = python:hgext.notify.hook Configuration information for this hook lives in the [notify] section of a hgrc file. * test By default, this hook does not send out email at all; instead, it prints the message that it would send. Set this item to false to allow email to be sent. The reason that sending of email is turned off by default is that it takes several tries to configure this extension exactly as you would like, and it would be bad form to spam subscribers with a number of “broken” notifications while you debug your configuration. * config The path to a configuration file that contains subscription information. This is kept separate from the main hgrc so that you can maintain it in a repository of its own. People can then clone that repository, update their subscriptions, and push the changes back to your server. * strip The number of leading path separator characters to strip from a repository’s path, when deciding whether a repository has subscribers. For example, if the repositories on your server live in /home/hg/repos, and notify is considering a repository named /home/hg/repos/shared/test, setting strip to 4 will cause notify to trim the path it considers down to shared/test, and it will match subscribers against that. * template The template text to use when sending messages. This specifies both the contents of the message header and its body. * maxdiff The maximum number of lines of diff data to append to the end of a message. If a diff is longer than this, it is truncated. By default, this is set to 300. Set this to 0 to omit diffs from notification emails. * sources A list of sources of changesets to consider. This lets you limit notify to only sending out email about changes that remote users pushed into this repository via a server, for example. See section 10.8.3 for the sources you can specify here. If you set the baseurl item in the [web] section, you can use it in a template; it will be available as webroot. Here is an example set of notify configuration information. 1 [notify] 2 # really send email 3 test = false 4 # subscriber data lives in the notify repo 5 config = /home/hg/repos/notify/notify.conf 6 # repos live in /home/hg/repos on server, so strip 4 "/" chars 7 strip = 4 8 template = X-Hg-Repo: {webroot} 9 Subject: {webroot}: {desc|firstline|strip} 10 From: {author} 11 12 changeset {node|short} in {root} 13 details: {baseurl}{webroot}?cmd=changeset;node={node|short} 14 description: 15 {desc|tabindent|strip} 16 17 [web] 18 baseurl = http://hg.example.com/ This will produce a message that looks like the following: 1 X-Hg-Repo: tests/slave 2 Subject: tests/slave: Handle error case when slave has no buffers 3 Date: Wed, 2 Aug 2006 15:25:46 -0700 (PDT) 4 5 changeset 3cba9bfe74b5 in /home/hg/repos/tests/slave 6 details: http://hg.example.com/tests/slave?cmd=changeset;node=3cba9bfe74b5 7 description: 8 Handle error case when slave has no buffers 9 diffs (54 lines): 10 11 diff -r 9d95df7cf2ad -r 3cba9bfe74b5 include/tests.h 12 --- a/include/tests.h Wed Aug 02 15:19:52 2006 -0700 13 +++ b/include/tests.h Wed Aug 02 15:25:26 2006 -0700 14 @@ -212,6 +212,15 @@ static __inline__ void test_headers(void ⋆h) 15 [...snip...] Тестирование и поиск ошибок Вы не должны забывать, что по умолчанию уведомляющие расширения не будут посылать никакие e-mail, до тех пор вы явно не сконфигурируете такое поведение, установив test в false. До тех пор пока вы это не сделаете, отсылаемое сообщение будет просто выводиться. 10.8. Информация для разработчиков ловушек 10.8.1 Выполнение внутрипроцессорых ловушек Все внутрипроцеcсные ловушки вызываются с аргументами в следующей форме: 1 def myhook(ui, repo,**kwargs): 2 pass Здесь параметр ui это объект mercurial.ui.ui, параметр repo - объект mercurial.localrepo.localrepository. Имена и значения параметров ** kwargs зависят от вызываемой ловушки и обладают общими чертами: Если параметр называется node или parentN, он будет содержать шестнадцатеричный ID набора изменений. Пустая строка используется для указания "null" (исходного) набора изменений, вместо строки нулей. * Если параметр называется url, он содержит URL удаленного репозитория, если он может быть определен. * Содержащие булевые значения параметры представлены в виде обектов bool Python-а. Внепроцессорные ловушки вызываются без смены рабочей директории процесса (в отличие от внешних ловушек, которые запускаются в корне репозитория). Они также не должны менять рабочую директорию процесса, в противном случае любые вызовы Mercurial API потерпят неудачу. Если ловушка возвращает булевое значение "false", считается что вызов прошел успешно. Если возвращается "true" или генерируется исключение, считается что вызов завершился неудачей. Полезно думать о соглашении о вызовах как "скажи мне, если ты потерпел неудачу". Помните, что ID набора изменений передается в Python как шестнадцатеричная строка, а не как бинарный хеш, который использует API Mercurial. Для конвертации ее в бинарный вид используйте функцию mercurial.node.bin. 10.8.2. Выполнение внешних ловушек Внешние ловушки передаются командной оболочке пользователя, запустившего Mercurial. Доступны фичи командной оболочки, как, например, подстановка переменных и команды перенаправления. Ловушка запускается в корневой директории хранилища (в отличие от in-process ловушек, которые работают в той же директории из которой был запущен Mercurial). Параметры ловушке передаются как переменные среды. Каждое имя переменной преобразуется в верхний регистр с префиксом "HG_". Например, если имя параметра "node", имя переменной среды, представляющее параметр, будет "HG_NODE". Булевы параметры представляется в виде строки "1" для "истинно", "0" для "ложь". Если переменная окружения называется HG_NODE, HG_PARENT1 или HG_PARENT2, она содержит ID набора изменений в виде шестнадцатеричной строки. Для представления "null changeset ID" используется пустая строка вместо строки нулей. Если переменная окружения называется HG_URL, она будет содержать адрес удалённого хранилища, если его можно определить. Если ловушка завершается с кодом возврата, равным нулю, то это успешное завершениие. Если она завершилась с ненулевым статусом, то "всё" плохо. 10.8.3. Как определить, откуда пришли изменения Ловушку, которая срабатывает при перемещении набора изменений между локальным хранилищем и каким-либо другим можно настроить на нахождение информации "о той стороне". Mercurial знает как набор изменений передаётся, и , во многих случаях, куда и откуда они передаются. Источники изменений Mercurial will tell a hook what means are, or were, used to transfer changesets between repositories. This is provided by Mercurial in a Python parameter named source, or an environment variable named HGt4ht@95xSOURCE. * serve Changesets are transferred to or from a remote repository over http or ssh. * pull Changesets are being transferred via a pull from one repository into another. * push Changesets are being transferred via a push from one repository into another. * bundle Changesets are being transferred to or from a bundle. Where changes are going—remote repository URLs When possible, Mercurial will tell a hook the location of the “far side” of an activity that transfers changeset data between repositories. This is provided by Mercurial in a Python parameter named url, or an environment variable named HGt4ht@95xURL. Эта информация не всегда известна. Если ловушка вызывается в репозитории, работа с которым ведется по http или ssh, Mercurial не может сказать где расположен удаленный репозиторий, но может знать клиента, который подключен к нему. В этом случае URL будет представлен в одной из следующих форм: * remote:ssh:ip-address—удаленный ssh клиент по указанному адресу. * remote:http:ip-address — удаленный http клиент по указанному адресу. Если клиент использует SSL, то адрес будет в виде: https:ip-address. * Пустой адрес — информация о удаленном клиенте недоступна. 10.9. Ловушки. Описание. 10.9.1. changegroup—после внесения внешних наборов изменений This hook is run after a group of pre-existing changesets has been added to the repository, for example via a “hg pull” or “hg unbundle”. This hook is run once per operation that added one or more changesets. This is in contrast to the incoming hook, which is run once per changeset, regardless of whether the changesets arrive in a group. Some possible uses for this hook include kicking off an automated build or test of the added changesets, updating a bug database, or notifying subscribers that a repository contains new changes. Параметры ловушки: * node — ID набора изменений. ID набора изменений первого набора изменений в добавляемой группе наборов. Все наборы изменений между this и tip, включительно, добавляются отдельными командами “hg pull”, “hg push” или “hg unbundle”. * source — Строка. Источник этих изменений. См. раздел 10.8.3 для деталей. * url — URL. Местанахождение удаленного репозитория, если оно известно. См. раздел 10.8.3 для деталей. См. также incoming (раздел 10.9.3), prechangegroup (раздел 10.9.5), pretxnchangegroup (раздел 10.9.9) 10.9.2. commit—после создания нового набора изменений Эта ловушка запускается после того, как создан новый набор изменений. Параметры ловушки: * node — ID набора изменений. ID набора изменений добавляемого набора. * parent1 - ID набора изменений. ID набора изменений первого родителя добавляемого набора. parent2 - ID набора изменений. ID набора изменений второго родителя добавляемого набора. См. также: precommit (раздел 10.9.6), pretxncommit (раздел 10.9.10) 10.9.3 incoming — после добавления одного удаленного набора изменений Эта ловушка запускается после добавления в хранилище ранеесуществующего набора изменений, например, с помощью "hg push". Если группа множественных изменений добавляется одной операцией, то эта ловушка вызывается для каждого добавленного набора изменений. Вы можете использовать эту ловушку для тех же целей что и ловушку changegroup (см. раздел 10.9.1); просто иногда более удобно выполнить один раз ловушку для группы наборов изменений, в то время как в другом случае удобнее выполнять ловушку один раз для набора изменений. Параметры этой ловушки: * node — ID набора изменений. ID вновь добавляемого набора изменений. * source — строка. Строка содержащая изменений. См. раздел 10.8.3 для деталей. * url — URL. Местанахождение удаленного репозитория, если оно известно. См. раздел 10.8.3 для деталей. См. также changegroup (раздел 10.9.1), prechangegroup (раздел 10.9.5), pretxnchangegroup (раздел 10.9.9) 10.9.4 outgoing— после размножения наборов изменений Эта ловушка запускается после того как группа ревизий будет распространена за пределы этого репозитория, например при использовании команд “hg push” или “hg bundle”. Одна единственная возможность использования этой ловушки - уведомление администраторов что были помещены изменения. Параметры этой ловушки: * node — ID набора изменений. ID первого посланого набора изменений в группе наборов. * source A string. The source of the of the operation (see section 10.8.3). If a remote client pulled changes from this repository, source will be serve. If the client that obtained changes from this repository was local, source will be bundle, pull, or push, depending on the operation the client performed. * url — URL. Местанахождение удаленного репозитория, если оно известно. См. раздел 10.8.3 для деталей. См. также: preoutgoing (раздел 10.9.7) 10.9.5 prechangegroup — до начала добавления удаленных наборов изменений Эта контролирующая ловушка запускается до того как Mercurial начнет добавлять группы наборов изменений из другого репозитория. Эта ловушка не содержит информации о добавляемом наборе изменений, потому что она запускается до разрешения передачи этих изменений. Если эта ловушка возвращает ошибку, изменения не будут переданы. Эту ловушку можно использовать для запрета внешним пользователям вносить изменения в хранилище. Например так можно заморозить ветку на сервере, на время или постоянно, от пользователей в то время как администратор будет модифицировать хранилище. Параметры ловушки: * source A string. The source of these changes. See section 10.8.3 for details. * url A URL. The location of the remote repository, if known. See section 10.8.3 for more information. See also: changegroup (section 10.9.1), incoming (section 10.9.3), , pretxnchangegroup (section 10.9.9) 10.9.6 precommit—перед началом внесения набора изменений Эта ловушка запускается до начала внесения Mercurial'ом нового набора изменений. Выполняется до того, как Mercurial сформировал какие-либо метаданные для передачи, такие как файлы, сообщение, или дата. Одно из использований этой ловушки - запрет возможности вносить новые изменения, позволяя в то же время входящие наборы изменений. Другое состоит в том, чтобы запустить сборку или тест, и разрешить внесение измений только в том случае, если сборка или тесты прошли успешно. Параметры ловушки: * parent1 A changeset ID. The changeset ID of the first parent of the working directory. * parent2 A changeset ID. The changeset ID of the second parent of the working directory. If the commit proceeds, the parents of the working directory will become the parents of the new changeset. See also: commit (section 10.9.2), pretxncommit (section 10.9.10) 10.9.7 preoutgoing—before starting to propagate changesets This hook is invoked before Mercurial knows the identities of the changesets to be transmitted. One use for this hook is to prevent changes from being transmitted to another repository. Параметры ловушки: * source A string. The source of the operation that is attempting to obtain changes from this repository (see section 10.8.3). See the documentation for the source parameter to the outgoing hook, in section 10.9.4, for possible values of this parameter. * url A URL. The location of the remote repository, if known. See section 10.8.3 for more information. Смотрите также: outgoing (раздел 10.9.4) 10.9.8 pretag—before tagging a changeset Этот контрольный хук вызывается перед тем, как создаётся тег. Если хук завершается успешно, создание тега продолжается. В случае, если хук завершается ошибкой, тег не создаётся. Параметры хука: * local A boolean. Whether the tag is local to this repository instance (i.e. stored in .hg/localtags) or managed by Mercurial (stored in .hgtags). * node A changeset ID. The ID of the changeset to be tagged. * tag A string. The name of the tag to be created. If the tag to be created is revision-controlled, the precommit and pretxncommit hooks (sections 10.9.2 and 10.9.10) will also be run. Смотрите также: tag (раздел 10.9.12) 10.9.9 pretxnchangegroup—before completing addition of remote changesets This controlling hook is run before a transaction—that manages the addition of a group of new changesets from outside the repository—completes. If the hook succeeds, the transaction completes, and all of the changesets become permanent within this repository. If the hook fails, the transaction is rolled back, and the data for the changesets is erased. This hook can access the metadata associated with the almost-added changesets, but it should not do anything permanent with this data. It must also not modify the working directory. While this hook is running, if other Mercurial processes access this repository, they will be able to see the almost-added changesets as if they are permanent. This may lead to race conditions if you do not take steps to avoid them. This hook can be used to automatically vet a group of changesets. If the hook fails, all of the changesets are “rejected” when the transaction rolls back. Параметры хука: * node идентификатор набора изменений. Идентификатор первого набора изменений в группе, который был добавлен. Все наборы изменений между этим набором и новейшим, включительно, были добавлены одной командой hg pull, hg push или hg unbundle. * source строка. Источник изменений. Подробнее в разделе 10.8.3. * url - URL. Расположение удалённого репозитория, если известно. Подробнее в разделе 10.8.3. Смотрите также: changegroup (раздел 10.9.1), incoming (раздел 10.9.3), prechangegroup (раздел 10.9.5) 10.9.10 pretxncommit—перед завершением внесения нового набора изменений Эта управляющая ловушка запускается перед транзакцией, которая сделает новый коммит завершённым. Если ловушка отработала успешно, транзакция завершается и изменения принимаются в хранилище. Если ловушка вернула ошибку - транзакция откатывается и принятые данные удаляются. Эта ловушка может получать доступ к метаданным, связанным с почти новый набором изменений, но она не должна ничего делать с этими постоянными данными. Она также не должна изменять рабочую директорию. Пока выполняется эта ловушка, другие процессы Mercurial, обращающиеся к этому хранилищу, видят временное состояние хранилища как постоянное. Может привести к косякам, если Вы не предусмотрели мер предотвращения этого. Параметры ловушки: * node A changeset ID. The changeset ID of the newly committed changeset. * parent1 A changeset ID. The changeset ID of the first parent of the newly committed changeset. * parent2 A changeset ID. The changeset ID of the second parent of the newly committed changeset. Смотрите также: precommit (раздел 10.9.6) 10.9.11. preupdate—перед обновлением или слиянием рабочей директории. Эта управляющая ловушка запускается перед началом обновления/слияния рабочей директории. Запускается только если внутренняя pre-update проверка Mercurial'а определила, что обновление/слияние возможны. Если ловушка возвращает успешный код возврата, обновление/слияние продолжается; иначе даже не начинаются. Параметры ловушки: * parent1 A changeset ID. The ID of the parent that the working directory is to be updated to. If the working directory is being merged, it will not change this parent. * parent2 A changeset ID. Only set if the working directory is being merged. The ID of the revision that the working directory is being merged with. Смотрите также: update (раздел 10.9.13) 10.9.12 tag—после создания метки набора изменений Ловушка выполняется после создания метки изменений. Параметры ловушки: * local - Логическая. Определяет является ли новый тег локальным для экземпляра репозитория (т.е. сохранен в .hg/localtags) или управляется Mercurial (сохранен в .hgtags) * node - ID набора изменений. ID набора изменений, которое было тегировано. * tag - Строка. Имя тега, который был создан. Если созданная метка попадает под контроль версий, то перед этой вызывается ловушка commit (раздел 10.9.2) Смотрите также: pretag (раздел 10.9.8) 10.9.13. update—после обновления или слияния рабочей директории Ловушка запускается после завершения обновления или объединения изменений. Поскольку слияние может не сработать (если внешняя команда hg merge обломалась на конфликтных файлах), то эта ловушка сообщит - успешно ли обновление/слияние. * error Логическая. Показывает, успешно ли прошло обновление или слияние. * parent1 - ID набора изменений. ID набора изменений родителя который обновляется этой рабочей копией. Если производится слияние рабочей директории это действие не меняет этого родителя. * parent2 - ID набора изменений. Устанавливается только когда производится слияние рабочей директории. ID ревизии, с которой производится слияние рабочей копии. Смотрите также: preupdate (раздел 10.9.11) Глава 11 Настройка выводимых данных Mercurial Mercurial предоставляет мощный механизм позволяющий контролировать как будет выводиться информация. Механизм базируется на шаблонах. Вы можете использовать шаблоны для генерации специфичного вывода одной команды или настроить весь вывод встроенного web интерфейса. 11.1 Использование предустановленых стилей Пакет Mercurial поставляется с некоторыми стилями вывода, которые вы можете использовать незамедлительно. Стиль - просто предустановленный шаблон, который кто-то написал и установил где-либо и который может найти Mercurial. До того как мы рассмотрим встроенные стили, давайте взглянем на обычный вывод. 1 $ hg log -r1 2 changeset: 1:b56ce7b07c52 3 tag: mytag 4 user: Bryan O'Sullivan 5 date: Thu Aug 21 18:22:25 2008 +0000 6 summary: added line to end of <> file. 7 Это информативно, но занимает довольно много места - пять линий на набор изменений. Компактный стиль уменьшает это до трех линий, представленных в разреженном виде. $ hg log --style compact 3[tip] 3ea944887979 2008-08-21 18:22 +0000 bos Added tag v0.1 for changeset 56bfb1da2094 4 2[v0.1] 56bfb1da2094 2008-08-21 18:22 +0000 bos Added tag mytag for changeset b56ce7b07c52 7 1[mytag] b56ce7b07c52 2008-08-21 18:22 +0000 bos added line to end of <> file. 10 0 bd2d1e24f0e0 2008-08-21 18:22 +0000 bos added hello 13 The changelog style hints at the expressive power of Mercurial’s templating engine. This style attempts to follow the GNU Project’s changelog guidelines[RS]. $ hg log --style changelog 2008-08-21 Bryan O'Sullivan 3 ⋆ .hgtags: Added tag v0.1 for changeset 56bfb1da2094 [3ea944887979] [tip] 7 ⋆ .hgtags: Added tag mytag for changeset b56ce7b07c52 [56bfb1da2094] [v0.1] 11 ⋆ goodbye, hello: added line to end of <> file. 14 in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye. [b56ce7b07c52] [mytag 18 ⋆ hello: added hello [bd2d1e24f0e0] 22 You will not be shocked to learn that Mercurial’s default output style is named default. 11.1.1 Установка стиля по умолчанию You can modify the output style that Mercurial will use for every command by editing your hgrc file, naming the style you would prefer to use. 1 [ui] 2 style = compact If you write a style of your own, you can use it by either providing the path to your style file, or copying your style file into a location where Mercurial can find it (typically the templates subdirectory of your Mercurial install directory). 11.2 Commands that support styles and templates All of Mercurial’s “log-like” commands let you use styles and templates: “hg incoming”, “hg log”, “hg outgoing”, and “hg tip”. As I write this manual, these are so far the only commands that support styles and templates. Since these are the most important commands that need customisable output, there has been little pressure from the Mercurial user community to add style and template support to other commands. 11.3 Основы шаблонизации At its simplest, a Mercurial template is a piece of text. Some of the text never changes, while other parts are expanded, or replaced with new text, when necessary. Для начала давайте посмотрим, как обычно Mercurial оформляет вывод. $ hg log -r1 changeset: 1:b56ce7b07c52 tag: mytag user: Bryan O'Sullivan date: Thu Aug 21 18:22:25 2008 +0000 summary: added line to end of <> file. 7 Now, let’s run the same command, but using a template to change its output. $ hg log -r1 --template 'i saw a changeset∖n' i saw a changeset The example above illustrates the simplest possible template; it’s just a piece of static text, printed once for each changeset. The --template option to the “hg log” command tells Mercurial to use the given text as the template when printing each changeset. Notice that the template string above ends with the text “\n”. This is an escape sequence, telling Mercurial to print a newline at the end of each template item. If you omit this newline, Mercurial will run each piece of output together. See section 11.5 for more details of escape sequences. A template that prints a fixed string of text all the time isn’t very useful; let’s try something a bit more complex. $ hg log --template 'i saw a changeset: {desc}∖n' i saw a changeset: Added tag v0.1 for changeset 56bfb1da2094 i saw a changeset: Added tag mytag for changeset b56ce7b07c52 i saw a changeset: added line to end of <> file. 5 in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye. i saw a changeset: added hello As you can see, the string “{desc}” in the template has been replaced in the output with the description of each changeset. Every time Mercurial finds text enclosed in curly braces (“{” and “}”), it will try to replace the braces and text with the expansion of whatever is inside. To print a literal curly brace, you must escape it, as described in section 11.5. 11.4 Common template keywords You can start writing simple templates immediately using the keywords below. * author String. The unmodified author of the changeset. * branches String. The name of the branch on which the changeset was committed. Will be empty if the branch name was default. * date Date information. The date when the changeset was committed. This is not human-readable; you must pass it through a filter that will render it appropriately. See section 11.6 for more information on filters. The date is expressed as a pair of numbers. The first number is a Unix UTC timestamp (seconds since January 1, 1970); the second is the offset of the committer’s timezone from UTC, in seconds. * desc String. The text of the changeset description. * files List of strings. All files modified, added, or removed by this changeset. * filet4ht@95xadds List of strings. Files added by this changeset. * filet4ht@95xdels List of strings. Files removed by this changeset. * node String. The changeset identification hash, as a 40-character hexadecimal string. * parents List of strings. The parents of the changeset. * rev Integer. The repository-local changeset revision number. * tags List of strings. Any tags associated with the changeset. A few simple experiments will show us what to expect when we use these keywords; you can see the results in figure 11.1. $ hg log -r1 --template 'author: {author}∖n' author: Bryan O'Sullivan $ hg log -r1 --template 'desc:∖n{desc}∖n' desc: added line to end of <> file. 6 in addition, added a file with the helpful name (at least i hope that some might consider it so) of goodbye. $ hg log -r1 --template 'files: {files}∖n' files: goodbye hello $ hg log -r1 --template 'file_adds: {file_adds}∖n' file_adds: goodbye $ hg log -r1 --template 'file_dels: {file_dels}∖n' file_dels: $ hg log -r1 --template 'node: {node}∖n' node: b56ce7b07c52de7d5fd79fb89701ea538af65746 $ hg log -r1 --template 'parents: {parents}∖n' parents: $ hg log -r1 --template 'rev: {rev}∖n' rev: 1 $ hg log -r1 --template 'tags: {tags}∖n' tags: mytag Figure 11.1: Template keywords in use As we noted above, the date keyword does not produce human-readable output, so we must treat it specially. This involves using a filter, about which more in section 11.6. $ hg log -r1 --template 'date: {date}∖n' date: 1219342945.00 $ hg log -r1 --template 'date: {date|isodate}∖n' date: 2008-08-21 18:22 +0000 11.5 Escape sequences Система шаблонов Mercurial распознаёт наиболее распространённые escape-последовательности в строках. Когда он видит обратный слеш ("\"), он смотрит на следующие за ним знак и заменяет пару символов одним, как описано ниже. * ∖∖ Backslash, “\”, ASCII 134. * ∖n Newline, ASCII 12. * ∖r Carriage return, ASCII 15. * ∖t Tab, ASCII 11. * ∖v Vertical tab, ASCII 13. * ∖{ Open curly brace, “{”, ASCII 173. * ∖} Close curly brace, “}”, ASCII 175. As indicated above, if you want the expansion of a template to contain a literal “\”, “{”, or “{” character, you must escape it. 11.6 Filtering keywords to change their results Some of the results of template expansion are not immediately easy to use. Mercurial lets you specify an optional chain of filters to modify the result of expanding a keyword. You have already seen a common filter, isodate, in action above, to make a date readable. Below is a list of the most commonly used filters that Mercurial supports. While some filters can be applied to any text, others can only be used in specific circumstances. The name of each filter is followed first by an indication of where it can be used, then a description of its effect. * addbreaks Any text. Add an XHTML “
” tag before the end of every line except the last. For example, “foo\nbar” becomes “foo
\nbar”. * age date keyword. Render the age of the date, relative to the current time. Yields a string like “10 minutes”. * basename Any text, but most useful for the files keyword and its relatives. Treat the text as a path, and return the basename. For example, “foo/bar/baz” becomes “baz”. * date date keyword. Render a date in a similar format to the Unix date command, but with timezone included. Yields a string like “Mon Sep 04 15:13:13 2006 -0700”. * domain Any text, but most useful for the author keyword. Finds the first string that looks like an email address, and extract just the domain component. For example, “Bryan O'Sullivan ” becomes “serpentine.com”. * email Any text, but most useful for the author keyword. Extract the first string that looks like an email address. For example, “Bryan O'Sullivan ” becomes “bos@serpentine.com”. * escape Any text. Replace the special XML/XHTML characters “&”, “<” and “>” with XML entities. * fill68 Any text. Wrap the text to fit in 68 columns. This is useful before you pass text through the tabindent filter, and still want it to fit in an 80-column fixed-font window. * fill76 Any text. Wrap the text to fit in 76 columns. * firstline Any text. Yield the first line of text, without any trailing newlines. * hgdate date keyword. Render the date as a pair of readable numbers. Yields a string like “1157407993 25200”. * isodate date keyword. Render the date as a text string in ISO 8601 format. Yields a string like “2006-09-04 15:13:13 -0700”. * obfuscate Any text, but most useful for the author keyword. Yield the input text rendered as a sequence of XML entities. This helps to defeat some particularly stupid screen-scraping email harvesting spambots. * person Any text, but most useful for the author keyword. Yield the text before an email address. For example, “Bryan O'Sullivan ” becomes “Bryan O'Sullivan”. * rfc822date date keyword. Render a date using the same format used in email headers. Yields a string like “Mon, 04 Sep 2006 15:13:13 -0700”. * short Changeset hash. Yield the short form of a changeset hash, i.e. a 12-byte hexadecimal string. * shortdate date keyword. Render the year, month, and day of the date. Yields a string like “2006-09-04”. * strip Any text. Strip all leading and trailing whitespace from the string. * tabindent Any text. Yield the text, with every line except the first starting with a tab character. * urlescape Any text. Escape all characters that are considered “special” by URL parsers. For example, foo bar becomes foo%20bar. * user Any text, but most useful for the author keyword. Return the “user” portion of an email address. For example, “Bryan O'Sullivan ” becomes “bos”. 1 $ hg log -r1 --template '{author}∖n' 2 Bryan O'Sullivan 3 $ hg log -r1 --template '{author|domain}∖n' 4 serpentine.com 5 $ hg log -r1 --template '{author|email}∖n' 6 bos@serpentine.com 7 $ hg log -r1 --template '{author|obfuscate}∖n' | cut -c-76 8 Bryan O'Sulli 9 $ hg log -r1 --template '{author|person}∖n' 10 Bryan O'Sullivan 11 $ hg log -r1 --template '{author|user}∖n' 12 bos 13 $ hg log -r1 --template 'looks almost right, but actually garbage: {date}∖n' 14 looks almost right, but actually garbage: 1219342945.00 15 $ hg log -r1 --template '{date|age}∖n' 16 1 second 17 $ hg log -r1 --template '{date|date}∖n' 18 Thu Aug 21 18:22:25 2008 +0000 19 $ hg log -r1 --template '{date|hgdate}∖n' 20 1219342945 0 21 $ hg log -r1 --template '{date|isodate}∖n' 22 2008-08-21 18:22 +0000 23 $ hg log -r1 --template '{date|rfc822date}∖n' 24 Thu, 21 Aug 2008 18:22:25 +0000 25 $ hg log -r1 --template '{date|shortdate}∖n' 26 2008-08-21 27 $ hg log -r1 --template '{desc}∖n' | cut -c-76 28 added line to end of <> file. 29 30 in addition, added a file with the helpful name (at least i hope that some m 31 $ hg log -r1 --template '{desc|addbreaks}∖n' | cut -c-76 32 added line to end of <> file.
33
34 in addition, added a file with the helpful name (at least i hope that some m 35 $ hg log -r1 --template '{desc|escape}∖n' | cut -c-76 36 added line to end of <> file. 37 38 in addition, added a file with the helpful name (at least i hope that some m 39 $ hg log -r1 --template '{desc|fill68}∖n' 40 added line to end of <> file. 41 42 in addition, added a file with the helpful name (at least i hope 43 that some might consider it so) of goodbye. 44 $ hg log -r1 --template '{desc|fill76}∖n' 45 added line to end of <> file. 46 47 in addition, added a file with the helpful name (at least i hope that some 48 might consider it so) of goodbye. 49 $ hg log -r1 --template '{desc|firstline}∖n' 50 added line to end of <> file. 51 $ hg log -r1 --template '{desc|strip}∖n' | cut -c-76 52 added line to end of <> file. 53 54 in addition, added a file with the helpful name (at least i hope that some m 55 $ hg log -r1 --template '{desc|tabindent}∖n' | expand | cut -c-76 56 added line to end of <> file. 57 58 in addition, added a file with the helpful name (at least i hope tha 59 $ hg log -r1 --template '{node}∖n' 60 b56ce7b07c52de7d5fd79fb89701ea538af65746 61 $ hg log -r1 --template '{node|short}∖n' 62 b56ce7b07c52 Figure 11.2: Template filters in action Note: If you try to apply a filter to a piece of data that it cannot process, Mercurial will fail and print a Python exception. For example, trying to run the output of the desc keyword into the isodate filter is not a good idea. 11.6.1 Combining filters It is easy to combine filters to yield output in the form you would like. The following chain of filters tidies up a description, then makes sure that it fits cleanly into 68 columns, then indents it by a further 8 characters (at least on Unix-like systems, where a tab is conventionally 8 characters wide). 1 $ hg log -r1 --template 'description:∖n∖t{desc|strip|fill68|tabindent}∖n' 2 description: 3 added line to end of <> file. 4 5 in addition, added a file with the helpful name (at least i hope 6 that some might consider it so) of goodbye. Note the use of “\t” (a tab character) in the template to force the first line to be indented; this is necessary since tabindent indents all lines except the first. Keep in mind that the order of filters in a chain is significant. The first filter is applied to the result of the keyword; the second to the result of the first filter; and so on. For example, using fill68|tabindent gives very different results from tabindent|fill68. 11.7 From templates to styles A command line template provides a quick and simple way to format some output. Templates can become verbose, though, and it’s useful to be able to give a template a name. A style file is a template with a name, stored in a file. More than that, using a style file unlocks the power of Mercurial’s templating engine in ways that are not possible using the command line --template option. 11.7.1 The simplest of style files Our simple style file contains just one line: 1 $ echo 'changeset = "rev: {rev}∖n"' > rev 2 $ hg log -l1 --style ./rev 3 rev: 3 This tells Mercurial, “if you’re printing a changeset, use the text on the right as the template”. 11.7.2 Style file syntax The syntax rules for a style file are simple. * The file is processed one line at a time. * Leading and trailing white space are ignored. * Empty lines are skipped. * If a line starts with either of the characters “#” or “;”, the entire line is treated as a comment, and skipped as if empty. * A line starts with a keyword. This must start with an alphabetic character or underscore, and can subsequently contain any alphanumeric character or underscore. (In regexp notation, a keyword must match [A-Za-z_][A-Za-z0-9_]⋆.) * The next element must be an “=” character, which can be preceded or followed by an arbitrary amount of white space. * If the rest of the line starts and ends with matching quote characters (either single or double quote), it is treated as a template body. * If the rest of the line does not start with a quote character, it is treated as the name of a file; the contents of this file will be read and used as a template body. 11.8 Style files by example To illustrate how to write a style file, we will construct a few by example. Rather than provide a complete style file and walk through it, we’ll mirror the usual process of developing a style file by starting with something very simple, and walking through a series of successively more complete examples. 11.8.1 Identifying mistakes in style files If Mercurial encounters a problem in a style file you are working on, it prints a terse error message that, once you figure out what it means, is actually quite useful. 1 $ cat broken.style 2 changeset = Notice that broken.style attempts to define a changeset keyword, but forgets to give any content for it. When instructed to use this style file, Mercurial promptly complains. 1 $ hg log -r1 --style broken.style 2 abort: broken.style:1: parse error This error message looks intimidating, but it is not too hard to follow. * The first component is simply Mercurial’s way of saying “I am giving up”. 1 abort: broken.style:1: parse error * Next comes the name of the style file that contains the error. 1 abort: broken.style:1: parse error * Following the file name is the line number where the error was encountered. 1 abort: broken.style:1: parse error * Finally, a description of what went wrong. 1 abort: broken.style:1: parse error The description of the problem is not always clear (as in this case), but even when it is cryptic, it is almost always trivial to visually inspect the offending line in the style file and see what is wrong. 11.8.2 Uniquely identifying a repository If you would like to be able to identify a Mercurial repository “fairly uniquely” using a short string as an identifier, you can use the first revision in the repository. 1 $ hg log -r0 --template '{node}' 2 bd2d1e24f0e0c1c1b33369d884ddac10ca35bd11 This is not guaranteed to be unique, but it is nevertheless useful in many cases. * It will not work in a completely empty repository, because such a repository does not have a revision zero. * Neither will it work in the (extremely rare) case where a repository is a merge of two or more formerly independent repositories, and you still have those repositories around. Here are some uses to which you could put this identifier: * As a key into a table for a database that manages repositories on a server. * As half of a {repository ID, revision ID} tuple. Save this information away when you run an automated build or other activity, so that you can “replay” the build later if necessary. 11.8.3 Mimicking Subversion’s output Let’s try to emulate the default output format used by another revision control tool, Subversion. 1 $ svn log -r9653 2 ------------------------------------------------------------------------ 3 r9653 | sean.hefty | 2006-09-27 14:39:55 -0700 (Wed, 27 Sep 2006) | 5 lines 4 5 On reporting a route error, also include the status for the error, 6 rather than indicating a status of 0 when an error has occurred. 7 8 Signed-off-by: Sean Hefty 9 10 ------------------------------------------------------------------------ Since Subversion’s output style is fairly simple, it is easy to copy-and-paste a hunk of its output into a file, and replace the text produced above by Subversion with the template values we’d like to see expanded. 1 $ cat svn.template 2 r{rev} | {author|user} | {date|isodate} ({date|rfc822date}) 3 4 {desc|strip|fill76} 5 6 ------------------------------------------------------------------------ There are a few small ways in which this template deviates from the output produced by Subversion. * Subversion prints a “readable” date (the “Wed, 27 Sep 2006” in the example output above) in parentheses. Mercurial’s templating engine does not provide a way to display a date in this format without also printing the time and time zone. * We emulate Subversion’s printing of “separator” lines full of “-” characters by ending the template with such a line. We use the templating engine’s header keyword to print a separator line as the first line of output (see below), thus achieving similar output to Subversion. * Subversion’s output includes a count in the header of the number of lines in the commit message. We cannot replicate this in Mercurial; the templating engine does not currently provide a filter that counts the number of items it is passed. It took me no more than a minute or two of work to replace literal text from an example of Subversion’s output with some keywords and filters to give the template above. The style file simply refers to the template. 1 $ cat svn.style 2 header = '------------------------------------------------------------------------∖n∖n' 3 changeset = svn.template We could have included the text of the template file directly in the style file by enclosing it in quotes and replacing the newlines with “ n” sequences, but it would have made the style file too difficult to read. Readability is a good guide when you’re trying to decide whether some text belongs in a style file, or in a template file that the style file points to. If the style file will look too big or cluttered if you insert a literal piece of text, drop it into a template instead. Глава 12 Управление изменениями с помощью Mercurial Queues Проблема управления патчами Вот вам обычный сценарий: вам надо установить программный пакет из исходного кода, но вы нашли баг, который вам необходимо пофиксить в этом коде прежде чем начать пользоваться программой. Вы вносите изменения и забываете о программе на некоторое время. Через несколько месяцев вам надо обновить программный пакет до более новой версии. Если в новой версия пакета всё ещё присутствует этот баг, то вы должны извлечь свои изменения из старого кода и применить их к новому. Это утомительное занятие, и в процессе довольно легко допустить ошибку. Это простой пример проблемы "управления патчами". У вас есть некий "развивающийся" код, который вы не можете изменять; одновременно вам надо сделать кое-какие локальные изменения поверх этого кода; и вы бы хотели хранить эти изменения отдельно, чтобы иметь возможность применять их на все последующие версии кода. Проблема управления патчами возникает в различных ситуациях. Вероятно самая частая ситуация, это когда пользователь Open Source проекта шлёт баг-фикс или новую фичу мейнтенеру (управляющему) проекта в виде патча. Производителям операционных систем, которые включают в дистрибутив Open Source пакеты, часто делают изменения в этих пакетах, чтобы они правильно собирались (компилировались) в необходимых исполняемых средах. Когда у вас всего несколько изменений с ними легко работать, применяя один за другим используя программы "diff" и "patch" (см. обсуждение в параграфе 12.4). Но когда количество изменений растёт, появляется смысл в обслуживании патчей дискретными "кусками работы", так, что, например, один патч будет содержать ровно один фикс (патч может изменять несколько файлов, но делать всего "одну работу"), и вы можете иметь много патчей для различных багов и изменений так сильно необходимых вам. В этой ситуации, если вы отправили мейнтейнерам проекта патч с фиксом бага и они выключили ваш фикс в следующий релиз, то вы можете просто забыть про этот патч когда будете обновляться до новой версии проекта. Применяя один единственный патч к дереву исходного кода - это довольно скучное и подверженное ошибкам, но не сложное занятие. Тем не менее, сложность проблемы резко возрастает, если количество патчей, которые вы обслуживаете, увеличивается. С чуть более чем небольшим количество патчей в руках, непонимание того, какие из них вы уже применили, а какие ещё надо применить, превращает это занятие из запутанного в непреодолимое. К счастью, у Mercurial есть мощное расширение Merqurial Queues (или просто "MQ"), которое значительно упрощает управление патчами. 12.2 Предыстория Mercurial Queues В конце 90-х несколько разработчиков ядра Linux начали применять "серии патчей", которые изменяли поведение ядра Linux. Некоторые из этих патчей были направленны на стабильность, некоторые на расширение возможностей, а другие были ещё более хитрыми. Размеры этих серий патчей резко росли. В 2002 Эндрю Мортон опубликовал несколько shell скриптов, которые он использовал для автоматизации задачи наложения своих патчей. Эндрю успешно использовал эти скрипты для применения сотен (иногда тысяч) патчей к ядру Linux. 12.2.1 Лоскутный килт (patchwork quilt) В начале 2003 года, Андрэас Грюнбахер и Мартин Квинсон одолжили подход у скриптов Эндрю и опубликовали инструмент (тулзу) под названием "лоскутный килт" (patchwork quilt), или просто "килт" (см. [Gru05] с описанием). Так как килт в главным образом автоматизировал управление патчами, он резко приобрёл популятность в мире open source. Килт управляет набором патчей поверх дерева папок. Сначала вы указываете килту директорию с деревом и указываете файлы для модификации; он сохраняет куда-то имена и содержимое файлов. Потом, чтобы пофиксить баг, вы создаёте новый патч (используя одну команду), редактируете все необходимые файлы, и потом "обновляете" (refresh) патч. Этот шаг "обновления" говорит килту просканировать дерево папок; и он создаёт патч со всеми теми изменениями, что вы внесли. Вы можете создать еще один патч поверх первого, который бы содержал изменения между "деревом папок с один патчем" и "деревом папок с двумя патчами". Вы можете изменять список наложенных на дерево патчей. Если вы "внимаете" (pop) какой-то патч, изменения внесённые этим патчем исчезнут из дерева папок. Килт помнит какие патчи вы "выкинули", поэтому вы можете "вбросить" (push) выкинутый патч обратно, и дерево папок будет восстановлено с этим патчем внутри. Самое важное это то, что вы можете в любой момент запустить команды "refresh" в любое время, и наложенные поверх дерева патчи будут обновлены автоматически. Это означает, что вы можете в любое время изменять и список наложенных патчей, и их содержимое (внесённые патчами изменения). Килт ничего не ведает о средствах управления версиями, это значит что он одинаково хорошо работает и с обычными распакованными пакетами исходного кода, и с рабочей копией Subversion проекта. 12.2.2 От лоскутного килта к Mercurial Queues В середние 2005-го Крис Мейсон взял возможности килта и написал расширение, которое она назвал Mercurial Queues. Оно добавляло килт-подобное поведение в Меркуриал. Отличительной чертой между килтом и MQ является то, что клит ничего не знает о системе контроле версий, тогда как MQ был интегрирован внутрь Меркуриала. Так как килт не парится про системы контроля версий, он всё ещё остаётся невероятно полезной утилитой в тех случаях, когда вы не можете использовать Меркуриал и MQ. 12.3 Огромное преимущество MQ Я не смогу преувеличить исключительную важность MQ для объединения патчей и системы контроля версий. Важной причиной того, что патчи сохранились в мире свободного и открытого ПО, не смотря на ежегодное увеличение возможностей систем контроля версий, является гибкость, которую они предоставляют. Традиционные системы контроля версий делают постоянные, необратимые записи всего-всего, что они делают. Но не смотря на то, что это важная особенность, она также чуток мешает. Если вдруг вы захотите провести сумасбродный эксперимент, вам придётся быть очень осторожным, ибо вы рискуете оставить бесполезные (или хуже - заблуждающие или дистабилизирующие) следы ваших упущений или ошибок в записях системы. Меркуриал отличается от них. Слияние распределённой системы контроля версий с патчами в виде MQ дало возможность изолировать вашу работу от всех остальных. Ваши патчи живут поверх нормальной истории ревизий, и вы можете их убрать или наложить по своему желанию. Если вам не нравится патч, вы можете его удалить. Если патч немного не такой как нужно, вы можете просто подправить его столько раз, сколько надо будет, до тех пор пока он не приобретёт желаемую форму. Например, интеграция патчей с системой версий невероятно упрощает понимание самих патчей и упрощает их отладку (и их взаимодействие с кодом, на котором они основаны). Так как каждый применённый патч имеет ассоциированный с ним чейнджсет, вы можете использовать "hg log filename" дабы увидеть какие чейнджсеты и патчи повлияли на файл. Вы можете использовать команду bisect, чтобы поискать двоичным поиском по всем чейнджсетам и применённым патчам, чтобы увидеть где был допущен или исправлен баг. Вы можете использовать команду "hg annotate", чтобы увидеть какие чейнджсеты или патчи изменили конкретную строку в файле. И так далее. 12.4 Понимание патчей Так как MQ не скрывает свою патч-ориентированную натуру, то полезно понимать, что же такое патчи, и знать утилиты для работы с ними. Традиционная команда Unix ОС "diff" сравнивает два файла и печатает список различий между ними. Команда "patch" понимает эти различия как модификации к файлу. Посмотрите на картинку 12.1, это простой пример работы этих команд. 1 $ echo 'this is my original thought' > oldfile 2 $ echo 'i have changed my mind' > newfile 3 $ diff -u oldfile newfile > tiny.patch 4 $ cat tiny.patch 5 --- oldfile 2008-08-21 18:22:22.000000000 +0000 6 +++ newfile 2008-08-21 18:22:22.000000000 +0000 7 @@ -1 +1 @@ 8 -this is my original thought 9 +i have changed my mind 10 $ patch < tiny.patch 11 patching file oldfile 12 $ cat oldfile 13 i have changed my mind Рисунок 12.1: Простое использование команд diff и patch Тип файла, который генерирует команда diff (и который принимает команда patch) называется 'патч' или 'диф'. Между ними нет различий. (Мы же будет использовать слово 'патч' как наиболее распространённое.) Патч-файл может начинаться с произвольного текста; команда patch игнорирует этот текст, но MQ использует его как комментарий к коммиту, когда создаёт чейнджсет. Чтобы найти начало непосредственно изменений, patch ищет первую строку, которая начинается с "diff -". MQ работает с унифицированными дифами (patch может работать с несколькими другими диф-форматами, но MQ - нет). Унифицированные дифы содержат два вида заголовков. Заголовок файла описывает файл, который надо изменять; заголовок содержит им этого файла. Когда patch видит новый заголовок файла, он ищет файл с таким именем и начинает его изменять. После заголовка файла следуют серии блоков. Каждый блок начинается с заголовка; он указывает ряд строк внутри изменяемого файла. За заголовком начинается блок и заканчивается несколькими (обычно тремя) строками не изменённого текста; на них называют контекстом блока. Если там всего несколько строк контекста между двумя последовательными блоками, то diff не печатает новый заголовок блока; он просто соединяет блоки вместе с помощью этих нескольких строк контекста. Каждая строка контекста начинается с символа пробела. Внутри блока строка, которая начинается с "-" считается строкой на удаление. Когда строка, начинается с "+", считается что это строка на добавление. Например, строка, которая была чуть-чуть изменена, будет представлена одной строкой на удаление и еще одной на добавление. Мы еще вернёмся к тонкостям патчей (в параграфе 12.6), но у вас уже достаточно знаний, чтобы начать пользоваться MQ. 12.5 Начало работы с Mercurial Queues Так как MQ реализовано в виде расширения, вы должны включить его прежде чем использовать. (Вам не нужно ничего скачивать; MQ поставляется со стандартным пакетом Mercurial.) Чтобы включить MQ отредактируйте свой ~/.hgrc файл, и добавьте следующие строки. [extensions] (для более ранних версий Меркуриала - прим. пер.) hgext.mq = (для последних версий Меркуриала - прим. пер.) mq= Рисунок 12.2 Содержимое файла ~/.hgrc для включения расширения MQ Как только включите расширение, у вас появится возможность вводить несколько новых команд. Чтобы удостовериться, что расширение работает, вы можете набрать "hg help qinit", чтобы убедится в доступности команды; см. пример на рисунке 12.3. $ hg help qinit hg qinit [-c] 3 4 init a new queue repository 5 6 The queue repository is unversioned by default. If -c is 7 specified, qinit will create a separate nested repository 8 for patches (qinit -c may also be run later to convert 9 an unversioned patch repository into a versioned one). 10 You can use qcommit to commit changes to this queue repository. 11 12 options: 13 14 -c --create-repo create queue repository 15 16 use "hg -v help qinit" to show global options Figure 12.3: How to verify that MQ is enabled Вы можете использовать MQ с любым репозиторием Меркуриала, все команды MQ оперируют исключительно с этим репозиторием. Чтобы начать, просто подготовьте репозиторий командой "hg qinit" (см. рис 12.4). Эта команда создаёт пустую директорию под названием .hg/patches, где MQ будет содержать свои служебные данные (metadata). Как и многие другие команды Меркуриала, команда "hg qinit" ничего не печатает, если проходит успешно. $ hg init mq-sandbox $ cd mq-sandbox $ echo 'line 1' > file1 $ echo 'another line 1' > file2 $ hg add file1 file2 $ hg commit -m'first change' $ hg qinit Рисунок 12.4: Подготовка репозитория для использования с MQ $ hg tip changeset: 0:953c70f1ae46 tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:23 2008 +0000 summary: first change 7 $ hg qnew first.patch $ hg tip changeset: 1:3cecf9205d0d tag: qtip tag: first.patch tag: tip tag: qbase user: Bryan O'Sullivan date: Thu Aug 21 18:22:23 2008 +0000 summary: [mq]: first.patch 18 $ ls .hg/patches first.patch series status Рисунок 12.5 Создание нового патча 12.5.1 Создание нового патча Чтобы начать работу над новым патчем используйте команду "hg qnew". Эта команда принимает один аргумент - имя создаваемого патча. MQ будет использовать это имя для файла в директории .hg/patches, вы можете это видеть на рисунке 12.5. Также в .hg/patches есть еще два других новых файла - series и status. Файл series перечисляет все патчи, о которых знает MQ в этом репозитории, один патч на строку. Файл status используется Меркуриалом для внутреннего учета; он отслеживает все патчи, что MQ применила к репозиторию. Заметка: Вы можете захотеть собственноручно отредактировать файл series; например, чтобы изменить очерёдность накладываемых патчей. Но, всё же, ручное редактирование status файла почти всегда плохая идея, так как можно легко сломать MQ, он перестанет адекватно понимать что происходит. Как только вы создали патч, вы можете редактировать файлы в своей рабочей копии так же, как вы делали это раньше. Все стандартные команды Mercurial, такие как "hg diff" и "hg annotate" будут работать так же как и раньше. 12.5.2 Обновление патчей Когда вы доделываете работу и хотите её сохранить, используйте команду "hg qrefresh" (рис. 12.5) чтобы обновить патч над которым работали. Команда внедряет ваши изменения из рабочей копии в ваш патч и обновляет соответствующий чейнджсет, который содержит эти изменения. 1 $ echo 'line 2' >> file1 2 $ hg diff 3 diff -r 3cecf9205d0d file1 4 --- a/file1 Thu Aug 21 18:22:23 2008 +0000 5 +++ b/file1 Thu Aug 21 18:22:23 2008 +0000 6 @@ -1,1 +1,2 @@ 7 line 1 8 +line 2 9 $ hg qrefresh 10 $ hg diff 11 $ hg tip --style=compact --patch 12 1[qtip,first.patch,tip,qbase] 436b96694df7 2008-08-21 18:22 +0000 bos 13 [mq]: first.patch 14 5 diff -r 953c70f1ae46 -r 436b96694df7 file1 16 --- a/file1 Thu Aug 21 18:22:23 2008 +0000 17 +++ b/file1 Thu Aug 21 18:22:23 2008 +0000 18 @@ -1,1 +1,2 @@ 19 line 1 20 +line 2 21 Рисунок 12.6: обновление патча Вы можете выполнять "hg qrefresh" так часто, как хотите, ибо это хороший способ "сохранить" вашу работу. Обновляйте (refresh) ваш патч при каждой возможности; попробуйте поэкспериментировать; и если вдруг эксперимент будет неудачным, то "hg revert" ваши изменения назад к последнему qrefresh. 1 $ echo 'line 3' >> file1 2 $ hg status 3 M file1 4 $ hg qrefresh 5 $ hg tip --style=compact --patch 6 1[qtip,first.patch,tip,qbase] a0074f16fee0 2008-08-21 18:22 +0000 bos 7 [mq]: first.patch 8 9 diff -r 953c70f1ae46 -r a0074f16fee0 file1 10 --- a/file1 Thu Aug 21 18:22:23 2008 +0000 11 +++ b/file1 Thu Aug 21 18:22:23 2008 +0000 12 @@ -1,1 +1,3 @@ 13 line 1 14 +line 2 15 +line 3 16 Рисунок 12.7: Обновления патча несколько для для накапливания изменений 12.5.3 Складирование и отслеживание патчей Как только вы закончили работу над патчем, или же вам нужно поработать надо другим, вы можете использовать команду "hg qnew" еще раз, чтобы создать новый патч. Меркуриал применить этот патч поверх уже существующего патча. См. рисунок 12.8. Заметьте, что патч содержит изменения в нашем предыдущем патче как его часть (это можно увидеть еще раньше в выводе команды "hg annotate"). 1 $ hg qnew second.patch 2 $ hg log --style=compact --limit=2 3 2[qtip,second.patch,tip] 1345123d4199 2008-08-21 18:22 +0000 bos 4 [mq]: second.patch 5 6 1[first.patch,qbase] a0074f16fee0 2008-08-21 18:22 +0000 bos 7 [mq]: first.patch 8 9 $ echo 'line 4' >> file1 10 $ hg qrefresh 11 $ hg tip --style=compact --patch 12 2[qtip,second.patch,tip] 773247d5c7fd 2008-08-21 18:22 +0000 bos 13 [mq]: second.patch 14 15 diff -r a0074f16fee0 -r 773247d5c7fd file1 16 --- a/file1 Thu Aug 21 18:22:23 2008 +0000 17 +++ b/file1 Thu Aug 21 18:22:24 2008 +0000 18 @@ -1,3 +1,4 @@ 19 line 1 20 line 2 21 line 3 22 +line 4 23 24 $ hg annotate file1 25 0: line 1 26 1: line 2 27 1: line 3 28 2: line 4 Figure 12.8: Stacking a second patch on top of the first До сих пор, за исключением "hg qnew" и "hg qrefresh", мы использовали только обычные команды Меркуриала. Хотя MQ предлагает множество команд, которые довольно просто использовать, чем вы могли бы подумать о работе с патчами. См. рисунок 12.9. * Команда "hg qseries" выводит список всех патчей, о которых знает MQ в этом репозитории, начиная с самых старых, заканчивая новыми (недавно созданными). * Команда "hg qapplied" выводит список всех патчей, которые MQ уже применило к данному репозиторию. Опять же, начиная с самых старых и заканчивая самым новыми (недавно применёнными). 1 $ hg qseries 2 first.patch 3 second.patch 4 $ hg qapplied 5 first.patch 6 second.patch Рисунок 12.9: Понимание стека патчей командами "hg qseries" и "hg qapplied" 12.5.4 Манипуляции со стеком патчей Только что мы сказали, что есть разница между "известными" и "применёнными" патчами. Так и есть. MQ может управлять патчем, который даже не был применён к репозиторию. Применённый патч имеет соответствующий чейнджсет в репозитории, и результат применения патча и чейнджсета явно видны в рабочей директории. Вы можете отменить (изъять) применение патча командой "hg qpop". Не смотря на это, MQ всё ещё знает (и может управлять) изъятым патчем, но патч уже не будет иметь соответствующий ему чейнджсет в репозитории, и рабочая директория не будет иметь изменений внесённых патчем. ЗДЕСЬ ДОЛЖЕН БЫЛ БЫТЬ ОЧЕРЕДНОЙ РИСУНОК Рисунок 12.10: Применённый и не применённый патч в стеке патчей MQ Вы можете опять применить не применённый (или изъятый) патч командой "hg push". Это создаст новый чейнджсет соответствующий патчу, и изменения патча опять появятся в рабочей директории. См. примеры использования "hg qpop" и "hg qpush" в рис. 12.11. Заметьте, после того как мы изъяли (qpop) патч или два, вывод команды "hg qseries" остался прежний, в то время как вывод команды "hg qapplied" изменился. 1 $ hg qapplied 2 first.patch 3 second.patch 4 $ hg qpop 5 Now at: first.patch 6 $ hg qseries 7 first.patch 8 second.patch 9 $ hg qapplied 10 first.patch 11 $ cat file1 12 line 1 13 line 2 14 line 3 Рисунок 12.11: Изменяя стек применённых патчей 12.5.5 Применение и изъятие нескольких патчей сразу "hg qpush" и "hg qpop" оперируют с одним патчем по-умолчанию, но вы можете применять и изымать много патчей за раз. Параметр "-a" к команде "hg qpush" применяет все неприменённые патчи, а параметр "-a" к команде "hg qpop" изымает все применённые патчи. (Другие пути применения и изъятия патчей см. в параграфе 12.7.) 1 $ hg qpush -a 2 applying second.patch 3 Now at: second.patch 4 $ cat file1 5 line 1 6 line 2 7 line 3 8 line 4 Рисунок 12.12: Применение неприменённых патчей 12.5.6 Safety checks, and overriding them Several MQ commands check the working directory before they do anything, and fail if they find any modifications. They do this to ensure that you won’t lose any changes that you have made, but not yet incorporated into a patch. Figure 12.13 illustrates this; the “hg qnew” command will not create a new patch if there are outstanding changes, caused in this case by the “hg add” of file3. 1 $ echo 'file 3, line 1' >> file3 2 $ hg qnew add-file3.patch 3 $ hg qnew -f add-file3.patch 4 abort: patch "add-file3.patch" already exists Figure 12.13: Forcibly creating a patch Commands that check the working directory all take an “I know what I’m doing” option, which is always named -f. The exact meaning of -f depends on the command. For example, “hg qnew -f” will incorporate any outstanding changes into the new patch it creates, but “hg qpop -f” will revert modifications to any files affected by the patch that it is popping. Be sure to read the documentation for a command’s -f option before you use it! 12.5.7 Working on several patches at once The “hg qrefresh” command always refreshes the topmost applied patch. This means that you can suspend work on one patch (by refreshing it), pop or push to make a different patch the top, and work on that patch for a while. Here’s an example that illustrates how you can use this ability. Let’s say you’re developing a new feature as two patches. The first is a change to the core of your software, and the second—layered on top of the first—changes the user interface to use the code you just added to the core. If you notice a bug in the core while you’re working on the UI patch, it’s easy to fix the core. Simply “hg qrefresh” the UI patch to save your in-progress changes, and “hg qpop” down to the core patch. Fix the core bug, “hg qrefresh” the core patch, and “hg qpush” back to the UI patch to continue where you left off. 12.6 Подробнее о патчах MQ uses the GNU patch command to apply patches, so it’s helpful to know a few more detailed aspects of how patch works, and about patches themselves. 12.6.1 The strip count If you look at the file headers in a patch, you will notice that the pathnames usually have an extra component on the front that isn’t present in the actual path name. This is a holdover from the way that people used to generate patches (people still do this, but it’s somewhat rare with modern revision control tools). Alice would unpack a tarball, edit her files, then decide that she wanted to create a patch. So she’d rename her working directory, unpack the tarball again (hence the need for the rename), and use the -r and -N options to diff to recursively generate a patch between the unmodified directory and the modified one. The result would be that the name of the unmodified directory would be at the front of the left-hand path in every file header, and the name of the modified directory would be at the front of the right-hand path. Since someone receiving a patch from the Alices of the net would be unlikely to have unmodified and modified directories with exactly the same names, the patch command has a -p option that indicates the number of leading path name components to strip when trying to apply a patch. This number is called the strip count. An option of “-p1” means “use a strip count of one”. If patch sees a file name foo/bar/baz in a file header, it will strip foo and try to patch a file named bar/baz. (Strictly speaking, the strip count refers to the number of path separators (and the components that go with them ) to strip. A strip count of one will turn foo/bar into bar, but /foo/bar (notice the extra leading slash) into foo/bar.) The “standard” strip count for patches is one; almost all patches contain one leading path name component that needs to be stripped. Mercurial’s “hg diff” command generates path names in this form, and the “hg import” command and MQ expect patches to have a strip count of one. If you receive a patch from someone that you want to add to your patch queue, and the patch needs a strip count other than one, you cannot just “hg qimport” the patch, because “hg qimport” does not yet have a -p option (see Mercurial bug no. 311). Your best bet is to “hg qnew” a patch of your own, then use “patch -pN” to apply their patch, followed by “hg addremove” to pick up any files added or removed by the patch, followed by “hg qrefresh”. This complexity may become unnecessary; see Mercurial bug no. 311 for details. 12.6.2 Стратегии применения патча When patch applies a hunk, it tries a handful of successively less accurate strategies to try to make the hunk apply. This falling-back technique often makes it possible to take a patch that was generated against an old version of a file, and apply it against a newer version of that file. First, patch tries an exact match, where the line numbers, the context, and the text to be modified must apply exactly. If it cannot make an exact match, it tries to find an exact match for the context, without honouring the line numbering information. If this succeeds, it prints a line of output saying that the hunk was applied, but at some offset from the original line number. If a context-only match fails, patch removes the first and last lines of the context, and tries a reduced context-only match. If the hunk with reduced context succeeds, it prints a message saying that it applied the hunk with a fuzz factor (the number after the fuzz factor indicates how many lines of context patch had to trim before the patch applied). When neither of these techniques works, patch prints a message saying that the hunk in question was rejected. It saves rejected hunks (also simply called “rejects”) to a file with the same name, and an added .rej extension. It also saves an unmodified copy of the file with a .orig extension; the copy of the file without any extensions will contain any changes made by hunks that did apply cleanly. If you have a patch that modifies foo with six hunks, and one of them fails to apply, you will have: an unmodified foo.orig, a foo.rej containing one hunk, and foo, containing the changes made by the five successful five hunks. 12.6.3 Some quirks of patch representation There are a few useful things to know about how patch works with files. * This should already be obvious, but patch cannot handle binary files. * Neither does it care about the executable bit; it creates new files as readable, but not executable. * patch treats the removal of a file as a diff between the file to be removed and the empty file. So your idea of “I deleted this file” looks like “every line of this file was deleted” in a patch. * It treats the addition of a file as a diff between the empty file and the file to be added. So in a patch, your idea of “I added this file” looks like “every line of this file was added”. * It treats a renamed file as the removal of the old name, and the addition of the new name. This means that renamed files have a big footprint in patches. (Note also that Mercurial does not currently try to infer when files have been renamed or copied in a patch.) * patch cannot represent empty files, so you cannot use a patch to represent the notion “I added this empty file to the tree”. 12.6.4 Beware the fuzz While applying a hunk at an offset, or with a fuzz factor, will often be completely successful, these inexact techniques naturally leave open the possibility of corrupting the patched file. The most common cases typically involve applying a patch twice, or at an incorrect location in the file. If patch or “hg qpush” ever mentions an offset or fuzz factor, you should make sure that the modified files are correct afterwards. It’s often a good idea to refresh a patch that has applied with an offset or fuzz factor; refreshing the patch generates new context information that will make it apply cleanly. I say “often,” not “always,” because sometimes refreshing a patch will make it fail to apply against a different revision of the underlying files. In some cases, such as when you’re maintaining a patch that must sit on top of multiple versions of a source tree, it’s acceptable to have a patch apply with some fuzz, provided you’ve verified the results of the patching process in such cases. 12.6.5 Handling rejection If “hg qpush” fails to apply a patch, it will print an error message and exit. If it has left .rej files behind, it is usually best to fix up the rejected hunks before you push more patches or do any further work. If your patch used to apply cleanly, and no longer does because you’ve changed the underlying code that your patches are based on, Mercurial Queues can help; see section 12.8 for details. Unfortunately, there aren’t any great techniques for dealing with rejected hunks. Most often, you’ll need to view the .rej file and edit the target file, applying the rejected hunks by hand. If you’re feeling adventurous, Neil Brown, a Linux kernel hacker, wrote a tool called wiggle [Bro], which is more vigorous than patch in its attempts to make a patch apply. Another Linux kernel hacker, Chris Mason (the author of Mercurial Queues), wrote a similar tool called mpatch [Mas], which takes a simple approach to automating the application of hunks rejected by patch. The mpatch command can help with four common reasons that a hunk may be rejected: * The context in the middle of a hunk has changed. * A hunk is missing some context at the beginning or end. * A large hunk might apply better—either entirely or in part—if it was broken up into smaller hunks. * A hunk removes lines with slightly different content than those currently present in the file. If you use wiggle or mpatch, you should be doubly careful to check your results when you’re done. In fact, mpatch enforces this method of double-checking the tool’s output, by automatically dropping you into a merge program when it has done its job, so that you can verify its work and finish off any remaining merges. 12.7 Получение максимальной производительности от MQ MQ is very efficient at handling a large number of patches. I ran some performance experiments in mid-2006 for a talk that I gave at the 2006 EuroPython conference [O’S06]. I used as my data set the Linux 2.6.17-mm1 patch series, which consists of 1,738 patches. I applied these on top of a Linux kernel repository containing all 27,472 revisions between Linux 2.6.12-rc2 and Linux 2.6.17. On my old, slow laptop, I was able to “hg qpush -a” all 1,738 patches in 3.5 minutes, and “hg qpop -a” them all in 30 seconds. (On a newer laptop, the time to push all patches dropped to two minutes.) I could “hg qrefresh” one of the biggest patches (which made 22,779 lines of changes to 287 files) in 6.6 seconds. Clearly, MQ is well suited to working in large trees, but there are a few tricks you can use to get the best performance of it. First of all, try to “batch” operations together. Every time you run “hg qpush” or “hg qpop”, these commands scan the working directory once to make sure you haven’t made some changes and then forgotten to run “hg qrefresh”. On a small tree, the time that this scan takes is unnoticeable. However, on a medium-sized tree (containing tens of thousands of files), it can take a second or more. The “hg qpush” and “hg qpop” commands allow you to push and pop multiple patches at a time. You can identify the “destination patch” that you want to end up at. When you “hg qpush” with a destination specified, it will push patches until that patch is at the top of the applied stack. When you “hg qpop” to a destination, MQ will pop patches until the destination patch is at the top. You can identify a destination patch using either the name of the patch, or by number. If you use numeric addressing, patches are counted from zero; this means that the first patch is zero, the second is one, and so on. 12.8 Updating your patches when the underlying code changes It’s common to have a stack of patches on top of an underlying repository that you don’t modify directly. If you’re working on changes to third-party code, or on a feature that is taking longer to develop than the rate of change of the code beneath, you will often need to sync up with the underlying code, and fix up any hunks in your patches that no longer apply. This is called rebasing your patch series. The simplest way to do this is to “hg qpop -a” your patches, then “hg pull” changes into the underlying repository, and finally “hg qpush -a” your patches again. MQ will stop pushing any time it runs across a patch that fails to apply during conflicts, allowing you to fix your conflicts, “hg qrefresh” the affected patch, and continue pushing until you have fixed your entire stack. This approach is easy to use and works well if you don’t expect changes to the underlying code to affect how well your patches apply. If your patch stack touches code that is modified frequently or invasively in the underlying repository, however, fixing up rejected hunks by hand quickly becomes tiresome. It’s possible to partially automate the rebasing process. If your patches apply cleanly against some revision of the underlying repo, MQ can use this information to help you to resolve conflicts between your patches and a different revision. The process is a little involved. 1. To begin, “hg qpush -a” all of your patches on top of the revision where you know that they apply cleanly. 2. Save a backup copy of your patch directory using “hg qsave -e -c”. This prints the name of the directory that it has saved the patches in. It will save the patches to a directory called .hg/patches.N, where N is a small integer. It also commits a “save changeset” on top of your applied patches; this is for internal book-keeping, and records the states of the series and status files. 3. Use “hg pull” to bring new changes into the underlying repository. (Don’t run “hg pull -u”; see below for why.) 4. Update to the new tip revision, using “hg update -C” to override the patches you have pushed. 5. Merge all patches using “hg qpush -m -a”. The -m option to “hg qpush” tells MQ to perform a three-way merge if the patch fails to apply. During the “hg qpush -m”, each patch in the series file is applied normally. If a patch applies with fuzz or rejects, MQ looks at the queue you “hg qsave”d, and performs a three-way merge with the corresponding changeset. This merge uses Mercurial’s normal merge machinery, so it may pop up a GUI merge tool to help you to resolve problems. When you finish resolving the effects of a patch, MQ refreshes your patch based on the result of the merge. At the end of this process, your repository will have one extra head from the old patch queue, and a copy of the old patch queue will be in .hg/patches.N. You can remove the extra head using “hg qpop -a -n patches.N” or “hg strip”. You can delete .hg/patches.N once you are sure that you no longer need it as a backup. 12.9 Identifying patches MQ commands that work with patches let you refer to a patch either by using its name or by a number. By name is obvious enough; pass the name foo.patch to “hg qpush”, for example, and it will push patches until foo.patch is applied. As a shortcut, you can refer to a patch using both a name and a numeric offset; foo.patch-2 means “two patches before foo.patch”, while bar.patch+4 means “four patches after bar.patch”. Referring to a patch by index isn’t much different. The first patch printed in the output of “hg qseries” is patch zero (yes, it’s one of those start-at-zero counting systems); the second is patch one; and so on MQ also makes it easy to work with patches when you are using normal Mercurial commands. Every command that accepts a changeset ID will also accept the name of an applied patch. MQ augments the tags normally in the repository with an eponymous one for each applied patch. In addition, the special tags qbase and qtip identify the “bottom-most” and topmost applied patches, respectively. These additions to Mercurial’s normal tagging capabilities make dealing with patches even more of a breeze. * Want to patchbomb a mailing list with your latest series of changes? 1 hg email qbase:qtip (Don’t know what “patchbombing” is? See section 14.4.) * Need to see all of the patches since foo.patch that have touched files in a subdirectory of your tree? hg log -r foo.patch:qtip subdir Because MQ makes the names of patches available to the rest of Mercurial through its normal internal tag machinery, you don’t need to type in the entire name of a patch when you want to identify it by name. $ hg qapplied first.patch second.patch $ hg log -r qbase:qtip changeset: 1:f1313a963bab tag: first.patch tag: qbase user: Bryan O'Sullivan date: Thu Aug 21 18:22:22 2008 +0000 summary: [mq]: first.patch 11 changeset: 2:e1db8b8f02dc tag: qtip tag: second.patch tag: tip user: Bryan O'Sullivan date: Thu Aug 21 18:22:22 2008 +0000 summary: [mq]: second.patch 19 $ hg export second.patch # HG changeset patch # User Bryan O'Sullivan # Date 1219342942 0 # Node ID e1db8b8f02dca51b143a44edc0a46a76ceaa6c81 # Parent f1313a963babfa9074d3ad141b53effc585ecbfb [mq]: second.patch 27 diff -r f1313a963bab -r e1db8b8f02dc other.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/other.c Thu Aug 21 18:22:22 2008 +0000 @@ -0,0 +1,1 @@ +double u; Figure 12.14: Using MQ’s tag features to work with patches Another nice consequence of representing patch names as tags is that when you run the “hg log” command, it will display a patch’s name as a tag, simply as part of its normal output. This makes it easy to visually distinguish applied patches from underlying “normal” revisions. Figure 12.14 shows a few normal Mercurial commands in use with applied patches. 12.10 Полезно знать There are a number of aspects of MQ usage that don’t fit tidily into sections of their own, but that are good to know. Here they are, in one place. * Normally, when you “hg qpop” a patch and “hg qpush” it again, the changeset that represents the patch after the pop/push will have a different identity than the changeset that represented the hash beforehand. See section B.1.13 for information as to why this is. * It’s not a good idea to “hg merge” changes from another branch with a patch changeset, at least if you want to maintain the “patchiness” of that changeset and changesets below it on the patch stack. If you try to do this, it will appear to succeed, but MQ will become confused. 12.11 Управление патчами в репозитории Because MQ’s .hg/patches directory resides outside a Mercurial repository’s working directory, the “underlying” Mercurial repository knows nothing about the management or presence of patches. This presents the interesting possibility of managing the contents of the patch directory as a Mercurial repository in its own right. This can be a useful way to work. For example, you can work on a patch for a while, “hg qrefresh” it, then “hg commit” the current state of the patch. This lets you “roll back” to that version of the patch later on. You can then share different versions of the same patch stack among multiple underlying repositories. I use this when I am developing a Linux kernel feature. I have a pristine copy of my kernel sources for each of several CPU architectures, and a cloned repository under each that contains the patches I am working on. When I want to test a change on a different architecture, I push my current patches to the patch repository associated with that kernel tree, pop and push all of my patches, and build and test that kernel. Managing patches in a repository makes it possible for multiple developers to work on the same patch series without colliding with each other, all on top of an underlying source base that they may or may not control. 12.11.1 MQ support for patch repositories MQ helps you to work with the .hg/patches directory as a repository; when you prepare a repository for working with patches using “hg qinit”, you can pass the -c option to create the .hg/patches directory as a Mercurial repository. Note: If you forget to use the -c option, you can simply go into the .hg/patches directory at any time and run “hg init”. Don’t forget to add an entry for the status file to the .hgignore file, though (“hg qinit -c” does this for you automatically); you really don’t want to manage the status file. As a convenience, if MQ notices that the .hg/patches directory is a repository, it will automatically “hg add” every patch that you create and import. MQ provides a shortcut command, “hg qcommit”, that runs “hg commit” in the .hg/patches directory. This saves some bothersome typing. Finally, as a convenience to manage the patch directory, you can define the alias mq on Unix systems. For example, on Linux systems using the bash shell, you can include the following snippet in your ~/.bashrc. 1 alias mq=‘hg -R $(hg root)/.hg/patches' You can then issue commands of the form “mq pull” from the main repository. 12.11.2 A few things to watch out for MQ’s support for working with a repository full of patches is limited in a few small respects. MQ cannot automatically detect changes that you make to the patch directory. If you “hg pull”, manually edit, or “hg update” changes to patches or the series file, you will have to “hg qpop -a” and then “hg qpush -a” in the underlying repository to see those changes show up there. If you forget to do this, you can confuse MQ’s idea of which patches are applied. 12.12 Сторонние утилиты для работы с патчами Once you’ve been working with patches for a while, you’ll find yourself hungry for tools that will help you to understand and manipulate the patches you’re dealing with. The diffstat command [Dic] generates a histogram of the modifications made to each file in a patch. It provides a good way to “get a sense of” a patch—which files it affects, and how much change it introduces to each file and as a whole. (I find that it’s a good idea to use diffstat’s -p option as a matter of course, as otherwise it will try to do clever things with prefixes of file names that inevitably confuse at least me.) $ diffstat -p1 remove-redundant-null-checks.patch drivers/char/agp/sgi-agp.c | 5 ++--- drivers/char/hvcs.c | 11 +++++------ drivers/message/fusion/mptfc.c | 6 ++---- drivers/message/fusion/mptsas.c | 3 +-- drivers/net/fs_enet/fs_enet-mii.c | 3 +-- drivers/net/wireless/ipw2200.c | 22 ++++++---------------- drivers/scsi/libata-scsi.c | 4 +--- drivers/video/au1100fb.c | 3 +-- 8 files changed, 19 insertions(+), 38 deletions(-) $ filterdiff -i '⋆/video/⋆' remove-redundant-null-checks.patch null-checks.patch --- a/drivers/video/au1100fb.c~remove-redundant-null-checks-before-free-in-drivers +++ a/drivers/video/au1100fb.c @@ -743,8 +743,7 @@ void __exit au1100fb_cleanup(void) { driver_unregister(&au1100fb_driver); 17 - if (drv_info.opt_mode) - kfree(drv_info.opt_mode); + kfree(drv_info.opt_mode); } 22 module_init(au1100fb_init); 12.15 Команды diffstat, filterdiff и lsdiff The patchutils package [Wau] is invaluable. It provides a set of small utilities that follow the “Unix philosophy;” each does one useful thing with a patch. The patchutils command I use most is filterdiff, which extracts subsets from a patch file. For example, given a patch that modifies hundreds of files across dozens of directories, a single invocation of filterdiff can generate a smaller patch that only touches files whose names match a particular glob pattern. See section 13.9.2 for another example. 12.13 Хорошие методы работы с патчами Работаете ли вы над набором патчей для отправки в проект свободного программного обеспечения и/или с открытым кодом, или же вы планируете интерпретировать набор патчей как последовательность обычных наборов изменений, вы можете использовать некоторые простые техники для хорошей организации работы. Give your patches descriptive names. A good name for a patch might be rework-device-alloc.patch, because it will immediately give you a hint what the purpose of the patch is. Long names shouldn’t be a problem; you won’t be typing the names often, but you will be running commands like “hg qapplied” and “hg qtop” over and over. Good naming becomes especially important when you have a number of patches to work with, or if you are juggling a number of different tasks and your patches only get a fraction of your attention. Be aware of what patch you’re working on. Use the “hg qtop” command and skim over the text of your patches frequently—for example, using “hg tip -p”)—to be sure of where you stand. I have several times worked on and “hg qrefresh”ed a patch other than the one I intended, and it’s often tricky to migrate changes into the right patch after making them in the wrong one. For this reason, it is very much worth investing a little time to learn how to use some of the third-party tools I described in section 12.12, particularly diffstat and filterdiff. The former will give you a quick idea of what changes your patch is making, while the latter makes it easy to splice hunks selectively out of one patch and into another. 12.14 MQ cookbook 12.14.1 Manage “trivial” patches Because the overhead of dropping files into a new Mercurial repository is so low, it makes a lot of sense to manage patches this way even if you simply want to make a few changes to a source tarball that you downloaded. Begin by downloading and unpacking the source tarball, and turning it into a Mercurial repository. $ download netplug-1.2.5.tar.bz2 $ tar jxf netplug-1.2.5.tar.bz2 $ cd netplug-1.2.5 $ hg init $ hg commit -q --addremove --message netplug-1.2.5 $ cd .. $ hg clone netplug-1.2.5 netplug updating working directory 18 files updated, 0 files merged, 0 files removed, 0 files unresolved Continue by creating a patch stack and making your changes. $ cd netplug $ hg qinit $ hg qnew -m 'fix build problem with gcc 4' build-fix.patch $ perl -pi -e 's/int addr_len/socklen_t addr_len/' netlink.c $ hg qrefresh $ hg tip -p changeset: 1:7dc864647915 tag: qtip tag: build-fix.patch tag: tip tag: qbase user: Bryan O'Sullivan date: Thu Aug 21 18:22:23 2008 +0000 summary: fix build problem with gcc 4 15 diff -r 4691e5a301c4 -r 7dc864647915 netlink.c --- a/netlink.c Thu Aug 21 18:22:23 2008 +0000 +++ b/netlink.c Thu Aug 21 18:22:23 2008 +0000 @@ -275,7 +275,7 @@ exit(1); } 22 - int addr_len = sizeof(addr); + socklen_t addr_len = sizeof(addr); 25 if (getsockname(fd, (struct sockaddr ⋆) &addr, &addr_len) == -1) { do_log(LOG_ERR, "Could not get socket details: %m"); 28 Let’s say a few weeks or months pass, and your package author releases a new version. First, bring their changes into the repository. $ hg qpop -a Patch queue now empty cd .. $ download netplug-1.2.8.tar.bz2 $ hg clone netplug-1.2.5 netplug-1.2.8 updating working directory 18 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cd netplug-1.2.8 $ hg locate -0 | xargs -0 rm $ cd .. $ tar jxf netplug-1.2.8.tar.bz2 $ cd netplug-1.2.8 $ hg commit --addremove --message netplug-1.2.8 The pipeline starting with “hg locate” above deletes all files in the working directory, so that “hg commit”’s --addremove option can actually tell which files have really been removed in the newer version of the source. Finally, you can apply your patches on top of the new tree. $ cd ../netplug $ hg pull ../netplug-1.2.8 pulling from ../netplug-1.2.8 searching for changes adding changesets adding manifests adding file changes added 1 changesets with 12 changes to 12 files (run 'hg update' to get a working copy) $ hg qpush -a (working directory not at tip) applying build-fix.patch Now at: build-fix.patch 12.14.2 Combining entire patches MQ provides a command, “hg qfold” that lets you combine entire patches. This “folds” the patches you name, in the order you name them, into the topmost applied patch, and concatenates their descriptions onto the end of its description. The patches that you fold must be unapplied before you fold them. The order in which you fold patches matters. If your topmost applied patch is foo, and you “hg qfold” bar and quux into it, you will end up with a patch that has the same effect as if you applied first foo, then bar, followed by quux. 12.14.3 Слияние частей из одного патча в другой Merging part of one patch into another is more difficult than combining entire patches. If you want to move changes to entire files, you can use filterdiff’s -i and -x options to choose the modifications to snip out of one patch, concatenating its output onto the end of the patch you want to merge into. You usually won’t need to modify the patch you’ve merged the changes from. Instead, MQ will report some rejected hunks when you “hg qpush” it (from the hunks you moved into the other patch), and you can simply “hg qrefresh” the patch to drop the duplicate hunks. If you have a patch that has multiple hunks modifying a file, and you only want to move a few of those hunks, the job becomes more messy, but you can still partly automate it. Use “lsdiff -nvv” to print some metadata about the patch. $ lsdiff -nvv remove-redundant-null-checks.patch 22 File #1 a/drivers/char/agp/sgi-agp.c 4 Hunk #1 static int __devinit agp_sgi_init(void) 37 File #2 a/drivers/char/hvcs.c 39 Hunk #1 static struct tty_operations hvcs_ops = 53 Hunk #2 static int hvcs_alloc_index_list(int n) 69 File #3 a/drivers/message/fusion/mptfc.c 8 71 Hunk #1 mptfc_GetFcDevPage0(MPT_ADAPTER ⋆ioc, in 85 File #4 a/drivers/message/fusion/mptsas.c 10 87 Hunk #1 mptsas_probe_hba_phys(MPT_ADAPTER ⋆ioc) 98 File #5 a/drivers/net/fs_enet/fs_enet-mii.c 12 100 Hunk #1 static struct fs_enet_mii_bus ⋆create_bu 111 File #6 a/drivers/net/wireless/ipw2200.c 14 113 Hunk #1 static struct ipw_fw_error ⋆ipw_alloc_er 126 Hunk #2 static ssize_t clear_error(struct device 140 Hunk #3 static void ipw_irq_tasklet(struct ipw_p 150 Hunk #4 static void ipw_pci_remove(struct pci_de 164 File #7 a/drivers/scsi/libata-scsi.c 166 Hunk #1 int ata_cmd_ioctl(struct scsi_device ⋆sc 178 File #8 a/drivers/video/au1100fb.c 180 Hunk #1 void __exit au1100fb_cleanup(void) This command prints three different kinds of number: * (in the first column) a file number to identify each file modified in the patch; * (on the next line, indented) the line number within a modified file where a hunk starts; and * (on the same line) a hunk number to identify that hunk. You’ll have to use some visual inspection, and reading of the patch, to identify the file and hunk numbers you’ll want, but you can then pass them to to filterdiff’s --files and --hunks options, to select exactly the file and hunk you want to extract. Once you have this hunk, you can concatenate it onto the end of your destination patch and continue with the remainder of section 12.14.2. 12.15 Сравнение quilt and MQ If you are already familiar with quilt, MQ provides a similar command set. There are a few differences in the way that it works. You will already have noticed that most quilt commands have MQ counterparts that simply begin with a “q”. The exceptions are quilt’s add and remove commands, the counterparts for which are the normal Mercurial “hg add” and “hg remove” commands. There is no MQ equivalent of the quilt edit command. Глава 13 Advanced uses of Mercurial Queues While it’s easy to pick up straightforward uses of Mercurial Queues, use of a little discipline and some of MQ’s less frequently used capabilities makes it possible to work in complicated development environments. In this chapter, I will use as an example a technique I have used to manage the development of an Infiniband device driver for the Linux kernel. The driver in question is large (at least as drivers go), with 25,000 lines of code spread across 35 source files. It is maintained by a small team of developers. While much of the material in this chapter is specific to Linux, the same principles apply to any code base for which you’re not the primary owner, and upon which you need to do a lot of development. 13.1 The problem of many targets The Linux kernel changes rapidly, and has never been internally stable; developers frequently make drastic changes between releases. This means that a version of the driver that works well with a particular released version of the kernel will not even compile correctly against, typically, any other version. To maintain a driver, we have to keep a number of distinct versions of Linux in mind. * One target is the main Linux kernel development tree. Maintenance of the code is in this case partly shared by other developers in the kernel community, who make “drive-by” modifications to the driver as they develop and refine kernel subsystems. * We also maintain a number of “backports” to older versions of the Linux kernel, to support the needs of customers who are running older Linux distributions that do not incorporate our drivers. (To backport a piece of code is to modify it to work in an older version of its target environment than the version it was developed for.) * Finally, we make software releases on a schedule that is necessarily not aligned with those used by Linux distributors and kernel developers, so that we can deliver new features to customers without forcing them to upgrade their entire kernels or distributions. 13.1.1 Tempting approaches that don’t work well There are two “standard” ways to maintain a piece of software that has to target many different environments. The first is to maintain a number of branches, each intended for a single target. The trouble with this approach is that you must maintain iron discipline in the flow of changes between repositories. A new feature or bug fix must start life in a “pristine” repository, then percolate out to every backport repository. Backport changes are more limited in the branches they should propagate to; a backport change that is applied to a branch where it doesn’t belong will probably stop the driver from compiling. The second is to maintain a single source tree filled with conditional statements that turn chunks of code on or off depending on the intended target. Because these “ifdefs” are not allowed in the Linux kernel tree, a manual or automatic process must be followed to strip them out and yield a clean tree. A code base maintained in this fashion rapidly becomes a rat’s nest of conditional blocks that are difficult to understand and maintain. Neither of these approaches is well suited to a situation where you don’t “own” the canonical copy of a source tree. In the case of a Linux driver that is distributed with the standard kernel, Linus’s tree contains the copy of the code that will be treated by the world as canonical. The upstream version of “my” driver can be modified by people I don’t know, without me even finding out about it until after the changes show up in Linus’s tree. These approaches have the added weakness of making it difficult to generate well-formed patches to submit upstream. In principle, Mercurial Queues seems like a good candidate to manage a development scenario such as the above. While this is indeed the case, MQ contains a few added features that make the job more pleasant. 13.2 Conditionally applying patches with guards Perhaps the best way to maintain sanity with so many targets is to be able to choose specific patches to apply for a given situation. MQ provides a feature called “guards” (which originates with quilt’s guards command) that does just this. To start off, let’s create a simple repository for experimenting in. $ 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 This gives us a tiny repository that contains two patches that don’t have any dependencies on each other, because they touch different files. The idea behind conditional application is that you can “tag” a patch with a guard, which is simply a text string of your choosing, then tell MQ to select specific guards to use when applying patches. MQ will then either apply, or skip over, a guarded patch, depending on the guards that you have selected. A patch can have an arbitrary number of guards; each one is positive (“apply this patch if this guard is selected”) or negative (“skip this patch if this guard is selected”). A patch with no guards is always applied. 13.3 Controlling the guards on a patch The “hg qguard” command lets you determine which guards should apply to a patch, or display the guards that are already in effect. Without any arguments, it displays the guards on the current topmost patch. $ hg qguard goodbye.patch: unguarded To set a positive guard on a patch, prefix the name of the guard with a “+”. hg qguard +foo $ hg qguard goodbye.patch: +foo To set a negative guard on a patch, prefix the name of the guard with a “-”. $ hg qguard hello.patch -quux $ hg qguard hello.patch hello.patch: -quux Note: The “hg qguard” command sets the guards on a patch; it doesn’t modify them. What this means is that if you run “hg qguard +a +b” on a patch, then “hg qguard +c” on the same patch, the only guard that will be set on it afterwards is +c. Mercurial stores guards in the series file; the form in which they are stored is easy both to understand and to edit by hand. (In other words, you don’t have to use the “hg qguard” command if you don’t want to; it’s okay to simply edit the series file.) $ cat .hg/patches/series hello.patch #-quux goodbye.patch #+foo 13.4 Selecting the guards to use The “hg qselect” command determines which guards are active at a given time. The effect of this is to determine which patches MQ will apply the next time you run “hg qpush”. It has no other effect; in particular, it doesn’t do anything to patches that are already applied. With no arguments, the “hg qselect” command lists the guards currently in effect, one per line of output. Each argument is treated as the name of a guard to apply. $ hg qpop -a 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 In case you’re interested, the currently selected guards are stored in the guards file. $ cat .hg/patches/guards oo We can see the effect the selected guards have when we run “hg qpush”. $ hg qpush -a applying hello.patch applying goodbye.patch Now at: goodbye.patch A guard cannot start with a “+” or “-” character. The name of a guard must not contain white space, but most othter characters are acceptable. If you try to use a guard with an invalid name, MQ will complain: $ hg qselect +foo abort: guard '+foo' starts with invalid character: '+' Changing the selected guards changes the patches that are applied. $ hg qselect quux 2 number of guarded, applied patches has changed from 0 to 2 $ hg qpop -a Patch queue now empty $ hg qpush -a patch series already fully applied You can see in the example below that negative guards take precedence over positive guards. $ hg qselect foo bar number of unguarded, unapplied patches has changed from 0 to 2 $ hg qpop -a o patches applied $ hg qpush -a applying hello.patch applying goodbye.patch Now at: goodbye.patch 13.5 Правила применения патчей MQ The rules that MQ uses when deciding whether to apply a patch are as follows. * A patch that has no guards is always applied. * If the patch has any negative guard that matches any currently selected guard, the patch is skipped. * If the patch has any positive guard that matches any currently selected guard, the patch is applied. * If the patch has positive or negative guards, but none matches any currently selected guard, the patch is skipped. 13.6 Настройка рабочей среды In working on the device driver I mentioned earlier, I don’t apply the patches to a normal Linux kernel tree. Instead, I use a repository that contains only a snapshot of the source files and headers that are relevant to Infiniband development. This repository is 1% the size of a kernel repository, so it’s easier to work with. I then choose a “base” version on top of which the patches are applied. This is a snapshot of the Linux kernel tree as of a revision of my choosing. When I take the snapshot, I record the changeset ID from the kernel repository in the commit message. Since the snapshot preserves the “shape” and content of the relevant parts of the kernel tree, I can apply my patches on top of either my tiny repository or a normal kernel tree. Normally, the base tree atop which the patches apply should be a snapshot of a very recent upstream tree. This best facilitates the development of patches that can easily be submitted upstream with few or no modifications. 13.7 Dividing up the series file I categorise the patches in the series file into a number of logical groups. Each section of like patches begins with a block of comments that describes the purpose of the patches that follow. The sequence of patch groups that I maintain follows. The ordering of these groups is important; I’ll describe why after I introduce the groups. * The “accepted” group. Patches that the development team has submitted to the maintainer of the Infiniband subsystem, and which he has accepted, but which are not present in the snapshot that the tiny repository is based on. These are “read only” patches, present only to transform the tree into a similar state as it is in the upstream maintainer’s repository. * The “rework” group. Patches that I have submitted, but that the upstream maintainer has requested modifications to before he will accept them. * The “pending” group. Patches that I have not yet submitted to the upstream maintainer, but which we have finished working on. These will be “read only” for a while. If the upstream maintainer accepts them upon submission, I’ll move them to the end of the “accepted” group. If he requests that I modify any, I’ll move them to the beginning of the “rework” group. * The “in progress” group. Patches that are actively being developed, and should not be submitted anywhere yet. * The “backport” group. Patches that adapt the source tree to older versions of the kernel tree. * The “do not ship” group. Patches that for some reason should never be submitted upstream. For example, one such patch might change embedded driver identification strings to make it easier to distinguish, in the field, between an out-of-tree version of the driver and a version shipped by a distribution vendor. Now to return to the reasons for ordering groups of patches in this way. We would like the lowest patches in the stack to be as stable as possible, so that we will not need to rework higher patches due to changes in context. Putting patches that will never be changed first in the series file serves this purpose. We would also like the patches that we know we’ll need to modify to be applied on top of a source tree that resembles the upstream tree as closely as possible. This is why we keep accepted patches around for a while. The “backport” and “do not ship” patches float at the end of the series file. The backport patches must be applied on top of all other patches, and the “do not ship” patches might as well stay out of harm’s way. 13.8 Maintaining the patch series In my work, I use a number of guards to control which patches are to be applied. * “Accepted” patches are guarded with accepted. I enable this guard most of the time. When I’m applying the patches on top of a tree where the patches are already present, I can turn this patch off, and the patches that follow it will apply cleanly. * Patches that are “finished”, but not yet submitted, have no guards. If I’m applying the patch stack to a copy of the upstream tree, I don’t need to enable any guards in order to get a reasonably safe source tree. * Those patches that need reworking before being resubmitted are guarded with rework. Для тех патчей, которые находятся в разработке, я использую devel * A backport patch may have several guards, one for each version of the kernel to which it applies. For example, a patch that backports a piece of code to 2.6.9 will have a 2.6.9 guard. This variety of guards gives me considerable flexibility in qdetermining what kind of source tree I want to end up with. For most situations, the selection of appropriate guards is automated during the build process, but I can manually tune the guards to use for less common circumstances. 13.8.1 The art of writing backport patches Using MQ, writing a backport patch is a simple process. All such a patch has to do is modify a piece of code that uses a kernel feature not present in the older version of the kernel, so that the driver continues to work correctly under that older version. A useful goal when writing a good backport patch is to make your code look as if it was written for the older version of the kernel you’re targeting. The less obtrusive the patch, the easier it will be to understand and maintain. If you’re writing a collection of backport patches to avoid the “rat’s nest” effect of lots of #ifdefs (hunks of source code that are only used conditionally) in your code, don’t introduce version-dependent #ifdefs into the patches. Instead, write several patches, each of which makes unconditional changes, and control their application using guards. There are two reasons to divide backport patches into a distinct group, away from the “regular” patches whose effects they modify. The first is that intermingling the two makes it more difficult to use a tool like the patchbomb extension to automate the process of submitting the patches to an upstream maintainer. The second is that a backport patch could perturb the context in which a subsequent regular patch is applied, making it impossible to apply the regular patch cleanly without the earlier backport patch already being applied. 13.9 Полезные советы для работы с MQ 13.9.1 Расположение патчей в директориях If you’re working on a substantial project with MQ, it’s not difficult to accumulate a large number of patches. For example, I have one patch repository that contains over 250 patches. If you can group these patches into separate logical categories, you can if you like store them in different directories; MQ has no problems with patch names that contain path separators. 13.9.2 Просмотр истории патча If you’re developing a set of patches over a long time, it’s a good idea to maintain them in a repository, as discussed in section 12.11. If you do so, you’ll quickly discover that using the “hg diff” command to look at the history of changes to a patch is unworkable. This is in part because you’re looking at the second derivative of the real code (a diff of a diff), but also because MQ adds noise to the process by modifying time stamps and directory names when it updates a patch. However, you can use the extdiff extension, which is bundled with Mercurial, to turn a diff of two versions of a patch into something readable. To do this, you will need a third-party package called patchutils [Wau]. This provides a command named interdiff, which shows the differences between two diffs as a diff. Used on two versions of the same diff, it generates a diff that represents the diff from the first to the second version. You can enable the extdiff extension in the usual way, by adding a line to the [extensions] section of your hgrc. [extensions] extdiff = The interdiff command expects to be passed the names of two files, but the extdiff extension passes the program it runs a pair of directories, each of which can contain an arbitrary number of files. We thus need a small program that will run interdiff on each pair of files in these two directories. This program is available as hg-interdiff in the examples directory of the source code repository that accompanies this book. #!/usr/bin/env python # # Adapter for using interdiff with mercurial's extdiff extension. # # Copyright 2006 Bryan O'Sullivan # # This software may be used and distributed according to the terms of # the GNU General Public License, incorporated herein by reference. 9 import os, sys 11 def walk(base): # yield all non-directories below the base path. for root, dirs, files in os.walk(base): for f in files: path = os.path.join(root, f) yield path[len(base)+1:], path else: if os.path.isfile(base): yield '', base 21 # create list of unique file names under both directories. files = dict(walk(sys.argv[1])) files.update(walk(sys.argv[2])) files = files.keys() iles.sort() 27 def name(base, f): if f: path = os.path.join(base, f) else: path = base # interdiff requires two files; use /dev/null if one is missing. if os.path.exists(path): return path eturn '/dev/null' 37 ret = 0 39 for f in files: if os.system('interdiff "%s" "%s"' % (name(sys.argv[1], f), name(sys.argv[2], f))): ret = 1 44 sys.exit(ret) With the hg-interdiff program in your shell’s search path, you can run it as follows, from inside an MQ patch directory: hg extdiff -p hg-interdiff -r A:B my-change.patch Since you’ll probably want to use this long-winded command a lot, you can get hgext to make it available as a normal Mercurial command, again by editing your hgrc. [extdiff] cmd.interdiff = hg-interdiff This directs hgext to make an interdiff command available, so you can now shorten the previous invocation of “hg extdiff” to something a little more wieldy. hg interdiff -r A:B my-change.patch Note: The interdiff command works well only if the underlying files against which versions of a patch are generated remain the same. If you create a patch, modify the underlying files, and then regenerate the patch, interdiff may not produce useful output. The extdiff extension is useful for more than merely improving the presentation of MQ patches. To read more about it, go to section 14.2. Глава 14 Дополнительная функциональность с расширениями While the core of Mercurial is quite complete from a functionality standpoint, it’s deliberately shorn of fancy features. This approach of preserving simplicity keeps the software easy to deal with for both maintainers and users. However, Mercurial doesn’t box you in with an inflexible command set: you can add features to it as extensions (sometimes known as plugins). We’ve already discussed a few of these extensions in earlier chapters. * Section 3.3 covers the fetch extension; this combines pulling new changes and merging them with local changes into a single command, “hg fetch”. * In chapter 10, we covered several extensions that are useful for hook-related functionality: acl adds access control lists; bugzilla adds integration with the Bugzilla bug tracking system; and notify sends notification emails on new changes. * The Mercurial Queues patch management extension is so invaluable that it merits two chapters and an appendix all to itself. Chapter 12 covers the basics; chapter 13 discusses advanced topics; and appendix B goes into detail on each command. In this chapter, we’ll cover some of the other extensions that are available for Mercurial, and briefly touch on some of the machinery you’ll need to know about if you want to write an extension of your own. * In section 14.1, we’ll discuss the possibility of huge performance improvements using the inotify extension. 14.1 Лучшая производительность с расширением inotify Are you interested in having some of the most common Mercurial operations run as much as a hundred times faster? Read on! Mercurial has great performance under normal circumstances. For example, when you run the “hg status” command, Mercurial has to scan almost every directory and file in your repository so that it can display file status. Many other Mercurial commands need to do the same work behind the scenes; for example, the “hg diff” command uses the status machinery to avoid doing an expensive comparison operation on files that obviously haven’t changed. Because obtaining file status is crucial to good performance, the authors of Mercurial have optimised this code to within an inch of its life. However, there’s no avoiding the fact that when you run “hg status”, Mercurial is going to have to perform at least one expensive system call for each managed file to determine whether it’s changed since the last time Mercurial checked. For a sufficiently large repository, this can take a long time. To put a number on the magnitude of this effect, I created a repository containing 150,000 managed files. I timed “hg status” as taking ten seconds to run, even when none of those files had been modified. Many modern operating systems contain a file notification facility. If a program signs up to an appropriate service, the operating system will notify it every time a file of interest is created, modified, or deleted. On Linux systems, the kernel component that does this is called inotify. Mercurial’s inotify extension talks to the kernel’s inotify component to optimise “hg status” commands. The extension has two components. A daemon sits in the background and receives notifications from the inotify subsystem. It also listens for connections from a regular Mercurial command. The extension modifies Mercurial’s behaviour so that instead of scanning the filesystem, it queries the daemon. Since the daemon has perfect information about the state of the repository, it can respond with a result instantaneously, avoiding the need to scan every directory and file in the repository. Recall the ten seconds that I measured plain Mercurial as taking to run “hg status” on a 150,000 file repository. With the inotify extension enabled, the time dropped to 0.1 seconds, a factor of one hundred faster. Before we continue, please pay attention to some caveats. * The inotify extension is Linux-specific. Because it interfaces directly to the Linux kernel’s inotify subsystem, it does not work on other operating systems. * It should work on any Linux distribution that was released after early 2005. Older distributions are likely to have a kernel that lacks inotify, or a version of glibc that does not have the necessary interfacing support. * Not all filesystems are suitable for use with the inotify extension. Network filesystems such as NFS are a non-starter, for example, particularly if you’re running Mercurial on several systems, all mounting the same network filesystem. The kernel’s inotify system has no way of knowing about changes made on another system. Most local filesystems (e.g. ext3, XFS, ReiserFS) should work fine. The inotify extension is not yet shipped with Mercurial as of May 2007, so it’s a little more involved to set up than other extensions. But the performance improvement is worth it! The extension currently comes in two parts: a set of patches to the Mercurial source code, and a library of Python bindings to the inotify subsystem. Note: There are two Python inotify binding libraries. One of them is called pyinotify, and is packaged by some Linux distributions as python-inotify. This is not the one you’ll need, as it is too buggy and inefficient to be practical. To get going, it’s best to already have a functioning copy of Mercurial installed. Note: If you follow the instructions below, you’ll be replacing and overwriting any existing installation of Mercurial that you might already have, using the latest “bleeding edge” Mercurial code. Don’t say you weren’t warned! 1. Clone the Python inotify binding repository. Build and install it. $ hg clone http://hg.kublai.com/python/inotify $ cd inotify $ python setup.py build --force $ sudo python setup.py install --skip-build 2. Clone the crew Mercurial repository. Clone the inotify patch repository so that Mercurial Queues will be able to apply patches to your cope of the crew repository. $ hg clone http://hg.intevation.org/mercurial/crew $ hg clone crew inotify $ hg clone http://hg.kublai.com/mercurial/patches/inotify inotify/.hg/patches 3. Make sure that you have the Mercurial Queues extension, mq, enabled. If you’ve never used MQ, read section 12.5 to get started quickly. 4. Go into the inotify repo, and apply all of the inotify patches using the -a option to the “hg qpush” command. $ cd inotify $ hg qpush -a Если вы получаете сообщение об ошибке при выполнении hg qpush, лучше не продолжать. Попробуйте обратиться за помощью. 5. Сборка и установка модифицированной версии Mercurial. $ python setup.py build --force $ sudo python setup.py install --skip-build Once you’ve build a suitably patched version of Mercurial, all you need to do to enable the inotify extension is add an entry to your hgrc. [extensions] inotify = When the inotify extension is enabled, Mercurial will automatically and transparently start the status daemon the first time you run a command that needs status in a repository. It runs one status daemon per repository. The status daemon is started silently, and runs in the background. If you look at a list of running processes after you’ve enabled the inotify extension and run a few commands in different repositories, you’ll thus see a few hg processes sitting around, waiting for updates from the kernel and queries from Mercurial. The first time you run a Mercurial command in a repository when you have the inotify extension enabled, it will run with about the same performance as a normal Mercurial command. This is because the status daemon needs to perform a normal status scan so that it has a baseline against which to apply later updates from the kernel. However, every subsequent command that does any kind of status check should be noticeably faster on repositories of even fairly modest size. Better yet, the bigger your repository is, the greater a performance advantage you’ll see. The inotify daemon makes status operations almost instantaneous on repositories of all sizes! If you like, you can manually start a status daemon using the “hg inserve” command. This gives you slightly finer control over how the daemon ought to run. This command will of course only be available when the inotify extension is enabled. When you’re using the inotify extension, you should notice no difference at all in Mercurial’s behaviour, with the sole exception of status-related commands running a whole lot faster than they used to. You should specifically expect that commands will not print different output; neither should they give different results. If either of these situations occurs, please report a bug. 14.2 Flexible diff support with the extdiff extension Mercurial’s built-in “hg diff” command outputs plaintext unified diffs. $ hg diff diff -r 6e46da80e472 myfile --- a/myfile Thu Aug 21 18:22:19 2008 +0000 +++ b/myfile Thu Aug 21 18:22:19 2008 +0000 @@ -1,1 +1,2 @@ The first line. +The second line. If you would like to use an external tool to display modifications, you’ll want to use the extdiff extension. This will let you use, for example, a graphical diff tool. The extdiff extension is bundled with Mercurial, so it’s easy to set up. In the [extensions] section of your hgrc, simply add a one-line entry to enable the extension. [extensions] extdiff = This introduces a command named “hg extdiff”, which by default uses your system’s diff command to generate a unified diff in the same form as the built-in “hg diff” command. $ hg extdiff --- a.6e46da80e472/myfile 2008-08-21 18:22:19.000000000 +0000 +++ /tmp/extdiffWVAH-7/a/myfile 2008-08-21 18:22:19.000000000 +0000 @@ -1 +1,2 @@ The first line. +The second line. The result won’t be exactly the same as with the built-in “hg diff” variations, because the output of diff varies from one system to another, even when passed the same options. As the “making snapshot” lines of output above imply, the “hg extdiff” command works by creating two snapshots of your source tree. The first snapshot is of the source revision; the second, of the target revision or working directory. The “hg extdiff” command generates these snapshots in a temporary directory, passes the name of each directory to an external diff viewer, then deletes the temporary directory. For efficiency, it only snapshots the directories and files that have changed between the two revisions. Snapshot directory names have the same base name as your repository. If your repository path is /quux/bar/foo, then foo will be the name of each snapshot directory. Each snapshot directory name has its changeset ID appended, if appropriate. If a snapshot is of revision a631aca1083f, the directory will be named foo.a631aca1083f. A snapshot of the working directory won’t have a changeset ID appended, so it would just be foo in this example. To see what this looks like in practice, look again at the “hg extdiff” example above. Notice that the diff has the snapshot directory names embedded in its header. The “hg extdiff” command accepts two important options. The -p option lets you choose a program to view differences with, instead of diff. With the -o option, you can change the options that “hg extdiff” passes to the program (by default, these options are “-Npru”, which only make sense if you’re running diff). In other respects, the “hg extdiff” command acts similarly to the built-in “hg diff” command: you use the same option names, syntax, and arguments to specify the revisions you want, the files you want, and so on. As an example, here’s how to run the normal system diff command, getting it to generate context diffs (using the -c option) instead of unified diffs, and five lines of context instead of the default three (passing 5 as the argument to the -C option). $ hg extdiff -o -NprcC5 ⋆⋆⋆ a.6e46da80e472/myfile Thu Aug 21 18:22:20 2008 --- /tmp/extdiffWVAH-7/a/myfile Thu Aug 21 18:22:19 2008 ⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆ ⋆⋆⋆ 1 ⋆⋆⋆⋆ --- 1,2 ---- The first line. + The second line. Launching a visual diff tool is just as easy. Here’s how to launch the kdiff3 viewer. $ hg extdiff -p kdiff3 -o '' If your diff viewing command can’t deal with directories, you can easily work around this with a little scripting. For an example of such scripting in action with the mq extension and the interdiff command, see section 13.9.2. 14.2.1 Defining command aliases It can be cumbersome to remember the options to both the “hg extdiff” command and the diff viewer you want to use, so the extdiff extension lets you define new commands that will invoke your diff viewer with exactly the right options. All you need to do is edit your hgrc, and add a section named [extdiff]. Inside this section, you can define multiple commands. Here’s how to add a kdiff3 command. Once you’ve defined this, you can type “hg kdiff3” and the extdiff extension will run kdiff3 for you. [extdiff] cmd.kdiff3 = If you leave the right hand side of the definition empty, as above, the extdiff extension uses the name of the command you defined as the name of the external program to run. But these names don’t have to be the same. Here, we define a command named “hg wibble”, which runs kdiff3. [extdiff] cmd.wibble = kdiff3 You can also specify the default options that you want to invoke your diff viewing program with. The prefix to use is “opts.”, followed by the name of the command to which the options apply. This example defines a “hg vimdiff” command that runs the vim editor’s DirDiff extension. [extdiff] cmd.vimdiff = vim 3 opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)' 14.3 Cherrypicking changes with the transplant extension Need to have a long chat with Brendan about this. 14.4 Отправка изменений по email использую расширение patchbomb Many projects have a culture of “change review”, in which people send their modifications to a mailing list for others to read and comment on before they commit the final version to a shared repository. Some projects have people who act as gatekeepers; they apply changes from other people to a repository to which those others don’t have access. Mercurial makes it easy to send changes over email for review or application, via its patchbomb extension. The extension is so namd because changes are formatted as patches, and it’s usual to send one changeset per email message. Sending a long series of changes by email is thus much like “bombing” the recipient’s inbox, hence “patchbomb”. As usual, the basic configuration of the patchbomb extension takes just one or two lines in your hgrc. [extensions] patchbomb = Once you’ve enabled the extension, you will have a new command available, named “hg email”. The safest and best way to invoke the “hg email” command is to always run it first with the -n option. This will show you what the command would send, without actually sending anything. Once you’ve had a quick glance over the changes and verified that you are sending the right ones, you can rerun the same command, with the -n option removed. The “hg email” command accepts the same kind of revision syntax as every other Mercurial command. For example, this command will send every revision between 7 and tip, inclusive. $ hg email -n 7:tip You can also specify a repository to compare with. If you provide a repository but no revisions, the “hg email” command will send all revisions in the local repository that are not present in the remote repository. If you additionally specify revisions or a branch name (the latter using the -b option), this will constrain the revisions sent. It’s perfectly safe to run the “hg email” command without the names of the people you want to send to: if you do this, it will just prompt you for those values interactively. (If you’re using a Linux or Unix-like system, you should have enhanced readline-style editing capabilities when entering those headers, too, which is useful.) When you are sending just one revision, the “hg email” command will by default use the first line of the changeset description as the subject of the single email message it sends. If you send multiple revisions, the “hg email” command will usually send one message per changeset. It will preface the series with an introductory message, in which you should describe the purpose of the series of changes you’re sending. 14.4.1 Changing the behaviour of patchbombs Not every project has exactly the same conventions for sending changes in email; the patchbomb extension tries to accommodate a number of variations through command line options. * You can write a subject for the introductory message on the command line using the -s option. This takes one argument, the text of the subject to use. * To change the email address from which the messages originate, use the -f option. This takes one argument, the email address to use. * The default behaviour is to send unified diffs (see section 12.4 for a description of the format), one per message. You can send a binary bundle instead with the -b option. * Unified diffs are normally prefaced with a metadata header. You can omit this, and send unadorned diffs, with the --plain option. * Diffs are normally sent “inline”, in the same body part as the description of a patch. This makes it easiest for the largest number of readers to quote and respond to parts of a diff, as some mail clients will only quote the first MIME body part in a message. If you’d prefer to send the description and the diff in separate body parts, use the -a option. * Instead of sending mail messages, you can write them to an mbox-format mail folder using the -m option. That option takes one argument, the name of the file to write to. * If you would like to add a diffstat-format summary to each patch, and one to the introductory message, use the -d option. The diffstat command displays a table containing the name of each file patched, the number of lines affected, and a histogram showing how much each file is modified. This gives readers a qualitative glance at how complex a patch is. Appendix A Command reference A.1 “hg add”—add files at the next commit --include, also -I --exclude, also -X --dry-run, also -n A.2 “hg diff”—print changes in history or working directory Show differences between revisions for the specified files or directories, using the unified diff format. For a description of the unified diff format, see section 12.4. By default, this command does not print diffs for files that Mercurial considers to contain binary data. To control this behaviour, see the -a and --git options. A.2.1 Options --nodates option Omit date and time information when printing diff headers. --ignore-blank-lines, also -B Do not print changes that only insert or delete blank lines. A line that contains only whitespace is not considered blank. --include, also -I Include files and directories whose names match the given patterns. --exclude, also -X Exclude files and directories whose names match the given patterns. --text, also -a If this option is not specified, “hg diff” will refuse to print diffs for files that it detects as binary. Specifying -a forces “hg diff” to treat all files as text, and generate diffs for all of them. This option is useful for files that are “mostly text” but have a few embedded NUL characters. If you use it on files that contain a lot of binary data, its output will be incomprehensible. --ignore-space-change, also -b Do not print a line if the only change to that line is in the amount of white space it contains. --git, also -g Print git-compatible diffs. XXX reference a format description. --show-function, also -p Display the name of the enclosing function in a hunk header, using a simple heuristic. This functionality is enabled by default, so the -p option has no effect unless you change the value of the showfunc config item, as in the following example. $ echo '[diff]' >> $HGRC $ echo 'showfunc = False' >> $HGRC $ hg diff diff -r 9741ec300459 myfile.c --- a/myfile.c Thu Aug 21 18:22:17 2008 +0000 +++ b/myfile.c Thu Aug 21 18:22:17 2008 +0000 @@ -1,4 +1,4 @@ int myfunc() { - return 1; + return 10; } $ hg diff -p diff -r 9741ec300459 myfile.c --- a/myfile.c Thu Aug 21 18:22:17 2008 +0000 +++ b/myfile.c Thu Aug 21 18:22:17 2008 +0000 @ -1,4 +1,4 @@ int myfunc() int myfunc() { - return 1; + return 10; } --rev, also -r Specify one or more revisions to compare. The “hg diff” command accepts up to two -r options to specify the revisions to compare. 1. Display the differences between the parent revision of the working directory and the working directory. 2. Display the differences between the specified changeset and the working directory. 3. Display the differences between the two specified changesets. You can specify two revisions using either two -r options or revision range notation. For example, the two revision specifications below are equivalent. $ hg diff -r 10 -r 20 $ hg diff -r10:20 When you provide two revisions, Mercurial treats the order of those revisions as significant. Thus, “hg diff -r10:20” will produce a diff that will transform files from their contents as of revision 10 to their contents as of revision 20, while “hg diff -r20:10” means the opposite: the diff that will transform files from their revision 20 contents to their revision 10 contents. You cannot reverse the ordering in this way if you are diffing against the working directory. --ignore-all-space, also -w A.3 “hg version”—print version and copyright information This command displays the version of Mercurial you are running, and its copyright license. There are four kinds of version string that you may see. * The string “unknown”. This version of Mercurial was not built in a Mercurial repository, and cannot determine its own version. * A short numeric string, such as “1.1”. This is a build of a revision of Mercurial that was identified by a specific tag in the repository where it was built. (This doesn’t necessarily mean that you’re running an official release; someone else could have added that tag to any revision in the repository where they built Mercurial.) * A hexadecimal string, such as “875489e31abe”. This is a build of the given revision of Mercurial. * A hexadecimal string followed by a date, such as “875489e31abe+20070205”. This is a build of the given revision of Mercurial, where the build repository contained some local changes that had not been committed. A.3.1 Tips and tricks Почему результаты "hg diff" и "hg status" отличаются? When you run the “hg status” command, you’ll see a list of files that Mercurial will record changes for the next time you perform a commit. If you run the “hg diff” command, you may notice that it prints diffs for only a subset of the files that “hg status” listed. There are two possible reasons for this. The first is that “hg status” prints some kinds of modifications that “hg diff” doesn’t normally display. The “hg diff” command normally outputs unified diffs, which don’t have the ability to represent some changes that Mercurial can track. Most notably, traditional diffs can’t represent a change in whether or not a file is executable, but Mercurial records this information. If you use the --git option to “hg diff”, it will display git-compatible diffs that can display this extra information. The second possible reason that “hg diff” might be printing diffs for a subset of the files displayed by “hg status” is that if you invoke it without any arguments, “hg diff” prints diffs against the first parent of the working directory. If you have run “hg merge” to merge two changesets, but you haven’t yet committed the results of the merge, your working directory has two parents (use “hg parents” to see them). While “hg status” prints modifications relative to both parents after an uncommitted merge, “hg diff” still operates relative only to the first parent. You can get it to print diffs relative to the second parent by specifying that parent with the -r option. There is no way to print diffs relative to both parents. Generating safe binary diffs If you use the -a option to force Mercurial to print diffs of files that are either “mostly text” or contain lots of binary data, those diffs cannot subsequently be applied by either Mercurial’s “hg import” command or the system’s patch command. If you want to generate a diff of a binary file that is safe to use as input for “hg import”, use the “hg diff”–git option when you generate the patch. The system patch command cannot handle binary patches at all. Appendix B Mercurial Queues reference B.1 MQ command reference For an overview of the commands provided by MQ, use the command “hg help mq”. B.1.1 “hg qapplied”—print applied patches The “hg qapplied” command prints the current stack of applied patches. Patches are printed in oldest-to-newest order, so the last patch in the list is the “top” patch. B.1.2 “hg qcommit”—commit changes in the queue repository The “hg qcommit” command commits any outstanding changes in the .hg/patches repository. This command only works if the .hg/patches directory is a repository, i.e. you created the directory using “hg qinit -c” or ran “hg init” in the directory after running “hg qinit”. This command is shorthand for “hg commit --cwd .hg/patches”. B.1.3 “hg qdelete”—delete a patch from the series file The “hg qdelete” command removes the entry for a patch from the series file in the .hg/patches directory. It does not pop the patch if the patch is already applied. By default, it does not delete the patch file; use the -f option to do that. Options: * -f Delete the patch file. B.1.4 “hg qdiff”—print a diff of the topmost applied patch The “hg qdiff” command prints a diff of the topmost applied patch. It is equivalent to “hg diff -r-2:-1”. B.1.5 “hg qfold”—merge (“fold”) several patches into one The “hg qfold” command merges multiple patches into the topmost applied patch, so that the topmost applied patch makes the union of all of the changes in the patches in question. The patches to fold must not be applied; “hg qfold” will exit with an error if any is. The order in which patches are folded is significant; “hg qfold a b” means “apply the current topmost patch, followed by a, followed by b”. The comments from the folded patches are appended to the comments of the destination patch, with each block of comments separated by three asterisk (“⋆”) characters. Use the -e option to edit the commit message for the combined patch/changeset after the folding has completed. Options: * -e Edit the commit message and patch description for the newly folded patch. * -l Use the contents of the given file as the new commit message and patch description for the folded patch. * -m Use the given text as the new commit message and patch description for the folded patch. B.1.6 “hg qheader”—display the header/description of a patch The “hg qheader” command prints the header, or description, of a patch. By default, it prints the header of the topmost applied patch. Given an argument, it prints the header of the named patch. B.1.7 “hg qimport”—import a third-party patch into the queue The “hg qimport” command adds an entry for an external patch to the series file, and copies the patch into the .hg/patches directory. It adds the entry immediately after the topmost applied patch, but does not push the patch. If the .hg/patches directory is a repository, “hg qimport” automatically does an “hg add” of the imported patch. B.1.8 “hg qinit”—prepare a repository to work with MQ The “hg qinit” command prepares a repository to work with MQ. It creates a directory called .hg/patches. Options: * -c Create .hg/patches as a repository in its own right. Also creates a .hgignore file that will ignore the status file. When the .hg/patches directory is a repository, the “hg qimport” and “hg qnew” commands automatically “hg add” new patches. B.1.9 “hg qnew”—create a new patch The “hg qnew” command creates a new patch. It takes one mandatory argument, the name to use for the patch file. The newly created patch is created empty by default. It is added to the series file after the current topmost applied patch, and is immediately pushed on top of that patch. If “hg qnew” finds modified files in the working directory, it will refuse to create a new patch unless the -f option is used (see below). This behaviour allows you to “hg qrefresh” your topmost applied patch before you apply a new patch on top of it. Опции: -f Создать новый патч, если содержимое рабочей директории менялось. Любые неучтённые изменения вносятся в создаваемый патч, так что после отработки команды рабочая директория более не считается содержащей изменения. -m Использовать заданный текст как комментарий при фиксации изменений. Этот текст будет сохранен в начале патч-файла, перед собственно данными патча. “hg qnext” - вывести имя следующего патча The “hg qnext” command prints the name name of the next patch in the series file after the topmost applied patch. This patch will become the topmost applied patch if you run “hg qpush”. B.1.11 “hg qpop”—pop patches off the stack The “hg qpop” command removes applied patches from the top of the stack of applied patches. By default, it removes only one patch. This command removes the changesets that represent the popped patches from the repository, and updates the working directory to undo the effects of the patches. This command takes an optional argument, which it uses as the name or index of the patch to pop to. If given a name, it will pop patches until the named patch is the topmost applied patch. If given a number, “hg qpop” treats the number as an index into the entries in the series file, counting from zero (empty lines and lines containing only comments do not count). It pops patches until the patch identified by the given index is the topmost applied patch. The “hg qpop” command does not read or write patches or the series file. It is thus safe to “hg qpop” a patch that you have removed from the series file, or a patch that you have renamed or deleted entirely. In the latter two cases, use the name of the patch as it was when you applied it. By default, the “hg qpop” command will not pop any patches if the working directory has been modified. You can override this behaviour using the -f option, which reverts all modifications in the working directory. Options: * -a Pop all applied patches. This returns the repository to its state before you applied any patches. * -f Forcibly revert any modifications to the working directory when popping. * -n Pop a patch from the named queue. The “hg qpop” command removes one line from the end of the status file for each patch that it pops. B.1.12 “hg qprev”—print the name of the previous patch The “hg qprev” command prints the name of the patch in the series file that comes before the topmost applied patch. This will become the topmost applied patch if you run “hg qpop”. B.1.13 “hg qpush”—push patches onto the stack The “hg qpush” command adds patches onto the applied stack. By default, it adds only one patch. This command creates a new changeset to represent each applied patch, and updates the working directory to apply the effects of the patches. The default data used when creating a changeset are as follows: * The commit date and time zone are the current date and time zone. Because these data are used to compute the identity of a changeset, this means that if you “hg qpop” a patch and “hg qpush” it again, the changeset that you push will have a different identity than the changeset you popped. * The author is the same as the default used by the “hg commit” command. * The commit message is any text from the patch file that comes before the first diff header. If there is no such text, a default commit message is used that identifies the name of the patch. If a patch contains a Mercurial patch header (XXX add link), the information in the patch header overrides these defaults. Options: * -a Push all unapplied patches from the series file until there are none left to push. * -l Add the name of the patch to the end of the commit message. * -m If a patch fails to apply cleanly, use the entry for the patch in another saved queue to compute the parameters for a three-way merge, and perform a three-way merge using the normal Mercurial merge machinery. Use the resolution of the merge as the new patch content. * -n Use the named queue if merging while pushing. The “hg qpush” command reads, but does not modify, the series file. It appends one line to the “hg status” file for each patch that it pushes. B.1.14 “hg qrefresh”—update the topmost applied patch The “hg qrefresh” command updates the topmost applied patch. It modifies the patch, removes the old changeset that represented the patch, and creates a new changeset to represent the modified patch. The “hg qrefresh” command looks for the following modifications: * Changes to the commit message, i.e. the text before the first diff header in the patch file, are reflected in the new changeset that represents the patch. * Modifications to tracked files in the working directory are added to the patch. * Changes to the files tracked using “hg add”, “hg copy”, “hg remove”, or “hg rename”. Added files and copy and rename destinations are added to the patch, while removed files and rename sources are removed. Even if “hg qrefresh” detects no changes, it still recreates the changeset that represents the patch. This causes the identity of the changeset to differ from the previous changeset that identified the patch. Options: * -e Modify the commit and patch description, using the preferred text editor. * -m Modify the commit message and patch description, using the given text. * -l Modify the commit message and patch description, using text from the given file. B.1.15 “hg qrename”—rename a patch The “hg qrename” command renames a patch, and changes the entry for the patch in the series file. With a single argument, “hg qrename” renames the topmost applied patch. With two arguments, it renames its first argument to its second. B.1.16 “hg qrestore”—restore saved queue state XXX No idea what this does. B.1.17 “hg qsave”—save current queue state XXX Likewise. B.1.18 “hg qseries”—print the entire patch series The “hg qseries” command prints the entire patch series from the series file. It prints only patch names, not empty lines or comments. It prints in order from first to be applied to last. B.1.19 “hg qtop”—print the name of the current patch The “hg qtop” prints the name of the topmost currently applied patch. B.1.20 “hg qunapplied”—print patches not yet applied The “hg qunapplied” command prints the names of patches from the series file that are not yet applied. It prints them in order from the next patch that will be pushed to the last. B.1.21 “hg strip”—remove a revision and descendants The “hg strip” command removes a revision, and all of its descendants, from the repository. It undoes the effects of the removed revisions from the repository, and updates the working directory to the first parent of the removed revision. The “hg strip” command saves a backup of the removed changesets in a bundle, so that they can be reapplied if removed in error. Options: * -b Save unrelated changesets that are intermixed with the stripped changesets in the backup bundle. * -f If a branch has multiple heads, remove all heads. XXX This should be renamed, and use -f to strip revs when there are pending changes. * -n Do not save a backup bundle. B.2 MQ file reference B.2.1 The series file The series file contains a list of the names of all patches that MQ can apply. It is represented as a list of names, with one name saved per line. Leading and trailing white space in each line are ignored. Lines may contain comments. A comment begins with the “#” character, and extends to the end of the line. Empty lines, and lines that contain only comments, are ignored. You will often need to edit the series file by hand, hence the support for comments and empty lines noted above. For example, you can comment out a patch temporarily, and “hg qpush” will skip over that patch when applying patches. You can also change the order in which patches are applied by reordering their entries in the series file. Placing the series file under revision control is also supported; it is a good idea to place all of the patches that it refers to under revision control, as well. If you create a patch directory using the -c option to “hg qinit”, this will be done for you automatically. B.2.2 The status file The status file contains the names and changeset hashes of all patches that MQ currently has applied. Unlike the series file, this file is not intended for editing. You should not place this file under revision control, or modify it in any way. It is used by MQ strictly for internal book-keeping. Приложение C Установка Mercurial из исходников C.1 На Unix-подобной системе Если вы используете Unix-подобную систему, в которой установлена последняя версия python (2.3 или новее), то установить Mercurial из исходников очень просто. 1. Загрузите последнюю версию исходников в tar-архиве со страницы http://www.selenic.com/mercurial/download. 2. Распакуйте tar-архив: $ gzip -dc mercurial-version.tar.gz | tar xf - 3. Перейдите в директорию с распакованными исходниками и запустите установочный скрипт. После этого Mercurial начнёт устанавливаться в домашнюю директорию. $ cd mercurial-version $ python setup.py install --force --home=$HOME Как только закончится установка, Mercurial появится в поддиректории bin вашей домашней директории. Не забудьте убедиться что эта директория включена в переменную PATH окружения. Возможно вам придётся установить и переменную окружения PYTHONPATH для того, чтобы запускаемый файл Mercurial смог найти остальные свои модули. Например, на своём ноутбуке, я должен установить эту переменную в /home/bos/lib/python. Точный путь, который вы должны использовать, зависит от того как установлен Python в вашей системе. Если вы не уверены, посмотрите на вывод установочного скрипта, и найдите в какую директорию устанавливались модули Mercurial. C2. В Windows Сборка и установка Mercurial под Windows требует множества инструментов, невероятных технических знаний и значительного терпения. Я настоятельно не рекомендую этот путь, если вы являетесь "простым пользователем". Если вы не собираетесь править код Mercurial, то я настоятельно советую вам воспользоваться готовым инсталлятором. Если вы настроены на сборку Mercurial из исходных кодов под Windows, следуйте по "тяжелому пути" на страницах Mercurial wiki по адресу http://www.selenic.com/mercurial/wiki/index.cgi/WindowsInstall и будьте готовы к большой работе. Приложение D Open Publication License Version 1.0, 8 June 1999 D.1 Requirements on both unmodified and modified versions The Open Publication works may be reproduced and distributed in whole or in part, in any medium physical or electronic, provided that the terms of this license are adhered to, and that this license or an incorporation of it by reference (with any options elected by the author(s) and/or publisher) is displayed in the reproduction. Proper form for an incorporation by reference is as follows: Copyright (c) year by author’s name or designee. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, vx.y or later (the latest version is presently available at http://www.opencontent.org/openpub/). The reference must be immediately followed with any options elected by the author(s) and/or publisher of the document (see section D.6). Commercial redistribution of Open Publication-licensed material is permitted. Any publication in standard (paper) book form shall require the citation of the original publisher and author. The publisher and author’s names shall appear on all outer surfaces of the book. On all outer surfaces of the book the original publisher’s name shall be as large as the title of the work and cited as possessive with respect to the title. D.2 Copyright The copyright to each Open Publication is owned by its author(s) or designee. D.3 Scope of license The following license terms apply to all Open Publication works, unless otherwise explicitly stated in the document. Mere aggregation of Open Publication works or a portion of an Open Publication work with other works or programs on the same media shall not cause this license to apply to those other works. The aggregate work shall contain a notice specifying the inclusion of the Open Publication material and appropriate copyright notice. Severability. If any part of this license is found to be unenforceable in any jurisdiction, the remaining portions of the license remain in force. No warranty. Open Publication works are licensed and provided “as is” without warranty of any kind, express or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose or a warranty of non-infringement. D.4 Requirements on modified works All modified versions of documents covered by this license, including translations, anthologies, compilations and partial documents, must meet the following requirements: 1. The modified version must be labeled as such. Человек, который внес изменения должен быть идентифицирован, а изменение датировано. 3. Acknowledgement of the original author and publisher if applicable must be retained according to normal academic citation practices. 4. The location of the original unmodified document must be identified. 5. The original author’s (or authors’) name(s) may not be used to assert or imply endorsement of the resulting document without the original author’s (or authors’) permission. D.5 Good-practice recommendations In addition to the requirements of this license, it is requested from and strongly recommended of redistributors that: 1. If you are distributing Open Publication works on hardcopy or CD-ROM, you provide email notification to the authors of your intent to redistribute at least thirty days before your manuscript or media freeze, to give the authors time to provide updated documents. This notification should describe modifications, if any, made to the document. 2. All substantive modifications (including deletions) be either clearly marked up in the document or else described in an attachment to the document. 3. Finally, while it is not mandatory under this license, it is considered good form to offer a free copy of any hardcopy and CD-ROM expression of an Open Publication-licensed work to its author(s). D.6 License options The author(s) and/or publisher of an Open Publication-licensed document may elect certain options by appending language to the reference to or copy of the license. These options are considered part of the license instance and must be included with the license (or its incorporation by reference) in derived works. 1. To prohibit distribution of substantively modified versions without the explicit permission of the author(s). “Substantive modification” is defined as a change to the semantic content of the document, and excludes mere changes in format or typographical corrections. To accomplish this, add the phrase “Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.” to the license reference or copy. 2. Чтобы запретить любую публикацию всей или какой-либо части данной работы или продукта, основанного на ней, в любом печатном виде в коммерческих целях, запрещённых без предварительного разрешения владельца прав. Чтобы добиться этого, добавьте фразу "Распространение данной работы или продукта, основанного на ней, в любом печатном виде запрещено, пока предварительно не будет получено разрешение от владельца прав." к лицензии или её копии. ------------------------------------------------------------------------------- http://translated.by/you/distributed-revision-control-with-mercurial/into-ru/trans/ Original (English): Distributed revision control with Mercurial (http://hgbook.red-bean.com/hgbook.html) Translation: © Dmitry, Игорь, ChivPointDJ, f73, hhg, vbor, Адель Чепкунов, wise, ktbjn, Александр, ak, Александр Соловьёв, dustin86, Евгений, hntr.spb.ru, scr, Zveroy, DeKar, certain, aafin, maxischenko, Mammoth, kolen, linuxena, Mithgol, tven, Andy, salnikov-a, MaksBR, Валерий, miramir, iav, flower-child, Денис, Demiodv Viktor, Владимир, zakhar, anatoly.rakovskiy, smilesrg, marie-lo, traditio, PhoeniXDN, vkhamianok, Sign, Passerby, wikiwikiweb, vour, dolinenko, minimus, ivan.borzenkov, kedo-kyn, Hikin, pioneer-hg, RANUX, Антон Ильин, russiankamikaze, dalek, levin-matveev, altanzar, poige, KorRomMar, vperlin, lakearo, Anton Abrosimov, boz, Александр Копилов, supdev, xalava, molnij, exvion, and_rew, Dieu_Mort, Андрей Дягель, Mirray, denger, murkt.org.ua, relanium-ilya, eugenius-nsk.livejournal.com, ladykosha, wert_lex, hyperon, laway. License: Open Publication License version 1.0 translated.by crowd