Mercurial > hgbook
view es/undo.tex @ 494:149ea8ae39c4
translated a couple of paragraphs of the "behind the scenes" chapter
author | Javier Rojas <jerojasro@devnull.li> |
---|---|
date | Tue, 04 Nov 2008 23:50:20 -0500 |
parents | b7d4d66c3ae5 |
children | b8d9066abcea |
line wrap: on
line source
\chapter{Encontrar y arreglar sus equivocaciones} \label{chap:undo} Errar es humano, pero tratar adecuadamente las consecuencias requiere un sistema de control de revisiones de primera categoría. En este capítulo, discutiremos algunas técnicas que puede usar cuando encuentra que hay un problema enraizado en su proyecto. Mercurial tiene unas características poderosas que le ayudarán a isolar las fuentes de los problemas, y a dar cuenta de ellas apropiadamente. \section{Borrar la historia local} \subsection{La consignación accidental} Tengo el problema ocasional, pero persistente de teclear más rápido de lo que pienso, que aveces resulta en consignar un conjunto de cambios incompleto o simplemente malo. En mi caso, el conjunto de cambios incompleto consiste en que creé un nuevo fichero fuente, pero olvidé hacerle \hgcmd{add}. Un conjunto de cambios``simplemente malo'' no es tan común, pero sí resulta muy molesto. \subsection{Hacer rollback una transacción} \label{sec:undo:rollback} En la sección~\ref{sec:concepts:txn}, mencioné que Mercurial trata modificación a un repositorio como una \emph{transacción}. Cada vez que consigna un conjunto de cambios o lo jala de otro repositorio, Mercurial recuerda lo que hizo. Puede deshacer, o hacer \emph{roll back}\ndt{El significado igual que en los ambientes de sistemas manejadores de bases de datos se refiere a la atomicidad e integridad al devolver un conjunto de acciones que permitan dejar el repositorio en un estado consistente previo}, exactamente una de tales acciones usando la orden \hgcmd{rollback}. (Ver en la sección~\ref{sec:undo:rollback-after-push} una anotación importante acerca del uso de esta orden.) A continuación una equivocación que me sucede frecuentemente: consignar un cambio en el cual he creado un nuevo fichero, pero he olvidado hacerle \hgcmd{add}. \interaction{rollback.commit} La salida de \hgcmd{status} después de la consignación confirma inmediatamente este error. \interaction{rollback.status} La consignación capturó los cambios en el fichero \filename{a}, pero no el nuevo fichero \filename{b}. Si yo publicara este conjunto de cambios a un repositorio compartido con un colega, es bastante probable que algo en \filename{a} se refiriera a \filename{b}, el cual podría no estar presente cuando jalen mis cambios del repositorio. Me convertiría el sujeto de cierta indignación. Como sea, la suerte me acompaña---Encontré mi error antes de publicar el conjunto de cambios. Uso la orden \hgcmd{rollback}, y Mercurial hace desaparecer el último conjunto de cambios. \interaction{rollback.rollback} El conjunto de cambios ya no está en la historia del repositorio, y el directorio de trabajo cree que el fichero \filename{a} ha sido modificado. La consignación y el roll back dejaron el directorio de trabajo exactamente como estaba antes de la consignación; el conjunto de cambios ha sido eliminado totlamente. Ahora puedo hacer \hgcmd{add} al fichero \filename{b}, y hacer de nuevo la consignación. \interaction{rollback.add} \subsection{Erroneamente jalado} Mantener ramas de desarrollo separadas de un proyecto en distintos repositorios es una práctica común con Mercurial. Su equipo de desarrollo puede tener un repositorio compartido para la versión ``0.9'' y otra con cambios distintos para la versión ``1.0''. Con este escenario, puede imaginar las consecuencias si tuviera un repositorio local ``0.9'', y jalara accidentalmente los cambios del repositorio compartido de la versión ``1.0'' en este. En el peor de los casos, por falta de atención, es posible que publique tales cambios en el árbol compartido ``0.9'', confundiendo a todo su equipo de trabajo(pero no se preocupe, volveremos a este terrorífico escenario posteriormente). En todo caso, es muy probable que usted se de cuenta inmediatamente, dado que Mercurial mostrará el URL de donde está jalando, o que vea jalando una sospechosa gran cantidad de cambios en el repositorio. La orden \hgcmd{rollback} excluirá eficientemente los conjuntos de cambios que haya acabado de jalar. Mercurial agrupa todos los cambios de un \hgcmd{pull} a una única transacción y bastará con un \hgcmd{rollback} para deshacer esta equivocación. \subsection{Después de publicar, un roll back es futil} \label{sec:undo:rollback-after-push} El valor de \hgcmd{rollback} se anula cuando ha publicado sus cambios a otro repositorio. Un cambio desaparece totalmente al hacer roll back, pero \emph{solamente} en el repositorio en el cual aplica \hgcmd{rollback}. Debido a que un roll back elimina la historia, no hay forma de que la desaparición de un cambio se propague entre repositorios. Si ha publicado un cambio en otro repositorio---particularmente si es un repositorio público---esencialmente está ``en terreno agreste,'' y tendrá que reparar la equivocación de un modo distinto. Lo que pasará si publica un conjunto de cambios en algún sitio, hacer rollback y después volver a jalar del repositorio del cual había publicado, es que el conjunto de cambios reaparecerá en su repositorio. (Si está absolutamente segruro de que el conjunto de cambios al que desea hacer rollback es el cambio más reciente del repositorio en el cual publicó, \emph{y} sabe que nadie más pudo haber jalado de tal repositorio, puede hacer rollback del conjunto de cambios allí, pero es mejor no confiar en una solución de este estilo. Si lo hace, tarde o temprano un conjunto de cambios logrará colarse en un repositorio que usted no controle directamente(o del cual se ha olvidado), y volverá a hostigarle.) \subsection{Solamente hay un roll back} Mercurial almacena exactamente una transacción en su bitácora de transacciones; tal transacción es la más reciente de las que haya ocurrido en el repositorio. Esto significa que solamente puede hacer roll back a una transacción. Si espera poder hacer roll back a una transacción después al antecesor, observará que no es el comportamiento que obtendrá. \interaction{rollback.twice} Una vez que haya aplicado un rollback en una transacción a un repositorio, no podrá volver a hacer rollback hasta que haga una consignación o haya jalado. \section{Revertir un cambio equivocado} Si modifica un fichero y se da cuenta que no quería realmente cambiar tal fichero, y todavía no ha consignado los cambios, la orden necesaria es \hgcmd{revert}. Observa el conjunto de cambios padre del directorio y restaura los contenidos del fichero al estado de tal conjunto de cambios. (Es una forma larga de decirlo, usualmente deshace sus modificaciones.) Ilustremos como actúa la orden \hgcmd{revert} con un ejemplo pequeño. Comenzaremos modificando un fichero al cual Mercurial ya está siguiendo. \interaction{daily.revert.modify} Si no queremos ese cambio, podemos aplicar \hgcmd{revert} al fichero. \interaction{daily.revert.unmodify} La orden \hgcmd{revert} nos brinda un grado adicional de seguridad guardando nuestro fichero modificado con la extensión \filename{.orig}. \interaction{daily.revert.status} Este es un resumen de casos en los cuales la orden \hgcmd{revert} es de utilidad. Describiremos cada uno de ellos con más detalle en la sección siguiente. \begin{itemize} \item Si usted modifica un fichero, lo restaurará a su estado sin modificación previo. \item Si usted hace \hgcmd{add} a un fichero, revertirá el estado de ``adicionado'' del fichero, pero no lo tocará \item Si borra un fichero sin decirle a Mercurial, restaurará el fichero con sus contenidos sin modificación. \item Si usa la orden \hgcmd{remove} para eliminar un fichero, deshará el estado ``removido'' del fichero, y lo restaurará con sus contenidos sin modificación. \end{itemize} \subsection{Errores al administrar ficheros} \label{sec:undo:mgmt} La orden \hgcmd{revert} es útil para más que ficheros modificados. Le permite reversar los resultados de todas las órdenes de administración de ficheros que provee Mercurial---\hgcmd{add}, \hgcmd{remove}, y las demás. Si usted hace \hgcmd{add} a un fichero, y no deseaba que Mercurial le diera seguimiento, use \hgcmd{revert} para deshacer la adición. No se preocupe; Mercurial no modificará de forma alguna el fichero. Solamente lo ``desmarcará''. \interaction{daily.revert.add} De forma similar, Si le solicita a Mercurial hacer \hgcmd{remove} a un fichero, puede usar \hgcmd{revert} para restarurarlo a los contenidos que tenía la revisión padre del directorio de trabajo. \interaction{daily.revert.remove} Funciona de la misma manera para un fichero que usted haya eliminado manualmente, sin decirle a Mercurial (recuerde que en la terminología de Mercurial esta clase de fichero se llama ``faltante''). \interaction{daily.revert.missing} Si usted revierte un \hgcmd{copy}, el fichero a donde se copió permanece en su directorio de trabajo, pero sin seguimiento. Dado que una copia no afecta el fichero fuente de copiado de ninguna maner, Mercurial no hace nada con este. \interaction{daily.revert.copy} \subsubsection{Un caso ligeramente especial:revertir un renombramiento} Si hace \hgcmd{rename} a un fichero, hay un detalle que debe tener en cuenta. Cuando aplica \hgcmd{revert} a un cambio de nombre, no es suficiente proveer el nombre del fichero destino, como puede verlo en el siguiente ejemplo. \interaction{daily.revert.rename} Como puede ver en la salida de \hgcmd{status}, el fichero con el nuevo nombre no se identifica más como agregado, pero el fichero con el nombre-\emph{inicial} se elimna! Esto es contra-intuitivo (por lo menos para mí), pero por lo menos es fácil arreglarlo. \interaction{daily.revert.rename-orig} Por lo tanto, recuerde, para revertir un \hgcmd{rename}, debe proveer \emph{ambos} nombres, la fuente y el destino. % TODO: the output doesn't look like it will be removed! (A propósito, si elimina un fichero, y modifica el fichero con el nuevo nombre, al revertir ambos componentes del renombramiento, cuando Mercurial restaure el fichero que fue eliminado como parte del renombramiento, no será modificado. Si necesita que las modificaciones en el archivo destino del renombramiento se muestren, no olvide copiarlas encima.) Estos aspectos engorrosos al revertir un renombramiento se constituyen discutiblemente en un fallo de Mercurial. \section{Tratar cambios consignados} Considere un caso en el que ha consignado el cambio $a$, y otro cambio $b$ sobre este; se ha dado cuenta que el cambio $a$ era incorrecto. Mercurial le permite ``retroceder'' un conjunto de cambios completo automáticamente, y construir bloques que le permitan revertir parte de un conjunto de cambios a mano. Antes de leer esta sección, hay algo para tener en cuenta: la orden \hgcmd{backout} deshace cambios \emph{adicionando} a la historia, sin modificar o borrar. Es la herramienta correcta si está arreglando fallos, pero no si está tratando de deshacer algún cambio que tiene consecuencias catastróficas. Para tratar con esos, vea la sección~\ref{sec:undo:aaaiiieee}. \subsection{Retroceder un conjunto de cambios} La orden \hgcmd{backout} le permite ``deshacer'' los efectos de todo un conjunto de cambios de forma automatizada. Dado que la historia de Mercurial es inmutable, esta orden \emph{no} se deshace del conjunto de cambios que usted desea deshacer. En cambio, crea un nuevo conjunto de cambios que \emph{reversa} el conjunto de cambios que usted indique. La operación de la orden \hgcmd{backout} es un poco intrincada, y lo ilustraremos con algunos ejemplos. Primero crearemos un repositorio con algunos cambios sencillos. \interaction{backout.init} La orden \hgcmd{backout} toma un ID de conjunto de cambios como su argumento; el conjunto de cambios a retroceder. Normalmente \hgcmd{backout} le ofrecerá un editor de texto para escribir el mensaje de la consignación, para dejar un registro de por qué está retrocediendo. En este ejemplo, colocamos un mensaje en la consignación usando la opción \hgopt{backout}{-m} . \subsection{Retroceder el conjunto de cambios punta} Comenzamos retrocediendo el último conjunto de cambios que consignamos. \interaction{backout.simple} Puede ver que la segunda línea de \filename{myfile} ya no está presente. La salida de \hgcmd{log} nos da una idea de lo que la orden \hgcmd{backout} ha hecho. \interaction{backout.simple.log} Vea que el nuevo conjunto de cambios que \hgcmd{backout} ha creado es un hijo del conjunto de cambios que retrocedimos. Es más sencillo de ver en la figura~\ref{fig:undo:backout}, que presenta una vista gráfica de la historia de cambios. Como puede ver, la historia es bonita y lineal. \begin{figure}[htb] \centering \grafix{undo-simple} \caption{Retroceso de un cambio con la orden \hgcmd{backout}} \label{fig:undo:backout} \end{figure} \subsection{Retroceso de un cambio que no es la punta} Si desea retrocede un cambio distinto al último que ha consignado, use la opción \hgopt{backout}{--merge} a la orden \hgcmd{backout}. \interaction{backout.non-tip.clone} Que resulta en un retroceso de un conjunto de cambios ``en un sólo tiro'', una operación que resulta normalmente rápida y sencilla. \interaction{backout.non-tip.backout} Si ve los contenidos del fichero \filename{myfile} después de finalizar el retroceso, verá que el primer y el tercer cambio están presentes, pero no el segundo. \interaction{backout.non-tip.cat} Como lo muestra la historia gráfica en la figura~\ref{fig:undo:backout-non-tip}, Mercurial realmente consigna \emph{dos} cambios en estas situaciones (los nodos encerrados en una caja son aquellos que Mercurial consigna automaticamente). Antes de que Mercurial comience el proceso de retroceso, primero recuerda cuál es el padre del directorio de trabajo. Posteriormente hace un retroceso al conjunto de cambios objetivo y lo consigna como un conjunto de cambios. Finalmente, fusiona con el padre anterior del directorio de trabajo, y consigna el resultado de la fusión. % TODO: to me it looks like mercurial doesn't commit the second merge automatically! \begin{figure}[htb] \centering \grafix{undo-non-tip} \caption{Retroceso automatizado de un cambio a algo que no es la punta con la orden \hgcmd{backout}} \label{fig:undo:backout-non-tip} \end{figure} El resultado es que usted termina ``donde estaba'', solamente con un poco de historia adicional que deshace el efecto de un conjunto de cambios que usted quería evitar. \subsubsection{Use siempre la opción \hgopt{backout}{--merge}} De hecho, dado que la opción \hgopt{backout}{--merge} siempre hara lo ``correcto'' esté o no retrocediendo el conjunto de cambios punta (p.e.~no tratará de fusionar si está retrocediendo la punta, dado que no es necesario), usted debería usar \emph{siempre} esta opción cuando ejecuta la orden \hgcmd{backout}. \subsection{Más control sobre el proceso de retroceso} A pesar de que recomiendo usar siempre la opción \hgopt{backout}{--merge} cuando está retrocediendo un cambio, la orden \hgcmd{backout} le permite decidir cómo mezclar un retroceso de un conjunto de cambios. Es muy extraño que usted necestite tomar control del proceso de retroceso de forma manual, pero puede ser útil entender lo que la orden \hgcmd{backout} está haciendo automáticamente para usted. Para ilustrarlo, clonemos nuestro primer repositorio, pero omitamos el retroceso que contiene. \interaction{backout.manual.clone} Como en el ejemplo anterior, consignaremos un tercer cambio, después haremos retroceso de su padre, y veremos qué pasa. \interaction{backout.manual.backout} Nuestro nuevo conjunto de cambios es de nuevo un descendiente del conjunto de cambio que retrocedimos; es por lo tanto una nueva cabeza, \emph{no} un descendiente del conjunto de cambios que era la punta. La orden \hgcmd{backout} fue muy explícita diciéndolo. \interaction{backout.manual.log} De nuevo, es más sencillo lo que pasó viendo una gráfica de la historia de revisiones, en la figura~\ref{fig:undo:backout-manual}. Esto nos aclara que cuando usamos \hgcmd{backout} para retroceder un cambio a algo que no sea la punta, Mercurial añade una nueva cabeza al repositorio (el cambio que consignó está encerrado en una caja). \begin{figure}[htb] \centering \grafix{undo-manual} \caption{Retroceso usando la orden \hgcmd{backout}} \label{fig:undo:backout-manual} \end{figure} Después de que la orden \hgcmd{backout} ha terminado, deja un nuevo conjunto de cambios de ``retroceso'' como el padre del directorio de trabajo. \interaction{backout.manual.parents} Ahora tenemos dos conjuntos de cambios aislados. \interaction{backout.manual.heads} Reflexionemos acerca de lo que esperamos ver como contenidos de \filename{myfile}. El primer cambio debería estar presente, porque nunca le hicimos retroceso. El segundo cambio debió desaparecer, puesto que es el que retrocedimos. Dado que la gráfica de la historia muestra que el tercer camlio es una cabeza separada, \emph{no} esperamos ver el tercer cambio presente en \filename{myfile}. \interaction{backout.manual.cat} Para que el tercer cambio esté en el archivo, hacemos una fusión usual de las dos cabezas. \interaction{backout.manual.merge} Después de eso, la historia gráfica de nuestro repositorio luce como la figura~\ref{fig:undo:backout-manual-merge}. \begin{figure}[htb] \centering \grafix{undo-manual-merge} \caption{Fusión manual de un retroceso} \label{fig:undo:backout-manual-merge} \end{figure} \subsection{Por qué \hgcmd{backout} hace lo que hace} Esta es una descripción corta de cómo trabaja la orden \hgcmd{backout}. \begin{enumerate} \item Se asegura de que el directorio de trabajo es ``limpio'', esto es, que la salida de \hgcmd{status} debería ser vacía. \item Recuerda el padre actual del directorio de trabajo. A este conjunto de cambio lo llamaremos \texttt{orig} \item Hace el equivalente de un \hgcmd{update} para sincronizar el directorio de trabajo con el conjunto de cambios que usted quiere retroceder. Lo llamaremos \texttt{backout} \item Encuentra el padre del conjunto de cambios. Lo llamaremos \texttt{parent}. \item Para cada archivo del conjunto de cambios que el \texttt{retroceso} afecte, hará el equivalente a \hgcmdargs{revert}{-r parent} sobre ese fichero, para restaurarlo a los contenidos que tenía antes de que el conjunto de cambios fuera consignado. \item Se consigna el resultado como un nuevo conjunto de cambios y tiene a \texttt{backout} como su padre. \item Si especifica \hgopt{backout}{--merge} en la línea de comandos, se fusiona con \texttt{orig}, y se consigna el resultado de la fusión. \end{enumerate} Una vía alternativa de implementar la orden \hgcmd{backout} sería usar \hgcmd{export} sobre el conjunto de cambios a retroceder como un diff y después usar laa opción \cmdopt{patch}{--reverse} de la orden \command{patch} para reversar el efecto del cambio sin molestar el directorio de trabajo. Suena mucho más simple, pero no funcionaría bien ni de cerca. La razón por la cual \hgcmd{backout} hace una actualización, una consignación, una fusión y otra consignación es para dar a la maquinaria de fusión la mayor oportunidad de hacer un buen trabajo cuando se trata con todos los cambios \emph{entre} el cambio que está retrocediendo y la punta actual. Si está retrocediendo un conjunto de cambios que está a unas ~100 atrás en su historia del proyecto, las posibilidades de que una orden \command{patch} sea capaz de ser aplicada a un diff reverso, claramente no son altas, porque los cambios que intervienen podrían ``no coincidir con el contexto'' que \command{patch} usa para determinar si puede aplicar un parche (si esto suena como cháchara, vea una discusión de la orden \command{patch} en \ref{sec:mq:patch}). Adicionalmente, la maquinaria de fusión de Mercurial manejará ficheros y directorios renombrados, cambios de permisos, y modificaciones a archivos binarios, nada de lo cual la orden \command{patch} puede manejar. \section{Cambios que nunca debieron ocurrir} \label{sec:undo:aaaiiieee} En la mayoría de los casos, la orden \hgcmd{backout} es exactamente lo que necesita para deshacer los efectos de un cambio. Deja un registro permanente y exacto de lo que usted hizo, cuando se consignó el conjunto de cambios original y cuando se hizo la limpieza. En ocasiones particulares, puede haber consignado un cambio que no debería estar de ninguna forma en el repositorio. Por ejemplo, sería muy inusual, y considerado como una equivocación, consignar los archivos objeto junto con el código fuente. los ficheros objeto no tienen valor intrínseco y son \emph{grandes}, por lo tanto aumentan el tamaño del repositorio y la cantidad de tiempo que se emplea al clonar o jalar cambios. Antes de discutir las opciones que tiene si consignó cambio del tipo ``bolsa de papel deschable'' (el tipo que es tan malo que le gustaría colocarse una bolsa de papel desechable en su cabeza), permítame discutir primero unas aproximaciones que probablemente no funcionen. Dado que Mercurial trata de forma acumulativa la historia---cada cambio se coloca encima de todos los cambios que leo preceden---usualmente usted no puede hacer que unos cambios desastros desaparezcan. La única excepción es cuando usted ha acabado de consignar un cambio y este no ha sido publicado o jalado en otro repositorio. Ahí es cuando puede usar la orden \hgcmd{rollback} con seguridad, Como detallé en la sección~\ref{sec:undo:rollback}. Después de que usted haya publicado un cambio en otro repositorio, usted \emph{podría} usar la orden \hgcmd{rollback} para hacer que en su copia local desaparezca el cambio, pero no tendrá las consecuencias que desea. El cambio estará presente en un repositorio remoto, y reaparecerá en su repositorio local la próxima vez que jale Si una situación como esta se presenta, y usted sabe en qué repositorios su mal cambio se ha propagado, puede \emph{intentar} deshacerse del conjunto de cambios de \emph{todos} los repositorios en los que se pueda encontrar. Esta por supuesto, no es una solución satisfactoria: si usted deja de hacerlo en un solo repositorio, mientras esté eliminándolo, el cambio todavía estará ``allí afuera'', y podría propagarse más tarde. Si ha consignado uno o más cambios \emph{después} del cambio que desea desaparecer, sus opciones son aún más reducidas. Mercurial no provee una forma de ``cabar un hueco'' en la historia, dejando los conjuntos de cambios intactos. %Dejamos de traducir lo que viene a continuación, porque será %modificado por upstream... XXX This needs filling out. The \texttt{hg-replay} script in the \texttt{examples} directory works, but doesn't handle merge changesets. Kind of an important omission. \subsection{Protect yourself from ``escaped'' changes} If you've committed some changes to your local repository and they've been pushed or pulled somewhere else, this isn't necessarily a disaster. You can protect yourself ahead of time against some classes of bad changeset. This is particularly easy if your team usually pulls changes from a central repository. By configuring some hooks on that repository to validate incoming changesets (see chapter~\ref{chap:hook}), you can automatically prevent some kinds of bad changeset from being pushed to the central repository at all. With such a configuration in place, some kinds of bad changeset will naturally tend to ``die out'' because they can't propagate into the central repository. Better yet, this happens without any need for explicit intervention. For instance, an incoming change hook that verifies that a changeset will actually compile can prevent people from inadvertantly ``breaking the build''. \section{Finding the source of a bug} \label{sec:undo:bisect} While it's all very well to be able to back out a changeset that introduced a bug, this requires that you know which changeset to back out. Mercurial provides an invaluable command, called \hgcmd{bisect}, that helps you to automate this process and accomplish it very efficiently. The idea behind the \hgcmd{bisect} command is that a changeset has introduced some change of behaviour that you can identify with a simple binary test. You don't know which piece of code introduced the change, but you know how to test for the presence of the bug. The \hgcmd{bisect} command uses your test to direct its search for the changeset that introduced the code that caused the bug. Here are a few scenarios to help you understand how you might apply this command. \begin{itemize} \item The most recent version of your software has a bug that you remember wasn't present a few weeks ago, but you don't know when it was introduced. Here, your binary test checks for the presence of that bug. \item You fixed a bug in a rush, and now it's time to close the entry in your team's bug database. The bug database requires a changeset ID when you close an entry, but you don't remember which changeset you fixed the bug in. Once again, your binary test checks for the presence of the bug. \item Your software works correctly, but runs~15\% slower than the last time you measured it. You want to know which changeset introduced the performance regression. In this case, your binary test measures the performance of your software, to see whether it's ``fast'' or ``slow''. \item The sizes of the components of your project that you ship exploded recently, and you suspect that something changed in the way you build your project. \end{itemize} From these examples, it should be clear that the \hgcmd{bisect} command is not useful only for finding the sources of bugs. You can use it to find any ``emergent property'' of a repository (anything that you can't find from a simple text search of the files in the tree) for which you can write a binary test. We'll introduce a little bit of terminology here, just to make it clear which parts of the search process are your responsibility, and which are Mercurial's. A \emph{test} is something that \emph{you} run when \hgcmd{bisect} chooses a changeset. A \emph{probe} is what \hgcmd{bisect} runs to tell whether a revision is good. Finally, we'll use the word ``bisect'', as both a noun and a verb, to stand in for the phrase ``search using the \hgcmd{bisect} command. One simple way to automate the searching process would be simply to probe every changeset. However, this scales poorly. If it took ten minutes to test a single changeset, and you had 10,000 changesets in your repository, the exhaustive approach would take on average~35 \emph{days} to find the changeset that introduced a bug. Even if you knew that the bug was introduced by one of the last 500 changesets, and limited your search to those, you'd still be looking at over 40 hours to find the changeset that introduced your bug. What the \hgcmd{bisect} command does is use its knowledge of the ``shape'' of your project's revision history to perform a search in time proportional to the \emph{logarithm} of the number of changesets to check (the kind of search it performs is called a dichotomic search). With this approach, searching through 10,000 changesets will take less than three hours, even at ten minutes per test (the search will require about 14 tests). Limit your search to the last hundred changesets, and it will take only about an hour (roughly seven tests). The \hgcmd{bisect} command is aware of the ``branchy'' nature of a Mercurial project's revision history, so it has no problems dealing with branches, merges, or multiple heads in a repoository. It can prune entire branches of history with a single probe, which is how it operates so efficiently. \subsection{Using the \hgcmd{bisect} command} Here's an example of \hgcmd{bisect} in action. \begin{note} In versions 0.9.5 and earlier of Mercurial, \hgcmd{bisect} was not a core command: it was distributed with Mercurial as an extension. This section describes the built-in command, not the old extension. \end{note} Now let's create a repository, so that we can try out the \hgcmd{bisect} command in isolation. \interaction{bisect.init} We'll simulate a project that has a bug in it in a simple-minded way: create trivial changes in a loop, and nominate one specific change that will have the ``bug''. This loop creates 35 changesets, each adding a single file to the repository. We'll represent our ``bug'' with a file that contains the text ``i have a gub''. \interaction{bisect.commits} The next thing that we'd like to do is figure out how to use the \hgcmd{bisect} command. We can use Mercurial's normal built-in help mechanism for this. \interaction{bisect.help} The \hgcmd{bisect} command works in steps. Each step proceeds as follows. \begin{enumerate} \item You run your binary test. \begin{itemize} \item If the test succeeded, you tell \hgcmd{bisect} by running the \hgcmdargs{bisect}{good} command. \item If it failed, run the \hgcmdargs{bisect}{--bad} command. \end{itemize} \item The command uses your information to decide which changeset to test next. \item It updates the working directory to that changeset, and the process begins again. \end{enumerate} The process ends when \hgcmd{bisect} identifies a unique changeset that marks the point where your test transitioned from ``succeeding'' to ``failing''. To start the search, we must run the \hgcmdargs{bisect}{--reset} command. \interaction{bisect.search.init} In our case, the binary test we use is simple: we check to see if any file in the repository contains the string ``i have a gub''. If it does, this changeset contains the change that ``caused the bug''. By convention, a changeset that has the property we're searching for is ``bad'', while one that doesn't is ``good''. Most of the time, the revision to which the working directory is synced (usually the tip) already exhibits the problem introduced by the buggy change, so we'll mark it as ``bad''. \interaction{bisect.search.bad-init} Our next task is to nominate a changeset that we know \emph{doesn't} have the bug; the \hgcmd{bisect} command will ``bracket'' its search between the first pair of good and bad changesets. In our case, we know that revision~10 didn't have the bug. (I'll have more words about choosing the first ``good'' changeset later.) \interaction{bisect.search.good-init} Notice that this command printed some output. \begin{itemize} \item It told us how many changesets it must consider before it can identify the one that introduced the bug, and how many tests that will require. \item It updated the working directory to the next changeset to test, and told us which changeset it's testing. \end{itemize} We now run our test in the working directory. We use the \command{grep} command to see if our ``bad'' file is present in the working directory. If it is, this revision is bad; if not, this revision is good. \interaction{bisect.search.step1} This test looks like a perfect candidate for automation, so let's turn it into a shell function. \interaction{bisect.search.mytest} We can now run an entire test step with a single command, \texttt{mytest}. \interaction{bisect.search.step2} A few more invocations of our canned test step command, and we're done. \interaction{bisect.search.rest} Even though we had~40 changesets to search through, the \hgcmd{bisect} command let us find the changeset that introduced our ``bug'' with only five tests. Because the number of tests that the \hgcmd{bisect} command performs grows logarithmically with the number of changesets to search, the advantage that it has over the ``brute force'' search approach increases with every changeset you add. \subsection{Cleaning up after your search} When you're finished using the \hgcmd{bisect} command in a repository, you can use the \hgcmdargs{bisect}{reset} command to drop the information it was using to drive your search. The command doesn't use much space, so it doesn't matter if you forget to run this command. However, \hgcmd{bisect} won't let you start a new search in that repository until you do a \hgcmdargs{bisect}{reset}. \interaction{bisect.search.reset} \section{Tips for finding bugs effectively} \subsection{Give consistent input} The \hgcmd{bisect} command requires that you correctly report the result of every test you perform. If you tell it that a test failed when it really succeeded, it \emph{might} be able to detect the inconsistency. If it can identify an inconsistency in your reports, it will tell you that a particular changeset is both good and bad. However, it can't do this perfectly; it's about as likely to report the wrong changeset as the source of the bug. \subsection{Automate as much as possible} When I started using the \hgcmd{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 \hgcmd{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 \hgcmd{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: \begin{itemize} \item always test for the same symptom, and \item always feed consistent input to the \hgcmd{bisect} command. \end{itemize} In my tutorial example above, the \command{grep} command tests for the symptom, and the \texttt{if} statement takes the result of this check and ensures that we always feed the same input to the \hgcmd{bisect} command. The \texttt{mytest} function marries these together in a reproducible way, so that every test is uniform and consistent. \subsection{Check your results} Because the output of a \hgcmd{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: \begin{itemize} \item The changeset that it reports as the first bad revision. Your test should still report this as bad. \item The parent of that changeset (either parent, if it's a merge). Your test should report this changeset as good. \item A child of that changeset. Your test should report this changeset as bad. \end{itemize} \subsection{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 \hgcmd{bisect} command cannot help you directly. Instead, you can mark a changeset as untested by running \hgcmdargs{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 \hgcmd{bisect}. Another useful situation in which to use \hgcmdargs{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. \subsection{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 \hgcmd{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 \hgcmd{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 \hgcmd{bisect} must perform, thanks to its logarithmic behaviour. %%% Local Variables: %%% mode: latex %%% TeX-master: "00book" %%% End: