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の大先生)がそう書いていて、それの孫引きをするからでしょうか。