Mercurial > hgbook
diff en/ch09-undo.xml @ 658:b90b024729f1
WIP DocBook snapshot that all compiles. Mirabile dictu!
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Wed, 18 Feb 2009 00:22:09 -0800 |
parents | |
children | 8fcd44708f41 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/en/ch09-undo.xml Wed Feb 18 00:22:09 2009 -0800 @@ -0,0 +1,997 @@ +<!-- vim: set filetype=docbkxml shiftwidth=2 autoindent expandtab tw=77 : --> + +<chapter id="chap:undo"> + <title>Finding and fixing your mistakes</title> + + <para>To err might be human, but to really handle the consequences + well takes a top-notch revision control system. In this chapter, + we'll discuss some of the techniques you can use when you find + that a problem has crept into your project. Mercurial has some + highly capable features that will help you to isolate the sources + of problems, and to handle them appropriately.</para> + + <sect1> + <title>Erasing local history</title> + + <sect2> + <title>The accidental commit</title> + + <para>I have the occasional but persistent problem of typing + rather more quickly than I can think, which sometimes results + in me committing a changeset that is either incomplete or + plain wrong. In my case, the usual kind of incomplete + changeset is one in which I've created a new source file, but + forgotten to <command role="hg-cmd">hg add</command> it. A + <quote>plain wrong</quote> changeset is not as common, but no + less annoying.</para> + + </sect2> + <sect2 id="sec:undo:rollback"> + <title>Rolling back a transaction</title> + + <para>In section <xref linkend="sec:concepts:txn"/>, I mentioned + that Mercurial treats each modification of a repository as a + <emphasis>transaction</emphasis>. Every time you commit a + changeset or pull changes from another repository, Mercurial + remembers what you did. You can undo, or <emphasis>roll + back</emphasis>, exactly one of these actions using the + <command role="hg-cmd">hg rollback</command> command. (See + section <xref linkend="sec:undo:rollback-after-push"/> for an + important caveat about the use of this command.)</para> + + <para>Here's a mistake that I often find myself making: + committing a change in which I've created a new file, but + forgotten to <command role="hg-cmd">hg add</command> it. <!-- + &interaction.rollback.commit; --> Looking at the output of + <command role="hg-cmd">hg status</command> after the commit + immediately confirms the error. <!-- + &interaction.rollback.status; --> The commit captured the + changes to the file <filename>a</filename>, but not the new + file <filename>b</filename>. If I were to push this changeset + to a repository that I shared with a colleague, the chances + are high that something in <filename>a</filename> would refer + to <filename>b</filename>, which would not be present in their + repository when they pulled my changes. I would thus become + the object of some indignation.</para> + + <para>However, luck is with me&emdash;I've caught my error + before I pushed the changeset. I use the <command + role="hg-cmd">hg rollback</command> command, and Mercurial + makes that last changeset vanish. <!-- + &interaction.rollback.rollback; --> Notice that the changeset + is no longer present in the repository's history, and the + working directory once again thinks that the file + <filename>a</filename> is modified. The commit and rollback + have left the working directory exactly as it was prior to the + commit; the changeset has been completely erased. I can now + safely <command role="hg-cmd">hg add</command> the file + <filename>b</filename>, and rerun my commit. <!-- + &interaction.rollback.add; --></para> + + </sect2> + <sect2> + <title>The erroneous pull</title> + + <para>It's common practice with Mercurial to maintain separate + development branches of a project in different repositories. + Your development team might have one shared repository for + your project's <quote>0.9</quote> release, and another, + containing different changes, for the <quote>1.0</quote> + release.</para> + + <para>Given this, you can imagine that the consequences could be + messy if you had a local <quote>0.9</quote> repository, and + accidentally pulled changes from the shared <quote>1.0</quote> + repository into it. At worst, you could be paying + insufficient attention, and push those changes into the shared + <quote>0.9</quote> tree, confusing your entire team (but don't + worry, we'll return to this horror scenario later). However, + it's more likely that you'll notice immediately, because + Mercurial will display the URL it's pulling from, or you will + see it pull a suspiciously large number of changes into the + repository.</para> + + <para>The <command role="hg-cmd">hg rollback</command> command + will work nicely to expunge all of the changesets that you + just pulled. Mercurial groups all changes from one <command + role="hg-cmd">hg pull</command> into a single transaction, + so one <command role="hg-cmd">hg rollback</command> is all you + need to undo this mistake.</para> + + </sect2> + <sect2 id="sec:undo:rollback-after-push"> + <title>Rolling back is useless once you've pushed</title> + + <para>The value of the <command role="hg-cmd">hg + rollback</command> command drops to zero once you've pushed + your changes to another repository. Rolling back a change + makes it disappear entirely, but <emphasis>only</emphasis> in + the repository in which you perform the <command + role="hg-cmd">hg rollback</command>. Because a rollback + eliminates history, there's no way for the disappearance of a + change to propagate between repositories.</para> + + <para>If you've pushed a change to another + repository&emdash;particularly if it's a shared + repository&emdash;it has essentially <quote>escaped into the + wild,</quote> and you'll have to recover from your mistake + in a different way. What will happen if you push a changeset + somewhere, then roll it back, then pull from the repository + you pushed to, is that the changeset will reappear in your + repository.</para> + + <para>(If you absolutely know for sure that the change you want + to roll back is the most recent change in the repository that + you pushed to, <emphasis>and</emphasis> you know that nobody + else could have pulled it from that repository, you can roll + back the changeset there, too, but you really should really + not rely on this working reliably. If you do this, sooner or + later a change really will make it into a repository that you + don't directly control (or have forgotten about), and come + back to bite you.)</para> + + </sect2> + <sect2> + <title>You can only roll back once</title> + + <para>Mercurial stores exactly one transaction in its + transaction log; that transaction is the most recent one that + occurred in the repository. This means that you can only roll + back one transaction. If you expect to be able to roll back + one transaction, then its predecessor, this is not the + behaviour you will get. <!-- &interaction.rollback.twice; --> + Once you've rolled back one transaction in a repository, you + can't roll back again in that repository until you perform + another commit or pull.</para> + + </sect2> + </sect1> + <sect1> + <title>Reverting the mistaken change</title> + + <para>If you make a modification to a file, and decide that you + really didn't want to change the file at all, and you haven't + yet committed your changes, the <command role="hg-cmd">hg + revert</command> command is the one you'll need. It looks at + the changeset that's the parent of the working directory, and + restores the contents of the file to their state as of that + changeset. (That's a long-winded way of saying that, in the + normal case, it undoes your modifications.)</para> + + <para>Let's illustrate how the <command role="hg-cmd">hg + revert</command> command works with yet another small example. + We'll begin by modifying a file that Mercurial is already + tracking. <!-- &interaction.daily.revert.modify; --> If we don't + want that change, we can simply <command role="hg-cmd">hg + revert</command> the file. <!-- + &interaction.daily.revert.unmodify; --> The <command + role="hg-cmd">hg revert</command> command provides us with an + extra degree of safety by saving our modified file with a + <filename>.orig</filename> extension. <!-- + &interaction.daily.revert.status; --></para> + + <para>Here is a summary of the cases that the <command + role="hg-cmd">hg revert</command> command can deal with. We + will describe each of these in more detail in the section that + follows.</para> + <itemizedlist> + <listitem><para>If you modify a file, it will restore the file + to its unmodified state.</para> + </listitem> + <listitem><para>If you <command role="hg-cmd">hg add</command> a + file, it will undo the <quote>added</quote> state of the + file, but leave the file itself untouched.</para> + </listitem> + <listitem><para>If you delete a file without telling Mercurial, + it will restore the file to its unmodified contents.</para> + </listitem> + <listitem><para>If you use the <command role="hg-cmd">hg + remove</command> command to remove a file, it will undo + the <quote>removed</quote> state of the file, and restore + the file to its unmodified contents.</para> + </listitem></itemizedlist> + + <sect2 id="sec:undo:mgmt"> + <title>File management errors</title> + + <para>The <command role="hg-cmd">hg revert</command> command is + useful for more than just modified files. It lets you reverse + the results of all of Mercurial's file management + commands&emdash;<command role="hg-cmd">hg add</command>, + <command role="hg-cmd">hg remove</command>, and so on.</para> + + <para>If you <command role="hg-cmd">hg add</command> a file, + then decide that in fact you don't want Mercurial to track it, + use <command role="hg-cmd">hg revert</command> to undo the + add. Don't worry; Mercurial will not modify the file in any + way. It will just <quote>unmark</quote> the file. <!-- + &interaction.daily.revert.add; --></para> + + <para>Similarly, if you ask Mercurial to <command + role="hg-cmd">hg remove</command> a file, you can use + <command role="hg-cmd">hg revert</command> to restore it to + the contents it had as of the parent of the working directory. + <!-- &interaction.daily.revert.remove; --> This works just as + well for a file that you deleted by hand, without telling + Mercurial (recall that in Mercurial terminology, this kind of + file is called <quote>missing</quote>). <!-- + &interaction.daily.revert.missing; --></para> + + <para>If you revert a <command role="hg-cmd">hg copy</command>, + the copied-to file remains in your working directory + afterwards, untracked. Since a copy doesn't affect the + copied-from file in any way, Mercurial doesn't do anything + with the copied-from file. <!-- + &interaction.daily.revert.copy; --></para> + + <sect3> + <title>A slightly special case: reverting a rename</title> + + <para>If you <command role="hg-cmd">hg rename</command> a + file, there is one small detail that you should remember. + When you <command role="hg-cmd">hg revert</command> a + rename, it's not enough to provide the name of the + renamed-to file, as you can see here. <!-- + &interaction.daily.revert.rename; --> As you can see from + the output of <command role="hg-cmd">hg status</command>, + the renamed-to file is no longer identified as added, but + the renamed-<emphasis>from</emphasis> file is still removed! + This is counter-intuitive (at least to me), but at least + it's easy to deal with. <!-- + &interaction.daily.revert.rename-orig; --> So remember, to + revert a <command role="hg-cmd">hg rename</command>, you + must provide <emphasis>both</emphasis> the source and + destination names.</para> + + <para>% TODO: the output doesn't look like it will be + removed!</para> + + <para>(By the way, if you rename a file, then modify the + renamed-to file, then revert both components of the rename, + when Mercurial restores the file that was removed as part of + the rename, it will be unmodified. If you need the + modifications in the renamed-to file to show up in the + renamed-from file, don't forget to copy them over.)</para> + + <para>These fiddly aspects of reverting a rename arguably + constitute a small bug in Mercurial.</para> + + </sect3> + </sect2> + </sect1> + <sect1> + <title>Dealing with committed changes</title> + + <para>Consider a case where you have committed a change $a$, and + another change $b$ on top of it; you then realise that change + $a$ was incorrect. Mercurial lets you <quote>back out</quote> + an entire changeset automatically, and building blocks that let + you reverse part of a changeset by hand.</para> + + <para>Before you read this section, here's something to keep in + mind: the <command role="hg-cmd">hg backout</command> command + undoes changes by <emphasis>adding</emphasis> history, not by + modifying or erasing it. It's the right tool to use if you're + fixing bugs, but not if you're trying to undo some change that + has catastrophic consequences. To deal with those, see section + <xref linkend="sec:undo:aaaiiieee"/>.</para> + + <sect2> + <title>Backing out a changeset</title> + + <para>The <command role="hg-cmd">hg backout</command> command + lets you <quote>undo</quote> the effects of an entire + changeset in an automated fashion. Because Mercurial's + history is immutable, this command <emphasis>does + not</emphasis> get rid of the changeset you want to undo. + Instead, it creates a new changeset that + <emphasis>reverses</emphasis> the effect of the to-be-undone + changeset.</para> + + <para>The operation of the <command role="hg-cmd">hg + backout</command> command is a little intricate, so let's + illustrate it with some examples. First, we'll create a + repository with some simple changes. <!-- + &interaction.backout.init; --></para> + + <para>The <command role="hg-cmd">hg backout</command> command + takes a single changeset ID as its argument; this is the + changeset to back out. Normally, <command role="hg-cmd">hg + backout</command> will drop you into a text editor to write + a commit message, so you can record why you're backing the + change out. In this example, we provide a commit message on + the command line using the <option + role="hg-opt-backout">-m</option> option.</para> + + </sect2> + <sect2> + <title>Backing out the tip changeset</title> + + <para>We're going to start by backing out the last changeset we + committed. <!-- &interaction.backout.simple; --> You can see + that the second line from <filename>myfile</filename> is no + longer present. Taking a look at the output of <command + role="hg-cmd">hg log</command> gives us an idea of what the + <command role="hg-cmd">hg backout</command> command has done. + <!-- &interaction.backout.simple.log; --> Notice that the new + changeset that <command role="hg-cmd">hg backout</command> has + created is a child of the changeset we backed out. It's + easier to see this in figure <xref + linkend="fig:undo:backout"/>, which presents a graphical + view of the change history. As you can see, the history is + nice and linear.</para> + + <informalfigure id="fig:undo:backout"> + <mediaobject><imageobject><imagedata + fileref="undo-simple"/></imageobject><textobject><phrase>XXX + add text</phrase></textobject><caption><para>Backing out + a change using the <command role="hg-cmd">hg + backout</command> + command</para></caption></mediaobject> + + </informalfigure> + + </sect2> + <sect2> + <title>Backing out a non-tip change</title> + + <para>If you want to back out a change other than the last one + you committed, pass the <option + role="hg-opt-backout">--merge</option> option to the + <command role="hg-cmd">hg backout</command> command. <!-- + &interaction.backout.non-tip.clone; --> This makes backing out + any changeset a <quote>one-shot</quote> operation that's + usually simple and fast. <!-- + &interaction.backout.non-tip.backout; --></para> + + <para>If you take a look at the contents of + <filename>myfile</filename> after the backout finishes, you'll + see that the first and third changes are present, but not the + second. <!-- &interaction.backout.non-tip.cat; --></para> + + <para>As the graphical history in figure <xref + linkend="fig:undo:backout-non-tip"/> illustrates, Mercurial + actually commits <emphasis>two</emphasis> changes in this kind + of situation (the box-shaped nodes are the ones that Mercurial + commits automatically). Before Mercurial begins the backout + process, it first remembers what the current parent of the + working directory is. It then backs out the target changeset, + and commits that as a changeset. Finally, it merges back to + the previous parent of the working directory, and commits the + result of the merge.</para> + + <para>% TODO: to me it looks like mercurial doesn't commit the + second merge automatically!</para> + + <informalfigure id="fig:undo:backout-non-tip"> + <mediaobject><imageobject><imagedata + fileref="undo-non-tip"/></imageobject><textobject><phrase>XXX + add text</phrase></textobject><caption><para>Automated + backout of a non-tip change using the <command + role="hg-cmd">hg backout</command> + command</para></caption></mediaobject> + </informalfigure> + + <para>The result is that you end up <quote>back where you + were</quote>, only with some extra history that undoes the + effect of the changeset you wanted to back out.</para> + + <sect3> + <title>Always use the <option + role="hg-opt-backout">--merge</option> option</title> + + <para>In fact, since the <option + role="hg-opt-backout">--merge</option> option will do the + <quote>right thing</quote> whether or not the changeset + you're backing out is the tip (i.e. it won't try to merge if + it's backing out the tip, since there's no need), you should + <emphasis>always</emphasis> use this option when you run the + <command role="hg-cmd">hg backout</command> command.</para> + + </sect3> + </sect2> + <sect2> + <title>Gaining more control of the backout process</title> + + <para>While I've recommended that you always use the <option + role="hg-opt-backout">--merge</option> option when backing + out a change, the <command role="hg-cmd">hg backout</command> + command lets you decide how to merge a backout changeset. + Taking control of the backout process by hand is something you + will rarely need to do, but it can be useful to understand + what the <command role="hg-cmd">hg backout</command> command + is doing for you automatically. To illustrate this, let's + clone our first repository, but omit the backout change that + it contains.</para> + + <para><!-- &interaction.backout.manual.clone; --> As with our + earlier example, We'll commit a third changeset, then back out + its parent, and see what happens. <!-- + &interaction.backout.manual.backout; --> Our new changeset is + again a descendant of the changeset we backout out; it's thus + a new head, <emphasis>not</emphasis> a descendant of the + changeset that was the tip. The <command role="hg-cmd">hg + backout</command> command was quite explicit in telling us + this. <!-- &interaction.backout.manual.log; --></para> + + <para>Again, it's easier to see what has happened by looking at + a graph of the revision history, in figure <xref + linkend="fig:undo:backout-manual"/>. This makes it clear + that when we use <command role="hg-cmd">hg backout</command> + to back out a change other than the tip, Mercurial adds a new + head to the repository (the change it committed is + box-shaped).</para> + + <informalfigure id="fig:undo:backout-manual"> + <mediaobject><imageobject><imagedata + fileref="undo-manual"/></imageobject><textobject><phrase>XXX + add text</phrase></textobject><caption><para>Backing out + a change using the <command role="hg-cmd">hg + backout</command> + command</para></caption></mediaobject> + + </informalfigure> + + <para>After the <command role="hg-cmd">hg backout</command> + command has completed, it leaves the new + <quote>backout</quote> changeset as the parent of the working + directory. <!-- &interaction.backout.manual.parents; --> Now + we have two isolated sets of changes. <!-- + &interaction.backout.manual.heads; --></para> + + <para>Let's think about what we expect to see as the contents of + <filename>myfile</filename> now. The first change should be + present, because we've never backed it out. The second change + should be missing, as that's the change we backed out. Since + the history graph shows the third change as a separate head, + we <emphasis>don't</emphasis> expect to see the third change + present in <filename>myfile</filename>. <!-- + &interaction.backout.manual.cat; --> To get the third change + back into the file, we just do a normal merge of our two + heads. <!-- &interaction.backout.manual.merge; --> Afterwards, + the graphical history of our repository looks like figure + <xref linkend="fig:undo:backout-manual-merge"/>.</para> + + <informalfigure id="fig:undo:backout-manual-merge"> + <mediaobject><imageobject><imagedata + fileref="undo-manual-merge"/></imageobject><textobject><phrase>XXX + add text</phrase></textobject><caption><para>Manually + merging a backout change</para></caption></mediaobject> + + </informalfigure> + + </sect2> + <sect2> + <title>Why <command role="hg-cmd">hg backout</command> works as + it does</title> + + <para>Here's a brief description of how the <command + role="hg-cmd">hg backout</command> command works.</para> + <orderedlist> + <listitem><para>It ensures that the working directory is + <quote>clean</quote>, i.e. that the output of <command + role="hg-cmd">hg status</command> would be empty.</para> + </listitem> + <listitem><para>It remembers the current parent of the working + directory. Let's call this changeset + <literal>orig</literal></para> + </listitem> + <listitem><para>It does the equivalent of a <command + role="hg-cmd">hg update</command> to sync the working + directory to the changeset you want to back out. Let's + call this changeset <literal>backout</literal></para> + </listitem> + <listitem><para>It finds the parent of that changeset. Let's + call that changeset <literal>parent</literal>.</para> + </listitem> + <listitem><para>For each file that the + <literal>backout</literal> changeset affected, it does the + equivalent of a <command role="hg-cmd">hg revert -r + parent</command> on that file, to restore it to the + contents it had before that changeset was + committed.</para> + </listitem> + <listitem><para>It commits the result as a new changeset. + This changeset has <literal>backout</literal> as its + parent.</para> + </listitem> + <listitem><para>If you specify <option + role="hg-opt-backout">--merge</option> on the command + line, it merges with <literal>orig</literal>, and commits + the result of the merge.</para> + </listitem></orderedlist> + + <para>An alternative way to implement the <command + role="hg-cmd">hg backout</command> command would be to + <command role="hg-cmd">hg export</command> the + to-be-backed-out changeset as a diff, then use the <option + role="cmd-opt-patch">--reverse</option> option to the + <command>patch</command> command to reverse the effect of the + change without fiddling with the working directory. This + sounds much simpler, but it would not work nearly as + well.</para> + + <para>The reason that <command role="hg-cmd">hg + backout</command> does an update, a commit, a merge, and + another commit is to give the merge machinery the best chance + to do a good job when dealing with all the changes + <emphasis>between</emphasis> the change you're backing out and + the current tip.</para> + + <para>If you're backing out a changeset that's 100 revisions + back in your project's history, the chances that the + <command>patch</command> command will be able to apply a + reverse diff cleanly are not good, because intervening changes + are likely to have <quote>broken the context</quote> that + <command>patch</command> uses to determine whether it can + apply a patch (if this sounds like gibberish, see <xref + linkend="sec:mq:patch"/> for a + discussion of the <command>patch</command> command). Also, + Mercurial's merge machinery will handle files and directories + being renamed, permission changes, and modifications to binary + files, none of which <command>patch</command> can deal + with.</para> + + </sect2> + </sect1> + <sect1 id="sec:undo:aaaiiieee"> + <title>Changes that should never have been</title> + + <para>Most of the time, the <command role="hg-cmd">hg + backout</command> command is exactly what you need if you want + to undo the effects of a change. It leaves a permanent record + of exactly what you did, both when committing the original + changeset and when you cleaned up after it.</para> + + <para>On rare occasions, though, you may find that you've + committed a change that really should not be present in the + repository at all. For example, it would be very unusual, and + usually considered a mistake, to commit a software project's + object files as well as its source files. Object files have + almost no intrinsic value, and they're <emphasis>big</emphasis>, + so they increase the size of the repository and the amount of + time it takes to clone or pull changes.</para> + + <para>Before I discuss the options that you have if you commit a + <quote>brown paper bag</quote> change (the kind that's so bad + that you want to pull a brown paper bag over your head), let me + first discuss some approaches that probably won't work.</para> + + <para>Since Mercurial treats history as accumulative&emdash;every + change builds on top of all changes that preceded it&emdash;you + generally can't just make disastrous changes disappear. The one + exception is when you've just committed a change, and it hasn't + been pushed or pulled into another repository. That's when you + can safely use the <command role="hg-cmd">hg rollback</command> + command, as I detailed in section <xref + linkend="sec:undo:rollback"/>.</para> + + <para>After you've pushed a bad change to another repository, you + <emphasis>could</emphasis> still use <command role="hg-cmd">hg + rollback</command> to make your local copy of the change + disappear, but it won't have the consequences you want. The + change will still be present in the remote repository, so it + will reappear in your local repository the next time you + pull.</para> + + <para>If a situation like this arises, and you know which + repositories your bad change has propagated into, you can + <emphasis>try</emphasis> to get rid of the changeefrom + <emphasis>every</emphasis> one of those repositories. This is, + of course, not a satisfactory solution: if you miss even a + single repository while you're expunging, the change is still + <quote>in the wild</quote>, and could propagate further.</para> + + <para>If you've committed one or more changes + <emphasis>after</emphasis> the change that you'd like to see + disappear, your options are further reduced. Mercurial doesn't + provide a way to <quote>punch a hole</quote> in history, leaving + changesets intact.</para> + + <para>XXX This needs filling out. The + <literal>hg-replay</literal> script in the + <literal>examples</literal> directory works, but doesn't handle + merge changesets. Kind of an important omission.</para> + + <sect2> + <title>Protect yourself from <quote>escaped</quote> + changes</title> + + <para>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.</para> + + <para>By configuring some hooks on that repository to validate + incoming changesets (see chapter <xref linkend="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 <quote>die out</quote> because they can't + propagate into the central repository. Better yet, this + happens without any need for explicit intervention.</para> + + <para>For instance, an incoming change hook that verifies that a + changeset will actually compile can prevent people from + inadvertantly <quote>breaking the build</quote>.</para> + + </sect2> + </sect1> + <sect1 id="sec:undo:bisect"> + <title>Finding the source of a bug</title> + + <para>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 <command role="hg-cmd">hg bisect</command>, that + helps you to automate this process and accomplish it very + efficiently.</para> + + <para>The idea behind the <command role="hg-cmd">hg + bisect</command> 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 <command role="hg-cmd">hg bisect</command> command uses your + test to direct its search for the changeset that introduced the + code that caused the bug.</para> + + <para>Here are a few scenarios to help you understand how you + might apply this command.</para> + <itemizedlist> + <listitem><para>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.</para> + </listitem> + <listitem><para>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.</para> + </listitem> + <listitem><para>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 <quote>fast</quote> or + <quote>slow</quote>.</para> + </listitem> + <listitem><para>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.</para> + </listitem></itemizedlist> + + <para>From these examples, it should be clear that the <command + role="hg-cmd">hg bisect</command> command is not useful only + for finding the sources of bugs. You can use it to find any + <quote>emergent property</quote> 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.</para> + + <para>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 + <emphasis>test</emphasis> is something that + <emphasis>you</emphasis> run when <command role="hg-cmd">hg + bisect</command> chooses a changeset. A + <emphasis>probe</emphasis> is what <command role="hg-cmd">hg + bisect</command> runs to tell whether a revision is good. + Finally, we'll use the word <quote>bisect</quote>, as both a + noun and a verb, to stand in for the phrase <quote>search using + the <command role="hg-cmd">hg bisect</command> + command</quote>.</para> + + <para>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 <emphasis>days</emphasis> 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.</para> + + <para>What the <command role="hg-cmd">hg bisect</command> command + does is use its knowledge of the <quote>shape</quote> of your + project's revision history to perform a search in time + proportional to the <emphasis>logarithm</emphasis> 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).</para> + + <para>The <command role="hg-cmd">hg bisect</command> command is + aware of the <quote>branchy</quote> nature of a Mercurial + project's revision history, so it has no problems dealing with + branches, merges, or multiple heads in a repository. It can + prune entire branches of history with a single probe, which is + how it operates so efficiently.</para> + + <sect2> + <title>Using the <command role="hg-cmd">hg bisect</command> + command</title> + + <para>Here's an example of <command role="hg-cmd">hg + bisect</command> in action.</para> + + <note> + <para> In versions 0.9.5 and earlier of Mercurial, <command + role="hg-cmd">hg bisect</command> was not a core command: + it was distributed with Mercurial as an extension. This + section describes the built-in command, not the old + extension.</para> + </note> + + <para>Now let's create a repository, so that we can try out the + <command role="hg-cmd">hg bisect</command> 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 <quote>bug</quote>. This loop creates 35 + changesets, each adding a single file to the repository. + We'll represent our <quote>bug</quote> with a file that + contains the text <quote>i have a gub</quote>. <!-- + &interaction.bisect.commits; --></para> + + <para>The next thing that we'd like to do is figure out how to + use the <command role="hg-cmd">hg bisect</command> command. + We can use Mercurial's normal built-in help mechanism for + this. <!-- &interaction.bisect.help; --></para> + + <para>The <command role="hg-cmd">hg bisect</command> command + works in steps. Each step proceeds as follows.</para> + <orderedlist> + <listitem><para>You run your binary test.</para> + <itemizedlist> + <listitem><para>If the test succeeded, you tell <command + role="hg-cmd">hg bisect</command> by running the + <command role="hg-cmd">hg bisect good</command> + command.</para> + </listitem> + <listitem><para>If it failed, run the <command + role="hg-cmd">hg bisect bad</command> + command.</para></listitem></itemizedlist> + </listitem> + <listitem><para>The command uses your information to decide + which changeset to test next.</para> + </listitem> + <listitem><para>It updates the working directory to that + changeset, and the process begins again.</para> + </listitem></orderedlist> + <para>The process ends when <command role="hg-cmd">hg + bisect</command> identifies a unique changeset that marks + the point where your test transitioned from + <quote>succeeding</quote> to <quote>failing</quote>.</para> + + <para>To start the search, we must run the <command + role="hg-cmd">hg bisect --reset</command> command. <!-- + &interaction.bisect.search.init; --></para> + + <para>In our case, the binary test we use is simple: we check to + see if any file in the repository contains the string <quote>i + have a gub</quote>. If it does, this changeset contains the + change that <quote>caused the bug</quote>. By convention, a + changeset that has the property we're searching for is + <quote>bad</quote>, while one that doesn't is + <quote>good</quote>.</para> + + <para>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 + <quote>bad</quote>. <!-- &interaction.bisect.search.bad-init; + --></para> + + <para>Our next task is to nominate a changeset that we know + <emphasis>doesn't</emphasis> have the bug; the <command + role="hg-cmd">hg bisect</command> command will + <quote>bracket</quote> 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 <quote>good</quote> changeset later.) <!-- + &interaction.bisect.search.good-init; --></para> + + <para>Notice that this command printed some output.</para> + <itemizedlist> + <listitem><para>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.</para> + </listitem> + <listitem><para>It updated the working directory to the next + changeset to test, and told us which changeset it's + testing.</para> + </listitem></itemizedlist> + + <para>We now run our test in the working directory. We use the + <command>grep</command> command to see if our + <quote>bad</quote> file is present in the working directory. + If it is, this revision is bad; if not, this revision is good. + <!-- &interaction.bisect.search.step1; --></para> + + <para>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, + <literal>mytest</literal>. <!-- + &interaction.bisect.search.step2; --> A few more invocations + of our canned test step command, and we're done. <!-- + &interaction.bisect.search.rest; --></para> + + <para>Even though we had 40 changesets to search through, the + <command role="hg-cmd">hg bisect</command> command let us find + the changeset that introduced our <quote>bug</quote> with only + five tests. Because the number of tests that the <command + role="hg-cmd">hg bisect</command> command performs grows + logarithmically with the number of changesets to search, the + advantage that it has over the <quote>brute force</quote> + search approach increases with every changeset you add.</para> + + </sect2> + <sect2> + <title>Cleaning up after your search</title> + + <para>When you're finished using the <command role="hg-cmd">hg + bisect</command> command in a repository, you can use the + <command role="hg-cmd">hg bisect reset</command> 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, <command + role="hg-cmd">hg bisect</command> won't let you start a new + search in that repository until you do a <command + role="hg-cmd">hg bisect reset</command>. <!-- + &interaction.bisect.search.reset; --></para> + + </sect2> + </sect1> + <sect1> + <title>Tips for finding bugs effectively</title> + + <sect2> + <title>Give consistent input</title> + + <para>The <command role="hg-cmd">hg bisect</command> 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 <emphasis>might</emphasis> 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.</para> + + </sect2> + <sect2> + <title>Automate as much as possible</title> + + <para>When I started using the <command role="hg-cmd">hg + bisect</command> 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.</para> + + <para>My initial problems with driving the <command + role="hg-cmd">hg bisect</command> 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 <command role="hg-cmd">hg bisect</command> 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.</para> + + <para>The key to automated testing is twofold:</para> + <itemizedlist> + <listitem><para>always test for the same symptom, and</para> + </listitem> + <listitem><para>always feed consistent input to the <command + role="hg-cmd">hg bisect</command> command.</para> + </listitem></itemizedlist> + <para>In my tutorial example above, the <command>grep</command> + command tests for the symptom, and the <literal>if</literal> + statement takes the result of this check and ensures that we + always feed the same input to the <command role="hg-cmd">hg + bisect</command> command. The <literal>mytest</literal> + function marries these together in a reproducible way, so that + every test is uniform and consistent.</para> + + </sect2> + <sect2> + <title>Check your results</title> + + <para>Because the output of a <command role="hg-cmd">hg + bisect</command> 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:</para> + <itemizedlist> + <listitem><para>The changeset that it reports as the first bad + revision. Your test should still report this as + bad.</para> + </listitem> + <listitem><para>The parent of that changeset (either parent, + if it's a merge). Your test should report this changeset + as good.</para> + </listitem> + <listitem><para>A child of that changeset. Your test should + report this changeset as bad.</para> + </listitem></itemizedlist> + + </sect2> + <sect2> + <title>Beware interference between bugs</title> + + <para>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.</para> + + <para>It is possible that this other bug completely + <quote>masks</quote> 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 <command role="hg-cmd">hg + bisect</command> command cannot help you directly. Instead, + you can mark a changeset as untested by running <command + role="hg-cmd">hg bisect --skip</command>.</para> + + <para>A different problem could arise if your test for a bug's + presence is not specific enough. If you check for <quote>my + program crashes</quote>, then both your crashing bug and an + unrelated crashing bug that masks it will look like the same + thing, and mislead <command role="hg-cmd">hg + bisect</command>.</para> + + <para>Another useful situation in which to use <command + role="hg-cmd">hg bisect --skip</command> 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.</para> + + </sect2> + <sect2> + <title>Bracket your search lazily</title> + + <para>Choosing the first <quote>good</quote> and + <quote>bad</quote> changesets that will mark the end points of + your search is often easy, but it bears a little discussion + nevertheless. From the perspective of <command + role="hg-cmd">hg bisect</command>, the <quote>newest</quote> + changeset is conventionally <quote>bad</quote>, and the older + changeset is <quote>good</quote>.</para> + + <para>If you're having trouble remembering when a suitable + <quote>good</quote> change was, so that you can tell <command + role="hg-cmd">hg bisect</command>, 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).</para> + + <para>Even if you end up <quote>early</quote> by thousands of + changesets or months of history, you will only add a handful + of tests to the total number that <command role="hg-cmd">hg + bisect</command> must perform, thanks to its logarithmic + behaviour.</para> + + </sect2> + </sect1> +</chapter> + +<!-- +local variables: +sgml-parent-document: ("00book.xml" "book" "chapter") +end: +-->