jj_lib/
workspace_store.rs1use std::fmt::Debug;
18use std::fs;
19use std::io::Write as _;
20use std::path::Path;
21use std::path::PathBuf;
22
23use jj_lib::file_util::BadPathEncoding;
24use jj_lib::file_util::IoResultExt as _;
25use jj_lib::file_util::PathError;
26use jj_lib::file_util::path_from_bytes;
27use jj_lib::file_util::path_to_bytes;
28use jj_lib::file_util::persist_temp_file;
29use jj_lib::file_util::relative_path;
30use jj_lib::file_util::slash_path;
31use jj_lib::lock::FileLock;
32use jj_lib::lock::FileLockError;
33use jj_lib::protos::simple_workspace_store;
34use jj_lib::ref_name::WorkspaceName;
35use prost::Message as _;
36use tempfile::NamedTempFile;
37use thiserror::Error;
38
39#[derive(Error, Debug)]
41pub enum WorkspaceStoreError {
42 #[error(transparent)]
44 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
45}
46
47pub trait WorkspaceStore: Send + Sync + Debug {
49 fn name(&self) -> &str;
51
52 fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError>;
54
55 fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError>;
57
58 fn rename(
60 &self,
61 old_name: &WorkspaceName,
62 new_name: &WorkspaceName,
63 ) -> Result<(), WorkspaceStoreError>;
64
65 fn get_workspace_path(
67 &self,
68 workspace_name: &WorkspaceName,
69 ) -> Result<Option<PathBuf>, WorkspaceStoreError>;
70}
71
72#[derive(Error, Debug)]
74pub enum SimpleWorkspaceStoreError {
75 #[error(transparent)]
77 Path(#[from] PathError),
78 #[error("Failed to lock workspace store")]
80 Lock(#[from] FileLockError),
81 #[error(transparent)]
83 ProstDecode(#[from] prost::DecodeError),
84 #[error(transparent)]
86 BadPathEncoding(#[from] BadPathEncoding),
87}
88
89impl From<SimpleWorkspaceStoreError> for WorkspaceStoreError {
90 fn from(err: SimpleWorkspaceStoreError) -> Self {
91 Self::Other(Box::new(err))
92 }
93}
94
95#[derive(Debug)]
97pub struct SimpleWorkspaceStore {
98 repo_path: PathBuf,
99 store_file: PathBuf,
100 lock_file: PathBuf,
101}
102
103impl SimpleWorkspaceStore {
104 pub fn load(repo_path: &Path) -> Result<Self, WorkspaceStoreError> {
106 let store_dir = repo_path.join("workspace_store");
107 let file = store_dir.join("index");
108
109 let store = Self {
110 repo_path: repo_path.to_path_buf(),
111 store_file: file.clone(),
112 lock_file: file.with_extension("lock"),
113 };
114
115 if !store_dir.exists() {
118 fs::create_dir(&store_dir)
119 .context(store_dir)
120 .map_err(SimpleWorkspaceStoreError::Path)?;
121
122 let _lock = store.lock()?;
123
124 store.write_store(simple_workspace_store::Workspaces::default())?;
125 }
126
127 Ok(store)
128 }
129
130 fn lock(&self) -> Result<FileLock, SimpleWorkspaceStoreError> {
131 Ok(FileLock::lock(self.lock_file.clone())?)
132 }
133
134 fn read_store(&self) -> Result<simple_workspace_store::Workspaces, SimpleWorkspaceStoreError> {
135 let workspace_data = fs::read(&self.store_file).context(&self.store_file)?;
136
137 let workspaces_proto = simple_workspace_store::Workspaces::decode(&*workspace_data)?;
138
139 Ok(workspaces_proto)
140 }
141
142 fn write_store(
143 &self,
144 workspaces_proto: simple_workspace_store::Workspaces,
145 ) -> Result<(), SimpleWorkspaceStoreError> {
146 let store_file_parent = self.store_file.parent().unwrap();
148 let temp_file = NamedTempFile::new_in(store_file_parent).context(store_file_parent)?;
149
150 temp_file
151 .as_file()
152 .write_all(&workspaces_proto.encode_to_vec())
153 .context(temp_file.path())?;
154
155 persist_temp_file(temp_file, &self.store_file).context(&self.store_file)?;
156
157 Ok(())
158 }
159}
160
161impl WorkspaceStore for SimpleWorkspaceStore {
162 fn name(&self) -> &'static str {
163 "simple"
164 }
165
166 fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError> {
167 let _lock = self.lock()?;
168
169 let mut workspaces_proto = self.read_store()?;
170
171 workspaces_proto
173 .workspaces
174 .retain(|w| w.name.as_str() != workspace_name.as_str());
175
176 let path_to_store = relative_path(&self.repo_path, path);
177 let path_to_store = if path_to_store.is_relative() {
178 slash_path(&path_to_store).into_owned()
179 } else {
180 path_to_store
181 };
182 workspaces_proto
183 .workspaces
184 .push(simple_workspace_store::Workspace {
185 name: workspace_name.as_str().to_string(),
186 path: path_to_bytes(&path_to_store)
187 .map_err(SimpleWorkspaceStoreError::BadPathEncoding)?
188 .to_owned(),
189 });
190
191 self.write_store(workspaces_proto)?;
192
193 Ok(())
194 }
195
196 fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError> {
197 let _lock = self.lock()?;
198
199 let mut workspaces_proto = self.read_store()?;
200
201 workspaces_proto.workspaces.retain(|w| {
202 !workspace_names
203 .iter()
204 .any(|name| w.name.as_str() == name.as_str())
205 });
206
207 self.write_store(workspaces_proto)?;
208
209 Ok(())
210 }
211
212 fn rename(
213 &self,
214 old_name: &WorkspaceName,
215 new_name: &WorkspaceName,
216 ) -> Result<(), WorkspaceStoreError> {
217 let _lock = self.lock()?;
218
219 let mut workspaces_proto = self.read_store()?;
220
221 for workspace in &mut workspaces_proto.workspaces {
222 if workspace.name.as_str() == old_name.as_str() {
223 workspace.name = new_name.as_str().to_string();
224 }
225 }
226
227 self.write_store(workspaces_proto)?;
228
229 Ok(())
230 }
231
232 fn get_workspace_path(
233 &self,
234 workspace_name: &WorkspaceName,
235 ) -> Result<Option<PathBuf>, WorkspaceStoreError> {
236 let workspace = self
237 .read_store()?
238 .workspaces
239 .iter()
240 .find(|w| w.name.as_str() == workspace_name.as_str())
241 .cloned();
242
243 Ok(workspace
244 .map(|w| {
245 path_from_bytes(&w.path)
246 .map(|p| p.to_path_buf())
247 .map_err(SimpleWorkspaceStoreError::BadPathEncoding)
248 })
249 .transpose()?)
250 }
251}