Skip to main content

hexz_cli/cmd/data/
workspace.rs

1//! Workspace management logic for Hexz (Git-style workflows).
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
7use std::path::{Path, PathBuf};
8
9/// Serializable workspace configuration stored in `config.json`.
10#[derive(Debug, Serialize, Deserialize)]
11pub struct WorkspaceConfig {
12    /// Path to the base archive this workspace derives from.
13    pub base_archive: Option<PathBuf>,
14    /// Path to the overlay directory for writable changes.
15    pub overlay_path: Option<PathBuf>,
16    /// Original working directory of the host when the workspace was created.
17    pub host_cwd: Option<PathBuf>,
18    /// Named remote endpoints for push/pull operations.
19    #[serde(default)]
20    pub remotes: std::collections::HashMap<String, String>,
21}
22
23/// A Hexz workspace rooted at a directory with associated configuration.
24#[derive(Debug)]
25pub struct Workspace {
26    /// Root directory of the workspace.
27    pub root: PathBuf,
28    /// Workspace configuration loaded from `config.json`.
29    pub config: WorkspaceConfig,
30}
31
32impl Workspace {
33    /// Initialize a new workspace at the given path with an optional base archive.
34    pub fn init(path: &Path, base_archive: Option<PathBuf>) -> Result<Self> {
35        let abs_path = std::fs::canonicalize(path)?;
36
37        // Centralized storage: ~/.hexz/workspaces/<hash_of_abs_path>
38        let mut s = DefaultHasher::new();
39        abs_path.hash(&mut s);
40        let id = format!("{:x}", s.finish());
41
42        let home = std::env::var("HOME").context("HOME not set")?;
43        let hexz_root = PathBuf::from(home).join(".hexz").join("workspaces").join(&id);
44
45        std::fs::create_dir_all(&hexz_root)?;
46
47        let overlay_dir = hexz_root.join("overlay");
48        std::fs::create_dir_all(overlay_dir)?;
49
50        let base_archive = if let Some(b) = base_archive {
51            Some(std::fs::canonicalize(b)?)
52        } else {
53            None
54        };
55
56        let config = WorkspaceConfig {
57            base_archive,
58            overlay_path: None,
59            host_cwd: None,
60            remotes: std::collections::HashMap::new(),
61        };
62
63        let config_path = hexz_root.join("config.json");
64        let f = std::fs::File::create(config_path)?;
65        serde_json::to_writer_pretty(f, &config)?;
66
67        Ok(Self {
68            root: abs_path,
69            config,
70        })
71    }
72
73    /// Persist the current workspace configuration to disk.
74    pub fn save(&self) -> Result<()> {
75        let config_path = self.metadata_dir().join("config.json");
76        let f = std::fs::File::create(config_path)?;
77        serde_json::to_writer_pretty(f, &self.config)?;
78        Ok(())
79    }
80
81    /// Search upward from `start_path` to find an existing workspace.
82    pub fn find(start_path: &Path) -> Result<Option<Self>> {
83        let mut current = if start_path.exists() {
84            std::fs::canonicalize(start_path)?
85        } else {
86            return Ok(None);
87        };
88
89        loop {
90            // First, try reading it locally (this works if FUSE is actively mounted!)
91            let local_config = current.join(".hexz").join("config.json");
92            if local_config.exists() {
93                let f = std::fs::File::open(local_config)?;
94                let config: WorkspaceConfig = serde_json::from_reader(f)?;
95                return Ok(Some(Self {
96                    root: current,
97                    config,
98                }));
99            }
100
101            // Fallback: If not mounted, calculate the global hash to check if it's a known workspace
102            let mut s = DefaultHasher::new();
103            current.hash(&mut s);
104            let id = format!("{:x}", s.finish());
105            let home = std::env::var("HOME").context("HOME not set")?;
106            let hexz_root = PathBuf::from(home).join(".hexz").join("workspaces").join(id);
107            let config_path = hexz_root.join("config.json");
108
109            if config_path.exists() {
110                 let f = std::fs::File::open(config_path)?;
111                 let config: WorkspaceConfig = serde_json::from_reader(f)?;
112                 return Ok(Some(Self {
113                     root: current,
114                     config,
115                 }));
116            }
117
118            if let Some(parent) = current.parent() {
119                current = parent.to_path_buf();
120            } else {
121                break;
122            }
123        }
124
125        Ok(None)
126    }
127
128    /// Return the path to the overlay directory for this workspace.
129    pub fn overlay_path(&self) -> PathBuf {
130        if let Some(ref p) = self.config.overlay_path {
131            return p.clone();
132        }
133        self.metadata_dir().join("overlay")
134    }
135
136    /// Return the path to the centralized metadata directory for this workspace.
137    pub fn metadata_dir(&self) -> PathBuf {
138        let mut s = DefaultHasher::new();
139        self.root.hash(&mut s);
140        let id = format!("{:x}", s.finish());
141        let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
142        PathBuf::from(home).join(".hexz").join("workspaces").join(id)
143    }
144}