Gitの操作履歴をreflogで確認し任意の状態へハードリセットする
Gitでコミットがおかしな状態になった時の最終手段は git reflog コマンドを用いて操作履歴を確認し、その場所へ reset --hard するという方法があります。これを C# のプログラムで実装してみましょう。
git reflog を実行すると結果が下記のように返ってきます。それぞれの行の最初の文字列チャンクがコミットハッシュです、その次のチャンクが操作履歴インデックスです、その次のチャンクが操作の実際を示しています。同じコミットハッシュが示されていて、それぞれに操作が違っていることに注目してください。
ab9adcc HEAD@{0}: reset: moving to HEAD@{6}
4744539 HEAD@{1}: reset: moving to 4744539
46eb0b1 HEAD@{2}: reset: moving to HEAD@{6}
ab9adcc HEAD@{3}: reset: moving to HEAD@{20}
46eb0b1 HEAD@{4}: reset: moving to HEAD@{4}
ab9adcc HEAD@{5}: reset: moving to HEAD@{0}
ab9adcc HEAD@{6}: reset: moving to HEAD@{0}
ab9adcc HEAD@{7}: reset: moving to HEAD@{0}
ab9adcc HEAD@{8}: reset: moving to ab9adcc
46eb0b1 HEAD@{9}: reset: moving to HEAD@{20}
90fb698 HEAD@{10}: reset: moving to HEAD@{0}
90fb698 HEAD@{11}: reset: moving to HEAD@{14}
4744539 HEAD@{12}: reset: moving to HEAD@{15}
90fb698 HEAD@{13}: reset: moving to HEAD
90fb698 HEAD@{14}: reset: moving to HEAD
90fb698 HEAD@{15}: reset: moving to HEAD@{10}
ab9adcc HEAD@{16}: reset: moving to ab9adcc
ab9adcc HEAD@{17}: reset: moving to HEAD@{2}
4744539 HEAD@{18}: reset: moving to 474453
ab9adcc HEAD@{19}: reset: moving to HEAD@{4}
ab9adcc HEAD@{20}: reset: moving to HEAD
ab9adcc HEAD@{21}: reset: moving to HEAD
ab9adcc HEAD@{22}: reset: moving to HEAD
ab9adcc HEAD@{23}: reset: moving to HEAD
ab9adcc HEAD@{24}: merge dev: Merge made by the 'ort' strategy.
4744539 HEAD@{25}: checkout: moving from dev to master
90fb698 HEAD@{26}: commit: button3 を追加した.
46eb0b1 HEAD@{27}: checkout: moving from master to dev
4744539 HEAD@{28}: merge dev: Merge made by the 'ort' strategy.
f34b964 HEAD@{29}: checkout: moving from dev to master
46eb0b1 HEAD@{30}: commit: button2 を追加した.
65ac2c0 HEAD@{31}: commit: button1 を追加した.
f34b964 HEAD@{32}: checkout: moving from master to dev
f34b964 HEAD@{33}: commit (initial): First commit ( this is test ).
これを手掛かりにして C# から git reset --hard するツールを作成します。下記のコンポーネントを配置してください。
textBox1: Gitコマンドを実行するディレクトリを指定
textBox2: 戻りたいコミットハッシュを指定
comboBox1: 戻りたい履歴 HEAD@{n} を指定
listBox1: git の実行結果を表示
button1: git reflog で履歴を確認する
button2: git reset --hard で強制リセットする( コミットハッシュを利用する場合)
button3: git reset --hard で強制リセットする ( HEAD@{履歴番号} を利用する場合)
button4: git log --pretty で簡略ログ表示する
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Windows.Forms;
namespace aaa
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load( object sender, EventArgs e )
{
String dir = "c:/tmp/sss";
textBox1.Text = dir;
comboBox1.Items.Clear();
for ( int k = 0; k < 100; k++ )
{
comboBox1.Items.Add( k.ToString() );
}
// これを指定すると最初に追加した値がデフォルトで表示される.
//comboBox1.SelectedIndex = 0;
button1.Text = "reflogで履歴を確認";
button2.Text = "コミットハッシュでハードリセット";
button3.Text = "HEAD@{n}でハードリセット";
button4.Text = "logでログを確認";
}
private void button1_Click( object sender, EventArgs e )
{
// Gitコマンドを実行するディレクトリ.
String dir = textBox1.Text;
// コマンドを実行する.
const String GIT_ARGS = "reflog";
List<String> ret_output = ExecGitCommand( GIT_ARGS, dir );
// コマンド結果の出力領域をいったんクリアする.
listBox1.Items.Clear();
// コマンド実行結果を表示する.
for ( int k = 0; k < ret_output.Count; k++ )
{
listBox1.Items.Add( ret_output[k] );
}
}
private void button2_Click( object sender, EventArgs e )
{
// Gitコマンドを実行するディレクトリ.
String dir = textBox1.Text;
// リセットしたいコミットハッシュを指定する.
String commit_hash = textBox2.Text.Trim();
if ( commit_hash == "" )
{
MessageBox.Show( "コミットハッシュを指定してください." );
return; // warning.
}
// コマンドを実行する.
String GIT_ARGS = $"reset --hard {commit_hash}";
List<String> ret_output = ExecGitCommand( GIT_ARGS, dir );
// コマンド結果の出力領域をいったんクリアする.
listBox1.Items.Clear();
// コマンド実行結果を表示する.
for ( int k = 0; k < ret_output.Count; k++ )
{
listBox1.Items.Add( ret_output[k] );
}
}
private void button3_Click( object sender, EventArgs e )
{
// Gitコマンドを実行するディレクトリ.
String dir = textBox1.Text;
// リセットしたい HEAD@{n} の番号を指定する.
String s = comboBox1.Text.Trim();
if ( s == "" )
{
MessageBox.Show( "HEAD@{n} の n を指定してください." );
return; // warning.
}
// 数値文字列を数値へ.
int index = int.Parse( s );
// コマンドを実行する.
String GIT_ARGS = $"reset --hard HEAD@{{{index}}}";
List<String> ret_output = ExecGitCommand( GIT_ARGS, dir );
// コマンド結果の出力領域をいったんクリアする.
listBox1.Items.Clear();
// コマンド実行結果を表示する.
for ( int k = 0; k < ret_output.Count; k++ )
{
listBox1.Items.Add( ret_output[k] );
}
}
private void button4_Click( object sender, EventArgs e )
{
// Gitコマンドを実行するディレクトリ.
String dir = textBox1.Text;
// コマンドを実行する.
String GIT_ARGS = "log --pretty=format:\"%h %ad %s\" --date=format:\"%Y/%m/%d_%H:%M:%S\"";
List<String> ret_output = ExecGitCommand( GIT_ARGS, dir );
// コマンド結果の出力領域をいったんクリアする.
listBox1.Items.Clear();
// コマンド実行結果を表示する.
for ( int k = 0; k < ret_output.Count; k++ )
{
listBox1.Items.Add( ret_output[k] );
}
}
private List<string> ExecGitCommand( string arguments, string working_directory )
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "git";
psi.Arguments = arguments;
psi.WorkingDirectory = working_directory;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8; // ここを指定しないと日本語は文字化けする.
psi.StandardErrorEncoding = Encoding.UTF8; // ここを指定しないと日本語は文字化けする.
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
List<String> output = new List<string>();
try
{
using ( Process proc = Process.Start( psi ) )
{
while ( !proc.StandardOutput.EndOfStream )
{
output.Add( proc.StandardOutput.ReadLine() );
}
string err = proc.StandardError.ReadToEnd();
proc.WaitForExit();
if ( proc.ExitCode != 0 )
{
throw new Exception( err );
}
}
}
catch ( Exception excp )
{
output.Add( excp.Message );
}
return output;
}
}
}
いろいろなブログを見ると「git reflog のあとで reset --hard HEAD@{n} で、強制リセットする」という解説がありますが、これは良いと思いません。HEAD@{n} の利用は操作の履歴を遡っているのであって、状態の履歴を遡っているわけではありません。
HEAD@{n} で戻すと余計に reflog の履歴が汚れて読みにくくなる場合が多いです。前もって log を実行して確実に戻れるコミットのハッシュを確認して一発で reset --hard したほうがいいです。
git reflog の解説を見ると、必ず次に git reset --hard HEAD@{n} と書いてあるのはなぜでしょうか?誰か偉い人(Gitの大先生)がそう書いていて、それの孫引きをするからでしょうか。