Skip to main content

hexz_cli/cmd/data/
commit.rs

1//! Commit changes from a writable mount to a new thin archive.
2
3use anyhow::{Context, Result};
4use std::path::PathBuf;
5use std::process::Command;
6use colored::Colorize;
7use indicatif::HumanBytes;
8
9use hexz_ops::pack::{PackConfig, PackAnalysisFlags, pack_archive};
10
11use super::workspace::Workspace;
12
13/// Execute the commit command to save workspace changes as a new thin archive.
14pub fn run(
15    mut output: PathBuf,
16    mountpoint: Option<PathBuf>,
17    base: Option<PathBuf>,
18) -> Result<()> {
19    let current_dir = std::env::current_dir()?;
20    let ws = Workspace::find(&current_dir)?;
21
22    let mountpoint = if let Some(m) = mountpoint {
23        std::fs::canonicalize(m)?
24    } else {
25        // Try to find workspace in CWD
26        if let Some(ref w) = ws {
27            w.root.clone()
28        } else {
29            anyhow::bail!("No mountpoint provided and no .hexz workspace found.");
30        }
31    };
32
33    // If output is relative and we have a host_cwd, resolve it
34    if output.is_relative() {
35        if let Some(ref w) = ws {
36            if let Some(ref host_cwd) = w.config.host_cwd {
37                output = host_cwd.join(&output);
38            }
39        }
40    }
41
42    // Try to infer base archive if not provided
43    let base = if let Some(b) = base {
44        Some(b)
45    } else {
46        // Check workspace first
47        if let Some(ref w) = ws {
48            if let Some(b) = w.config.base_archive.clone() {
49                Some(b)
50            } else {
51                infer_base_archive(&mountpoint)
52            }
53        } else {
54            infer_base_archive(&mountpoint)
55        }
56    };
57
58    println!("{} Committing to {}", "╭".dimmed(), output.display().to_string().cyan());
59    if let Some(ref b) = base {
60        println!("{} Base:         {}", "╰".dimmed(), b.display().to_string().bright_black());
61    } else {
62        println!("{} Base:         {}", "╰".dimmed(), "(none)".bright_black());
63    }
64
65    let config = PackConfig {
66        input: mountpoint,
67        base,
68        output: output.clone(),
69        compression: "zstd".to_string(), // Default to zstd for commits
70        analysis: PackAnalysisFlags {
71            use_dcam: true,
72            show_progress: true,
73            ..Default::default()
74        },
75        ..Default::default()
76    };
77
78    pack_archive(&config, None::<&fn(u64, u64)>).context("Commit failed during packing")?;
79
80    let file_size = std::fs::metadata(&output).map_or(0, |m| m.len());
81    let size_str = HumanBytes(file_size).to_string();
82
83    println!("\n  {} Commit complete {}", "✓".green(), format!("({size_str} delta)").bright_black());
84    Ok(())
85}
86fn infer_base_archive(mountpoint: &std::path::Path) -> Option<PathBuf> {
87    // On Linux, we can try to find the archive path from the mount options
88    if cfg!(target_os = "linux") {
89        let output = Command::new("findmnt")
90            .arg("-n")
91            .arg("-o")
92            .arg("SOURCE")
93            .arg(mountpoint)
94            .output()
95            .ok()?;
96
97        if output.status.success() {
98            let source = String::from_utf8_lossy(&output.stdout).trim().to_string();
99            let p = PathBuf::from(source);
100            if p.exists() && p.extension().is_some_and(|ext| ext == "hxz") {
101                return Some(p);
102            }
103        }
104    }
105    None
106}