コミット履歴が失われました

開発者から連絡があったのはもう夕方でした。マスターブランチにパッチがありません-デッドビーフコミット。







私は証拠を見せられました:2つのコマンドの出力。最初のものは



 git show deadbeef 
      
      





-ファイルへの変更を示しました。Page.phpと呼びましょう。canBeEditedメソッドとその使用法が追加されました。



そして2番目のコマンドの出力で-



 git log -p Page.php 
      
      





-デッドビーフのコミットはありませんでした。また、Page.phpファイルの現在のバージョンには、canBeEditedメソッドはありませんでした。



すぐに解決策が見つからなかったので、マスターに別のパッチを作成し、変更を分解しました。そして、私は新鮮な心で問題に戻ることにしました。



"オフトピック"
, Git. , , .





わざとやったの?ファイルの名前が変更されましたか?



私はリリースエンジニアチームのチャットで助けを求めることから問題を探し始めました。彼らは、とりわけ、リポジトリをホストし、Git関連のプロセスを自動化する責任があります。正直なところ、彼らはおそらくパッチを削除することができたでしょうが、彼らは跡形もなくそれをしたでしょう。





リリースエンジニアの1人が、-followオプションを指定してgitlogを実行することを提案しました。おそらくファイルの名前が変更されたため、Gitは一部の変更を表示しません。

--follow

名前の変更を超えてファイルの履歴を一覧表示し続けます(単一のファイルに対してのみ機能します)。

(名前を変更した後にファイル履歴を表示します(単一ファイルでのみ機能します))



出力git log --follow Page.php



にデッドビーフがあり ましたが、ファイルの削除や名前の変更はありませんでした。それでも、canBeEditedメソッドがどこかで削除されたことはわかりませんでした。フォローオプションがこのストーリーで役割を果たしているように見えましたが、変更がどこに行ったのかはまだ不明でした。



残念ながら、問題のリポジトリは私たちが持っている最大のリポジトリの1つです。最初のパッチが導入されてから消えるまで、21,000のコミットがありました。必要なファイルが編集されたのはそのうちの10個だけだったのも幸運でした。私はそれらすべてを研究しましたが、興味深いものは何も見つかりませんでした。



目撃者を募集しています!ライブベアが必要です



やめる!デッドビーフを探していたの?論理的に考えてみましょう。コミットが必要です。これをlivebearと呼びましょう。その後、デッドビーフはファイル履歴に表示されなくなります。おそらくこれは私たちに何も与えないでしょう、しかしそれは私たちにいくつかの考えを与えるでしょう。



Git履歴を検索するためのgitbisectコマンドがあります。ドキュメントよる と、バグが最初に発生したコミットを見つけることができます。実際には、その瞬間が到来したかどうかを判断する方法を知っていれば、履歴内の任意の瞬間を見つけるために使用できます。私たちのバグは、コードに変更がないことでした。別のコマンドで確認できます-gitgrep。結局のところ、Page.phpにcanBeEditedメソッドがあるかどうかを知るだけで十分でした。少しデバッグしてドキュメントを読む:



livebear [ビルド]:ブランチオリジン/ XXXをbuild_web_yyyy.mm.dd.hhにマージします



タスクブランチとリリースブランチの通常のマージコミットのように見えます。しかし、このコミットで私は問題を再現することができました:



$ git checkout -b test livebear^1 2>/dev/null
$ grep -c canBeEdited Page.php
2
$ git merge —-no-edit -—no-stat livebear^2
Removing …
Removing …
Merge made by the ‘recursive’ strategy.

$ grep -c canBeEdited Page.php
0
$ git log -p Page.php | grep -c canBeEdited
0

      
      





確かに、livebearには興味深いものは何も見つかりませんでした。また、問題との関係は不明なままでした。少し考えて、検索結果を開発者に送信しました。本当のことを言っても、複製スキームが複雑になりすぎて、将来的にはこのようなことに対して保険をかけることができないことに同意しました。そのため、正式に検索を停止することにしました。



しかし、私の好奇心は満たされないままでした。



永続性は悪ではありませんが、非常に嫌です



何度か問題に戻り、git bisectを実行して、ますます多くのコミットを見つけました。すべてが疑わしく、すべてが合併ですが、それは私に何も与えませんでした。その後、あるコミットが他のコミットよりも頻繁に私に出くわしたように思えますが、最終的に犯人が彼であったかどうかはわかりません。



もちろん、他の検索方法も試しました。たとえば、問題が発生したときに行われた21,000のコミットを数回実行しました。あまりエキサイティングではありませんでしたが、面白いパターンに出くわしました。同じコマンドを実行しました:



git grep -c canBeEdited {commit} -- Page.php
      
      





必要なコードがない「悪い」コミットが同じブランチにあることが判明しました。そして、このスレッドを検索すると、すぐに手がかりが得られました。



changekillerブランチ 'master'をTICKET-XXX_descriptionにマージします



これも2つのブランチのマージでした。そして、それをローカルで繰り返そうとすると、必要なファイルであるPage.phpに競合が発生しました。リポジトリの状態から判断すると、開発者は自分のバージョンのファイルを残し、マスターからの変更を破棄しました(つまり、変更は失われました)。長い時間が経過し、開発者は正確に何が起こったのかを覚えていませんでしたが、実際には状況は単純なシーケンスで再現されました。



git checkout -b test changekiller^1
git merge -s ours changekiller^2

      
      





正当な一連の行動がどのようにそのような結果につながるのかはまだ分からない。ドキュメントでそれについて何も見つからなかったので、私はソースコードに入りました。



キラーGitですか?





ドキュメントには、gitログは入力として複数のコミットを受け取り、前に^を付けて送信されたコミットの親を除いて、親のコミットをユーザーに表示する必要があると記載されています。 git log A ^ Bは、B



親ではなく、Aの親であるコミットを表示する必要があることがわかりました。 コマンドコードは非常に複雑であることが判明しました。メモリを操作するためのさまざまな最適化があり、一般に、Cコードを読むことは私には非常に楽しい経験とは思えませんでした。基本的なロジックは、次の疑似コードで表すことができます。



//    ,   
commit commit;
rev_info revs;

revs = setup_revisions(revisions_range);
while (commit = get_revision(revs)) {
	log_tree_commit(commit);
}

      
      





ここで、get_revision関数は、制御フラグのセットであるrevを入力として受け入れます。その呼び出しのそれぞれは、正しい順序で処理するための次のコミット(または最後に到達したときの空)を与えるように見えるはずです。 revs構造を埋めるsetup_revisions関数と、画面に情報を表示するlog_tree_commitもあります。



問題をどこで探すべきかがわかったような気がしました。私はその変更にのみ興味があったので、特定のファイル(Page.php)をコマンドに渡しました。これは、gitログに「余分な」コミットをフィルタリングするための何らかのロジックが必要であることを意味します。 setup_revisions関数とget_revision関数は多くの場所で使用されていますが、問題はほとんどありません。それはlog_tree_commitを残しました。



私の言いようのない喜びに、この関数には、特定のコミットで行われた変更を計算するコードが実際にありました。一般的なロジックは次のようになります。



void log_tree_commit(commit) {
	if (tree_has_changed(commit, commit->parents)) {
		log_tree_commit_1(commit);
}
}

      
      





しかし、実際のコードを長く見るほど、自分が間違っていることに気づきました。この関数はメッセージのみを出力します。だから、その後のあなたの気持ちを信じてください!



setup_revisions関数とget_revision関数に戻りました。彼らの仕事の論理を理解するのは困難でした-補助機能の「霧」が干渉し、そのいくつかはポインタとメモリで正しく機能するために必要でした。すべてが、メインロジックがコミットツリーの単純な幅優先のトラバース、つまり、かなり標準的なアルゴリズムであるかのように見えました。



rev_info setup_revisions(revisions_range, ...) {
	rev_info rev;
	commit commit;
	
	//       —   
	for (commit = get_commit_from_range(revisions_range)) {
		revs->commits = commit_list_append(commit, revs->commits)
	}
}

commit get_revision(rev_info revs) {
	commit c;
	commit l;

c = get_revision_1(revs);
	for (l = c->parents; l; l = l->next) {
		commit_list_insert(l, &revs->commits);
	}
	return c;
}

commit get_revision_1(rev_info revs) {
	return pop_commit(revs->commits);
}

      
      





リストが作成され(revs-> commits)、コミットツリーの最初の(最上位の)要素がそこに配置されます。次に、最初からのコミットがこのリストから徐々に取得され、その親が最後に追加されます。



コードを読んでみると、ヘルパー関数の「霧」の中に、コミットをフィルタリングするための複雑なロジックがあり、それを探していました。これは、get_revision_1関数で発生します。



commit get_revision_1(rev_info revs) {
	commit commit;
	commit = pop_commit(revs->commits);
	try_to_sipmlify_commit(commit);
	return commit;
}

void try_to_simplify_commit(commit commit) {
	for (parent = commit->parents; parent; parent = parent->next) {
		if (rev_compare_tree(revs, parent, commit) == REV_TREE_SAME) {
			parent->next = NULL;
			commit->parents = parent;
		}
	}
}

      
      





複数のブランチがマージされている場合、ファイルの状態がそれらの1つと同じままであれば、他のブランチを考慮することは意味がありません。ファイルの状態がどこにも変更されていない場合は、最初のブランチのみを残します。



例。ファイルが変更されていないコミットを0で示し、ファイルが変更されたコミットを1で示し、ブランチのマージをXで示します。







この状況では、コードは機能ブランチを考慮しません-変更はありません。そこでファイルが変更された場合、Xでは変更が「破棄」されました。これは、それらの履歴があまり関連性がないことを意味します。このコードはもう存在しません。



同様のことが私たちにも起こりました。 2人の開発者が同じファイルに変更を加えました。1つはmasterブランチ、deadbeef commit、もう1つはtaskブランチです。



2番目の開発者がマスターブランチからタスクブランチに変更をマージしたとき、解決の過程で競合が発生し、マスターから変更を破棄しただけでした。時間が経過し、彼はタスクの作業を終了し、タスクブランチがマスターにアップロードされたため、デッドビーフコミットから変更が削除されました。



コミット自体は残った。ただし、Page.phpパラメーターを指定してgit logを実行すると、出力にdeadbeefcommitが表示されません。



最適化はありがたい仕事です



私は急いで、変更やバグをGit自体に送信するためのルールを注意深く調べました。結局のところ、私は本当に深刻な問題を見つけたと思いました。考えてみてください。一部のコミットは出力から消えてしまいます。これがデフォルトの動作です。幸いなことに、ルールは膨大で、時間が遅く、翌朝、私のヒューズはなくなりました。



この最適化により、私たちのような大規模なリポジトリでのGitのパフォーマンスが大幅に向上することに気付きました。man git-rev-listもドキュメントがあり 、この動作は非常に簡単にオフにできます。



ちなみに、-followはこの話にどのように関与していますか?



実際、このロジックの動作に影響を与える方法はたくさんあります。具体的には、Gitコードのフォローフラグについて、13年前にコメントが見つかりました。



次の名前変更でコミットをプルーニングできません:パスが変更されます。

(翻訳:名前の変更が進行中の場合、コミットをスローできません:パスは変更される可能性があります)





PS

私自身、Badooのリリースエンジニアリングチームに数年在籍しており、社内の多くの人がGitを理解していると信じています。





(翻訳。オリジナル:xkcd.com/1597



この点で、このシステムで発生する問題に対処する必要があります。たとえば、この記事で説明されているように、それらのいくつかは非常に好奇心が強いようです。多くの場合、問題はすぐに解決されます。すでに多くの問題が発生しています。ドキュメントには何かが詳しく説明されています。このケースは例外でした。



実際、ドキュメントには確かに履歴の簡略化セクションがありましたが、それはgit rev-listコマンド専用であり、私はそこを見るとは思いませんでした。6か月前、このセクションはgit logコマンドのマニュアルに含まれていましたが、私たちのケースは少し前に発生しました。この記事を終了する時間がなかっただけです。(*)



そして最後に、最後まで読んだ人にはちょっとしたボーナスがあります。問題が再現される非常に小さなリポジトリがあります。



$ git clone https://github.com/Md-Cake/lost-changes.git
Cloning into 'lost-changes'...

$ git log --oneline test.php
edfd6a4 master: print 3 between 1 and 2
096d4cf init

$ git log --oneline --full-history test.php
afea493 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'changekiller'
57041b8 (origin/changekiller) print 4 between 1 and 2
edfd6a4 master: print 3 between 1 and 2
096d4cf init

      
      





ご清聴ありがとうございました!



(*)UPD:履歴の簡略化のセクションが6か月よりもはるかに長い間git logコマンドのドキュメントに含まれていたことが判明したので、スキップしました。ありがとうございました youROCKこれに注目が集まった!



All Articles