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