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)
44            .join(".hexz")
45            .join("workspaces")
46            .join(&id);
47
48        std::fs::create_dir_all(&hexz_root)?;
49
50        let overlay_dir = hexz_root.join("overlay");
51        std::fs::create_dir_all(overlay_dir)?;
52
53        let base_archive = if let Some(b) = base_archive {
54            Some(std::fs::canonicalize(b)?)
55        } else {
56            None
57        };
58
59        let config = WorkspaceConfig {
60            base_archive,
61            overlay_path: None,
62            host_cwd: None,
63            remotes: std::collections::HashMap::new(),
64        };
65
66        let config_path = hexz_root.join("config.json");
67        let f = std::fs::File::create(config_path)?;
68        serde_json::to_writer_pretty(f, &config)?;
69
70        Ok(Self {
71            root: abs_path,
72            config,
73        })
74    }
75
76    /// Persist the current workspace configuration to disk.
77    pub fn save(&self) -> Result<()> {
78        let config_path = self.metadata_dir().join("config.json");
79        let f = std::fs::File::create(config_path)?;
80        serde_json::to_writer_pretty(f, &self.config)?;
81        Ok(())
82    }
83
84    /// Search upward from `start_path` to find an existing workspace.
85    pub fn find(start_path: &Path) -> Result<Option<Self>> {
86        let mut current = if start_path.exists() {
87            std::fs::canonicalize(start_path)?
88        } else {
89            return Ok(None);
90        };
91
92        loop {
93            // First, try reading it locally (this works if FUSE is actively mounted!)
94            let local_config = current.join(".hexz").join("config.json");
95            if local_config.exists() {
96                let f = std::fs::File::open(local_config)?;
97                let config: WorkspaceConfig = serde_json::from_reader(f)?;
98                return Ok(Some(Self {
99                    root: current,
100                    config,
101                }));
102            }
103
104            // Fallback: If not mounted, calculate the global hash to check if it's a known workspace
105            let mut s = DefaultHasher::new();
106            current.hash(&mut s);
107            let id = format!("{:x}", s.finish());
108            let home = std::env::var("HOME").context("HOME not set")?;
109            let hexz_root = PathBuf::from(home)
110                .join(".hexz")
111                .join("workspaces")
112                .join(id);
113            let config_path = hexz_root.join("config.json");
114
115            if config_path.exists() {
116                let f = std::fs::File::open(config_path)?;
117                let config: WorkspaceConfig = serde_json::from_reader(f)?;
118                return Ok(Some(Self {
119                    root: current,
120                    config,
121                }));
122            }
123
124            if let Some(parent) = current.parent() {
125                current = parent.to_path_buf();
126            } else {
127                break;
128            }
129        }
130
131        Ok(None)
132    }
133
134    /// Return the path to the overlay directory for this workspace.
135    pub fn overlay_path(&self) -> PathBuf {
136        if let Some(ref p) = self.config.overlay_path {
137            return p.clone();
138        }
139        self.metadata_dir().join("overlay")
140    }
141
142    /// Return the path to the centralized metadata directory for this workspace.
143    pub fn metadata_dir(&self) -> PathBuf {
144        let mut s = DefaultHasher::new();
145        self.root.hash(&mut s);
146        let id = format!("{:x}", s.finish());
147        let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
148        PathBuf::from(home)
149            .join(".hexz")
150            .join("workspaces")
151            .join(id)
152    }
153}