cuenv_ci/provider/
local.rs

1use super::{CIContext, CIProvider};
2use crate::report::{CheckHandle, PipelineReport};
3use async_trait::async_trait;
4use cuenv_core::Result;
5use std::collections::HashSet;
6use std::path::PathBuf;
7
8pub struct LocalProvider {
9    context: CIContext,
10    base_ref: Option<String>,
11}
12
13impl LocalProvider {
14    /// Create a `LocalProvider` that compares against a specific base reference.
15    /// This will detect changes between the base ref and HEAD, plus uncommitted changes.
16    #[must_use]
17    pub fn with_base_ref(base_ref: String) -> Self {
18        Self {
19            context: CIContext {
20                provider: "local".to_string(),
21                event: "manual".to_string(),
22                ref_name: "current".to_string(),
23                base_ref: Some(base_ref.clone()),
24                sha: "current".to_string(),
25            },
26            base_ref: Some(base_ref),
27        }
28    }
29}
30
31#[async_trait]
32impl CIProvider for LocalProvider {
33    fn detect() -> Option<Self> {
34        // Always available as fallback
35        Some(Self {
36            context: CIContext {
37                provider: "local".to_string(),
38                event: "manual".to_string(),
39                ref_name: "current".to_string(),
40                base_ref: None,
41                sha: "current".to_string(),
42            },
43            base_ref: None,
44        })
45    }
46
47    fn context(&self) -> &CIContext {
48        &self.context
49    }
50
51    async fn changed_files(&self) -> Result<Vec<PathBuf>> {
52        let mut changed: HashSet<PathBuf> = HashSet::new();
53
54        // If we have a base_ref, get committed changes since that ref
55        if let Some(ref base_ref) = self.base_ref {
56            // Use three-dot syntax to get changes between base_ref and HEAD
57            // This shows what's in HEAD that isn't in base_ref
58            let output = std::process::Command::new("git")
59                .args(["diff", "--name-only", &format!("{base_ref}...HEAD")])
60                .output()
61                .ok();
62
63            if let Some(output) = output {
64                let stdout = String::from_utf8_lossy(&output.stdout);
65                for line in stdout.lines() {
66                    changed.insert(PathBuf::from(line));
67                }
68            }
69        }
70
71        // Always include uncommitted changes (staged + unstaged)
72        let output = std::process::Command::new("git")
73            .args(["diff", "--name-only", "HEAD"])
74            .output()
75            .ok();
76
77        if let Some(output) = output {
78            let stdout = String::from_utf8_lossy(&output.stdout);
79            for line in stdout.lines() {
80                changed.insert(PathBuf::from(line));
81            }
82        }
83
84        Ok(changed.into_iter().collect())
85    }
86
87    async fn create_check(&self, _name: &str) -> Result<CheckHandle> {
88        Ok(CheckHandle {
89            id: "local".to_string(),
90        })
91    }
92
93    async fn update_check(&self, _handle: &CheckHandle, _summary: &str) -> Result<()> {
94        Ok(())
95    }
96
97    async fn complete_check(&self, _handle: &CheckHandle, _report: &PipelineReport) -> Result<()> {
98        Ok(())
99    }
100
101    async fn upload_report(&self, _report: &PipelineReport) -> Result<Option<String>> {
102        Ok(None)
103    }
104}