このページは EditingHistory の翻訳です。

改変履歴

リポジトリの履歴を改変する方法。

1. 履歴を改変する別の方法

チェンジセットを一つ前に戻したいときは、変更履歴が記録されていることを気にしなければ、チェンジセットを元に戻す hg backout コマンドを使うことができます。変更内容を失う可能性が無く、他の開発者は同じミスを避けるために履歴を使用することができるので、このコマンドは履歴の改変より一般的に好まれます。

2. なぜ履歴の改変は難しいのか

第一に、チェンジセットについて考察してみましょう。それぞれのチェンジセットは、チェンジセットのデータの暗号的に強力なハッシュ値であるチェンジセットIDを持っています。チェンジセットIDは、その親のIDと同様に、全てのチェンジセットの内容(データとメタデータ)を再帰的に含んでいます。チェンジセットそれ自体やベースとなった履歴を何か変更すると、そのIDも変更することになります(?)。Mercurialはこのようなチェンジセットの改竄を防ぎます(?)。あるチェンジセットに対応するチェンジセットIDを持つ、改竄されたチェンジセットを作成することは、コンピューター処理的に不可能なのです。

第二に、Mercurialのネットワーク・プロトコルは、履歴がappend-only(追加のみ)であることを前提にしています。pushやpullを行うと毎回リポジトリに履歴を追加し、履歴を削除することはありません。変更したい履歴が誰でもアクセスできるリポジトリに既に公開されている場合、リポジトリをpullした人全員に協力してもらうという一般的ではない方法を除いて、履歴を変更する方法がありません。

3. 改変履歴の流れ

リポジトリの履歴を改変する場合、複数のチェンジセットID(すなわちチェンジセットの識別子)が改変の時点以降まで変更されます。例えば、まずい(BAD)リビジョンが3番だったとします。すると、改変前は、リポジトリは下図のようになります:

改変後、改変されたBAD、R4、R5は新しいチェンジセットIDを持つようになります:

改変前に誰もあなたのリポジトリを参照していない限りは、何の問題もありません。しかし、他の人たちがあなたのリポジトリを改変前にpullしていた場合、話はより複雑になります。ある人があなたのリポジトリを、まずい(BAD)リビジョンの改変前にクローンし、その後にpullしたとします。ある人がBADa、R4a 、R5aという新しいチェンジセットを全て見ることができるなら、下図のようにチェンジセットのツリーが見えるでしょう。:

これは当然のことです。他のリポジトリから変更をpullしたとき、Mercurialはいつも次のように動作します。それは、二つの方向を持つacyclicなグラフができ(一方の方向はあなたのリポジトリを表し、他方の方向はあなたがpullしたリポジトリを表します)、そして新しいacyclicなグラフを作るためにグラフ上の共通のノードをマージする、という動作です。

R4、R4a、R5、R5aは同一の変更です(あなたはいずれも変更してません)が、これらは(BADaにより)異なる履歴を持つため、異なるハッシュ値を持つことに注意してください。

さて、あなたは友人に単にBADを取り除くよう頼むことができます。その要請はR4とR5も同様に取り除くものであり、この要請による変更はR4aとR5aとして保存されます。この要請によって、友人のリポジトリはあなたのリポジトリと同一になります。:

さらに、もしも友人がトップのR5に新しい仕事を既にコミットしていた場合を考えると、友人のリポジトリはpull前に以下のようになります。:

友人がpullした後は以下のようになります。

この時点では、R6とR7での仕事を破棄してしまうため、BADを取り除くだけは単に十分ではありません。しかし、R6とR7をMQにimportし、そのキューをpopし、BADを取り除いてキューを再度(この時点ではR5aへ)適用するよう、あなたは友人に頼むことができます。これは、R5上の友人の変更をrebaseすることです。:

 hg qimport -r R6:R7
 hg qpop -a
 hg strip BAD
 hg update -C R5a # might not be necessary
 hg qpush -a
 hg qdelete -r qbase:qtip

4. Motivation

Having said all of this, there are good reasons why people might want to change history. Here are some examples:

As long as you understand the implications, it is possible to do this.

The key implication is that the changeset IDs will change from the point at which the revision occurs. This means that developers with clones will need to rebase their changes, and care must be taken to manage the effects of the revision. This document does not attempt to cover this process. It assumes that if you need to do this, you will ensure that you know what to do, and you make sure it happens.

5. Basic changeset removal with clone

One of the simplest tasks is removing the most recent commits in a repository. This can be done non-destructively with clone:

hg clone -r LASTGOODREVISION oldrepo newrepo

- and then perhaps move oldrepo away as a backup and rename newrepo to take its place. If the old repository have multiple heads you might want to pull them too.

6. Editing recent history with MQ

Recent history can be modified fairly easily with the MQ extension:

Some caveats exist. First, MQ can't operate on merge changesets. Second, by default MQ works with textual changes. If the history you edit contains binary files, permission changes or other non-textual changes, enable extended diffs for your repo. Add the following section to your hgrc:

[diff]
git = True

(alternatively remember to add --git to every qimport and qrefresh invocation below).

Let's pretend you comitted the file which should not be commited in revision BAD. Then doing

hg qimport -r BAD:tip

will import the changesets into MQ. You can find newly created patches in .hg/patches.

Those patches are nevertheless still applied, to strip them from the history, you need the qpop command. Issue

hg qpop -a

Now all changes since revision BAD are no longer available in your repository history. They are saved as patches in .hg/patches - and only there.

If you want to undo the entire changeset BAD (obliterate it!), then do this:

 hg qdelete BAD.diff

If you only want to edit the changeset (remove some edits, avoid commiting one file while leaving the remaining changes etc), then do this:

 hg qpush BAD.diff
 # edit files, remove passwords, revert newly added files etc.
 hg qrefresh

Note that qdelete removes whole patch, while qrefresh modifies it.

Now if you want to combine the next two patches (say GOOD1.diff and GOOD2.diff) into one patch, do this:

 hg qpush GOOD1.diff
 hg qfold GOOD2.diff

The changes from GOOD2.diff have been integrated ("folded") into GOOD1.diff and the GOOD2.diff patch itself has been deleted from the queue (it can be kept there but unmanaged, by using qfold -k).

To go back to standard Mercurial changesets you do

 hg qpush -a
 hg qfinish -a

The qfinish command turns an applied patch into a real Mercurial changeset. Here we use it to turn all applied patches into normal changesets.

One thing this process over-simplifies is that the hg qpush -a step may fail, if later changes depend on the obliterated data. In that case, you have to fix the problem manually - there's no easy answer here, after you edit history, you need to manage the consequences, in your own repository as well as elsewhere. So you need to push conflicting patches one by one, edit them as appropriate, and qrefresh the changes. Read chapter about Mercurial Queues from Mercurial Book for details.

Here's a real example (captured on Windows). I add a file I shouldn't in revision 1, edit it in revision 2, then try to obliterate revision 1. After doing so, I need to resolve the issue with revision 2 (editing what is now a nonexistent file). In this case it's easy, we just drop revision 2 as well. In other cases, this could be much harder to deal with, possibly even so much harder that you decide it's not worth it. It depends on your data (and possibly your lawyers!).

>hg init
>echo Line 1 >a
>hg commit --addremove -m "Added a"
adding a

>rem Add the launch codes for the nuclear arsenal here...
>echo Super secret >b
>hg commit --addremove -m "Added b"
adding b

>hg log
changeset:   1:65bcb0d3f953
tag:         tip
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:43:00 2008 +0000
summary:     Added b

changeset:   0:5dd6949828e1
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:42:40 2008 +0000
summary:     Added a

>rem Here we compound the error...
>echo More secret stuff >>b
>hg commit -m "Edited b"

>echo More safe stuff >>a
>hg commit -m "Edited a"

>hg log
changeset:   3:ea4f8ad48048
tag:         tip
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:43:46 2008 +0000
summary:     Edited a

changeset:   2:6bb0d654a0a6
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:43:32 2008 +0000
summary:     Edited b

changeset:   1:65bcb0d3f953
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:43:00 2008 +0000
summary:     Added b

changeset:   0:5dd6949828e1
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:42:40 2008 +0000
summary:     Added a

>rem We realise our mistake. We need to get rid of changeset 1,
>rem so that file b is no longer in out repository!
>hg qinit
>hg qimport -r 1:tip
>hg qpop -a
Patch queue now empty

>rem Delete changeset 1
>hg qdelete 1.diff

>rem Now start to put everything back
>hg qpush -a
applying 2.diff
unable to find 'b' for patching
1 out of 1 hunk FAILED -- saving rejects to file b.rej
patch failed, unable to continue (try -v)
b: No such file or directory
b not tracked!
patch failed, rejects left in working dir
Errors during apply, please fix and refresh 2.diff

>rem Hmm, change 2 depends on file b. Fix things up. Luckily, this is easy, just delete change 2 as well.
>rem In reality, change 2 may contain other edits, and we'd need to do some further fixing.
>hg qdelete 2.diff
abort: cannot delete applied patch 2.diff

>rem Even this isn't as simple as all that. Back out change 2 so we can delete it.
>hg qpop -a
Patch queue now empty

>hg qdelete 2.diff

>rem And now we're good to go.
>hg qpush -a
applying 3.diff
Now at: 3.diff

>hg qdelete -r qbase:qtip

>rem No sign of file b, and the world is safe again.
>rem Except, of course, that evil Doctor Death pulled from us 5 minutes ago.
>rem But at least as we all get blown up, we can be glad that it's not a technical problem :-)
>hg log
changeset:   1:a50e33884959
tag:         tip
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:43:46 2008 +0000
summary:     Edited a

changeset:   0:5dd6949828e1
user:        "Paul Moore <user@example.com>"
date:        Sat Mar 22 16:42:40 2008 +0000
summary:     Added a

The process of going up through the patch stack, tidying up the debris (as in our example, where change 2 wouldn't apply as it depended on the obliterated file "b"), is what is generally referred to as "rebasing" the changes. It can be simple, in the case of a localised change, but it can be arbitrarily complex. Before you start editing history, you need to be sure that you know what to do to rebase.

7. Other options

There are other options that may be more appropriate in particular circumstances.

8. See also


CategoryHowTo