ggen_cli_lib/cmds/graph/
snapshot.rs

1use clap::Args;
2use ggen_core::graph::Graph;
3use ggen_core::snapshot::{Snapshot, SnapshotManager};
4use ggen_utils::error::Result;
5use std::path::{Path, PathBuf};
6
7#[derive(Args, Debug)]
8pub struct SnapshotArgs {
9    #[command(subcommand)]
10    pub command: SnapshotCommand,
11}
12
13#[derive(clap::Subcommand, Debug)]
14pub enum SnapshotCommand {
15    /// Create a new snapshot
16    Create {
17        /// Name for the snapshot
18        #[arg(short, long)]
19        name: String,
20
21        /// Graph to snapshot
22        #[arg(short, long)]
23        graph: Option<String>,
24
25        /// Files to include in snapshot
26        #[arg(short, long)]
27        files: Vec<PathBuf>,
28
29        /// Templates to include in snapshot
30        #[arg(short, long)]
31        templates: Vec<PathBuf>,
32
33        /// Snapshot directory
34        #[arg(short, long, default_value = ".ggen/snapshots")]
35        snapshot_dir: PathBuf,
36    },
37
38    /// List available snapshots
39    List {
40        /// Snapshot directory
41        #[arg(short, long, default_value = ".ggen/snapshots")]
42        snapshot_dir: PathBuf,
43    },
44
45    /// Show details of a snapshot
46    Show {
47        /// Name of snapshot to show
48        #[arg(short, long)]
49        name: String,
50
51        /// Snapshot directory
52        #[arg(short, long, default_value = ".ggen/snapshots")]
53        snapshot_dir: PathBuf,
54    },
55
56    /// Delete a snapshot
57    Delete {
58        /// Name of snapshot to delete
59        #[arg(short, long)]
60        name: String,
61
62        /// Snapshot directory
63        #[arg(short, long, default_value = ".ggen/snapshots")]
64        snapshot_dir: PathBuf,
65    },
66
67    /// Verify a snapshot against current state
68    Verify {
69        /// Name of snapshot to verify
70        #[arg(short, long)]
71        name: String,
72
73        /// Snapshot directory
74        #[arg(short, long, default_value = ".ggen/snapshots")]
75        snapshot_dir: PathBuf,
76
77        /// Exit with non-zero code if drift detected
78        #[arg(short, long)]
79        exit_code: bool,
80    },
81}
82
83pub async fn run(args: &SnapshotArgs) -> Result<()> {
84    match &args.command {
85        SnapshotCommand::Create {
86            name,
87            graph,
88            files,
89            templates,
90            snapshot_dir,
91        } => create_snapshot(name, graph, files, templates, snapshot_dir).await,
92        SnapshotCommand::List { snapshot_dir } => list_snapshots(snapshot_dir).await,
93        SnapshotCommand::Show { name, snapshot_dir } => show_snapshot(name, snapshot_dir).await,
94        SnapshotCommand::Delete { name, snapshot_dir } => delete_snapshot(name, snapshot_dir).await,
95        SnapshotCommand::Verify {
96            name,
97            snapshot_dir,
98            exit_code,
99        } => verify_snapshot(name, snapshot_dir, *exit_code).await,
100    }
101}
102
103async fn create_snapshot(
104    name: &str, graph_name: &Option<String>, files: &[PathBuf], templates: &[PathBuf],
105    snapshot_dir: &Path,
106) -> Result<()> {
107    // Load graph if specified
108    let graph = if let Some(_graph_name) = graph_name {
109        // Would need to load from named graph - simplified for now
110        Graph::new()?
111    } else {
112        Graph::new()?
113    };
114
115    // Read file contents
116    let mut file_contents = Vec::new();
117    for file_path in files {
118        if file_path.exists() {
119            let content = std::fs::read_to_string(file_path)?;
120            file_contents.push((file_path.clone(), content));
121        }
122    }
123
124    // Read template contents
125    let mut template_contents = Vec::new();
126    for template_path in templates {
127        if template_path.exists() {
128            let content = std::fs::read_to_string(template_path)?;
129            template_contents.push((template_path.clone(), content));
130        }
131    }
132
133    // Create snapshot
134    let snapshot = Snapshot::new(name.to_string(), &graph, file_contents, template_contents)?;
135
136    // Save snapshot
137    let manager = SnapshotManager::new(snapshot_dir.to_path_buf())?;
138    manager.save(&snapshot)?;
139
140    println!("Snapshot '{}' created successfully", name);
141    Ok(())
142}
143
144async fn list_snapshots(snapshot_dir: &Path) -> Result<()> {
145    let manager = SnapshotManager::new(snapshot_dir.to_path_buf())?;
146    let snapshots = manager.list()?;
147
148    if snapshots.is_empty() {
149        println!("No snapshots found.");
150    } else {
151        println!("Available snapshots:");
152        for snapshot in snapshots {
153            println!("  {}", snapshot);
154        }
155    }
156
157    Ok(())
158}
159
160async fn show_snapshot(name: &str, snapshot_dir: &Path) -> Result<()> {
161    let manager = SnapshotManager::new(snapshot_dir.to_path_buf())?;
162    let snapshot = manager.load(name)?;
163
164    println!("Snapshot: {}", snapshot.name);
165    println!(
166        "Created:  {}",
167        snapshot.created_at.format("%Y-%m-%d %H:%M:%S UTC")
168    );
169    println!("Graph hash: {}", snapshot.graph.hash);
170    println!("Triples: {}", snapshot.graph.triple_count);
171    println!("Files: {}", snapshot.files.len());
172    println!("Templates: {}", snapshot.templates.len());
173
174    if !snapshot.metadata.is_empty() {
175        println!("Metadata:");
176        for (key, value) in &snapshot.metadata {
177            println!("  {}: {}", key, value);
178        }
179    }
180
181    Ok(())
182}
183
184async fn delete_snapshot(name: &str, snapshot_dir: &Path) -> Result<()> {
185    let manager = SnapshotManager::new(snapshot_dir.to_path_buf())?;
186    manager.delete(name)?;
187    println!("Snapshot '{}' deleted", name);
188    Ok(())
189}
190
191async fn verify_snapshot(name: &str, snapshot_dir: &Path, exit_code: bool) -> Result<()> {
192    let manager = SnapshotManager::new(snapshot_dir.to_path_buf())?;
193    let snapshot = manager.load(name)?;
194
195    // Load current graph (simplified - would need to match snapshot's graph source)
196    let current_graph = Graph::new()?;
197
198    let is_compatible = snapshot.is_compatible_with(&current_graph)?;
199
200    if is_compatible {
201        println!("✓ Snapshot '{}' is compatible with current state", name);
202        if !exit_code {
203            std::process::exit(0);
204        }
205    } else {
206        println!("✗ Snapshot '{}' has drifted from current state", name);
207        println!("  Graph hash mismatch:");
208        println!("    Snapshot: {}", snapshot.graph.hash);
209        println!("    Current:  {}", current_graph.compute_hash()?);
210
211        if exit_code {
212            std::process::exit(1);
213        }
214    }
215
216    Ok(())
217}