Содержание
Человек может ошибиться, но высококлассная система управления версиями берется исправить последствия. В этой главе мы расскажем о некоторых способах поиска проблем, закравшихся в ваш проект. Mercurial предоставляет высокоэффективные инструменты, позволяющие изолировать источник проблем и должным образом образом обработать.
Не часто, но постоянно у меня возникает проблема, что печатаю быстрее, чем думаю, в результате, зафиксированные изменения либо не законченные, либо просто ошибочные. Стандартный для меня тип не завершенных изменений, в том ,что я создал новый исходник, но забыл добавить (hg add) его. «Просто ошибочные» изменения не так часты, но не менее досаждающие.
В разделе Раздел 4.2.2, «Безопасность работы» я упоминал, что Mercurial рассматривает каждую модификацию хранилища как транзакцию. Каждый раз, когда вы фиксируете изменения или подтягиваете изменения из другого хранилища, Mercurial запоминает, что вы сделали. Вы можете отменить, или откатить, только одно из этих действий с помощью команды hg rollback. (Смотрите раздел Раздел 9.1.4, «hg rollback бесполезен если изменения уже внесены.» о важном предупреждении о использовании данной команды.)
Приведу пример частой собственной ошибки: фиксация изменений, в которых я создал новый файл, но забыл выполнить hg add для него.
$
hg status
M a$
echo b > b
$
hg commit -m 'Add file b'
Вывод команды hg status после фиксации подтверждает ошибку.
$
hg status
? b$
hg tip
changeset: 1:e74fb1f34241 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:06 2012 +0000 summary: Add file b
Были зафиксированы изменения для файла a
, но не для нового файла b
. Если бы я отправил эти изменения в общее с коллегами хранилище, то велик шанс того, что что-то в a
ссылается на b
, отсутствующий в репозиториях коллег после того, как они получат мои изменения. В таком случае я мог бы стать объектом некоторого негодования.
Однако, мне повезло — я заметил ошибку прежде, чем отослал изменения. Я воспользовался командой hg rollback, и Mercurial убрал последние измения.
$
hg rollback
repository tip rolled back to revision 0 (undo commit) working directory now based on revision 0$
hg tip
changeset: 0:f50222def92b tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:10:06 2012 +0000 summary: First commit$
hg status
M a ? b
Обратите внимание, что изменение отсутствует в истории хранилища, и репозиторий снова считает, что файл a
в рабочей директории имеет не фиксированные модификации. Commit и rollback оставили рабочую директорию в том же состоянии, в котором она была перед commit; изменения полностью уничтожены. Теперь я могу безопасно выполнить hg add файл b
, и снова запустить commit.
$
hg add b
$
hg commit -m 'Add file b, this time for real'
Стандартной методикой работы с Mercurial является поддержка разработки отдельных веток проекта в отдельных хранилищах. Ваша комманда разработчиков может содержать один репозиторий для релиза проекта версии «0.9», и другой, для других изменений, войдущих в версию «1.0».
На этом примере, можете представить себе неприятные последствия случайного вытягивания изменений из общего хранилища ветки «1.0» в ваш локальный репозиторий релиза «0.9». В худшем случае, вы заплатите за невнимательность втолкнув те самые изменения в общее «0.9» дерево, запутав остальных разработчиков. (Не волнуйтесь, к этому ужасному сценарию мы вернёмся позже). Впрочем, вероятнее всего, Вы заметите это сразу же, поскольку Mercurial покажет вам URL из которого происходит вытягивание. Или Вы обратите внимание на подозрительно большое число изменений в хранилище.
Для исключения этих, только что втянутых правок, отлично подходит команда hg rollback. Mercurial группирует все изменения, внесенные командой hg pull, в одну трансакцию, поэтому всё, что вам нужно для исправления ошибки — одна команда hg rollback.
Ценность команды hg rollback падает до нуля, как только вы втолкнете изменения в другое хранилище. Конечно, эта команда исправит ошибку, но только в том хранилище к которому вы её применяете. Поскольку rollback уничтожает историю, нет способа остановить распространение ошибочного изменения между хранилищами.
Если Вы втолкнули изменение в общее хранилище, оно по сути «вырвалось на свободу» и вам придётся исправлять ошибку другим способом. Что будет, если вы втолкнули набор изменений куда-либо, после чего стерли их у себя, а потом втянули то что вы только что проталкивали? Набор изменений, от которого (по вашему мнению) вы избавились, опять появится в Вашем хранилище.
(Если вы абсолютно точно знаете, что те изменения, которые вы собираетесь откатить, самые свежие в том самом хранилище, и вы знаете, что никто еще не успел их вытянуть из этого хранилища, то вы можете стереть их и там тоже. Но вам действительно не стоит ожидать что это будет работать надежно. Рано или поздно, изменения наконец таки попадут в неподконтрольное вам хранилище и вернутся назад, чтобы укусить вас.)
Mercurial хранит в своем логе только одну транзакцию — ту, что была выполнена последней в данном хранилище. Это значит, что вы можете отменить только одну транзакцию. Если вы попытаетесь отменить еще одну, предшествующую последней, то вам не удастся этого сделать.
$
hg rollback
repository tip rolled back to revision 0 (undo commit) working directory now based on revision 0$
hg rollback
no rollback information available
Если вы произведете отмену транзакции, вы не сможете снова выполнять roll back до тех пор, пока не произведете другие операции commit или pull.
Если вы изменили файл, но потом решили, что вообще не хотите его изменять, и если вы ещё не зафиксировали изменения, то команда hg revert — то, что вам нужно. Она смотрит на предыдущую ревизию рабочей директории и восстанавливает содержимое файла к состоянию из этой ревизии. (Это многоречивый способ сказать, что обычно эта команда отменяет ваши изменения.)
Давайте посмотрим, как работает команда hg revert. В этом маленьком примере мы начнем с изменения файла, о котором Mercurial уже знает.
$
cat file
original content$
echo unwanted change >> file
$
hg diff file
diff -r af8f1f5e8d54 file --- a/file Thu Feb 02 14:09:48 2012 +0000 +++ b/file Thu Feb 02 14:09:48 2012 +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 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, целевой (скопированный) файл все равно остается в вашей рабочей директории, однако изменения в нем не отслеживаются. Поскольку операция копирования в любом случае не затрагивает исходный файл, Mercurial никак не изменяет исходный файл.
$
hg copy file new-file
$
hg revert new-file
$
hg status
? new-file
Рассмотрим случай, когда вы зафиксировали изменение a и другое изменение b поверх него. Затем вы обнаружили, что изменение a было некорректным. Mercurial позволяет вам автоматически «вернуть» (back out) изменение целиком, и создаёт блоки, которые позволяют вам отменить часть изменения вручную.
Перед прочтением этой части руководства, вы должны четко представлять себе следующее: команда hg backout отменяет изменения добавляя новые записи к истории, но ни в коем случае не редактируя и не удаляя уже существующую в истории информацию. Эта утилита хорошо подходит для исправления небольших багов, но не для отмены больших изменений, приведших к серьезным проблемам. Чтобы разобраться с такими проблемами, смотрите секцию Раздел 9.4, «Изменения, которых быть не должно».
Команда 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
.
Начнем с отзыва последней ревизии.
$
hg backout -m 'back out second change' tip
reverting myfile changeset 2:9c57b44505fe backs out changeset 1:37eb97592c2d$
cat myfile
first change
Как видите, в myfile
второй строки уже нет. Взгляд на вывод команды hg log позволяет понять, что сделала hg backout.
$
hg log --style compact
2[tip] 9c57b44505fe 2012-02-02 14:09 +0000 bos back out second change 1 37eb97592c2d 2012-02-02 14:09 +0000 bos second change 0 b48ef65237ab 2012-02-02 14:09 +0000 bos first change
Заметим, что новый набор изменений, который был создан командой hg backout является потомком отозванного набора. Это легко увидеть на рисунке Рисунок 9.1, «Отмена изменения используя команду hg backout», на котором показана история изменений в графическом виде. Как вы можете видеть, история изящная и прямолинейная.
Если Вы хотите отозвать не последний набор изменений добавляйте к hg backout опцию --merge
.
$
cd ..
$
hg clone -r1 myrepo non-tip-repo
adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating to branch default 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:5d235a7062b8 backs out changeset 1:37eb97592c2d merging with changeset 3:5d235a7062b8 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, «Автоматический возврат не последнего набора изменений с помощью команды hg backout» ) видим, что в этой ситуации, Mercurial так же фиксирует только одно изменение (прямоугольники — это узлы, которые Mercurial коммитит автоматически), но граф ревизий выглядит иначе. Перед тем, как Mercurial начнёт процесс отзыва, он запомнит текущего родителя рабочей директории. Затем он отзывает указанную ревизию и фиксирует этот отзыв. И наконец, он выполняет слияние с предыдущим родителем рабочей директории, но заметьте, что результат слияния не фиксируется. Репозиторий в настоящее время содержит 2 головы, и рабочий каталог в состоянии слияния.
В результате вы оказываетесь «там же, где и были» лишь с небольшим увеличением истории, в которой отражён откат.
У вас может возникнуть вопрос, почему Mercurial не фиксирует результат слияния, которое он выполнил. Причина в осторожном поведении Mercurial: очевидно, что слияние имеет значительно большее поле для возможных ошибок, чем просто отмена эффекта последней ревизии, так что ваша работа будет безопаснее, если вы сперва проверите (и протестируете!) результат слияния, и только потом его зафиксируете.
Фактически, поскольку опция --merge
делает то что надо, независимо от того, является ли отменяемый набор изменений вершиной, или нет (т.е. не пытается зафиксировать возврат набора изменений, являющегося вершиной, поскольку в этом нет необходимости), вам следует всегда использовать эту опцию команды hg backout.
До сих пор я рекомендовал вам всегда использовать опцию --merge
, когда вы возвращаете изменение, однако команда hg backout позволяет вам выбрать, каким образом произвести фиксацию возврата изменения. Получение полного контроля над на тем, что происходит в процессе возврата — это то, что вам понадобится достаточно редко, однако полезно понимать, что именно команда hg backout делает в автоматическом режиме. Чтобы проиллюстрировать этот процесс, создадим копию репозитория, исключив возвращенные изменения, которые он содержит.
$
cd ..
$
hg clone -r1 myrepo newrepo
adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating to branch default 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 merging myfile 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
Наш новый набор изменений опять является потомком набора изменений, который мы возвращаем, а не того, который являлся вершиной. Команда hg backout вполне явно сообщает нам об этом.
$
hg log --style compact
2[tip] 54d16b160c16 2012-02-02 14:09 +0000 bos third change 1 37eb97592c2d 2012-02-02 14:09 +0000 bos second change 0 b48ef65237ab 2012-02-02 14:09 +0000 bos first change
И снова чтобы увидеть что произошло, проще взглянуть на граф истории ревизий на рисунке Рисунок 9.3, «Возврат изменений с помощью команды hg backout». Он явно показывает, что когда мы использовали hg backout для возврата изменений, не являющихся вершиной, Меркуриал добавляет новый head в репозиторий (изменение, которое было зафиксировано, обозначено квадратом).
После выполнения команды hg backout, новый «backout» набор изменений становится родителем рабочего каталога.
$
hg parents
changeset: 2:54d16b160c16 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:22 2012 +0000 summary: third change
Теперь у нас есть два отдельных набора изменений.
$
hg heads
changeset: 2:54d16b160c16 tag: tip user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:22 2012 +0000 summary: third change
Давайте подумаем о том, что мы ожидаем увидеть, в содержимом myfile
сейчас. Первое изменение, должно присутствовать, потому что мы никогда не возвращали его. Второе изменение должно пропасть, как изменение, которое мы вернули. Поскольку граф истории показывает третье изменение в качестве отдельной головы, мы не ожидаем увидеть третье изменение в myfile
.
$
cat myfile
first change
Чтобы получить третье изменение обратно в файл, мы просто делаем нормальное слияние двух наших голов.
$
hg merge
abort: there is nothing to merge$
hg commit -m 'merged backout with previous tip'
$
cat myfile
first change
После этого графическая история нашего хранилища показана на Рисунок 9.4, «Ручное слияние возвращённых изменений».
Вот краткое описание как работает команда hg backout.
Она гарантирует, что рабочий каталог будет «чистым», т.е., вывод команды hg status будет пустым.
Она запоминает текущего родителя рабочего каталога. Назовем эту ревизию оригинальной
Она не эквивалентна команде hg update, синхронизирующей текущий каталог с ревизией к которой вы хотите вернуться. Назовем эту ревизию откатываемой.
Она находит родителя ревизии, которую Вы хотите откатить. Назовем ее ревизия-родитель.
Для всех файлов, измененных в откатываемой ревизии, делается эквивалент команды hg revert -r parent, для восстановления их до содержимого которое они имели перед этой ревизией.
Результат сохраняется как новая ревизия. Эта ревизия имеет родителем откатываемую ревизию.
Если Вы указали ключ --merge
в коммандной строке, делается слияние с оригинальной ревизией и результат слияния сохраняется как новая ревизия
Альтернативный способ реализации hg backout команда hg export применённую к откатываемому набору изменений как diff-у, а затем использовать опцию --reverse
для команды patch для ликвидации последствий изменения без возни с рабочей директорией. Это звучит гораздо проще, но работает это не так хорошо.
Причина, по которой hg backout делает обновление, фиксацию, слияние, и ещё одну фиксацию используя механизм слияния в том, что это лучший шанс сделать работу хорошо, когда речь идет об сохранении всех изменения между ревизией которую мы откатываем, и текущей ревизией.
Если вы отказываясь ревизии, которая была 100 ревизий назад в истории вашего проекта, вероятность того, что команда patch сможет применить обратный diff аккуратно не велика, потому что вмешательство последующих ревизий возможно «нарушило контекст», который использует patch, чтобы определить, может ли она применить патч (если это звучит для вас как бред, смотрите раздел Раздел 12.4, «Понимание патчей» для обсуждения строк команды patch). Кроме того, механизм слияние Mercurial будет работать с файлами и каталогами, который был переименованы, изменениями прав, и изменения в двоичных файлах, ни с одним из этих вариантов patch не может работать.
Большую часть времени, команда hg backout именно то, что вам нужно, если вы хотите устранить последствия изменений. Она оставляет постоянную запись именно того, что вы сделали, как при фиксации оригинального набора изменений, так и после очистки.
В редких случаях, однако, вы можете обнаружить, что вы зафиксировали изменения, которые в действительности не должны присутствовать в общем репозитории. Например, было бы очень необычно, и обычно считается ошибкой, фиксация объектных файлов приложения, также как его исходных файлов. Объектные файлы не имеют почти никакой ценностью, и они большие, так что они увеличивают размер хранилища и количество времени, необходимое для клонирования или вытягивания изменении.
Прежде чем обсудить варианты, которые у вас есть, если вы зафиксировали «грязный пакет» изменений (выглядящий так плохо, что вы хотите выкинуть грязный пакет из головой), позвольте мы сначала обсудим некоторые подходы, которые, вероятно, не будут работать.
Mercurial рассматривает историю как накопление — каждое изменение создаётся поверх всех изменений, которые ей предшествовали — в общем вы не можете просто убрать внесённые изменения. Исключением является тот случай, когда вы только что совершили изменения, и не передали или вытянули их в другой репозиторий. Вот тогда можно смело использовать команду hg rollback, как я уже подробно описаны в разделе Раздел 9.1.2, «Откат транзакции».
После того как вы передали плохие изменения в другой репозиторий, вы все равно сможете использовать hg rollback в локальной копии изменения исчезают, но это не будет иметь последствий которых вы хотели. Изменения будут присутствовать в удаленном хранилище, поэтому они снова появится в вашем хранилище в следующий раз вы будете забирать из него изменения.
Если возникает такая ситуация, и вы знаете, в какие репозитории распространили свои плохие изменения, можно попробовать избавиться от изменении в каждом из этих репозиториев. Это, конечно, не является удовлетворительным решением: если вы пропустите один из репозиториев пока вы стираете, изменения находящиеся в «диком» мире, и может распространяться дальше.
Если вы зафиксировали одно или несколько изменений после изменений, которые вы хотели бы удалить, ваши ревизии в будущем будут раздроблены. Mercurial не предоставляет способа «удалить дыру» в истории, оставив нетронутыми ревизии.
После слияния зачастую сложно, это не неслыханно для слияния чтоб оно было плохим, но можно ошибочно зафиксировать его. Mercurial предоставляет гарантии против плохих слияний, отказываясь фиксировать неразрешенные файлы, но и человеческая изобретательность безгранична, и беспорядок все еще возможен беспорядок при слиянии и его фиксации.
Учитывая плохие слияния, которые были зафиксированы, как правило, лучший способ исправить его — просто попытаться исправить ущерб вручную. Полная катастрофа, которая не может быть легко исправлена своими руками должен происходить очень редко, но команда hg backout может помочь в упрощении очистки. Он предлагает — опцию --parent
, которая позволяет определить, к какому из родителей возвращаться при откате слияния.
Предположим, мы имеем граф ревизий, как на рисунке Рисунок 9.5, «Плохое слияние». Мы бы хотели повторить это слияние ревизий 2 и 3.
Один из способов сделать это будет выглядеть следующим образом.
Вызываем hg backout --rev=4 --parent=2. Это говорит hg backout откатить ревизию 4, которая является плохим слиянием, а также при определяет какая ревизия выбрать родительской 2, одну из родителей слияния. Эффект может быть показан на рисунке Рисунок 9.6, «Откат слияния, в пользу одного из родителей».
Вызываем hg backout --rev=4 --parent=3. Это говорит hg backout откатить ревизию 4, но на этот раз выбираем родителя 3, другого родителя слияния. В результате показано на рисунке Рисунок 9.7, «Поддержка отката слияния в пользу другого родителя», на котором в это время репозиторий содержит три головы.
Повторяете плохое слияние путем объединения двух отозванных голов, что снижает количество голов в хранилище до двух, как это видно на рисунке Рисунок 9.8, «Слияние откатов».
Слияние с фиксацией, того что было сделано после плохого слияния, как показано на рисунке Рисунок 9.9, «Слияние откатов».
Если ты совершил некоторые изменения в локальном репозитории, и они были отправлены или вытянуты куда-то, это не всегда катастрофа. Вы можете защитить себя заранее в отношении некоторых классов плохих изменений. Это особенно удобно, если ваша команда обычно вытягивает изменения из центрального хранилища.
При настройке некоторых хуков в репозитории для проверки входящих ревизий (см. Глава 10, Обработка событий в репозитории с помощью ловушек), вы можете автоматически предотвращать некоторые виды плохих ревизий вынуждая всех отправлять изменения в центральное хранилище. При такой конфигурации на месте, некоторые виды плохих ревизий естественно, как правило, «отмирают», поскольку они не могут появится в центральном хранилище. А самое хорошее то, что это происходит без необходимости явного вмешательства.
Например, хук на входящие изменения проверяющий, что набор изменений может быть скомпилирован, может помешать людям нечаянно «Нарушить сборку».
Даже тщательно запущенный проект может пострадать от печального события, такого как фиксация и неконтролируемое распространение файла, который содержит важные пароли.
Если что-то подобное произойдет с вами, и информация, которая случайно распространяется, действительно чувствительна, ваш первый шаг должен быть для смягчения последствий утечки, не пытаясь контролировать утечку самостоятельно. Если вы еще не 100% уверены, что вы знаете точно, кто мог увидеть изменения, вы должны немедленно сменить пароли, отменить кредитную карту, или найти другой способ, чтобы убедиться, что информация, с которой произошла утечка, уже бесполезна. Иными словами, предполагайте, что изменение распространится далеко и широко, и что ничего вы не можете сделать.
Можно надеяться, что нет механизмов, которые можно использовать для выяснения того, кто видел изменения и удалить изменения сразу во всем мире, но есть веские причины, почему это не представляется возможным.
Mercurial не предоставляет аудита, кто вытянул изменения из репозитория, потому что, как правило, либо возможность записи такой информации невозможна или тривиально обманывается. В многопользовательской или сетевой среде, вы должны, таким образом, весьма скептически относится к себе, если вы думаете, что вы определили все места, чувствительные ревизии могут распространится. Не забывайте, что люди могут и будут отправлять разветвления по электронной почте, имеют программное обеспечение резервного копирования данных вне офиса, переносят репозиторий на usb-брелках, и найти другие пути совершенно невинные, чтобы посрамить ваши попытки отследить каждую копию проблемных ревизий.
Mercurial также не дает способ сделать файл или набор изменений которые навсегда исчезают из истории, потому что нет возможности реализовать в своей копии удаление, кто-то может легко изменить свою копию Mercurial игнорировав такие директивы. Кроме того, даже если Mercurial обеспечит возможности, запретить вытягивание файла «отмеченного как удалённый» ревизии не будут затронуты ею, равно как и сканерам, сохраняющими образ жесткого диска или других механизмов. В самом деле, не распределенная система контроля версий может сделать чтобы данные надежно исчезли. Предоставляя иллюзию такого контроля, и могут легко дать ложное чувство безопасности, и это хуже, чем не предоставлять его вовсе.
Конечно, возможность откатить изменение, которое внесло ошибку, это хорошо, но сначала нужно узнать, какой именно набор изменений, нужно откатить. 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, поэтому у него нет проблем, связанных с ветвлениями, слияниями, или несколькими головами в репозитории. Он может обрезать все ветви истории одной проверкой, который, как она работает так эффективно.
Вот пример hg bisect в действии.
Теперь давайте создадим репозитарий, так что мы сможем попробовать команду hg bisect отдельно.
$
hg init mybug
$
cd mybug
Мы будем моделировать проект, который содержит ошибку простым образом: создать незначительные изменения в цикле, и назначить одно конкретное изменение, которое будет иметь «ошибку». Этот цикл создает 35 ревизий, каждое добавление одного файла в хранилище. Мы представляем нашу «ошибку» с файлом, который содержит текст «У меня есть губы».
$
buggy_change=22
$
for (( i = 0; i < 35; i++ )); do
>
if [[ $i = $buggy_change ]]; then
>
echo 'i have a gub' > myfile$i
>
hg commit -q -A -m 'buggy changeset'
>
else
>
echo 'nothing to see here, move along' > myfile$i
>
hg commit -q -A -m 'normal changeset'
>
fi
>
done
Следующее, что мы хотели бы сделать, это понять, как использовать команду hg bisect. Для этого мы можем использовать встроенная справочная механизмом Mercurial.
$
hg help bisect
hg bisect [-gbsr] [-U] [-c CMD] [REV] subdivision search of changesets This command helps to find changesets which introduce problems. To use, mark the earliest changeset you know exhibits the problem as bad, then mark the latest changeset which is free from the problem as good. Bisect will update your working directory to a revision for testing (unless the -U/--noupdate option is specified). Once you have performed tests, mark the working directory as good or bad, and bisect will either update to another candidate changeset or announce that it has found the bad revision. As a shortcut, you can also use the revision argument to mark a revision as good or bad without checking it out first. If you supply a command, it will be used for automatic bisection. Its exit status will be used to mark revisions as good or bad: status 0 means good, 125 means to skip the revision, 127 (command not found) will abort the bisection, and any other non-zero exit status means the revision is bad. Returns 0 on success. options: -r --reset reset bisect state -g --good mark changeset good -b --bad mark changeset bad -s --skip skip testing changeset -e --extend extend the bisect range -c --command CMD use command to check changeset state -U --noupdate do not update to target use "hg -v help bisect" to show more info
Команда hg bisect работает по шагам. Каждый шаг происходит следующим образом.
Процесс заканчивается, когда hg bisect идентифицирует уникальный набор изменений, который знаменует собой точку, где Ваш тест перешол из «успешног» в «неуспешный».
Чтобы начать поиск, должны запустить команду hg bisect --reset.
$
hg bisect --reset
В нашем случае мы используем очень простой бинарный тест: мы проверим, содержит ли какой-то файл в хранилище строку «У меня есть жук». Если это так, эта ревизия содержит изменения, которые «вызвали ошибку». По соглашению, ревизия, которая обладает свойством «плохо», а не ту, которая не является «хорошим».
Чаще всего, ревизия с которой синхронизирован рабочий каталог (как правило, 'typ' — последняя голова), уже столкнулись с проблемой внесенной бажной правкой, поэтому мы помечаем его как «плохое».
$
hg bisect --bad
Нашей следующей задачей является назначить ревизию, про которую известно, что она не содержит искомую ошибку; Команда hg bisect ограничивает свой поиск между первой парой «хорошая — плохая» ревизиия. В нашем случае, мы знаем, что 10-я ревизия не имела ошибок. (Я расскажу несколько слов о выборе первой «хорошей» ревизии позднее).
$
hg bisect --good 10
Testing changeset 22:e9e43d57c12e (24 changesets remaining, ~4 tests) 0 files updated, 0 files merged, 12 files removed, 0 files unresolved
Обратите внимание, эта команда что-то выводит [на экран]
Теперь запустим наш тест в рабочием каталоге. Мы используем команду grep, чтобы увидеть, находится ли наш «плохой» файл в рабочей директории. Если это так, эта ревизия является плохой, если нет — хорошей.
$
if grep -q 'i have a gub' *
>
then
>
result=bad
>
else
>
result=good
>
fi
$
echo this revision is $result
this revision is bad$
hg bisect --$result
Testing changeset 16:a20d4936611f (12 changesets remaining, ~3 tests) 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
Этот тест выглядит идеальным кандидатом для автоматизации, так что давайте превратим его в функцию shell.
$
mytest() {
>
if grep -q 'i have a gub' *
>
then
>
result=bad
>
else
>
result=good
>
fi
>
echo this revision is $result
>
hg bisect --$result
>
}
Теперь мы можем запустить весь тест одной командой mytest
.
$
mytest
this revision is good Testing changeset 19:d018a2afc0b4 (6 changesets remaining, ~2 tests) 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
Теперь мы можем запустить весь шаг тестирования с одной командой, mytest.
$
mytest
this revision is good Testing changeset 20:850a595df5d3 (3 changesets remaining, ~1 tests) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
mytest
this revision is good Testing changeset 21:a3b0fef2a78f (2 changesets remaining, ~1 tests) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
mytest
this revision is good The first bad revision is: changeset: 22:e9e43d57c12e user: Bryan O'Sullivan <bos@serpentine.com> date: Thu Feb 02 14:09:26 2012 +0000 summary: buggy changeset
Хотя у нас было 40 ревизий для поиска, hg bisect сумел найти «ошибку» с пяти испытаний. Поскольку количество тестов, для hg bisect растет логарифмически с числом ревизий для поиска, то преимущество перед «методом грубой силы» увеличивается с каждой добавленной ревизией.
Когда вы закончили работать с hg bisect, вы можете использовать hg bisect --reset чтобы сбросить информацию, которую он использовал для проведения вашего поиска. Команда не использует много места, поэтому не страшно, если вы забыли запустить эту команду. Тем не менее, hg bisect не позволит вам начать новый поиск в этом хранилище, пока вы не cделаете hg bisect --reset.
$
hg bisect --reset
Команда hg bisect требует, чтобы вы правильно сообщали о результатах каждого теста, который вы выполняете. Если вы скажете, что это испытание не прошло, когда это действительно удалось, иногда можно обнаружить несоответствие. Если удалось определить несоответствия в Ваших отчетах, Mercurial сообщит Вам, что некоторая ревизия и хорошая и плохая. Однако, он не может делать это всегда, чаще он укажет на неправильную ревизию в качестве источника ошибки.
Когда я начал использовать команду hg bisect, я пытался несколько раз выполнить свои тесты вручную, в командной строке. С таким подходом я убил по крайней мере, первую половину дня. После нескольких попыток, я обнаружил, что я делал много ошибок, из-за которых мне приходилось перезапускать поиск несколько раз, чтобы наконец, получить правильные результаты.
Мои первоначальные проблемы с управлением командой hg bisect вручную происходили даже с простым поиском по малым репозиториям, а если проблема, которую вы ищете более тонкая, или количество тестов, которые hg bisect необходимо выполнить возрастает, вероятность ошибки оператора испортить поиск гораздо выше. Как только я начал автоматизировать мои тесты, я получал лучше результаты.
Ключ к автоматизированному тестированию является двойным:
В моем учебном примере приведенном выше, команда grep тестирует симптом, и условие if
принимает результат этой проверки и гарантирует, что мы всегда передаём тот же самое на вход команды hg bisect. Функция mytest
связывается с этими тегами воспроизводимым образом, чтобы каждый тест являлся однородным и последовательным.
Потому что вывод поиска hg bisect хороша лишь на входных данных которые вы ему передаёте, не принимайте этот отчёт с набором ревизий, как абсолютную истину. Самым простым способом для перекрестной проверки его отчёта, будет запуск вручную тестов на каждой из следующих ревизий:
Ревизия, которая помечена первой плохой ревизией. Ваш тест должен сообщать о ней что она по прежнему плохая.
Родителей, этой ревизии (каждого из родителей, если это слияние). Ваш тест должен сообщить об этих ревизиях как хороших.
Потомка этой ревизии. Ваш тест должен сообщить об этой ревизии как о плохой.
Вполне возможно, что ваш поиск одной ошибки может быть нарушен присутствием другой. Например, предположим, что в вашей программе ошибка в ревизии 100, и она работала правильно на ревизии 50. Кто-то неизвестный вам вставил различные ошибки в ревизию 60, и исправил их в ревизии 80. Это может исказить ваш результат одним из нескольких способов.
Вполне возможно, что эта совершенно другая ошибка «маскирует» вашу, и есть шанс, что эта ошибка проявит себя перед вашей ошибкой. Если вы не можете избежать этой другой ошибки (например, она не даёт вашему проекту собраться), и поэтому нельзя сказать есть ли ваша ошибка в конкретной ревизии, команда hg bisect не может помочь вам непосредственно. Вместо этого, вы можете пометить непроверяемую ревизию, запустив hg bisect --skip.
Другая проблема может возникнуть, если тест на наличие ошибка не является достаточно точным. Если вы проверяете условие «моя программа падает», то и ваша ошибка и сбой связанный с ошибкой, маскирующей вашу, будет выглядеть одинаково, и введёт в заблуждение hg bisect.
Еще одна ситуация, в которой полезно использовать hg bisect --skip, если вы не можете проверить ревизию, так как ваш проект был в сломан и, следовательно, нетестируем в той ревизии, возможно, что кто-то уже проверил в изменениях, которые приводили к провалу сборки проекта.
Выбрать первые «хорошую» и «плохую» ревизии, которые означают конечные точки поиска, зачастую нелегко, но тем не менее это приводит к небольшой дискуссии. С точки зрения hg bisect, «новейшая» ревизия условно обозначается как «плохая», а самая старшая ревизия как «хорошая».
Если вы не помните, когда была подходящая «хорошая» ревизия, о которой вы можете сказать hg bisect, вы можете сделать хуже, чем при тестировании ревизий наугад. Просто помните, для ликвидации ошибок, важны моменты когда ошибка появится не может(возможно, потому что программы с ошибкой ещё не было), и те моменты, когда одна проблема маскирует другую (как я говорил выше).
Даже если вы в конечном итоге «ранняя» ревизия будет за тысячу ревизий или месяцев истории, вы увидите добавление только нескольких тестов hg bisect, которые необходимо выполнить, благодаря логарифмическому поведению.