Skip to main content

gitstack/topology/
types.rs

1//! トポロジービュー用のデータ構造定義
2
3use chrono::{DateTime, Local};
4
5/// ブランチステータス
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BranchStatus {
8    /// 現在のブランチ(HEAD)
9    Active,
10    /// 通常のブランチ
11    Normal,
12    /// 未コミット変更あり(WIP)
13    #[allow(dead_code)]
14    Wip,
15    /// 非活性(30日以上コミットなし)
16    Stale,
17    /// マージ済み
18    Merged,
19}
20
21impl BranchStatus {
22    /// ステータスの表示文字列を取得
23    pub fn label(&self) -> &'static str {
24        match self {
25            Self::Active => "HEAD",
26            Self::Normal => "",
27            Self::Wip => "WIP",
28            Self::Stale => "stale",
29            Self::Merged => "merged",
30        }
31    }
32
33    /// ステータスに対応する色インデックスを取得
34    pub fn color_index(&self) -> usize {
35        match self {
36            Self::Active => 0, // Green
37            Self::Normal => 1, // Blue
38            Self::Wip => 2,    // Yellow
39            Self::Stale => 3,  // Gray
40            Self::Merged => 4, // Magenta
41        }
42    }
43}
44
45/// ブランチ関係情報
46#[derive(Debug, Clone)]
47pub struct BranchRelation {
48    /// ベースブランチ名
49    pub base: String,
50    /// 対象ブランチ名
51    pub branch: String,
52    /// マージベースのコミットハッシュ
53    pub merge_base: String,
54    /// ベースより進んでいるコミット数
55    pub ahead_count: usize,
56    /// ベースより遅れているコミット数
57    pub behind_count: usize,
58    /// マージ済みかどうか
59    pub is_merged: bool,
60}
61
62impl BranchRelation {
63    /// 新しいブランチ関係を作成
64    pub fn new(base: String, branch: String) -> Self {
65        Self {
66            base,
67            branch,
68            merge_base: String::new(),
69            ahead_count: 0,
70            behind_count: 0,
71            is_merged: false,
72        }
73    }
74
75    /// 状態サマリーを取得
76    pub fn summary(&self) -> String {
77        if self.is_merged {
78            "merged".to_string()
79        } else if self.ahead_count == 0 && self.behind_count == 0 {
80            "up to date".to_string()
81        } else {
82            let mut parts = Vec::new();
83            if self.ahead_count > 0 {
84                parts.push(format!("{} ahead", self.ahead_count));
85            }
86            if self.behind_count > 0 {
87                parts.push(format!("{} behind", self.behind_count));
88            }
89            parts.join(", ")
90        }
91    }
92}
93
94/// ブランチ健全性の警告種別
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum HealthWarning {
97    /// 非活性(30日以上コミットなし)
98    Stale,
99    /// 長寿命(60日以上経過)
100    LongLived,
101    /// mainから大幅に遅れている(50コミット以上)
102    FarBehind,
103    /// 大きな乖離(ahead/behind両方が多い)
104    LargeDivergence,
105}
106
107impl HealthWarning {
108    /// 警告の説明を取得
109    pub fn description(&self) -> &'static str {
110        match self {
111            Self::Stale => "No activity for 30+ days",
112            Self::LongLived => "Branch exists for 60+ days",
113            Self::FarBehind => "50+ commits behind main",
114            Self::LargeDivergence => "Large divergence from main",
115        }
116    }
117
118    /// 警告アイコンを取得
119    pub fn icon(&self) -> &'static str {
120        match self {
121            Self::Stale => "⚠",
122            Self::LongLived => "⏳",
123            Self::FarBehind => "⬇",
124            Self::LargeDivergence => "⚡",
125        }
126    }
127}
128
129/// ブランチの健全性評価
130#[derive(Debug, Clone, Default)]
131pub struct BranchHealth {
132    /// 健全性の警告リスト
133    pub warnings: Vec<HealthWarning>,
134}
135
136impl BranchHealth {
137    /// 新しい健全性評価を作成
138    pub fn new() -> Self {
139        Self {
140            warnings: Vec::new(),
141        }
142    }
143
144    /// 警告を追加
145    pub fn add_warning(&mut self, warning: HealthWarning) {
146        if !self.warnings.contains(&warning) {
147            self.warnings.push(warning);
148        }
149    }
150
151    /// 健全かどうか(警告がない)
152    pub fn is_healthy(&self) -> bool {
153        self.warnings.is_empty()
154    }
155
156    /// 警告数を取得
157    pub fn warning_count(&self) -> usize {
158        self.warnings.len()
159    }
160
161    /// 警告アイコンを連結して取得
162    pub fn warning_icons(&self) -> String {
163        self.warnings
164            .iter()
165            .map(|w| w.icon())
166            .collect::<Vec<_>>()
167            .join("")
168    }
169}
170
171/// トポロジー用ブランチ情報
172#[derive(Debug, Clone)]
173pub struct TopologyBranch {
174    /// ブランチ名
175    pub name: String,
176    /// HEADコミットのハッシュ
177    pub head_hash: String,
178    /// ブランチステータス
179    pub status: BranchStatus,
180    /// 最終活動日時
181    pub last_activity: DateTime<Local>,
182    /// ベースブランチとの関係
183    pub relation: Option<BranchRelation>,
184    /// ブランチ上のコミット数
185    pub commit_count: usize,
186    /// 健全性評価
187    pub health: BranchHealth,
188}
189
190impl TopologyBranch {
191    /// 新しいトポロジーブランチを作成
192    pub fn new(name: String, head_hash: String, last_activity: DateTime<Local>) -> Self {
193        Self {
194            name,
195            head_hash,
196            status: BranchStatus::Normal,
197            last_activity,
198            relation: None,
199            commit_count: 0,
200            health: BranchHealth::new(),
201        }
202    }
203
204    /// ステータスを設定
205    pub fn with_status(mut self, status: BranchStatus) -> Self {
206        self.status = status;
207        self
208    }
209
210    /// 関係情報を設定
211    pub fn with_relation(mut self, relation: BranchRelation) -> Self {
212        self.relation = Some(relation);
213        self
214    }
215
216    /// コミット数を設定
217    pub fn with_commit_count(mut self, count: usize) -> Self {
218        self.commit_count = count;
219        self
220    }
221
222    /// 非活性かどうか判定(指定日数以上前)
223    pub fn is_stale(&self, threshold_days: i64) -> bool {
224        let now = Local::now();
225        let diff = now.signed_duration_since(self.last_activity);
226        diff.num_days() >= threshold_days
227    }
228
229    /// ベースブランチより進んでいるか
230    pub fn is_ahead(&self) -> bool {
231        self.relation.as_ref().is_some_and(|r| r.ahead_count > 0)
232    }
233
234    /// ベースブランチより遅れているか
235    pub fn is_behind(&self) -> bool {
236        self.relation.as_ref().is_some_and(|r| r.behind_count > 0)
237    }
238
239    /// 健全性を計算・更新
240    pub fn calculate_health(&mut self, config: &TopologyConfig) {
241        self.health = BranchHealth::new();
242
243        // Stale警告(デフォルト30日)
244        if self.is_stale(config.stale_threshold_days) {
245            self.health.add_warning(HealthWarning::Stale);
246        }
247
248        // LongLived警告(60日以上)
249        let now = Local::now();
250        let age_days = now.signed_duration_since(self.last_activity).num_days();
251        if age_days >= config.long_lived_threshold_days {
252            self.health.add_warning(HealthWarning::LongLived);
253        }
254
255        // FarBehind警告(50コミット以上遅れ)
256        if let Some(ref relation) = self.relation {
257            if relation.behind_count >= config.far_behind_threshold {
258                self.health.add_warning(HealthWarning::FarBehind);
259            }
260
261            // LargeDivergence警告(ahead/behind両方が多い)
262            if relation.ahead_count >= config.divergence_threshold
263                && relation.behind_count >= config.divergence_threshold
264            {
265                self.health.add_warning(HealthWarning::LargeDivergence);
266            }
267        }
268    }
269}
270
271/// トポロジー設定
272#[derive(Debug, Clone)]
273pub struct TopologyConfig {
274    /// 非活性と見なす日数
275    pub stale_threshold_days: i64,
276    /// 長寿命と見なす日数
277    pub long_lived_threshold_days: i64,
278    /// 大幅に遅れていると見なすコミット数
279    pub far_behind_threshold: usize,
280    /// 乖離が大きいと見なすコミット数(ahead/behind両方)
281    pub divergence_threshold: usize,
282    /// 表示するブランチの最大数
283    pub max_branches: usize,
284}
285
286impl Default for TopologyConfig {
287    fn default() -> Self {
288        Self {
289            stale_threshold_days: 30,
290            long_lived_threshold_days: 60,
291            far_behind_threshold: 50,
292            divergence_threshold: 20,
293            max_branches: 50,
294        }
295    }
296}
297
298/// ブランチトポロジー全体
299#[derive(Debug, Clone)]
300pub struct BranchTopology {
301    /// メインブランチ名
302    pub main_branch: String,
303    /// ブランチ一覧
304    pub branches: Vec<TopologyBranch>,
305    /// 最大カラム数(レイアウト用)
306    pub max_column: usize,
307    /// 設定
308    pub config: TopologyConfig,
309}
310
311impl BranchTopology {
312    /// 新しいトポロジーを作成
313    pub fn new(main_branch: String) -> Self {
314        Self {
315            main_branch,
316            branches: Vec::new(),
317            max_column: 0,
318            config: TopologyConfig::default(),
319        }
320    }
321
322    /// ブランチを追加
323    pub fn add_branch(&mut self, branch: TopologyBranch) {
324        self.branches.push(branch);
325    }
326
327    /// ブランチ数を取得
328    pub fn branch_count(&self) -> usize {
329        self.branches.len()
330    }
331
332    /// アクティブブランチを取得
333    pub fn active_branch(&self) -> Option<&TopologyBranch> {
334        self.branches
335            .iter()
336            .find(|b| b.status == BranchStatus::Active)
337    }
338
339    /// 非活性ブランチの数を取得
340    pub fn stale_count(&self) -> usize {
341        self.branches
342            .iter()
343            .filter(|b| b.status == BranchStatus::Stale)
344            .count()
345    }
346
347    /// マージ済みブランチの数を取得
348    pub fn merged_count(&self) -> usize {
349        self.branches
350            .iter()
351            .filter(|b| b.status == BranchStatus::Merged)
352            .count()
353    }
354
355    /// 全ブランチの健全性を計算
356    pub fn calculate_all_health(&mut self) {
357        let config = self.config.clone();
358        for branch in &mut self.branches {
359            // mainブランチとマージ済みブランチは健全性チェックをスキップ
360            if branch.name != self.main_branch && branch.status != BranchStatus::Merged {
361                branch.calculate_health(&config);
362            }
363        }
364    }
365
366    /// 健全性警告があるブランチの数を取得
367    pub fn unhealthy_count(&self) -> usize {
368        self.branches
369            .iter()
370            .filter(|b| !b.health.is_healthy())
371            .count()
372    }
373
374    /// 特定の警告を持つブランチの数を取得
375    pub fn warning_count(&self, warning: HealthWarning) -> usize {
376        self.branches
377            .iter()
378            .filter(|b| b.health.warnings.contains(&warning))
379            .count()
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    fn create_test_branch(name: &str) -> TopologyBranch {
388        TopologyBranch::new(name.to_string(), "abc1234".to_string(), Local::now())
389    }
390
391    #[test]
392    fn test_branch_status_label() {
393        assert_eq!(BranchStatus::Active.label(), "HEAD");
394        assert_eq!(BranchStatus::Normal.label(), "");
395        assert_eq!(BranchStatus::Stale.label(), "stale");
396        assert_eq!(BranchStatus::Merged.label(), "merged");
397    }
398
399    #[test]
400    fn test_branch_relation_summary_merged() {
401        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
402        relation.is_merged = true;
403        assert_eq!(relation.summary(), "merged");
404    }
405
406    #[test]
407    fn test_branch_relation_summary_up_to_date() {
408        let relation = BranchRelation::new("main".to_string(), "feature".to_string());
409        assert_eq!(relation.summary(), "up to date");
410    }
411
412    #[test]
413    fn test_branch_relation_summary_ahead() {
414        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
415        relation.ahead_count = 3;
416        assert_eq!(relation.summary(), "3 ahead");
417    }
418
419    #[test]
420    fn test_branch_relation_summary_behind() {
421        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
422        relation.behind_count = 2;
423        assert_eq!(relation.summary(), "2 behind");
424    }
425
426    #[test]
427    fn test_branch_relation_summary_ahead_and_behind() {
428        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
429        relation.ahead_count = 3;
430        relation.behind_count = 2;
431        assert_eq!(relation.summary(), "3 ahead, 2 behind");
432    }
433
434    #[test]
435    fn test_topology_branch_new() {
436        let branch = create_test_branch("feature");
437        assert_eq!(branch.name, "feature");
438        assert_eq!(branch.status, BranchStatus::Normal);
439    }
440
441    #[test]
442    fn test_topology_branch_with_status() {
443        let branch = create_test_branch("feature").with_status(BranchStatus::Active);
444        assert_eq!(branch.status, BranchStatus::Active);
445    }
446
447    #[test]
448    fn test_topology_branch_is_ahead() {
449        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
450        relation.ahead_count = 3;
451        let branch = create_test_branch("feature").with_relation(relation);
452        assert!(branch.is_ahead());
453    }
454
455    #[test]
456    fn test_topology_branch_is_behind() {
457        let mut relation = BranchRelation::new("main".to_string(), "feature".to_string());
458        relation.behind_count = 2;
459        let branch = create_test_branch("feature").with_relation(relation);
460        assert!(branch.is_behind());
461    }
462
463    #[test]
464    fn test_branch_topology_new() {
465        let topology = BranchTopology::new("main".to_string());
466        assert_eq!(topology.main_branch, "main");
467        assert_eq!(topology.branch_count(), 0);
468    }
469
470    #[test]
471    fn test_branch_topology_add_branch() {
472        let mut topology = BranchTopology::new("main".to_string());
473        topology.add_branch(create_test_branch("feature"));
474        assert_eq!(topology.branch_count(), 1);
475    }
476
477    #[test]
478    fn test_branch_topology_active_branch() {
479        let mut topology = BranchTopology::new("main".to_string());
480        topology.add_branch(create_test_branch("feature"));
481        topology.add_branch(create_test_branch("main").with_status(BranchStatus::Active));
482
483        let active = topology.active_branch();
484        assert!(active.is_some());
485        assert_eq!(active.unwrap().name, "main");
486    }
487
488    #[test]
489    fn test_branch_topology_stale_count() {
490        let mut topology = BranchTopology::new("main".to_string());
491        topology.add_branch(create_test_branch("feature1"));
492        topology.add_branch(create_test_branch("feature2").with_status(BranchStatus::Stale));
493        topology.add_branch(create_test_branch("feature3").with_status(BranchStatus::Stale));
494
495        assert_eq!(topology.stale_count(), 2);
496    }
497
498    #[test]
499    fn test_branch_topology_merged_count() {
500        let mut topology = BranchTopology::new("main".to_string());
501        topology.add_branch(create_test_branch("feature1"));
502        topology.add_branch(create_test_branch("feature2").with_status(BranchStatus::Merged));
503
504        assert_eq!(topology.merged_count(), 1);
505    }
506}