Skip to main content

lit/commands/
workspace.rs

1use crate::core::{find_repo_root, list_refs, read_head};
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6/// Workspace represents a set of parallel (virtual) branches applied simultaneously.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct VirtualBranch {
9    pub name: String,
10    pub head: String,
11    pub files: Vec<String>,
12    pub active: bool,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16pub enum WorkspaceResponse {
17    List {
18        branches: Vec<VirtualBranch>,
19    },
20    Create {
21        name: String,
22        message: String,
23    },
24    Apply {
25        name: String,
26        message: String,
27    },
28    Unapply {
29        name: String,
30        message: String,
31    },
32    MoveFile {
33        file: String,
34        from: String,
35        to: String,
36        message: String,
37    },
38}
39
40impl CommandResponse for WorkspaceResponse {
41    fn command_name(&self) -> &'static str {
42        "workspace"
43    }
44    fn human_readable(&self) -> String {
45        match self {
46            WorkspaceResponse::List { branches } => {
47                let mut out = String::from("Virtual branches:\n");
48                for br in branches {
49                    let status = if br.active { "active" } else { "unapplied" };
50                    out.push_str(&format!(
51                        "  {} [{}] ({} files) - {}\n",
52                        br.name,
53                        &br.head[..8.min(br.head.len())],
54                        br.files.len(),
55                        status
56                    ));
57                }
58                if branches.is_empty() {
59                    out.push_str("  No virtual branches\n");
60                }
61                out
62            }
63            WorkspaceResponse::Create { name, message } => {
64                format!("Created virtual branch '{}': {}\n", name, message)
65            }
66            WorkspaceResponse::Apply { name, message } => {
67                format!("Applied '{}': {}\n", name, message)
68            }
69            WorkspaceResponse::Unapply { name, message } => {
70                format!("Unapplied '{}': {}\n", name, message)
71            }
72            WorkspaceResponse::MoveFile {
73                file,
74                from,
75                to,
76                message,
77            } => format!(
78                "Moved '{}' from '{}' to '{}': {}\n",
79                file, from, to, message
80            ),
81        }
82    }
83}
84
85/// Workspace metadata path
86fn workspace_path(repo_root: &std::path::Path) -> std::path::PathBuf {
87    repo_root.join(".lit").join("workspace.json")
88}
89
90/// Workspace metadata
91#[derive(Debug, Default, Serialize, Deserialize)]
92struct WorkspaceMeta {
93    branches: Vec<VirtualBranchMeta>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97struct VirtualBranchMeta {
98    name: String,
99    branch_ref: String,
100    files: Vec<String>,
101    active: bool,
102}
103
104fn load_workspace(repo_root: &std::path::Path) -> WorkspaceMeta {
105    let path = workspace_path(repo_root);
106    if path.exists() {
107        match std::fs::read_to_string(&path) {
108            Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
109            Err(_) => WorkspaceMeta::default(),
110        }
111    } else {
112        WorkspaceMeta::default()
113    }
114}
115
116fn save_workspace(repo_root: &std::path::Path, meta: &WorkspaceMeta) -> Result<(), LitError> {
117    let path = workspace_path(repo_root);
118    let data = serde_json::to_string_pretty(meta)
119        .map_err(|e| LitError::general(format!("Failed to serialize workspace: {}", e)))?;
120    std::fs::write(path, data)
121        .map_err(|e| LitError::io(format!("Failed to write workspace: {}", e)))?;
122    Ok(())
123}
124
125/// List all virtual branches in the workspace
126pub fn execute_list() -> Result<WorkspaceResponse, LitError> {
127    let repo_root = find_repo_root()?;
128    let meta = load_workspace(&repo_root);
129    let refs = list_refs(&repo_root, "heads").unwrap_or_default();
130
131    let branches = meta
132        .branches
133        .iter()
134        .map(|vb| {
135            let head = refs
136                .iter()
137                .find(|r| format!("refs/heads/{}", r.name) == vb.branch_ref || r.name == vb.name)
138                .map(|r| r.hash.clone())
139                .unwrap_or_else(|| "unknown".to_string());
140            VirtualBranch {
141                name: vb.name.clone(),
142                head,
143                files: vb.files.clone(),
144                active: vb.active,
145            }
146        })
147        .collect();
148
149    Ok(WorkspaceResponse::List { branches })
150}
151
152/// Create a new virtual branch
153pub fn execute_create(name: String) -> Result<WorkspaceResponse, LitError> {
154    let repo_root = find_repo_root()?;
155    let head_hash = read_head(&repo_root)?;
156    let mut meta = load_workspace(&repo_root);
157
158    // Check for duplicate
159    if meta.branches.iter().any(|b| b.name == name) {
160        return Err(LitError::general(format!(
161            "Virtual branch '{}' already exists",
162            name
163        )));
164    }
165
166    // Create the underlying branch ref
167    crate::core::write_ref(&repo_root, &format!("heads/{}", name), &head_hash)?;
168
169    meta.branches.push(VirtualBranchMeta {
170        name: name.clone(),
171        branch_ref: format!("refs/heads/{}", name),
172        files: Vec::new(),
173        active: true,
174    });
175    save_workspace(&repo_root, &meta)?;
176
177    Ok(WorkspaceResponse::Create {
178        name,
179        message: "Virtual branch created and applied to workspace".to_string(),
180    })
181}
182
183/// Apply a virtual branch to the workspace
184pub fn execute_apply(name: String) -> Result<WorkspaceResponse, LitError> {
185    let repo_root = find_repo_root()?;
186    let mut meta = load_workspace(&repo_root);
187
188    let branch = meta
189        .branches
190        .iter_mut()
191        .find(|b| b.name == name)
192        .ok_or_else(|| LitError::general(format!("Virtual branch '{}' not found", name)))?;
193
194    branch.active = true;
195    save_workspace(&repo_root, &meta)?;
196
197    Ok(WorkspaceResponse::Apply {
198        name,
199        message: "Virtual branch applied to workspace".to_string(),
200    })
201}
202
203/// Unapply a virtual branch from the workspace
204pub fn execute_unapply(name: String) -> Result<WorkspaceResponse, LitError> {
205    let repo_root = find_repo_root()?;
206    let mut meta = load_workspace(&repo_root);
207
208    let branch = meta
209        .branches
210        .iter_mut()
211        .find(|b| b.name == name)
212        .ok_or_else(|| LitError::general(format!("Virtual branch '{}' not found", name)))?;
213
214    branch.active = false;
215    save_workspace(&repo_root, &meta)?;
216
217    Ok(WorkspaceResponse::Unapply {
218        name,
219        message: "Virtual branch unapplied from workspace".to_string(),
220    })
221}
222
223/// Move a file from one virtual branch to another
224pub fn execute_move_file(
225    file: String,
226    from: String,
227    to: String,
228) -> Result<WorkspaceResponse, LitError> {
229    let repo_root = find_repo_root()?;
230    let mut meta = load_workspace(&repo_root);
231
232    // Remove file from source branch
233    if let Some(src) = meta.branches.iter_mut().find(|b| b.name == from) {
234        src.files.retain(|f| f != &file);
235    } else {
236        return Err(LitError::general(format!(
237            "Source branch '{}' not found",
238            from
239        )));
240    }
241
242    // Add file to destination branch
243    if let Some(dst) = meta.branches.iter_mut().find(|b| b.name == to) {
244        if !dst.files.contains(&file) {
245            dst.files.push(file.clone());
246        }
247    } else {
248        return Err(LitError::general(format!(
249            "Destination branch '{}' not found",
250            to
251        )));
252    }
253
254    save_workspace(&repo_root, &meta)?;
255
256    Ok(WorkspaceResponse::MoveFile {
257        file,
258        from,
259        to,
260        message: "File moved between virtual branches".to_string(),
261    })
262}