Skip to content

Commit ac26d71

Browse files
authored
Merge pull request #11 from tonbiattack/add_comment
feat: abortコマンドのユーザー入力正規化機能を改善し、テストを追加
2 parents c6cf812 + c2bcdd0 commit ac26d71

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

cmd/branch/abort.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,54 +73,82 @@ func runAbortCommand(_ *cobra.Command, args []string) error {
7373

7474
// normalizeAbortOperation はユーザー入力をサポートする操作名に変換します
7575
func normalizeAbortOperation(op string) (string, error) {
76+
// 文字列の正規化処理
77+
// - `strings.TrimSpace` で前後の空白を除去
78+
// - `strings.ToLower` で大文字小文字を統一
79+
// - `strings.ReplaceAll` でアンダースコアをハイフンに置換
80+
// これにより、ユーザー入力のバリエーション(例: " ReBase", "cherrypick")を
81+
// 受け付けやすくしています。
7682
normalized := strings.ToLower(strings.TrimSpace(op))
83+
// 入力の区切り文字を統一するため、アンダースコアをハイフンに置換します。
84+
// 例: "cherry_pick" -> "cherry-pick" として扱うことで、
85+
// ユーザーがアンダースコア/ハイフンどちらを使っても同一操作として扱えるようにします。
7786
normalized = strings.ReplaceAll(normalized, "_", "-")
7887

88+
// switch 文で受け付ける操作名を決定します。
89+
// - 複数の case を列挙することで同義の入力を一つの正規形にまとめています。
90+
// - 成功時は正規化された操作名(例: "rebase")を返し、エラー時は説明付きで返します。
7991
switch normalized {
8092
case "merge":
8193
return "merge", nil
8294
case "rebase":
8395
return "rebase", nil
8496
case "cherry", "cherry-pick", "cherrypick":
97+
// "cherry" を許容して "cherry-pick" に統一
8598
return "cherry-pick", nil
8699
case "revert":
87100
return "revert", nil
88101
default:
102+
// サポート外の操作の場合はエラーを返す
89103
return "", fmt.Errorf("サポートされていない操作です: %s", op)
90104
}
91105
}
92106

93107
// detectAbortOperation は現在のGitディレクトリから進行中の操作を判定します
94108
func detectAbortOperation() (string, error) {
109+
// getGitDir で .git ディレクトリの絶対パスを取得します。
110+
// エラーがあれば検出不能としてそのまま返します。
95111
gitDir, err := getGitDir()
96112
if err != nil {
97113
return "", err
98114
}
99115

116+
// rebase は 2 種類の作業ディレクトリを持つため両方を確認します。
117+
// - rebase-apply: 非対話的/メールベースの rebase で使われる場合がある
118+
// - rebase-merge: 対話的 rebase 等で使われる場合がある
100119
rebaseDirs := []string{"rebase-apply", "rebase-merge"}
101120
for _, dir := range rebaseDirs {
121+
// filepath.Join は複数のパス要素を OS に依存しない形で結合します。
102122
if pathExists(filepath.Join(gitDir, dir)) {
123+
// 見つかった時点で rebase が進行中と判定
103124
return "rebase", nil
104125
}
105126
}
106127

128+
// CHERRY_PICK_HEAD が存在すればチェリーピック中
107129
if pathExists(filepath.Join(gitDir, "CHERRY_PICK_HEAD")) {
108130
return "cherry-pick", nil
109131
}
110132

133+
// REVERT_HEAD が存在すればリバート中
111134
if pathExists(filepath.Join(gitDir, "REVERT_HEAD")) {
112135
return "revert", nil
113136
}
114137

138+
// MERGE_HEAD が存在すればマージ中
115139
if pathExists(filepath.Join(gitDir, "MERGE_HEAD")) {
116140
return "merge", nil
117141
}
118142

143+
// どの操作も検出できない場合はエラーを返して引数による指定を促す
119144
return "", fmt.Errorf("中止できる操作が検出されませんでした。引数で操作を指定してください")
120145
}
121146

122147
// abortOperation は指定された操作を実際に中止します
123148
func abortOperation(operation string) error {
149+
// 実際の Git コマンドを実行する箇所。
150+
// gitcmd.RunWithIO は呼び出し元の標準入出力に接続してコマンドを実行するため、
151+
// ユーザー対話やエラー出力がそのまま端末に表示されます。
124152
switch operation {
125153
case "merge":
126154
return gitcmd.RunWithIO("merge", "--abort")
@@ -131,12 +159,15 @@ func abortOperation(operation string) error {
131159
case "revert":
132160
return gitcmd.RunWithIO("revert", "--abort")
133161
default:
162+
// 想定外の操作名が来た場合は明示的にエラーを返す
134163
return fmt.Errorf("未対応の操作です: %s", operation)
135164
}
136165
}
137166

138167
// abortOperationLabel は日本語の表示名を返します
139168
func abortOperationLabel(operation string) string {
169+
// 表示用に日本語ラベルを返すヘルパー関数
170+
// switch 文で対応する日本語を返し、未対応の文字列はそのまま返します。
140171
switch operation {
141172
case "merge":
142173
return "マージ"
@@ -153,29 +184,45 @@ func abortOperationLabel(operation string) string {
153184

154185
// getGitDir は現在のリポジトリの .git ディレクトリへの絶対パスを返します
155186
func getGitDir() (string, error) {
187+
// git rev-parse --git-dir はリポジトリの .git ディレクトリのパスを返します。
188+
// - 絶対パスが返る場合と相対パスが返る場合がある(サブモジュール等)ため、
189+
// 相対パスだった場合はカレントディレクトリと結合して絶対パスに直します。
156190
output, err := gitcmd.Run("rev-parse", "--git-dir")
157191
if err != nil {
158192
return "", fmt.Errorf("Gitディレクトリの取得に失敗しました: %w", err)
159193
}
160194

195+
// gitcmd.Run の返す値は出力(末尾に改行が含まれることがある)なので
196+
// strings.TrimSpace で余分な空白や改行を削除します。
161197
dir := strings.TrimSpace(string(output))
198+
199+
// filepath.IsAbs で絶対パスかどうか判定します。
200+
// - 絶対パスならそのまま返す
162201
if filepath.IsAbs(dir) {
163202
return dir, nil
164203
}
165204

205+
// 相対パスの場合は現在の作業ディレクトリを取得して結合します。
206+
// - os.Getwd は現在のカレントワーキングディレクトリの絶対パスを返します。
166207
cwd, err := os.Getwd()
167208
if err != nil {
168209
return "", fmt.Errorf("カレントディレクトリの取得に失敗しました: %w", err)
169210
}
170211

212+
// filepath.Join で OS に依存しない形でパス結合
171213
return filepath.Join(cwd, dir), nil
172214
}
173215

174216
// pathExists はファイルまたはディレクトリの存在を確認します
175217
func pathExists(path string) bool {
218+
// 空文字列は存在しないとみなす
176219
if path == "" {
177220
return false
178221
}
222+
223+
// os.Stat はファイル情報を返し、存在しない場合はエラーを返す。
224+
// - 存在する場合: err == nil
225+
// - 存在しない場合: err != nil(詳細を判定するには os.IsNotExist(err) を利用可能)
179226
_, err := os.Stat(path)
180227
return err == nil
181228
}

cmd/branch/abort_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ import (
99
)
1010

1111
// TestNormalizeAbortOperation はユーザー入力の正規化をテストします
12+
//
13+
// 詳細 (文法/意図):
14+
// - このテストは表形式 (table-driven) で複数の入力ケースを定義し、
15+
// 各ケースごとに期待される正規化結果を検証します。
16+
// - `t.Parallel()` を呼んでいるため、サブテストは並列実行される可能性があります。
17+
// そのためサブテスト内で使用するループ変数を再バインドしてクロージャキャプチャ問題を避けています。
18+
// - テストの構造:
19+
// 1. `tests` スライスに入力と期待値を列挙
20+
// 2. for-range で各ケースを取り出し、`t.Run` でサブテストを作成
21+
// 3. サブテスト内で `normalizeAbortOperation` を呼び出し、結果と期待値を比較
22+
//
23+
// 文法レベルのポイント:
24+
// - table-driven テストは新しいケースを追加しやすく、期待値を明示しやすい。
25+
// - 並列化 (`t.Parallel()`) はテスト速度の向上に寄与するが、クロージャのキャプチャに注意が必要。
1226
func TestNormalizeAbortOperation(t *testing.T) {
1327
t.Parallel()
1428

@@ -25,9 +39,21 @@ func TestNormalizeAbortOperation(t *testing.T) {
2539
}
2640

2741
for _, tt := range tests {
42+
// ループ変数を新しいローカル変数に再バインドします。
43+
// 理由:
44+
// - Go の for-range ではループ変数が再利用されるため、クロージャがその変数を参照すると
45+
// 並列実行時(t.Parallel())にすべてのサブテストが同じ最終値を参照してしまう可能性があります。
46+
// - 各イテレーションごとに `tt := tt` で新しい変数に再バインドすることで、クロージャは
47+
// そのイテレーション固有の値を捕捉し、並列サブテストでも安全に動作します。
2848
tt := tt
49+
50+
// サブテスト: 各入力ケースを個別のサブテストとして実行します。
51+
// - 第一引数はテスト名(ここでは入力文字列)
52+
// - 無名関数内で再度 `t.Parallel()` を呼ぶことで各サブテスト自身も並列化されます。
2953
t.Run(tt.input, func(t *testing.T) {
3054
t.Parallel()
55+
56+
// 実際の呼び出しと検証
3157
actual, err := normalizeAbortOperation(tt.input)
3258
if err != nil {
3359
t.Fatalf("normalizeAbortOperation(%q) returned error: %v", tt.input, err)
@@ -110,36 +136,66 @@ func TestDetectAbortOperation_NoOp(t *testing.T) {
110136

111137
// withRepo は一時Gitリポジトリでコールバックを実行します
112138
func withRepo(t *testing.T, fn func(gitDir string)) {
139+
// withRepo は一時的なテスト用 Git リポジトリを作成し、指定のコールバックを実行します。
140+
//
141+
// 文法/意図:
142+
// - テストで必要な前処理(リポジトリ作成、初期コミット)を集約することで各テストの冗長性を減らす。
143+
// - 作成したリポジトリのディレクトリにカレントディレクトリを移動してからコールバックを呼ぶ。
144+
// これにより、`git` コマンドや `getGitDir` の動作が期待通りにリポジトリを参照できる。
145+
// - 終了時に `defer` を使って元の作業ディレクトリへ復帰させることで、テスト間の副作用を防止する。
146+
//
147+
// 実装の流れ:
148+
// 1. `testutil.NewGitRepo` で一時リポジトリを作成
149+
// 2. 簡単なファイルを作ってコミット(`git init` 後の最小セットアップ)
150+
// 3. 現在の作業ディレクトリを保存し、テスト用リポジトリへ `chdir`
151+
// 4. コールバックに `.git` ディレクトリのパスを渡す
113152
t.Helper()
114153

115154
repo := testutil.NewGitRepo(t)
116155
repo.CreateFile("README.md", "# Test")
117156
repo.Commit("initial")
118157

158+
// 元の作業ディレクトリを取得して保存
119159
oldDir, err := os.Getwd()
120160
if err != nil {
121161
t.Fatalf("failed to get working directory: %v", err)
122162
}
163+
// テスト終了後に元のディレクトリへ戻す(副作用のクリーンアップ)
123164
defer func() { _ = os.Chdir(oldDir) }()
124165

166+
// テスト用リポジトリのルートへ移動する
125167
if err := os.Chdir(repo.Dir); err != nil {
126168
t.Fatalf("failed to chdir: %v", err)
127169
}
128170

171+
// コールバックには `.git` ディレクトリの絶対パスを渡す
129172
fn(filepath.Join(repo.Dir, ".git"))
130173
}
131174

132175
// writeIndicator は指定されたパスに空ファイルを作成します
133176
func writeIndicator(t *testing.T, path string) {
177+
// 文法/意図:
178+
// - Git の進行中操作判定では特定のファイル(例: MERGE_HEAD, CHERRY_PICK_HEAD)や
179+
// ディレクトリ(rebase-merge 等)の存在を確認します。このヘルパーはその指標ファイルを
180+
// テスト用に作成するためのものです。
181+
// - 第2引数 `path` は作成するインジケータファイルのパス(通常は <repo>/.git/<NAME>)。
134182
t.Helper()
183+
184+
// ファイルを書き込む際のパーミッションはテスト内のみの利用なので 0o600 を指定しています。
185+
// - 所有者に読み/書き、他に権限なし(セキュリティ的に最小限の権限)
135186
if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil {
136187
t.Fatalf("failed to write indicator file: %v", err)
137188
}
138189
}
139190

140191
// createDir は指定されたディレクトリを作成します
141192
func createDir(t *testing.T, path string) {
193+
// 文法/意図:
194+
// - テスト内で rebase 等の進行中状態を模倣するためにディレクトリを作成するユーティリティ。
195+
// - `os.MkdirAll` を使うことで中間ディレクトリが存在しなくても確実に作成できます。
196+
// - パーミッション 0o755 は所有者に書き込みを許可し、グループ/その他に読み/実行を許可します。
142197
t.Helper()
198+
143199
if err := os.MkdirAll(path, 0o755); err != nil {
144200
t.Fatalf("failed to create directory: %v", err)
145201
}

0 commit comments

Comments
 (0)