Содержание
Mercurial предоставляет вам несколько механизмов для управления проектом, которые работают на множестве направлений одновременно. Для понимания этих механизмов сперва кратко рассмотрим довольно распространенные примеры структуры программных проектов.
Многие програмные проекты выпускают переодические «major» релизы, которые содержат некоторые новые существенные возможности. В тоже время они выпускают множество «minor» релизов. Они, как правило, идентичны основным релизам на которых они основаны, но исправляют ошибки.
В этой главе вы поговорим о том как вести учет вех проекта, таких как релизы. Затем мы продолжим разговор о рабочем процессе между двумя разными фазами проекта и увидим как Mercurial может помочь вам изолировать и управлять этим процессом.
Как только вы решаете что хотели бы сделать определенную ревизию релизом — хорошая идея сделать идентифицирующую запись об этом. Это позволит вам позднее получить релиз в любое время (воспроизвести баг, портировать на новую платформу и т.д.).
$
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 не устанавливает ограничения на имена тегов, кроме некоторых, чтобы гарантировать что тег может быть проанализирован однозначно. Имя тега не может содержать ни один из следующих символов:
Вы можете использовать hg tags для показа тегов в вашем репозитории. В выводе каждая тегированая ревизия идентифицируется сначала по имени, затем по номеру ревизии, и потом по уникальному хешу ревизии.
$
hg tags
tip 1:2f120dd1b81c v1.0 0:2765f2245422
Обратите внимание, что в выводе hg tags показан тег tip
(окончание ветки). Тег tip
это специальный «плавающий» тег, который всегда указывает на новейшую ревизию в репозитории.
В выводе команды hg tags теги показаны в обратном (относительно номеров ревизий) порядке. Обычно это значит, что более новые теги будут показаны перед более старыми. Также это означает, что tip
всегда будет показан первым в выводе hg tags.
Когда вы запускаете hg log, если она показывает номер ревизии, которая имеет ассоциацию с тегами, то печатаются и эти теги:
$
hg log
changeset: 1:2f120dd1b81c tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:07 2012 +0000 summary: Added tag v1.0 for changeset 2765f2245422 changeset: 0:2765f2245422 tag: v1.0 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:07 2012 +0000 summary: Initial commit
Всегда, когда вам необходимо подставить идентификатор ревизии в команду Mercurial, можно подставлять имя соответствующего тега. Внутри себя Mercurial переводит имя тега в идентификатор ревизии.
$
echo goodbye > myfile2
$
hg commit -A -m 'Second commit'
adding myfile2$
hg log -r v1.0
changeset: 0:2765f2245422 tag: v1.0 user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:07 2012 +0000 summary: Initial commit
Для отдельной ревизии или всего репозитория не существует лимита на количество тегов, которые вы можете использовать. На практике это означает что «слишком много тегов» (число которое будет сильно изменяться от проекта к проекту) не очень хорошая идея, просто потому что теги, как предполагается, помогают вам находить ревизии. Если же у вас много тегов, простота их использования очень сильно уменьшается.
К примеру, если ваш проект будет создавать вехи каждые пару дней совершенно разумно помечать их тегами. Но если вы используете систему непрерывной интеграции, которая гарантирует чистоту сборки каждой ревизии, тегирование каждой успешной сборки внесет неясности. Взамен вы можете помечать тегом ревизии, чья сборка завершилась неудачей (при условии что они редки!), или просто не использовать теги для отслеживания процесса сборки.
Если вы хотите удалить более не используемый тег используйте команду hg tag --remove.
$
hg tag --remove v1.0
$
hg tags
tip 3:7aa8dea37120
Вы также можете изменять тег в любое время, т.о. для идентифицирования разных ревизий выполните новую команду hg tag. Вы также можете использовать опцию -f
, чтобы указать что вы действительно хотите изменить тег.
$
hg tag -r 1 v1.1
$
hg tags
tip 4:e38b5c6c84f1 v1.1 1:2f120dd1b81c$
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:c224d1a9e578 v1.1 2:36034fbfb874
В выводе все еще присутствуют старые записи тегов, но Mercurial не будет использовать их в дальнейшем.Таким образом нет никакой беды, если вы установили тег для неверной ревизии — все что нужно изменить неверный тег и скорректировать ревизию как только вы обнаружите ошибку.
Mercurial хранит теги как обычный файл с контролем ревизий в вашем репозитории. Если вы создаете любые теги, вы сможете найти их все в файле .hgtags
. Когда вы запускаете hg tag, Mercurial модифицирует этот файл, а затем автоматически комитит изменения в нем. Это означает что на каждый запуск hg tag вы можете найти соответствующий набор изменений в выводе команды hg log.
$
hg tip
changeset: 5:c224d1a9e578 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:08 2012 +0000 summary: Added tag v1.1 for changeset 36034fbfb874
Вы не должны особо заботиться о файле .hgtags
, но иногда он дает о себе знать при слиянии изменений. Формат файла очень прост: он состоит из строк. Каждая строка начинается с хеша набора изменений, за который через пробел следует имя тега.
Если вы исправляете конфликт в файле .hgtags
, есть одно замечание про его модификацию: когда Mercurial читает теги в репозитории, он никогда не читает рабочую копию файла .hgtags
. Вместо этого он читает наиболее старшую ревизию этого файла.
Неудачное последствие такого дизайна состоит в том, что вы не можете проверить правильность вашего файла .hgtags
после слияния до тех пор, пока вы не закомитите изменения. Таким образом, если вы решаете конфликт в .hgtags
во время слияния убедитесь что вы запустили hg tags после слияния. Если эта команда найдет ошибку в файле .hgtags
, она сообщит о месте ее возникновения. Эта информация поможет вам в исправлении ошибки и повторном комите. Вы должны запустить hg tags вновь, как только будете уверены в правильности файла.
Возможно вы обратили внимание, что команда hg clone имеет опцию -r
, которая позволяет клонировать точную копию репозитория как частичные наборы изменений. Новый клон не будет содержать истории проекта, которая появилась позже указанной вами ревизии. Это относится и к тегам, что может стать неожиданностью для неосторожных.
Вспомните что тег сохранен как ревизия в файле .hgtags
, так что когда вы создаете тег, набору изменений в котором он записан нужно обратиться к старшему набору изменений. Когда вы запускаете команду hg clone -r foo для клонирования репозитория по имени тега foo
, новый клон не будет содержать истории создания клона. Результатом этого станет то, что вы получите полное подмножество истории проекта в новом репозитории, а не тег, который вы, возможно, ожидали.
Итак, так как теги в Mercurial подвержены версионности и связаны с историей проекта, любой кто работает с ними может видеть созданые вами теги. Именованая ревизия 4237e45506ee
будет доступна по простому тегу v2.0.2
. Если же вы пытаетесь отыскать сложноуловимую ошибку, вы возможно захотите чтобы тег напоминал вам нечто вроде «Анна видела признаки ошибки в этой ревизии» (речь тут идет о «пометках на полях для себя», прим. переводчика).
Для подобных целей вы можете захотеть использовать локальные теги. Вы можете создать локальный тег используя опцию -l
команды hg tag. Это позволит записать тег в файл .hg/localtags
, вместо .hgtags
. Версия файла .hg/localtags
не отслеживается. Любые теги созданые с опцией -l
остаются локальными для вашего репозитория.
Вернёмся к схеме, которую я нарисовал в начале главы и в то же время подумаем о проекте как о множественных конкурирующих кусках работы под разработкой.
Это может быть толчком для нового «основного» релиза; нового «небольшого» релиза с исправленными багами для последнего «основного» релиза; и неожиданного «горячего» изменения для старого релиза, который сейчас ещё поддерживается.
Обычно люди называют разные конкурирующие направления «ветками». Однако мы уже видели несколько раз, что Mercurial обрабатывает всю историю как серию «веток» и «слияний». На самом дела у нас есть 2 идеи, которые плохо связаны, но имеют одинаковые названия.
Самый простой путь изолировать «большую картинку» веток в 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 to branch default 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 to branch default 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-repookUgeL/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 to branch default 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-repookUgeL/myproject searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files
Во многих случаях если вы должны исправить баг в «основной» ветке, есть большие шансы, что этот баг есть в ваших остальных ветках. Редкий разработчик хочет исправлять один и тот же баг несколько раз. Давайте рассмотрим несколько вариантов, когда Mercurial может помочь вам исправить баги без дублирования вашей работы.
В простейшнм случае всё, что вам нужно сделать — это извлечь изменения с «основной» ветки в вашу локальную копию этой ветки.
$
cd ..
$
hg clone myproject myproject-merge
updating to branch default 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-repookUgeL/myproject searching for changes adding changesets adding manifests adding file changes added 2 changesets with 1 changes to 1 files
Во многих случаях изоляция веток в хранилищах — это хорошее решение. Это просто понять; и также сложно сделать ошибку. Это отношения один-к-одному между ветками, с которыми вы работаете и папками(директориями) в вашей системе. Тогда вы можете использовать нормальные(ничего не знающие о Mercurial) программы, чтобы работать с файлами в ветке/хранилище.
Если вы (как и ваши коллеги) «продвинутый пользователь», то вы можете рассматривать другой способ трактовки веток. Я уже упоминал разницу человеческого уровня между «маленькой картинкой» и «большой картинкой» веток. Пока Mercurial всё время работает с несколькими «маленькими картинками» в хранилище (например, когда вы отослали изменения, но ещё не соединили), Mercurial также может работать с несколькими ветками «больших картинок».
Ключ к работе в этом направлении в том, что Mercurial позволяет вам дать постоянное имя ветке. И всегда существует ветка, названная default
(по-умолчанию). Даже перед тем, как вы переименуете ветку сами, вы можете найти историю ветки default
, если поищите.
И как пример, когда вы запускаете команду hg commit, и вы попадаете в редактор, где вы можете ввести commit, посмотрите на верхнюю линию, которая содержит текст «HG: branch default
». Это говорит вам о том, что ваш commit случится с веткой, названной default
.
Чтобы начать работу с именованными ветками используйте команду hg branches. Эта команда покажет вам список именованных веток, которые есть в хранилище, расскажет, какая ветка есть изменение другой.
$
hg tip
changeset: 0:f9dffe1ca22e tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:29 2012 +0000 summary: Initial commit$
hg branches
default 0:f9dffe1ca22e
Пока вы не создали ни одной именованной ветки, существует только одна, названная default
.
Найти, какая «текущая» ветка сейчас, запустите команду 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:f9dffe1ca22e tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:29 2012 +0000 summary: Initial commit
Ничего не изменилось в рабочей папке и не было создано новой истории. Запуск команды hg branch не производит постоянного эффекта; она только говорит Mercurial, ветку с каким именем использовать для последующих фиксаций изменений (commit).
Когда вы подтверждаете изменения, Mercurial записывает имя ветки, которую вы изменяете. Один раз переключив ветку default
на другую и подтвердив, вы увидите имя новой ветки в результатах hg log, hg tip, и других комманд, которые показывают эту информацию.
$
echo 'hello again' >> myfile
$
hg commit -m 'Second commit'
$
hg tip
changeset: 1:5dc3c3ccfa6d branch: foo tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:29 2012 +0000 summary: Second commit
Команды как hg log напечатают имя ветки для каждого изменения, которое не в ветке default
. Как результат, если вы никогда не именовали ветки — вы никогда не увидите эту информацию.
Один раз дав имя ветке и подтвердив изменение с этим именем каждое последующее изменение будет иметь то же имя ветки. Вы можете сменить имя ветки в любое время, используя команду hg branch.
$
hg branch
foo$
hg branch bar
marked working directory as branch bar$
echo new file > newfile
$
hg commit -A -m 'Third commit'
adding newfile$
hg tip
changeset: 2:1536e3edee0d branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:30 2012 +0000 summary: Third commit
На практике это то, что вы не должны делать часто, имена веток имеют тенденцию существовать долгое время(это не правило, лишь наблюдение).
Если у вас болльше чем 1 ветка с именем в хранилище, Mercurial будет помнить ветку вашей рабочей папки когда вы запустите такую команду, как hg update или hg pull -u. Это обновит рабочую папку с этой веткой. Обновить ветку с другим именем вы можете использовать опцию -C
с командой hg update.
Посмотрим это на практике. Сперва вспомните, какая ветка сейчас «текущая» и какие ветки есть в нашем хранилище.
$
hg parents
changeset: 2:1536e3edee0d branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:30 2012 +0000 summary: Third commit$
hg branches
bar 2:1536e3edee0d foo 1:5dc3c3ccfa6d (inactive) default 0:f9dffe1ca22e (inactive)
Мы в bar
ветке, но так же существует старая ветка hg foo.
Мы можем использовать hg update назад и вперед между foo
и bar
ветками без нужды использовать опцию -C
, потому-что это всего лишь перемещение по линейной истории наших изменений.
$
hg update foo
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg parents
changeset: 1:5dc3c3ccfa6d branch: foo user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:29 2012 +0000 summary: Second commit$
hg update bar
1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
hg parents
changeset: 2:1536e3edee0d branch: bar tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:30 2012 +0000 summary: Third commit
Если мы вернёмся к ветке foo
и затем запустим hg update, мы останемся в foo
, не перемещаясь к вершине bar
.
$
hg update foo
0 files updated, 0 files merged, 1 files removed, 0 files unresolved$
hg update
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
Внесение нового изменения в ветку foo
создаст новую голову.
$
echo something > somefile
$
hg commit -A -m 'New file'
adding somefile$
hg heads
changeset: 3:e22fb67a4dba branch: foo tag: tip parent: 1:5dc3c3ccfa6d user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:31 2012 +0000 summary: New file changeset: 2:1536e3edee0d branch: bar user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:30 2012 +0000 summary: Third commit changeset: 0:f9dffe1ca22e user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:29 2012 +0000 summary: Initial commit
Как вам вероятно уже известно, слияния в 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
.
Это необычно, если хранилище хранит несколько голов, с одним именем ветки. Скажем, я работаю с веткой foo
, как и вы. Мы сохраняем разные изменения; я забираю ваши изменения; у меня сейчас головы, обе претендующие быть в ветке foo
. Результатом слияния будет одна голова в ветке foo
, как вы могли надеяться.
Но если я работаю с веткой bar
, то результатом слияния bar и foo
будет ветка bar
.
$
hg branch
bar$
hg merge foo
1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)$
hg commit -m 'Merge'
$
hg tip
changeset: 4:2240616f6d14 branch: bar tag: tip parent: 2:1536e3edee0d parent: 3:e22fb67a4dba user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:31 2012 +0000 summary: Merge
Более конкретный пример: если я работаю с веткой bleeding-edge
и хочу внести в неё последние фиксы из ветки stable
, то Mercurial выберет имя «правильной» ветки (bleeding-edge
), когда вытащу и объединю изменения из ветки stable
.
Вам не стоит думать об именовании веток только когда несколько «устойчивых» веток находятся в одном хранилище. Это так же очень полезно даже для случая одна-ветка-на-хранилище.
В простом случае, если вы дадите имя каждой ветке, вы будете знать, какой ветке принадлежит фиксация. У вас будет больше контекста, когда вы попытаетесь проследить историю изменений.
Если вы работаете с общедоступным хранилищем, то вы можете задать pretxnchangegroup
перехватчик, который будет блокировать все приходящие изменения с «неправильным» именем ветки. Это простой, но эффективный способ против случайного слияния изменений с «нестабильной» ветки в «стабильную». Так перехватчик может находится внутри общедоступного /.hgrc
репозитория.
[hooks] pretxnchangegroup.branch = hg heads --template '{branches} ' | grep mybranch