1use crate::core::{find_repo_root, list_refs, read_head};
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6#[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
85fn workspace_path(repo_root: &std::path::Path) -> std::path::PathBuf {
87 repo_root.join(".lit").join("workspace.json")
88}
89
90#[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
125pub 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
152pub 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 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 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
183pub 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
203pub 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
223pub 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 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 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}