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