2018年12月30日日曜日

3: Gitでファイル変更日時を格納・復元する方法

<このシリーズの前の記事 | このシリーズの目次 |

Gitがファイル変更日時を格納/復元しないというのは、特殊な趣味に基づいた決定であり、その趣味を誰もが共有しなければならないわけではありません。

話題


About: Git

この記事の目次


開始コンテキスト


  • 読者は、Gitの全体像の知識を持っている。
  • 読者は、Gitのいくつかの基本操作(ファイル群をステージングする、ファイル群をステージング解除する、ファイル群を削除する、変更群をコミットする、コミットまたはファイル群をチェックアウトする)の知識を持っている。

ターゲットコンテキスト



  • 読者は、ファイル変更日時をGitレポジトリに格納する方法およびチェックアウトされたファイル群にファイル変更日時を復元する方法を理解する。

オリエンテーション


Hypothesizer 7
Gitは、ファイル変更日時を格納しない(コミット日時は、格納されるが、ファイル変更日時の代替にはならない、少なくとも私にとっては)。ふーむ...

「なぜGitはファイル変更日時を維持しないか」への答えであると主張するドキュメントがあり、そのドキュメント内の「維持」は、'チェックアウトされたファイル群に復元する'ことを意味すると思われる。しかし、私は、「なぜGitはチェックアウトされたファイル群にファイル変更日時を復元しないか」を尋ねているのではなく(少なくとも当座は(後には尋ねる))、「なぜGitはファイル変更日時をレポジトリに格納しないか」を尋ねているのだ。

ファイル変更日時は管理上の一部の目的に役立つ情報であって、私は、断じて、それらがレポジトリに記録されてほしい。それらをレポジトリで参照できればよいわけだ。実際、チェックアウトされたファイル群にファイル変更日時を復元しないということは、ファイル変更日時をレポジトリに格納しないことを、必要としないし、正当化もしない。

結局のところ、Gitがファイル変更日時を格納しないのは、前掲のドキュメントで述べられている懸念のためではなく、コンパイルされるべきファイル群をビルドツールが特定するためだけにファイル変更日時が存在するとする特殊な趣味のためであり、その趣味を私は共有しないし、共有するよう強いられるべき理由を認めもしない。

ファイル変更日時を復元することについても、私は前掲ドキュメントに賛成しない。私があるブランチをチェックアウトする場合、どのみち私はプロジェクトを間違いなくcleanする、なぜなら、そうしないと、チェックアウトされたブランチ内に対応するソースファイルがない、不要で問題を引き起こす可能性のある派生ファイル群が、ごみとして残ってしまうかもしれないから。そうした派生ファイル群が問題を引き起こす可能性があるのは、そうした幻の派生ファイル群を使用してもエラーとして検知されないし、ごみがJarファイルに含まれてしまうから(申し訳ない、私の第一プログラミング言語はJavaだ)。ブランチをチェックアウトすることは、プロジェクトの大きく異なっているかもしれないバージョンに対することになることを意味するので、プロジェクトをcleanするのがリーズナブルだ、私の意見では。

他方では、ファイル群チェックアウトでは事情が違い(コミットチェックアウトとファイル群チェックアウトの違いはこの記事で論じられている)、ファイル変更日時を復元しないことは意味をなす(少なくとも一定程度)(確かに、数個のソースファイルを置換しただけのために、私はプロジェクトをcleanしたくない)。プロジェクトの同一バージョンに対したままなので、プロジェクトをcleanしないのがリーズナブルだ。

したがって、ファイル群チェックアウトでファイル変更日時を復元しないのは構わないが(チェックアウトされたファイル群はチェックアウトに際して変更を受けたのであり、変更後の内容が、レポジトリに登録された一部のファイル群の内容に一致しただけであると自然に考えることができる)、コミットチェックアウトでは、ファイル変更日時が復元されてほしいと思う(チェックアウトされたブランチの全てのファイルがチェックアウトに際して変更されたと考えるのは不自然だ)。

そもそも、Gitは、コンパイラープログラミング言語プロジェクトのためだけのものなのだろうか?前掲ドキュメントに述べられている懸念は、非コンパイラープログラミング言語プロジェクトやドキュメントファイル群レポジトリには全然関係ない。

Gitは元々、ある特定のプロジェクトのために開発されたと聞いており、確かに、私は、自分がメンバーでもないある単一のプロジェクトのためだけに存在するソフトウェアを批判する立場にない(その決定は、そのプロジェクトの多数派メンバーの趣味または独裁者の趣味に適っていたのだろう)。もし、その特定プロジェクトのためだけのものであり続けることをGitが選択するのであれば、私たち、外部者は、それを放っておいて、一般のニーズに耳を傾ける別のソフトウェアを使おうということになる。

そこで、私は、別のバージョン管理システムを採用するか、Gitの動作に調整を加えるか(可能であれば)、考えを巡らせたが、後者を試してみることにした、まずは。


本体


1: 'Metastore'があるが...


Hypothesizer 7
'Metastore'と呼ばれるソフトウェアがあることを私は発見したが、それが私の関心事にぴたりと応えるものでないことも発見した: それは、レポジトリに登録されたファイル群の変更日時を格納するのではなく、ワーキングツリー内のファイル群の変更日時を格納する。

例えば、あるファイルがワーキングツリー内で変更されてステージングされない場合、次のコミットが実行される際に、'Metastore'はワーキングツリー内変更日時を格納する、その変更がレポジトリに反映されないにもかかわらず。

実際、'Metastore'は変更日時をコミット時に取得する(少なくとも、それに含まれているプリコミットフックを使用する使用法においては)が、それでは遅すぎる。なぜなら、ステージングされたバージョンの変更日時は、ステージング後、いつでも失われる可能性があるから。

また、ポストチェックアウトフックで、登録された変更日時を全てワーキングツリー内ファイル群に単純に復元するのは、複雑なチェックアウト動作がゆえに正確な結果を実現しない(ワーキングツリー内のキャリーオーバーされたファイルたちの変更日時はそのままにしておかなければならない)。

さらに、メタデータファイルがバイナリフォーマットであることは、複数のレポジトリからの'プッシュ'からの競合を解決するのに問題となるようだ(私個人としてその問題をまだ経験してはいないのだが)。


2: おおまかなアイデア


Hypothesizer 7
しかし、その基本的アイデアは使える、というより、別のものを私は考えつかない: 当該ファイル群(当該コミットに属するファイル群)の変更日時をあるファイル(それを私は、'ファイルメタデータバンドルファイル'と呼ぶ)に格納し、このファイルをレポジトリに当該コミットの一部として登録する(各コミットに1つのファイルメタデータバンドルファイルがあるということになる)。

ファイルの変更日時は、ファイルがステージングされるタイミングで記録されなければならない(前セクションで論じたとおり、'コミットが実行されるタイミング'では遅すぎる)。どうすれば、そうできるのか?

ステージングに対するフックというものは持てないが、ファイルがステージングされる際に呼ばれるフィルターは作れる。

そうしたフィルターを使うことを検討したが、その方法にはいくつか難点があることが分かった。第1に、そのフィルターは、'git add'コマンドが実行される際にのみ呼ばれるのではなく、他のよく理解できない(私には)機会(一部のコミットや一部のチェックアウト)にも呼ばれ、予期しない結果を引き起こしかねない。第2に、ファイル変更日時データは、ファイル群がステージングされた場合だけでなく、ファイル群がステージング解除されたり削除されたりした場合にもメンテナンスされなければならないが、その場合にフィルターは呼ばれない。

ふーむ...、結局、フックやフィルターでは駄目であって、'git'コマンドのラッパーを作成するしか選択肢がなさそうだ。


3: このラッパーは、'git'コマンドの可能な使用法全体の一部のみをカバーする


Hypothesizer 7
実のところ、'git'コマンドの可能な使用法全体をこのラッパーにカバーさせるつもりは私にはない。一部の使用法をラッパーにカバーさせるのはかなり骨が折れる(不可能ではないが)一方、そうした使用法を使用することに私は関心がないか、そうした使用法は不可欠ではないからだ。

そうした使用法には、'add'サブコマンドのインタラクティブモード('-i'スイッチ)が含まれる: 単純に言って、その必要性を私は全く感じない。

'add'および'reset'サブコマンドのパッチングモード('-p'スイッチまたは'--patch'スイッチ)は、もっと考慮を要求する。それらを私は必要とするだろうか?...ふーむ、それらは、ワーキングツリー内のファイルをまず編集することなく、ステージングエリア内のファイルを直接、編集するものであって、基本的に、私はそれをしない、なぜなら、私は、通常、ファイルをコミットする前に、ファイルをワーキングツリー内で検査する(例えば、プロジェクトをビルドしてプログラムをテストしたり、ドキュメントを校正したりして)必要性を感じるから。そもそも、なぜ私は、ステージングエリア内だけでファイルを編集したがるのだろうか?...たぶん、当該プロジェクトのスピンオフ版を私は作成したいのだろうが、それならば、私にとっては、ただマスターブランチにコミットを作成するだけでは用を成さない: 派生ブランチを私は欲するだろう。したがって、私はむしろ、マスターブランチをスタッシュし、派生ブランチを作成し、スタッシュを派生ブランチに適用し、ワーキングツリー内のファイルを編集し、変更を検査し、変更を派生ブランチにステージング・コミットし、マスターブランチに戻り、スタッシュをマスターブランチにポップするだろう、パッチング機能を使用することなしに。それが多くのステップを含んでいることは知っているが、派生ブランチを必要としているわけだから、ただ、ステージングエリアでファイルをパッチして、そのパッチをマスターブランチにコミットしても、用を足していない。パッチングが場合によって便利かもしれないことを特に否定しないが、それは、骨の折れる作業を経てまでその機能をラッパーにカバーさせようと私を動機づけるものではない。

他に含まれるものに、'HEAD'でないコミットを使用してファイルをリセットするというものがある: それも、ワーキングツリー内のファイルをまず編集することなく、ステージングエリア内のファイルを直接、編集するものであって、私は行なわない。実際、私は、'reset'を、ステージングをキャンセルする(ファイルをアンステージングする)(それは、場合によって、必要な操作だ)ためだけに使用する。

ファイル群チェックアウトのパッチングモード('checkout'サブコマンドの'-p'スイッチまたは'--patch'スイッチ)にはいくらかの魅力がある、確かに...、しかし、ラッパーはそれもカバーしない、なぜなら、ラッパーは実際にはファイル群チェックアウト'checkout'サブコマンドを呼ばない(ある後述セクションに記されているとおり)し、正直に言って、ラッパーにその機能をカバーさせるのは骨折りだからだ。

実のところ、私の視野にあるのは、'add'('-i'も'-p'や'--patch'もなしで)を使用してファイル群をステージングし、'reset'を使用してただステージングをキャンセルし('HEAD'の状態に戻す)、'rm'を使用してファイル群を削除し、'commit'を使用してステージング済み変更をコミットし、'checkout'('-p'も'--patch'もなしで)を使用して別のコミット(典型的にはブランチ)を操作するよう準備する(コミットチェックアウト)か別のコミットから一部のファイルをカレントコミットに取り込む(ファイル群チェックアウト)ことだけだ。


4: ファイルメタデータバンドルファイルのフォーマット


Hypothesizer 7
ファイルメタデータバンドルファイルのフォーマットを決定しよう。

ファイルメタデータバンドルファイル(各コミットが1つ持つ)は、拡張されたJSONファイルとする。「拡張されたJSONファイル」?...実は、私は個人的に、JSONフォーマットに日付、時刻、日時のタイプを追加した、というのも、それらは私には不可欠だから。

以下がファイルメタデータバンドルファイルのフォーマットだ。

[%コミット日時%, {%ファイルパス%: [%ステージングされたファイルの変更日時%, %登録されたファイルの変更日時%], . . .}]

そこにコミット日時があるのは、各ファイルメタデータバンドルファイルを他全てのファイルメタデータバンドルファイルと違えるためだ。登録されたファイルの変更日時が保持されているのは、ファイルがリセットされた際に、ステージングされたファイルの変更日時を元に戻すためだ。

当該ファイルがまだコミットされていなければ、'%登録されたファイルの変更日時%'は'null'になる。当該ファイルがコミットされだ後に削除されたが削除がまだコミットされていなければ、'%ステージングされたファイルの変更日時%'は'null'になる。両方が同時に'null'になることはない、なぜなら、そうした場合には、そのファイルはファイルメタデータバンドルファイルから取り除かれるから。


5: ファイル変更日時を格納するためにラッパーは何をしなければならないか


Hypothesizer 7
方針として、ラッパーは、ステージングエリアへのいかなる変更(いかなるファイルのいかなる追加、変更、削除)をも、変更が行われた時に記録しなければならない。そうした記録は、ファイルメタデータバンドルファイル内に行なわれ、ファイルメタデータバンドルファイルは、記録後、即座にステージングされる(そう、コミットが実行される時ではない、後記される理由のために)。

ワーキングツリーへの変更はこの際関係ない、なぜなら、登録されたファイル群の変更日時が私たちの関心事であって、ワーキングツリーへの変更が直接レポジトリに行くことはないから。

ステージングエリアでファイルはどのように変更されうるのか?

1つの明白は方法は、'git add'コマンド実行で指定されることであり、それは問題ない(ワーキングツリー内のファイル変更日時を取得し、それをそのファイルのステージングされたファイル変更日時として、ファイルメタデータバンドルファイル内に記録する)。

別の方法は、'git reset'コマンド実行で指定される(上述したとおり、'HEAD'の状態にリセットされることだけが考慮の対象となっている)ことであり、それは問題ない(ファイルメタデータバンドルファイル内で、もし当該ファイルの登録されたファイル変更日時が'null'でなければ、そのファイルの登録されたファイル変更日時値をそのファイルのステージングされたファイル変更日時スロットに入れ、もしそうでなければ、そのファイルのエントリーを削除する)。

別の方法は、'git rm'コマンド実行で指定されることであり、それは問題ない(ファイルメタデータバンドルファイル内で、もし当該ファイルの登録されたファイル変更日時が'null'でなければ、'null'をそのファイルのステージングされたファイル変更日時スロットに入れ、もしそうでなければ、そのファイルのエントリーを削除する)。

別の方法は、コミットチェックアウトによってキャリーオーバーされることであり(前記事の'A-1'、'A-2'、'A-3'、'M-7'、'M-8'、'M-9'、'M-10'、'R-6'、'R-7'、'R-8'を参照)、それは問題ある、なぜなら、それは、密かに起こりうる(前記事の'A-3'および'M-10'を参照)から。ふーむ、そのキャリーオーバー動作を私は全く欲しくないので、その発生をブロックしよう(方法は後で述べる)。

別の方法は、ファイル群チェックアウトによって自動的にステージングされる ことであり、それには対処可能だ(ファイル変更日時を復元するにしろ、しないにしろ)。しかし、むしろ、私には迷惑である自動ステージング自体をキャンセルできないだろうか?...ふーむ、ステージングエリアのファイルを'HEAD'状態にただリセットするのではうまくいかない、なぜなら、ある変更が既にステージングされた状態にあった可能性があり、その変更が失われてしまうから...。むしろ、ラッパー内で、ファイル群チェックアウト操作を'show'サブコマンド実行群に置き換えて、その結果群をファイル群にリダイレクトしよう(まず、複数である可能性のあるファイル群を特定し、各ファイルに対して'show'サブコマンドを実行しなければならないだろう)。

考慮しなければならない点は、ステージングエリア内で変更されたファイル群をいかに特定するかということだ。...'add'および'rm'サブコマンドについては、それは簡単だ、なぜなら、それらは、きちんと、そうしたファイル群をレポートする('add'サブコマンドでは'-v'スイッチを付け、'rm'サブコマンドでは'-q'スイッチも'--quiet'スイッチも付けないことによって)から。しかし、'reset'サブコマンドはきちんとしていない...。ふーむ、どうやら、'reset'サブコマンド実行に渡されたファイル群指定表現からファイル群を特定しなければならないないようだ。そうしたファイル群指定表現のフォーマットは'glob'に違いないと自然に(私は自然だと思う)考えたのだが、...そうでなく、正規表現フォーマットでもないことが判明した...。それでは、何なのか?...実は、それは、異様なGitオリジナルフォーマットであって、'aa*.txt'は、 'aaa.txt'や'aaa/aaa.txt'にマッチするが'bbb/aaa.txt'にはマッチせず、'b*a.txt'は、'bbb/aaa.txt'にマッチする(冗談抜きで?)。ふーむ...。その一方で、'checkout'サブコマンド実行のファイル群指定表現のフォーマットは別物だ: 'aa*.txt'は、'aaa.txt'にマッチするが'aaa/aaa.txt'にも'bbb/aaa.txt'にもマッチせず、'b*a.txt'は、'bbb/aaa.txt'にマッチせず、'*/aa*.txt'は何にもマッチせず、'aaa/aa*.txt'は、'aaa/aaa.txt'にマッチする。ふーむ...。ところで、'ls-tree'サブコマンドのファイル群指定表現のフォーマットは、ワイルドカードを全く受け付けない、マニュアルが、表現は「実際には生のパス名」ではなく、「むしろ、マッチさせるパターンのリスト」であると言っているにもかかわらず...。正直、私はGitが本当に嫌いになり始めた...。それはともかく、問題は、「私のラッパーは、そうしたばかげた(それはばかげていると私は謹んで述べさせてもらう)動作を踏襲すべきだろうか?」ということだ。...本当に、私は、'b*a.txt'を指定した時に、'bbb/aaa.txt'がリセットされてほしくない。...そこで、ラッパーは、そうしたばかげた動作をシンプルなglob動作に置き換える(ラッパーは、glob表現を受け付け、それらを展開し、展開されたファイルパス群を'git'コマンドに渡す)。


6: 問題のあるコミットチェックアウトをどうやってブロックするか?


Hypothesizer 7
実のところ、問題のあるコミットチェックアウトは、上述した処置によって既にブロックされている: ファイルメタデータバンドルファイルがそうしたチェックアウトにエラーを引き起こす。

実際、各ファイルメタデータバンドルファイルは、自身のコミット日時(実用上、各コミットは独自のコミット日時を持っていると想定できる)を持っているから、新カレントコミットのファイルメタデータバンドルファイルが前カレントコミットのファイルメタデータバンドルファイルと同一コンテンツを持っているということはありえず('前カレントコミット'および'新カレントコミット'で私が何を意味しているかを知るには、この記事を参照されたい)、そのため、もし、前カレントコミットのワーキングツリー内およびステージングエリア内でファイルメタデータバンドルファイルが変更されていれば、チェックアウトにエラーが起こる(前記事の'M-17'および'A-7'を参照されたい)。もし、そうでなければ、ファイル変更日時を格納するという目的において、チェックアウトがブロックされなくても問題がない、なぜなら、キャリーオーバーされる変更がステージングエリア内に無いから(ワーキングツリー内の変更はキャリーオーバーされる可能性があるが、それは、ファイル変更日時を格納するという目的において問題ない(ファイル変更日時を復元するという目的においては確かに問題ある))。ファイルメタデータバンドルファイルが変更後即座にステージングされなければならない理由は、コミットされたファイルが全然無い状態からチェックアウトが行われる場合に状況を'A-7'に合わせるためだ(ファイルメタデータバンドルファイルは、ステージングされていなければ、ただの追跡されていないファイルなので、チェックアウトをブロックしないだろう)。


7: コミットチェックアウトでファイル変更日時を復元するためにラッパーがしなければならないこと


Hypothesizer 7
コミットチェックアウトが行なわれた後はいつでも、私たちはファイル変更日時を復元したい。

新カレントコミット内のファイルメタデータバンドルファイルは、レポジトリからワーキングツリーへ取り出されているはずだ: 前カレントコミットからキャリーオーバーされたはずはない、なぜならそういう動作はブロックされているから。

しかしながら、そのメタデータファイルに登録されている全てのファイル変更日時をワーキングツリー内ファイル群に単純にセットするわけにはいかない、なぜなら、一部のファイルは前カレントコミットからキャリーオーバーされたものであるかもしれないから。実際、'M-6'および'R-5'はそうしたケースだ。

とにかく、キャリーオーバーされたファイル群はチェックアウトのメッセージから検知できるので、ラッパーはそうしたファイル群をそのままにしておくことができる。


8: ファイル群チェックアウトでファイル変更日時を復元する(そう希望した場合)ためにラッパーがしなければならないこと


Hypothesizer 7
既に述べたように、一般論としては、ファイル群チェックアウトでファイル変更日時が復元されなくても、私は構わない。しかし、それらを復元するというオプションがあっても、私は構わない。

基本的には、それは単純だ: 指定されたコミットからファイルメタデータバンドルファイルを取得して(チェックアウトするのではない)、そのファイルメタデータバンドルファイル内に登録されている変更日時をチェックアウトされたファイル群にセットする。


9: 結びとその先


Hypothesizer 7
これで、ファイル変更日時をGitレポジトリに格納し、それらをチェックアウトされたファイル群に復元する方法を私は理解したようだ: 'git'コマンドのラッパー(上述したことを行なう)を作成する必要がある。

...それで終わり?...そのラッパーはどこにあるわけ?...実は、制作中であり、将来の記事で公開されるだろう。


参考資料


  • Unknown. (2013/03/09). Git FAQ - Git SCM Wiki. Retrieved from https://git.wiki.kernel.org/index.php/GitFaq#Why_isn.27t_Git_preserving_modification_time_on_files.3F
  • Przemoc. (2018/01/06). Przemoc's software. Retrieved from http://software.przemoc.net/#metastore
<このシリーズの前の記事 | このシリーズの目次 |