How to modify history in TortoiseHg

Risks

Firstly there is no risk to modifying history on commits that you have not pushed. The risk with modifying history that you have pushed is if someone else has already pulled the commits down that you plan to modify. Take for example this:

If another developer were to modify the “Add workspace to app” commit and you pulled down that change, this is what it would look like on your local repository:

The reason this happens is because commits are identified by their hash number (see identifying commits for more details). So because the “Add workspace to app” commit was modified it now has a different hash (including all descendant commits since they’re modified too to update the parent hash reference since it’s changed), so it will appear as a different commit in the commit history alongside the original commit.

To fix you would have to strip the obsolete commit. If you have local changes not yet pushed like in this example you would first have to move them over to the new commit using rebase before stripping the obsolete commits. This is something every developer has to do if they’ve pulled down the original commit. This might be easy to communicate if you’re working within a small team but becomes much harder to communicate if it’s a large team or an open source project online. It can also cause issues with automated test build systems that keeps the repository and just updates to the latest revision between builds. So in general it is recommended not to modify history that has become public unless you know it’ll have no impact. See changeset evolution on how this might not be such a problem in the future. If you want to undo a commit alternatively you can right click the commit and select Backout instead which will create a new commit that un-does the changes without modifying history.

Phases

Now in the commit history you will notice there is a column called Phase. There are three values this can be:

  • Public - this commit has been pushed to a public repository (commits pushed to repos marked non-publishing remain in draft)
  • Draft - this commit has not yet been pushed to a public repository
  • Secret - this commit will not be pushed, pulled or cloned

The idea of the phases is so you know whether it’s safe to modify the commit or not. By default you cannot modify a commit that is public until you change it back to draft by right clicking the commit and selecting Change Phase to > draft. Secret is helpful when you are working on changes locally but don’t want to accidently push the commit when pushing other commits.

Append

Append is a quick way to allow you to modify the most recent commit. To do this you click the down arrow to the right of the commit button and select Ammend current revision:

Append allows you to do things like modify the commit message, or add/remove/modify files. But it doesn’t allow you to undo removing or adding a file from the commit, for that see MQ. Note a backup of the original commit is kept under .hg/strip-backup.

Strip

To use strip you will need to enable the strip extension. Strip allows you to remove commits from your repository. Let’s take this example:

Let’s say you wanted to remove the “Renamed help file, add service” commit and descendant. Just right click on the “Renamed help file, add service” commit then select Modify history > Strip:

To remove only the selected commit but keep the decendant commits see MQ. Note a backup of the commits you removed is kept under .hg/strip-backup. These files can be restored by going to View > Synchronize then clicking the Unbundle icon and selecting the file under the strip-backup folder. Note in order to remove commits on a remote repository the strip command will need to be run on that repository, you cannot do this through push. Services like Bitbucket add an option in settings that allow you to specify a commit that you want to strip.

Rebase

To use rebase you will need to enable the rebase extension. Rebase allows you to move one or more commits to the head of another branch. Lets take the example that you have local commits and you’ve just done a pull and find someone else has already done a commit so that you now have two heads:

First update to the commit you want to move your commit to:

Then right click on the commit you want to move and select Modify history > Rebase. This will change the parent of the selected commit to the commit you are currently updated to (which doesn’t have to be the tip).

You can also use rebase to fold commits, just update to the parent commit then rebase the child commit to the same location but select the collapse the rebased changesets option. This collapses all descendant commits, if you want control over which commits to collapse see MQ.

MQ

To use mq you will need to enable the mq extension. MQ stands for mercurial queues and provides a way to import commits into a queue (turns them into patches) where you can make changes and then reapply the patches. Unlike the Append and Strip commands MQ does not provide a backup of the original commits, so if you want a backup before starting you can just clone your local repository.

Import commits into a queue

Select the commit you would like to modify then right click and select Modify history > Import to MQ.

Here you can see I imported the “Add unit tests” commit so it and it’s descendants now have patches. The filenames of the patches, e.g. 2.diff, have been added as tags to the commits.

Refresh a patch

To change a commit double click it (this either applies the patch or unapplies the one after which can also be done by right clicking and accessing the option from the Modify history menu). If the commit is selected, you should now see that the commit button says QRefresh, in this example I’m modifying the “Add unit tests” commit.

You can now make changes to the files, here you can untick a file to undo add/delete from the commit unlike Append. Once you have finished click the QRefresh button.

Delete a patch

To delete a patch first make sure it’s unapplied (by double clicking the commit before it) then right click and select Delete patches. This allows you to delete a commit in the middle of the history unlike strip.

Create a new patch

To create a new patch double click the commit you want to appear before. Then make your file changes and then select the working directory in commit history and click QNew just like a normal commit.

Change patch order

To change patch order, first unapply all the patches you want to change order, then you can drag and drop the patches in the order you want them.

Fold patches

To fold patches double click the first commit to be folded (in this example it’s the “Add unit tests” commit). Then select the commits you would like to merge into it then right click and select Fold patches.

Finish patch

Once you have finished double click the last commit, in order to apply all the patches, then you can right click the last commit and choose Modify history > Finish Patch, then they will no longer be in the queue.

Create a new queue

You can view the queue of patches bottom left of TortoiseHg by selecting View > Show Patch Queue. Here you can create a new queue and use the dropdown to switch between queues.

The commit to queue actually commits the patches to a new repository created in the .hg/queue-name folder.

HistEdit

To use histedit you need to enable the histedit extension. Histedit provides similar capabilities to MQ in what you can do to alter history but without the patches and queues. There is no GUI support for histedit so you run it from the console which you can view within TortoiseHg by going to View > Show Console. See below for details on how to use this extension.

http://mercurial.selenic.com/wiki/HisteditExtension

Changeset Evolution

The risks section explained the dangers of rewriting public history. The idea behind the changeset evolution set of extensions, is instead of stripping the original commit from history, it adds an obsolescence marker to the commit along with a reference to the commit that supersedes it. Then when other developers pull down the changes the original commits will now be marked obselete and therefore hidden, and their local changes can be automatically moved to the new commit that superseded it. Note this supports append, rebase and histedit but not mq. For more details see:

http://mercurial.selenic.com/wiki/ChangesetEvolution