Skip to main content

gitstack/graph/
builder.rs

1//! グラフ構築ロジック
2
3use std::collections::{HashMap, HashSet};
4
5use super::cell::GraphCell;
6use super::colors::{ColorContext, PenaltyBasedColorAssigner};
7use super::layout::{GraphLayout, GraphRow};
8use crate::event::{GitEvent, GitEventKind};
9
10/// 列追跡管理
11struct ColumnTracker {
12    /// 各列が追跡中のコミットハッシュ(Noneは空き列)
13    columns: Vec<Option<String>>,
14    /// ハッシュ→色インデックスのマッピング
15    hash_colors: HashMap<String, usize>,
16    /// 列→色インデックスのマッピング
17    column_colors: HashMap<usize, usize>,
18    /// 色割り当て管理(ペナルティベース)
19    color_assigner: PenaltyBasedColorAssigner,
20    /// コミットハッシュ→分岐元ハッシュ(fork point検出用)
21    fork_points: HashMap<String, String>,
22}
23
24impl ColumnTracker {
25    fn new() -> Self {
26        Self {
27            columns: Vec::new(),
28            hash_colors: HashMap::new(),
29            column_colors: HashMap::new(),
30            color_assigner: PenaltyBasedColorAssigner::new(),
31            fork_points: HashMap::new(),
32        }
33    }
34
35    /// 指定ハッシュを追跡中の列を探す
36    fn find_column(&self, hash: &str) -> Option<usize> {
37        self.columns
38            .iter()
39            .position(|h| h.as_ref().is_some_and(|h| h == hash))
40    }
41
42    /// 空き列を探すか、新規列を作成
43    fn find_or_create_column(&mut self) -> usize {
44        if let Some(pos) = self.columns.iter().position(|h| h.is_none()) {
45            pos
46        } else {
47            self.columns.push(None);
48            self.columns.len() - 1
49        }
50    }
51
52    /// 列を解放
53    fn release_column(&mut self, col: usize) {
54        if col < self.columns.len() {
55            self.columns[col] = None;
56            self.column_colors.remove(&col);
57            self.color_assigner.release_lane(col);
58        }
59    }
60
61    /// 列にハッシュを設定
62    fn set_column(&mut self, col: usize, hash: String, color_idx: usize) {
63        while self.columns.len() <= col {
64            self.columns.push(None);
65        }
66        self.columns[col] = Some(hash.clone());
67        self.hash_colors.insert(hash, color_idx);
68        self.column_colors.insert(col, color_idx);
69    }
70
71    /// 列の色を取得
72    fn get_column_color(&self, col: usize) -> Option<usize> {
73        self.column_colors.get(&col).copied()
74    }
75
76    /// ハッシュの色を取得
77    fn get_hash_color(&self, hash: &str) -> Option<usize> {
78        self.hash_colors.get(hash).copied()
79    }
80
81    /// メインブランチ色を割り当て
82    fn assign_main_color(&mut self, col: usize) -> usize {
83        let ctx = ColorContext::new(col).with_main_branch(true);
84        self.color_assigner.assign_with_context(&ctx)
85    }
86
87    /// コンテキストを考慮して色を割り当て
88    fn assign_color_with_context(
89        &mut self,
90        col: usize,
91        fork_point: Option<String>,
92        parent_color: Option<usize>,
93    ) -> usize {
94        let ctx = ColorContext::new(col)
95            .with_fork_point(fork_point)
96            .with_parent_color(parent_color);
97        self.color_assigner.assign_with_context(&ctx)
98    }
99
100    /// 分岐元を記録
101    fn set_fork_point(&mut self, hash: &str, fork_point: &str) {
102        self.fork_points
103            .insert(hash.to_string(), fork_point.to_string());
104    }
105
106    /// 分岐元を取得
107    fn get_fork_point(&self, hash: &str) -> Option<String> {
108        self.fork_points.get(hash).cloned()
109    }
110
111    /// アクティブな列の最大インデックス
112    fn max_active_column(&self) -> usize {
113        self.columns
114            .iter()
115            .enumerate()
116            .filter(|(_, h)| h.is_some())
117            .map(|(i, _)| i)
118            .max()
119            .unwrap_or(0)
120    }
121}
122
123/// グラフを構築
124pub fn build_graph(events: &[GitEvent], head_hash: Option<&str>) -> GraphLayout {
125    if events.is_empty() {
126        return GraphLayout::empty();
127    }
128
129    let mut tracker = ColumnTracker::new();
130    let mut rows: Vec<GraphRow> = Vec::new();
131    let mut max_column: usize = 0;
132
133    // ハッシュ→行インデックスのマッピング
134    let hash_to_idx: HashMap<&str, usize> = events
135        .iter()
136        .enumerate()
137        .map(|(i, e)| (e.short_hash.as_str(), i))
138        .collect();
139
140    // 既に表示されたコミットのハッシュ
141    let mut shown_commits: HashSet<String> = HashSet::new();
142
143    for (idx, event) in events.iter().enumerate() {
144        // 分岐点検出:複数の列が同じコミットを追跡している場合
145        // コネクタ行を挿入して合流を視覚化
146        let converging_columns: Vec<usize> = tracker
147            .columns
148            .iter()
149            .enumerate()
150            .filter(|(_, h)| h.as_ref().is_some_and(|h| h == &event.short_hash))
151            .map(|(i, _)| i)
152            .collect();
153
154        if converging_columns.len() >= 2 {
155            // 複数列が合流:コネクタ行を挿入
156            let main_col = *converging_columns.iter().min().unwrap();
157            let merging_cols: Vec<usize> = converging_columns
158                .iter()
159                .filter(|&&c| c != main_col)
160                .copied()
161                .collect();
162
163            // 最大列数を更新
164            for &col in &merging_cols {
165                max_column = max_column.max(col);
166            }
167            max_column = max_column.max(main_col);
168
169            // コネクタ行のセルを構築
170            let connector_cells = build_connector_cells(
171                main_col,
172                &merging_cols,
173                &tracker.columns,
174                &tracker.column_colors,
175                max_column,
176            );
177
178            // コネクタ行を追加(コミット情報なし)
179            let main_color = tracker.get_column_color(main_col).unwrap_or(0);
180            rows.push(GraphRow::new(None, main_col, main_color, connector_cells));
181
182            // 合流した列を解放(メイン列以外)
183            for col in merging_cols {
184                tracker.release_column(col);
185            }
186        }
187
188        // このコミットを追跡中の列を探す
189        let commit_col = tracker
190            .find_column(&event.short_hash)
191            .unwrap_or_else(|| tracker.find_or_create_column());
192
193        // 分岐元を検出(第1親があれば設定)
194        if let Some(first_parent) = event.parent_hashes.first() {
195            tracker.set_fork_point(&event.short_hash, first_parent);
196        }
197
198        // 色を決定
199        let commit_color = if idx == 0 {
200            // 最初のコミット(メインブランチ)
201            let color = tracker.assign_main_color(commit_col);
202            tracker.set_column(commit_col, event.short_hash.clone(), color);
203            color
204        } else if let Some(color) = tracker.get_column_color(commit_col) {
205            // 既存の列の色を継続
206            color
207        } else {
208            // 新しい色を割り当て(fork pointを考慮)
209            let fork_point = tracker.get_fork_point(&event.short_hash);
210            let parent_color = if let Some(first_parent) = event.parent_hashes.first() {
211                tracker.get_hash_color(first_parent)
212            } else {
213                None
214            };
215            let color = tracker.assign_color_with_context(commit_col, fork_point, parent_color);
216            tracker.set_column(commit_col, event.short_hash.clone(), color);
217            color
218        };
219
220        // この列をクリア(コミット処理完了)
221        if commit_col < tracker.columns.len() {
222            tracker.columns[commit_col] = None;
223        }
224
225        // 親コミットの処理
226        // (列, 色, 既存追跡中か, 既に表示されたか)
227        let mut parent_info: Vec<(usize, usize, bool, bool)> = Vec::new();
228
229        for (parent_idx, parent_hash) in event.parent_hashes.iter().enumerate() {
230            // 親が表示範囲内にあるか確認
231            let in_display_range = hash_to_idx.contains_key(parent_hash.as_str());
232
233            if !in_display_range {
234                // 親が表示範囲外でも、第1親は同じ列で追跡を続ける(縦線継続のため)
235                if parent_idx == 0 {
236                    tracker.set_column(commit_col, parent_hash.clone(), commit_color);
237                }
238                continue;
239            }
240
241            // 親を追跡中の列を探す
242            let existing_col = tracker.find_column(parent_hash);
243
244            // 親が既に表示されたか
245            let already_shown = shown_commits.contains(parent_hash);
246
247            let (parent_col, parent_color, is_existing) = if let Some(col) = existing_col {
248                // 既存の列が親を追跡している
249                let color = tracker
250                    .get_column_color(col)
251                    .or_else(|| tracker.get_hash_color(parent_hash))
252                    .unwrap_or(col);
253
254                if parent_idx == 0 && col != commit_col {
255                    // 第1親が別の列で追跡されている場合(分岐コミット)
256                    // → 現在の列でも追跡を開始(分岐点検出のため)
257                    tracker.set_column(commit_col, parent_hash.clone(), commit_color);
258                    // → 横線は描画しない(commit_colを返す)
259                    //   分岐点はコネクタ行で視覚化される
260                    (commit_col, commit_color, true)
261                } else {
262                    // 第2親以降(マージ元)、または同じ列の場合
263                    // → 横線を描画する
264                    (col, color, true)
265                }
266            } else if parent_idx == 0 {
267                // 第1親: 同じ列で継続
268                tracker.set_column(commit_col, parent_hash.clone(), commit_color);
269                (commit_col, commit_color, false)
270            } else {
271                // 第2親以降: 新しい列(fork pointを考慮)
272                let new_col = tracker.find_or_create_column();
273                // 第2親以降は現在のコミットを分岐元として記録
274                let fork_point = Some(event.short_hash.clone());
275                let new_color = tracker.assign_color_with_context(new_col, fork_point, None);
276                tracker.set_column(new_col, parent_hash.clone(), new_color);
277                (new_col, new_color, false)
278            };
279
280            parent_info.push((parent_col, parent_color, is_existing, already_shown));
281        }
282
283        // 最大列数を更新
284        max_column = max_column.max(commit_col).max(tracker.max_active_column());
285
286        // セル配列を構築
287        let cells = build_cells(
288            commit_col,
289            commit_color,
290            &parent_info,
291            &tracker.columns,
292            &tracker.column_colors,
293            max_column,
294            event.kind == GitEventKind::Merge,
295            head_hash == Some(&event.short_hash),
296        );
297
298        // 行を追加
299        let is_head = head_hash == Some(&event.short_hash);
300        let row =
301            GraphRow::new(Some(event.clone()), commit_col, commit_color, cells).with_head(is_head);
302        rows.push(row);
303
304        // このコミットを表示済みとしてマーク
305        shown_commits.insert(event.short_hash.clone());
306
307        // 注: 列解放はここでは行わない
308        // 列は自然にリサイクルされる(コミット処理時にクリア → find_or_create_columnで再利用)
309        // 早期の列解放は、複数マージが連続する場合に縦線が消えるバグを引き起こす
310    }
311
312    GraphLayout { rows, max_column }
313}
314
315/// 1行分のセル配列を構築
316#[allow(clippy::too_many_arguments)]
317fn build_cells(
318    commit_col: usize,
319    commit_color: usize,
320    parent_info: &[(usize, usize, bool, bool)], // (列, 色, 既存追跡中か, 既に表示されたか)
321    active_columns: &[Option<String>],
322    column_colors: &HashMap<usize, usize>,
323    max_col: usize,
324    is_merge: bool,
325    is_head: bool,
326) -> Vec<GraphCell> {
327    // セル数: 各列に2文字分(ノード用と間隔用)
328    let cell_count = (max_col + 1) * 2;
329    let mut cells = vec![GraphCell::Empty; cell_count];
330
331    // 1. アクティブ列に縦線を描画
332    for (col, hash) in active_columns.iter().enumerate() {
333        if hash.is_some() && col != commit_col {
334            let cell_idx = col * 2;
335            if cell_idx < cells.len() {
336                // 列の色を使用
337                let color_idx = column_colors.get(&col).copied().unwrap_or(col % 8);
338                cells[cell_idx] = GraphCell::Vertical { color_idx };
339            }
340        }
341    }
342
343    // 2. コミット位置にノードを描画
344    let commit_cell_idx = commit_col * 2;
345    if commit_cell_idx < cells.len() {
346        cells[commit_cell_idx] = if is_head {
347            GraphCell::HeadNode {
348                color_idx: commit_color,
349            }
350        } else if is_merge {
351            GraphCell::MergeNode {
352                color_idx: commit_color,
353            }
354        } else {
355            GraphCell::Node {
356                color_idx: commit_color,
357            }
358        };
359    }
360
361    // 3. 親への接続線を描画
362    for &(parent_col, parent_color, is_existing, already_shown) in parent_info {
363        if parent_col == commit_col {
364            // 同じ列: 接続線不要(次の行で縦線として描画)
365            continue;
366        }
367
368        if parent_col > commit_col {
369            // 右への接続
370            draw_horizontal_connection(
371                &mut cells,
372                commit_col,
373                parent_col,
374                parent_color,
375                is_existing,
376                already_shown,
377                true, // 右向き
378            );
379        } else {
380            // 左への接続
381            draw_horizontal_connection(
382                &mut cells,
383                parent_col,
384                commit_col,
385                parent_color,
386                is_existing,
387                already_shown,
388                false, // 左向き
389            );
390        }
391    }
392
393    cells
394}
395
396/// 横方向の接続線を描画
397fn draw_horizontal_connection(
398    cells: &mut [GraphCell],
399    left_col: usize,
400    right_col: usize,
401    color_idx: usize,
402    is_existing: bool,
403    already_shown: bool,
404    is_right_direction: bool, // コミットから見て右方向か
405) {
406    let start_idx = left_col * 2 + 1;
407    let end_idx = right_col * 2;
408
409    // 横線を描画
410    for idx in start_idx..end_idx {
411        if idx < cells.len() {
412            match cells[idx] {
413                GraphCell::Vertical { color_idx: v_color } => {
414                    // 縦線との交差
415                    cells[idx] = GraphCell::Cross {
416                        h_color: color_idx,
417                        v_color,
418                    };
419                }
420                GraphCell::Empty => {
421                    cells[idx] = GraphCell::Horizontal { color_idx };
422                }
423                _ => {}
424            }
425        }
426    }
427
428    // 端点の記号を決定
429    // keifuのロジック参考:
430    // - is_existing && already_shown: 親が既に表示済み → マージ終了 (╯/╰)
431    // - is_existing && !already_shown: 親がまだ表示されていない → T字接続 (┤/├)
432    // - !is_existing: 新しいブランチ開始 → 曲線 (╮/╭)
433    if is_right_direction {
434        // 右端の記号
435        if end_idx < cells.len() {
436            cells[end_idx] = if is_existing && already_shown {
437                GraphCell::CurveDownLeft { color_idx } // ╯ マージ終了
438            } else if is_existing {
439                GraphCell::TeeLeft { color_idx } // ┤ T字接続(下に続く)
440            } else {
441                GraphCell::CurveUpLeft { color_idx } // ╮ 新しいブランチ開始
442            };
443        }
444    } else {
445        // 左端の記号
446        let left_idx = left_col * 2;
447        if left_idx < cells.len() {
448            cells[left_idx] = if is_existing && already_shown {
449                GraphCell::CurveDownRight { color_idx } // ╰ マージ終了
450            } else if is_existing {
451                GraphCell::TeeRight { color_idx } // ├ T字接続(下に続く)
452            } else {
453                GraphCell::CurveUpRight { color_idx } // ╭ 新しいブランチ開始
454            };
455        }
456    }
457}
458
459/// 分岐点コネクタ行のセルを構築
460/// 複数の列が同じコミットに合流する際の視覚化
461/// 例: ├─┴─╯ (メイン列からT字、中間は┴、最右は╯)
462fn build_connector_cells(
463    main_col: usize,
464    merging_cols: &[usize],
465    active_columns: &[Option<String>],
466    column_colors: &HashMap<usize, usize>,
467    max_col: usize,
468) -> Vec<GraphCell> {
469    let cell_count = (max_col + 1) * 2;
470    let mut cells = vec![GraphCell::Empty; cell_count];
471
472    // 合流列をソート(左から右へ処理)
473    let mut sorted_merging: Vec<usize> = merging_cols.to_vec();
474    sorted_merging.sort();
475
476    // 最右の合流列
477    let rightmost_col = sorted_merging.last().copied().unwrap_or(main_col);
478
479    // 1. アクティブ列に縦線を描画(メイン列と合流列以外)
480    for (col, hash) in active_columns.iter().enumerate() {
481        if hash.is_some() && col != main_col && !sorted_merging.contains(&col) {
482            let cell_idx = col * 2;
483            if cell_idx < cells.len() {
484                let color_idx = column_colors.get(&col).copied().unwrap_or(col % 8);
485                cells[cell_idx] = GraphCell::Vertical { color_idx };
486            }
487        }
488    }
489
490    // 2. メイン列にT字右(├)を描画
491    let main_color = column_colors.get(&main_col).copied().unwrap_or(0);
492    let main_cell_idx = main_col * 2;
493    if main_cell_idx < cells.len() {
494        cells[main_cell_idx] = GraphCell::TeeRight {
495            color_idx: main_color,
496        };
497    }
498
499    // 3. メイン列から各合流列へ横線を描画
500    for &merge_col in &sorted_merging {
501        let merge_color = column_colors
502            .get(&merge_col)
503            .copied()
504            .unwrap_or(merge_col % 8);
505
506        // メイン列から合流列への横線
507        let start_idx = main_col * 2 + 1;
508        let end_idx = merge_col * 2;
509
510        for idx in start_idx..end_idx {
511            if idx < cells.len() {
512                match cells[idx] {
513                    GraphCell::Vertical { color_idx: v_color } => {
514                        // 縦線との交差
515                        cells[idx] = GraphCell::Cross {
516                            h_color: merge_color,
517                            v_color,
518                        };
519                    }
520                    GraphCell::Empty => {
521                        cells[idx] = GraphCell::Horizontal {
522                            color_idx: merge_color,
523                        };
524                    }
525                    GraphCell::Horizontal { .. } => {
526                        // 既に横線がある場合はそのまま
527                    }
528                    _ => {}
529                }
530            }
531        }
532
533        // 合流列の終端記号
534        if end_idx < cells.len() {
535            if merge_col == rightmost_col {
536                // 最右列は╯(合流終了)
537                cells[end_idx] = GraphCell::CurveDownLeft {
538                    color_idx: merge_color,
539                };
540            } else {
541                // 中間列は┴(T字上、さらに右へ続く)
542                cells[end_idx] = GraphCell::TeeUp {
543                    color_idx: merge_color,
544                };
545            }
546        }
547    }
548
549    cells
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use chrono::Local;
556
557    fn create_event(hash: &str, parents: Vec<&str>) -> GitEvent {
558        let mut event = GitEvent::commit(
559            hash.to_string(),
560            format!("commit {}", hash),
561            "author".to_string(),
562            Local::now(),
563            0,
564            0,
565        );
566        event.parent_hashes = parents.into_iter().map(|s| s.to_string()).collect();
567        event
568    }
569
570    #[test]
571    fn test_build_graph_empty() {
572        let layout = build_graph(&[], None);
573        assert!(layout.is_empty());
574    }
575
576    #[test]
577    fn test_build_graph_single_commit() {
578        let events = vec![create_event("abc1234", vec![])];
579        let layout = build_graph(&events, None);
580        assert_eq!(layout.len(), 1);
581        assert_eq!(layout.rows[0].column, 0);
582    }
583
584    #[test]
585    fn test_build_graph_linear_history() {
586        let events = vec![
587            create_event("commit3", vec!["commit2"]),
588            create_event("commit2", vec!["commit1"]),
589            create_event("commit1", vec![]),
590        ];
591        let layout = build_graph(&events, None);
592        assert_eq!(layout.len(), 3);
593        // 全て同じ列
594        assert_eq!(layout.rows[0].column, 0);
595        assert_eq!(layout.rows[1].column, 0);
596        assert_eq!(layout.rows[2].column, 0);
597    }
598
599    #[test]
600    fn test_build_graph_with_merge() {
601        // マージコミットを作成(2つの親を持つ)
602        let mut merge_event = create_event("merge", vec!["main2", "feat1"]);
603        merge_event.kind = GitEventKind::Merge;
604
605        let events = vec![
606            merge_event,
607            create_event("feat1", vec!["main1"]),
608            create_event("main2", vec!["main1"]),
609            create_event("main1", vec![]),
610        ];
611        let layout = build_graph(&events, None);
612        // イベント行は4つ(コネクタ行が追加される可能性あり)
613        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
614        assert_eq!(event_rows.len(), 4);
615        // マージコミットはセルにMergeNodeを持つ
616        assert!(event_rows[0]
617            .cells
618            .iter()
619            .any(|c| matches!(c, GraphCell::MergeNode { .. })));
620    }
621
622    #[test]
623    fn test_build_graph_head_marker() {
624        let events = vec![create_event("abc1234", vec![])];
625        let layout = build_graph(&events, Some("abc1234"));
626        assert!(layout.rows[0].is_head);
627        assert!(layout.rows[0]
628            .cells
629            .iter()
630            .any(|c| matches!(c, GraphCell::HeadNode { .. })));
631    }
632
633    #[test]
634    fn test_column_tracker_find_or_create() {
635        let mut tracker = ColumnTracker::new();
636        let col1 = tracker.find_or_create_column();
637        tracker.set_column(col1, "hash1".to_string(), 0);
638        let col2 = tracker.find_or_create_column();
639        assert_ne!(col1, col2);
640    }
641
642    #[test]
643    fn test_column_tracker_release() {
644        let mut tracker = ColumnTracker::new();
645        let col = tracker.find_or_create_column();
646        tracker.set_column(col, "hash1".to_string(), 0);
647        tracker.release_column(col);
648        assert!(tracker.find_column("hash1").is_none());
649    }
650
651    /// 複数ブランチが並行する場合のテスト
652    ///
653    /// コミット履歴:
654    /// ```
655    /// merge ─┬─ main2 ─┬─ base
656    ///        └─ feat1 ─┘
657    /// ```
658    #[test]
659    fn test_parallel_branches() {
660        let mut merge_event = create_event("merge", vec!["main2", "feat1"]);
661        merge_event.kind = GitEventKind::Merge;
662
663        let events = vec![
664            merge_event,
665            create_event("main2", vec!["base"]),
666            create_event("feat1", vec!["base"]),
667            create_event("base", vec![]),
668        ];
669        let layout = build_graph(&events, None);
670        // イベント行は4つ(コネクタ行が追加される可能性あり)
671        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
672        assert_eq!(event_rows.len(), 4);
673
674        // main2とfeat1は異なる列に配置されるはず
675        let main2_row = event_rows
676            .iter()
677            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "main2"))
678            .unwrap();
679        let feat1_row = event_rows
680            .iter()
681            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "feat1"))
682            .unwrap();
683        assert_ne!(main2_row.column, feat1_row.column);
684    }
685
686    /// 分岐点のテスト(同じコミットから複数ブランチが分岐)
687    ///
688    /// コミット履歴:
689    /// ```
690    /// feat2 ─┐
691    /// feat1 ─┼─ base
692    /// main  ─┘
693    /// ```
694    #[test]
695    fn test_fork_point() {
696        let events = vec![
697            create_event("feat2", vec!["base"]),
698            create_event("feat1", vec!["base"]),
699            create_event("main", vec!["base"]),
700            create_event("base", vec![]),
701        ];
702        let layout = build_graph(&events, None);
703        // イベント行は4つ(コネクタ行が追加される可能性あり)
704        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
705        assert_eq!(event_rows.len(), 4);
706
707        // 全てbaseを親に持つコミットは異なる列またはbaseに統合される
708        // baseは列0に配置されるはず
709        let base_row = event_rows
710            .iter()
711            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "base"))
712            .unwrap();
713        assert_eq!(base_row.column, 0);
714    }
715
716    /// 色割り当ての一意性テスト
717    #[test]
718    fn test_color_assignment_unique() {
719        let mut merge_event = create_event("merge", vec!["main2", "feat1"]);
720        merge_event.kind = GitEventKind::Merge;
721
722        let events = vec![
723            merge_event,
724            create_event("main2", vec!["base"]),
725            create_event("feat1", vec!["base"]),
726            create_event("base", vec![]),
727        ];
728        let layout = build_graph(&events, None);
729
730        // イベント行のみを抽出
731        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
732
733        // main2とfeat1のイベント行を取得
734        let main2_row = event_rows
735            .iter()
736            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "main2"))
737            .unwrap();
738        let feat1_row = event_rows
739            .iter()
740            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "feat1"))
741            .unwrap();
742
743        // 異なる列のコミットは異なる色を持つはず
744        // feat1は新しい列に配置されるので、異なる色が割り当てられる
745        if main2_row.column != feat1_row.column {
746            assert_ne!(main2_row.color_idx, feat1_row.color_idx);
747        }
748    }
749
750    /// 長いマージチェーンのテスト
751    #[test]
752    fn test_long_merge_chain() {
753        let mut merge1 = create_event("merge1", vec!["main3", "feat1"]);
754        merge1.kind = GitEventKind::Merge;
755
756        let events = vec![
757            merge1,
758            create_event("main3", vec!["main2"]),
759            create_event("feat1", vec!["main1"]),
760            create_event("main2", vec!["main1"]),
761            create_event("main1", vec![]),
762        ];
763        let layout = build_graph(&events, Some("merge1"));
764        // イベント行は5つ(コネクタ行が追加される可能性あり)
765        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
766        assert_eq!(event_rows.len(), 5);
767
768        // マージコミット(最初のイベント行)はHeadNodeを持つ
769        assert!(event_rows[0]
770            .cells
771            .iter()
772            .any(|c| matches!(c, GraphCell::HeadNode { .. })));
773    }
774
775    /// 横線接続のテスト
776    #[test]
777    fn test_horizontal_connection() {
778        let mut merge_event = create_event("merge", vec!["main", "feat"]);
779        merge_event.kind = GitEventKind::Merge;
780
781        let events = vec![
782            merge_event,
783            create_event("main", vec!["base"]),
784            create_event("feat", vec!["base"]),
785            create_event("base", vec![]),
786        ];
787        let layout = build_graph(&events, None);
788
789        // マージコミットは横線(Horizontal)または曲線(Curve)を含むはず
790        let has_connection = layout.rows[0].cells.iter().any(|c| {
791            matches!(
792                c,
793                GraphCell::Horizontal { .. }
794                    | GraphCell::CurveUpRight { .. }
795                    | GraphCell::CurveUpLeft { .. }
796                    | GraphCell::CurveDownRight { .. }
797                    | GraphCell::CurveDownLeft { .. }
798            )
799        });
800        assert!(has_connection);
801    }
802
803    /// 分岐点コネクタ行のテスト
804    /// 複数のブランチが同じコミットから分岐する場合、コネクタ行が挿入される
805    ///
806    /// コミット履歴:
807    /// ```
808    /// feat2 ─┐
809    /// feat1 ─┼─ base
810    /// ```
811    #[test]
812    fn test_convergence_connector_row() {
813        // feat1とfeat2が同じbaseから分岐
814        let events = vec![
815            create_event("feat2", vec!["base"]),
816            create_event("feat1", vec!["base"]),
817            create_event("base", vec![]),
818        ];
819        let layout = build_graph(&events, None);
820
821        // feat2, feat1, コネクタ行, base の4行
822        // コネクタ行が挿入されている場合、行数が4になる
823        assert_eq!(
824            layout.len(),
825            4,
826            "Expected 4 rows (feat2, feat1, connector, base)"
827        );
828
829        // コネクタ行があればTeeRightを含む
830        let connector_row = layout.rows.iter().find(|row| row.event.is_none());
831        assert!(
832            connector_row.is_some(),
833            "Expected a connector row (event=None)"
834        );
835
836        let connector_row = connector_row.unwrap();
837        let has_tee_right = connector_row
838            .cells
839            .iter()
840            .any(|c| matches!(c, GraphCell::TeeRight { .. }));
841        assert!(has_tee_right, "Connector row should have TeeRight symbol");
842
843        // baseの行は必ず存在
844        let has_base = layout
845            .rows
846            .iter()
847            .any(|row| row.event.as_ref().is_some_and(|e| e.short_hash == "base"));
848        assert!(has_base);
849    }
850
851    /// コネクタ行のセル配置を厳密に検証
852    /// コネクタ行は ├─╯ の形式で、メイン列にTeeRight、横線、右端にCurveDownLeft
853    #[test]
854    fn test_connector_row_cell_layout() {
855        let events = vec![
856            create_event("feat2", vec!["base"]),
857            create_event("feat1", vec!["base"]),
858            create_event("base", vec![]),
859        ];
860        let layout = build_graph(&events, None);
861
862        // コネクタ行を取得
863        let connector_row = layout
864            .rows
865            .iter()
866            .find(|row| row.event.is_none())
867            .expect("Connector row should exist");
868
869        // セル配列を文字列に変換して検証
870        let cells_str: String = connector_row.cells.iter().map(|c| c.to_char()).collect();
871
872        // TeeRight (├) が含まれる
873        assert!(
874            cells_str.contains('├'),
875            "Connector row should contain TeeRight (├): got '{}'",
876            cells_str
877        );
878
879        // CurveDownLeft (╯) が含まれる(最右の合流列)
880        assert!(
881            cells_str.contains('╯'),
882            "Connector row should contain CurveDownLeft (╯): got '{}'",
883            cells_str
884        );
885    }
886
887    /// マージコミットの横線接続を厳密に検証
888    /// マージコミットは第2親への横線(Horizontal)と曲線(CurveUpLeft)を含む
889    #[test]
890    fn test_merge_commit_horizontal_line() {
891        let mut merge_event = create_event("merge", vec!["main", "feat"]);
892        merge_event.kind = GitEventKind::Merge;
893
894        let events = vec![
895            merge_event,
896            create_event("main", vec!["base"]),
897            create_event("feat", vec!["base"]),
898            create_event("base", vec![]),
899        ];
900        let layout = build_graph(&events, None);
901
902        // マージ行を取得
903        let merge_row = layout
904            .rows
905            .iter()
906            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "merge"))
907            .expect("Merge row should exist");
908
909        // MergeNodeまたはHeadNodeが存在
910        let has_merge_or_head_node = merge_row
911            .cells
912            .iter()
913            .any(|c| matches!(c, GraphCell::MergeNode { .. } | GraphCell::HeadNode { .. }));
914        assert!(has_merge_or_head_node, "Merge row should have MergeNode");
915
916        // 横線(Horizontal)が存在
917        let has_horizontal = merge_row
918            .cells
919            .iter()
920            .any(|c| matches!(c, GraphCell::Horizontal { .. }));
921        assert!(
922            has_horizontal,
923            "Merge row should have Horizontal line for second parent"
924        );
925
926        // 曲線(CurveUpLeft)が存在(新しいブランチ開始)
927        let has_curve = merge_row
928            .cells
929            .iter()
930            .any(|c| matches!(c, GraphCell::CurveUpLeft { .. }));
931        assert!(
932            has_curve,
933            "Merge row should have CurveUpLeft for branch start"
934        );
935    }
936
937    /// 分岐コミットでは横線が描画されないことを検証
938    /// ブランチを切った先のコミットは親と同じ列で縦線のみ
939    #[test]
940    fn test_branch_commit_no_horizontal_line() {
941        // mainから分岐したfeatブランチ
942        // main: base -> main1
943        // feat: base -> feat1 (分岐コミット)
944        let events = vec![
945            create_event("feat1", vec!["base"]),
946            create_event("main1", vec!["base"]),
947            create_event("base", vec![]),
948        ];
949        let layout = build_graph(&events, None);
950
951        // feat1の行を取得
952        let feat1_row = layout
953            .rows
954            .iter()
955            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "feat1"))
956            .expect("feat1 row should exist");
957
958        // feat1行には横線(Horizontal)がない
959        // 分岐コミットは横線を描画しない
960        let has_horizontal = feat1_row
961            .cells
962            .iter()
963            .any(|c| matches!(c, GraphCell::Horizontal { .. }));
964        assert!(
965            !has_horizontal,
966            "Branch commit should NOT have Horizontal line"
967        );
968    }
969
970    /// 親が表示範囲外の場合でも縦線が継続することを検証
971    #[test]
972    fn test_parent_outside_display_range() {
973        // commit2の親commit1は表示範囲内だが、commit1の親は範囲外
974        let events = vec![
975            create_event("commit2", vec!["commit1"]),
976            create_event("commit1", vec!["outside_parent"]), // outside_parentは範囲外
977        ];
978        let layout = build_graph(&events, None);
979
980        assert_eq!(layout.len(), 2);
981
982        // commit1の行でも縦線が継続している(列が追跡されている)
983        // 親が範囲外でも列は追跡される
984        let commit1_row = layout
985            .rows
986            .iter()
987            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "commit1"))
988            .expect("commit1 row should exist");
989
990        // commit1は列0に配置
991        assert_eq!(commit1_row.column, 0);
992    }
993
994    /// 3つ以上のブランチが並行する場合のテスト
995    #[test]
996    fn test_three_parallel_branches() {
997        let mut merge = create_event("merge", vec!["main3", "feat1", "feat2"]);
998        merge.kind = GitEventKind::Merge;
999
1000        let events = vec![
1001            merge,
1002            create_event("main3", vec!["main2"]),
1003            create_event("feat1", vec!["main1"]),
1004            create_event("feat2", vec!["main1"]),
1005            create_event("main2", vec!["main1"]),
1006            create_event("main1", vec![]),
1007        ];
1008        let layout = build_graph(&events, None);
1009
1010        // イベント行は6つ
1011        let event_rows: Vec<_> = layout.rows.iter().filter(|r| r.event.is_some()).collect();
1012        assert_eq!(event_rows.len(), 6);
1013
1014        // 最大列数が2以上(3つのブランチが並行)
1015        assert!(
1016            layout.max_column >= 2,
1017            "Should have at least 3 columns for 3 parallel branches"
1018        );
1019    }
1020
1021    /// 線形履歴では全てのコミットが同じ列に配置される
1022    #[test]
1023    fn test_linear_history_same_column() {
1024        let events = vec![
1025            create_event("c5", vec!["c4"]),
1026            create_event("c4", vec!["c3"]),
1027            create_event("c3", vec!["c2"]),
1028            create_event("c2", vec!["c1"]),
1029            create_event("c1", vec![]),
1030        ];
1031        let layout = build_graph(&events, None);
1032
1033        // 全てのコミットが列0
1034        for row in &layout.rows {
1035            assert_eq!(
1036                row.column, 0,
1037                "All commits in linear history should be in column 0"
1038            );
1039        }
1040
1041        // コネクタ行がない(分岐がない)
1042        let connector_count = layout.rows.iter().filter(|r| r.event.is_none()).count();
1043        assert_eq!(
1044            connector_count, 0,
1045            "Linear history should have no connector rows"
1046        );
1047    }
1048
1049    /// 縦線(Vertical)の色が正しいことを検証
1050    #[test]
1051    fn test_vertical_line_colors() {
1052        let mut merge = create_event("merge", vec!["main", "feat"]);
1053        merge.kind = GitEventKind::Merge;
1054
1055        let events = vec![
1056            merge,
1057            create_event("feat", vec!["base"]),
1058            create_event("main", vec!["base"]),
1059            create_event("base", vec![]),
1060        ];
1061        let layout = build_graph(&events, None);
1062
1063        // 各行の縦線の色を確認
1064        for row in layout.rows.iter().filter(|r| r.event.is_some()) {
1065            for cell in &row.cells {
1066                if let GraphCell::Vertical { color_idx } = cell {
1067                    // 色インデックスは有効な範囲内
1068                    assert!(*color_idx < 8, "Color index should be < 8");
1069                }
1070            }
1071        }
1072    }
1073
1074    /// Cross(交差)が正しく生成されることを検証
1075    #[test]
1076    fn test_cross_generation() {
1077        // 複雑なマージで交差が発生するケース
1078        // main: base -> main1 -> main2
1079        // feat1: base -> feat1
1080        // feat2: base -> feat2
1081        // merge: main2 + feat1 + feat2
1082        let mut merge = create_event("merge", vec!["main2", "feat1", "feat2"]);
1083        merge.kind = GitEventKind::Merge;
1084
1085        let events = vec![
1086            merge,
1087            create_event("main2", vec!["main1"]),
1088            create_event("feat1", vec!["base"]),
1089            create_event("feat2", vec!["base"]),
1090            create_event("main1", vec!["base"]),
1091            create_event("base", vec![]),
1092        ];
1093        let layout = build_graph(&events, None);
1094
1095        // 交差(Cross)が存在するか確認
1096        // 注: 交差が発生するかどうかは構成による
1097        let has_cross = layout
1098            .rows
1099            .iter()
1100            .any(|r| r.cells.iter().any(|c| matches!(c, GraphCell::Cross { .. })));
1101
1102        // 交差がある場合、h_colorとv_colorが設定されている
1103        if has_cross {
1104            for row in &layout.rows {
1105                for cell in &row.cells {
1106                    if let GraphCell::Cross { h_color, v_color } = cell {
1107                        assert!(*h_color < 8, "h_color should be valid");
1108                        assert!(*v_color < 8, "v_color should be valid");
1109                    }
1110                }
1111            }
1112        }
1113    }
1114
1115    /// マージ後の列解放を検証
1116    #[test]
1117    fn test_column_release_after_merge() {
1118        let mut merge1 = create_event("merge1", vec!["main2", "feat1"]);
1119        merge1.kind = GitEventKind::Merge;
1120
1121        let events = vec![
1122            merge1,
1123            create_event("feat1", vec!["main1"]),
1124            create_event("main2", vec!["main1"]),
1125            create_event("main1", vec![]),
1126            create_event("after_merge", vec!["main1"]), // マージ後のコミット
1127        ];
1128        let layout = build_graph(&events, None);
1129
1130        // マージ後は列が解放され、再利用される
1131        // max_columnが過度に大きくならない
1132        assert!(
1133            layout.max_column <= 2,
1134            "Columns should be reused after merge"
1135        );
1136    }
1137
1138    /// ノード種別が正しく設定されることを検証
1139    #[test]
1140    fn test_node_types() {
1141        let mut merge = create_event("merge", vec!["main", "feat"]);
1142        merge.kind = GitEventKind::Merge;
1143
1144        let events = vec![
1145            merge,
1146            create_event("main", vec!["base"]),
1147            create_event("base", vec![]),
1148        ];
1149
1150        // HEADなし
1151        let layout_no_head = build_graph(&events, None);
1152        let merge_row = layout_no_head
1153            .rows
1154            .iter()
1155            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "merge"))
1156            .unwrap();
1157        assert!(
1158            merge_row
1159                .cells
1160                .iter()
1161                .any(|c| matches!(c, GraphCell::MergeNode { .. })),
1162            "Merge commit without HEAD should have MergeNode"
1163        );
1164
1165        // HEADあり
1166        let layout_with_head = build_graph(&events, Some("merge"));
1167        let merge_row_head = layout_with_head
1168            .rows
1169            .iter()
1170            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "merge"))
1171            .unwrap();
1172        assert!(
1173            merge_row_head
1174                .cells
1175                .iter()
1176                .any(|c| matches!(c, GraphCell::HeadNode { .. })),
1177            "Merge commit with HEAD should have HeadNode"
1178        );
1179
1180        // 通常コミット
1181        let normal_row = layout_no_head
1182            .rows
1183            .iter()
1184            .find(|r| r.event.as_ref().is_some_and(|e| e.short_hash == "main"))
1185            .unwrap();
1186        assert!(
1187            normal_row
1188                .cells
1189                .iter()
1190                .any(|c| matches!(c, GraphCell::Node { .. })),
1191            "Normal commit should have Node"
1192        );
1193    }
1194
1195    /// TeeLeft (┤) の生成を検証
1196    #[test]
1197    fn test_tee_left_generation() {
1198        let mut merge = create_event("merge", vec!["main", "feat"]);
1199        merge.kind = GitEventKind::Merge;
1200
1201        let events = vec![
1202            merge,
1203            // featを先に配置して左方向にmergeを接続
1204            create_event("feat", vec!["base"]),
1205            create_event("main", vec!["base"]),
1206            create_event("base", vec![]),
1207        ];
1208        let layout = build_graph(&events, None);
1209
1210        // TeeLeft (┤) または CurveUpLeft (╮) が存在
1211        let has_tee_or_curve = layout.rows.iter().any(|r| {
1212            r.cells
1213                .iter()
1214                .any(|c| matches!(c, GraphCell::TeeLeft { .. } | GraphCell::CurveUpLeft { .. }))
1215        });
1216        assert!(
1217            has_tee_or_curve,
1218            "Should have TeeLeft or CurveUpLeft for rightward connection"
1219        );
1220    }
1221
1222    /// 空のイベントリストでパニックしないことを検証
1223    #[test]
1224    fn test_empty_events_no_panic() {
1225        let layout = build_graph(&[], None);
1226        assert!(layout.is_empty());
1227        assert_eq!(layout.max_column, 0);
1228    }
1229
1230    /// 単一コミットでパニックしないことを検証
1231    #[test]
1232    fn test_single_commit_no_panic() {
1233        let events = vec![create_event("only", vec![])];
1234        let layout = build_graph(&events, Some("only"));
1235
1236        assert_eq!(layout.len(), 1);
1237        assert!(layout.rows[0].is_head);
1238        assert!(layout.rows[0]
1239            .cells
1240            .iter()
1241            .any(|c| matches!(c, GraphCell::HeadNode { .. })));
1242    }
1243
1244    /// 複数マージが連続する場合、縦線が正しく描画されることを検証
1245    /// (v2.10.3でのバグ修正: 早期列解放による縦線消失の防止)
1246    #[test]
1247    fn test_consecutive_merges_vertical_lines_preserved() {
1248        // 構造:
1249        // merge2 ─┬─ feat2 ─────────┐
1250        //         │                 │
1251        //         └─ main2 ─┬─ merge1
1252        //                   └─ feat1
1253        let mut merge2 = create_event("merge2", vec!["main2", "feat2"]);
1254        merge2.kind = GitEventKind::Merge;
1255        let mut merge1 = create_event("merge1", vec!["base", "feat1"]);
1256        merge1.kind = GitEventKind::Merge;
1257
1258        let events = vec![
1259            merge2,
1260            create_event("feat2", vec!["merge1"]),
1261            create_event("main2", vec!["merge1"]),
1262            merge1,
1263            create_event("feat1", vec!["base"]),
1264            create_event("base", vec![]),
1265        ];
1266        let layout = build_graph(&events, None);
1267
1268        // feat2とmain2の間のコネクタ行があれば、縦線が存在するか確認
1269        // 全行で縦線セルの数をカウント
1270        let mut vertical_count = 0;
1271        for row in &layout.rows {
1272            for cell in &row.cells {
1273                if matches!(cell, GraphCell::Vertical { .. }) {
1274                    vertical_count += 1;
1275                }
1276            }
1277        }
1278
1279        // 縦線が適切に描画されていることを確認(0でなければOK)
1280        assert!(
1281            vertical_count > 0,
1282            "Vertical lines should be preserved between consecutive merges"
1283        );
1284
1285        // 列が過度に大きくならないことも確認
1286        assert!(
1287            layout.max_column <= 3,
1288            "Columns should be reused efficiently, got max_column={}",
1289            layout.max_column
1290        );
1291    }
1292
1293    /// 10本以上の並列ブランチが正しく描画されることを検証
1294    #[test]
1295    fn test_many_parallel_branches() {
1296        // 構造: 12本のブランチが同じbaseから分岐し、最終的にマージ
1297        //
1298        // final_merge ─┬─ branch01 ─┐
1299        //              ├─ branch02 ─┤
1300        //              ├─ branch03 ─┤
1301        //              ├─ branch04 ─┤
1302        //              ├─ branch05 ─┤
1303        //              ├─ branch06 ─┤
1304        //              ├─ branch07 ─┤
1305        //              ├─ branch08 ─┤
1306        //              ├─ branch09 ─┤
1307        //              ├─ branch10 ─┤
1308        //              ├─ branch11 ─┤
1309        //              └─ branch12 ─┴─ base
1310
1311        let mut events = vec![];
1312
1313        // final_merge: 12本のブランチをマージ
1314        let mut final_merge = create_event(
1315            "final",
1316            vec![
1317                "branch01", "branch02", "branch03", "branch04", "branch05", "branch06", "branch07",
1318                "branch08", "branch09", "branch10", "branch11", "branch12",
1319            ],
1320        );
1321        final_merge.kind = GitEventKind::Merge;
1322        events.push(final_merge);
1323
1324        // 各ブランチ(全て同じbaseを親に持つ)
1325        for i in 1..=12 {
1326            events.push(create_event(&format!("branch{:02}", i), vec!["base"]));
1327        }
1328
1329        // base コミット
1330        events.push(create_event("base", vec![]));
1331
1332        let layout = build_graph(&events, None);
1333
1334        // パニックせずに完了することを確認
1335        assert!(!layout.rows.is_empty());
1336
1337        // 各ブランチが異なる列に配置されていることを確認
1338        let mut branch_columns: Vec<usize> = vec![];
1339        for row in &layout.rows {
1340            if let Some(ref event) = row.event {
1341                if event.short_hash.starts_with("branch") {
1342                    branch_columns.push(row.column);
1343                }
1344            }
1345        }
1346        // 12本のブランチ列が存在
1347        assert_eq!(branch_columns.len(), 12);
1348
1349        // max_columnが少なくとも11(0から11で12列)以上
1350        assert!(
1351            layout.max_column >= 11,
1352            "Expected at least 12 columns for 12 parallel branches, got max_column={}",
1353            layout.max_column
1354        );
1355    }
1356
1357    /// 複雑な交差パターンが正しく描画されることを検証
1358    #[test]
1359    fn test_complex_cross_pattern() {
1360        // 構造: 左右のブランチが交差するパターン
1361        //
1362        // merge_left ─┬─ left_branch ─────┬─ base
1363        //             └─ middle ─┬─ right_branch
1364        //                        └─ right2
1365        //
1366        // left_branchとright_branchの線が交差する
1367
1368        let mut merge_left = create_event("merge_left", vec!["left_branch", "middle"]);
1369        merge_left.kind = GitEventKind::Merge;
1370
1371        let mut middle = create_event("middle", vec!["right_branch", "right2"]);
1372        middle.kind = GitEventKind::Merge;
1373
1374        let events = vec![
1375            merge_left,
1376            create_event("left_branch", vec!["base"]),
1377            middle,
1378            create_event("right_branch", vec!["base"]),
1379            create_event("right2", vec!["base"]),
1380            create_event("base", vec![]),
1381        ];
1382
1383        let layout = build_graph(&events, None);
1384
1385        // パニックせずに完了
1386        assert!(!layout.rows.is_empty());
1387
1388        // 交差セル(Cross)が存在するか確認(交差がある場合)
1389        let has_any_horizontal = layout.rows.iter().any(|row| {
1390            row.cells.iter().any(|c| {
1391                matches!(
1392                    c,
1393                    GraphCell::Horizontal { .. }
1394                        | GraphCell::Cross { .. }
1395                        | GraphCell::TeeLeft { .. }
1396                        | GraphCell::TeeRight { .. }
1397                )
1398            })
1399        });
1400        // マージがあれば横線関連のセルが存在するはず
1401        assert!(
1402            has_any_horizontal,
1403            "Complex merge pattern should have horizontal connections"
1404        );
1405    }
1406
1407    /// 16色を超えるブランチでもラップアラウンドで正しく動作することを検証
1408    #[test]
1409    fn test_color_wraparound_with_many_branches() {
1410        // 20本のブランチを作成(16色パレットを超える)
1411        let events: Vec<GitEvent> = (1..=20)
1412            .map(|i| {
1413                let hash = format!("commit{:02}", i);
1414                let parent = if i == 1 {
1415                    vec![]
1416                } else {
1417                    vec![format!("commit{:02}", i - 1)]
1418                };
1419                let mut event = GitEvent::commit(
1420                    hash.clone(),
1421                    format!("commit {}", hash),
1422                    "author".to_string(),
1423                    Local::now(),
1424                    0,
1425                    0,
1426                );
1427                event.parent_hashes = parent;
1428                event
1429            })
1430            .collect();
1431
1432        let layout = build_graph(&events, None);
1433
1434        // パニックせずに完了
1435        assert_eq!(layout.rows.len(), 20);
1436
1437        // 色インデックスがパレットサイズ内に収まっていることを確認
1438        for row in &layout.rows {
1439            assert!(
1440                row.color_idx < 16,
1441                "Color index {} should be within palette size",
1442                row.color_idx
1443            );
1444        }
1445    }
1446
1447    /// 深いネストのマージが正しく処理されることを検証
1448    #[test]
1449    fn test_deeply_nested_merges() {
1450        // 構造: 連続したマージのチェーン
1451        //
1452        // merge5 ─┬─ merge4 ─┬─ merge3 ─┬─ merge2 ─┬─ merge1 ─┬─ main
1453        //         │          │          │          │          └─ branch1
1454        //         │          │          │          └─ branch2
1455        //         │          │          └─ branch3
1456        //         │          └─ branch4
1457        //         └─ branch5
1458
1459        let mut events = vec![];
1460
1461        for i in (1..=5).rev() {
1462            let parent1 = if i == 1 {
1463                "main".to_string()
1464            } else {
1465                format!("merge{}", i - 1)
1466            };
1467            let parent2 = format!("branch{}", i);
1468            let mut merge = create_event(&format!("merge{}", i), vec![]);
1469            merge.parent_hashes = vec![parent1, parent2];
1470            merge.kind = GitEventKind::Merge;
1471            events.push(merge);
1472        }
1473
1474        for i in (1..=5).rev() {
1475            events.push(create_event(&format!("branch{}", i), vec!["base"]));
1476        }
1477        events.push(create_event("main", vec!["base"]));
1478        events.push(create_event("base", vec![]));
1479
1480        let layout = build_graph(&events, None);
1481
1482        // パニックせずに完了
1483        assert!(!layout.rows.is_empty());
1484
1485        // マージノードが5つ存在することを確認
1486        let merge_count = layout
1487            .rows
1488            .iter()
1489            .filter(|row| {
1490                row.cells
1491                    .iter()
1492                    .any(|c| matches!(c, GraphCell::MergeNode { .. }))
1493            })
1494            .count();
1495        assert_eq!(merge_count, 5, "Should have 5 merge nodes");
1496    }
1497
1498    /// ダイヤモンドパターン(分岐→マージ)が連続しても線が途切れないことを検証
1499    #[test]
1500    fn test_diamond_pattern_chain() {
1501        // 構造: ダイヤモンドパターンが3回連続
1502        //
1503        // merge3 ─┬─ feat3 ─┬─ merge2 ─┬─ feat2 ─┬─ merge1 ─┬─ feat1 ─┬─ base
1504        //         └─────────┘          └─────────┘          └─────────┘
1505        //
1506        // 各ダイヤモンドで分岐→並行→マージが発生
1507
1508        let mut events = vec![];
1509
1510        // 3つのダイヤモンドパターン
1511        for i in (1..=3).rev() {
1512            let parent = if i == 3 {
1513                "base".to_string()
1514            } else {
1515                format!("merge{}", i + 1)
1516            };
1517
1518            // マージコミット
1519            let mut merge = create_event(&format!("merge{}", i), vec![]);
1520            merge.parent_hashes = vec![format!("feat{}", i), parent.clone()];
1521            merge.kind = GitEventKind::Merge;
1522            events.push(merge);
1523
1524            // フィーチャーブランチ
1525            events.push(create_event(&format!("feat{}", i), vec![&parent]));
1526        }
1527
1528        events.push(create_event("base", vec![]));
1529
1530        let layout = build_graph(&events, None);
1531
1532        // パニックせずに完了
1533        assert!(!layout.rows.is_empty());
1534
1535        // マージノードが3つ
1536        let merge_count = layout
1537            .rows
1538            .iter()
1539            .filter(|row| {
1540                row.cells
1541                    .iter()
1542                    .any(|c| matches!(c, GraphCell::MergeNode { .. }))
1543            })
1544            .count();
1545        assert_eq!(merge_count, 3, "Should have 3 merge nodes");
1546
1547        // 縦線が存在すること(ツリー構造が保持されている)
1548        let vertical_count: usize = layout
1549            .rows
1550            .iter()
1551            .map(|row| {
1552                row.cells
1553                    .iter()
1554                    .filter(|c| matches!(c, GraphCell::Vertical { .. }))
1555                    .count()
1556            })
1557            .sum();
1558
1559        assert!(
1560            vertical_count > 0,
1561            "Vertical lines should exist in diamond pattern chain"
1562        );
1563    }
1564
1565    /// 交互マージパターン(左右から交互にマージ)でも線が途切れないことを検証
1566    #[test]
1567    fn test_alternating_merge_pattern() {
1568        // 構造: 左右から交互にマージされるパターン
1569        //
1570        // final ─┬─ right3 ───────────────────────────┐
1571        //        │                                    │
1572        //        └─ merge_r2 ─┬─ left2 ───────────────┼─┐
1573        //                     │                       │ │
1574        //                     └─ merge_l1 ─┬─ right1 ─┘ │
1575        //                                  │            │
1576        //                                  └─ left1 ────┴─ base
1577
1578        let mut final_merge = create_event("final", vec!["merge_r2", "right3"]);
1579        final_merge.kind = GitEventKind::Merge;
1580
1581        let mut merge_r2 = create_event("merge_r2", vec!["merge_l1", "left2"]);
1582        merge_r2.kind = GitEventKind::Merge;
1583
1584        let mut merge_l1 = create_event("merge_l1", vec!["left1", "right1"]);
1585        merge_l1.kind = GitEventKind::Merge;
1586
1587        let events = vec![
1588            final_merge,
1589            create_event("right3", vec!["base"]),
1590            merge_r2,
1591            create_event("left2", vec!["base"]),
1592            merge_l1,
1593            create_event("right1", vec!["base"]),
1594            create_event("left1", vec!["base"]),
1595            create_event("base", vec![]),
1596        ];
1597
1598        let layout = build_graph(&events, None);
1599
1600        // パニックせずに完了
1601        assert!(!layout.rows.is_empty());
1602
1603        // マージノードが3つ
1604        let merge_count = layout
1605            .rows
1606            .iter()
1607            .filter(|row| {
1608                row.cells
1609                    .iter()
1610                    .any(|c| matches!(c, GraphCell::MergeNode { .. }))
1611            })
1612            .count();
1613        assert_eq!(merge_count, 3, "Should have 3 merge nodes");
1614
1615        // 全ての期待するコミットが存在
1616        let hashes: Vec<_> = layout
1617            .rows
1618            .iter()
1619            .filter_map(|row| row.event.as_ref().map(|e| e.short_hash.as_str()))
1620            .collect();
1621
1622        assert!(hashes.contains(&"final"));
1623        assert!(hashes.contains(&"right3"));
1624        assert!(hashes.contains(&"merge_r2"));
1625        assert!(hashes.contains(&"left2"));
1626        assert!(hashes.contains(&"merge_l1"));
1627        assert!(hashes.contains(&"right1"));
1628        assert!(hashes.contains(&"left1"));
1629        assert!(hashes.contains(&"base"));
1630    }
1631
1632    /// ツリー構造が保持されることを検証(行の順序と接続性)
1633    #[test]
1634    fn test_tree_structure_preserved() {
1635        // 複雑なツリー構造
1636        //
1637        // root ─┬─ child1 ─┬─ grandchild1
1638        //       │          └─ grandchild2
1639        //       └─ child2 ─┬─ grandchild3
1640        //                  └─ grandchild4
1641
1642        let mut root = create_event("root", vec!["child1", "child2"]);
1643        root.kind = GitEventKind::Merge;
1644
1645        let mut child1 = create_event("child1", vec!["grandchild1", "grandchild2"]);
1646        child1.kind = GitEventKind::Merge;
1647
1648        let mut child2 = create_event("child2", vec!["grandchild3", "grandchild4"]);
1649        child2.kind = GitEventKind::Merge;
1650
1651        let events = vec![
1652            root,
1653            child1,
1654            create_event("grandchild1", vec!["base"]),
1655            create_event("grandchild2", vec!["base"]),
1656            child2,
1657            create_event("grandchild3", vec!["base"]),
1658            create_event("grandchild4", vec!["base"]),
1659            create_event("base", vec![]),
1660        ];
1661
1662        let layout = build_graph(&events, None);
1663
1664        // パニックせずに完了
1665        assert!(!layout.rows.is_empty());
1666
1667        // 全コミットが存在
1668        let event_count = layout.rows.iter().filter(|r| r.event.is_some()).count();
1669        assert_eq!(event_count, 8, "All 8 commits should be present");
1670
1671        // 縦線と横線が適切に存在(ツリー構造の接続)
1672        let has_vertical = layout.rows.iter().any(|row| {
1673            row.cells
1674                .iter()
1675                .any(|c| matches!(c, GraphCell::Vertical { .. }))
1676        });
1677        let has_horizontal = layout.rows.iter().any(|row| {
1678            row.cells.iter().any(|c| {
1679                matches!(
1680                    c,
1681                    GraphCell::Horizontal { .. }
1682                        | GraphCell::CurveUpLeft { .. }
1683                        | GraphCell::CurveUpRight { .. }
1684                        | GraphCell::CurveDownLeft { .. }
1685                        | GraphCell::CurveDownRight { .. }
1686                )
1687            })
1688        });
1689
1690        assert!(has_vertical, "Tree should have vertical connections");
1691        assert!(has_horizontal, "Tree should have horizontal connections");
1692
1693        // 列が効率的に使用されている(過度に多くない)
1694        assert!(
1695            layout.max_column <= 5,
1696            "Columns should be reused efficiently, got max_column={}",
1697            layout.max_column
1698        );
1699    }
1700}