Skip to main content

jj_lib/
workspace_store.rs

1// Copyright 2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Workspace store for managing workspace metadata.
16
17use 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::lock::FileLock;
30use jj_lib::lock::FileLockError;
31use jj_lib::protos::simple_workspace_store;
32use jj_lib::ref_name::WorkspaceName;
33use prost::Message as _;
34use tempfile::NamedTempFile;
35use thiserror::Error;
36
37/// Errors that can occur when interacting with a workspace store.
38#[derive(Error, Debug)]
39pub enum WorkspaceStoreError {
40    /// An unspecified error occurred.
41    #[error(transparent)]
42    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
43}
44
45/// A storage backend for workspace metadata.
46pub trait WorkspaceStore: Send + Sync + Debug {
47    /// Returns the name of this workspace store implementation.
48    fn name(&self) -> &str;
49
50    /// Adds a workspace with the given name and path to the store.
51    fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError>;
52
53    /// Forgets the workspaces with the given names.
54    fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError>;
55
56    /// Renames a workspace from `old_name` to `new_name`.
57    fn rename(
58        &self,
59        old_name: &WorkspaceName,
60        new_name: &WorkspaceName,
61    ) -> Result<(), WorkspaceStoreError>;
62
63    /// Gets the path of the workspace with the given name, if it exists.
64    fn get_workspace_path(
65        &self,
66        workspace_name: &WorkspaceName,
67    ) -> Result<Option<PathBuf>, WorkspaceStoreError>;
68}
69
70/// Errors specific to the `SimpleWorkspaceStore` implementation.
71#[derive(Error, Debug)]
72pub enum SimpleWorkspaceStoreError {
73    /// An I/O error related to a file path.
74    #[error(transparent)]
75    Path(#[from] PathError),
76    /// An error occurred while trying to lock the workspace store.
77    #[error("Failed to lock workspace store")]
78    Lock(#[from] FileLockError),
79    /// An error occurred while decoding Protobuf data.
80    #[error(transparent)]
81    ProstDecode(#[from] prost::DecodeError),
82    /// An error occurred due to bad path encoding.
83    #[error(transparent)]
84    BadPathEncoding(#[from] BadPathEncoding),
85}
86
87impl From<SimpleWorkspaceStoreError> for WorkspaceStoreError {
88    fn from(err: SimpleWorkspaceStoreError) -> Self {
89        Self::Other(Box::new(err))
90    }
91}
92
93/// A simple file-based implementation of `WorkspaceStore`.
94#[derive(Debug)]
95pub struct SimpleWorkspaceStore {
96    store_file: PathBuf,
97    lock_file: PathBuf,
98}
99
100impl SimpleWorkspaceStore {
101    /// Loads the workspace store from the given repository path.
102    pub fn load(repo_path: &Path) -> Result<Self, WorkspaceStoreError> {
103        let store_dir = repo_path.join("workspace_store");
104        let file = store_dir.join("index");
105
106        let store = Self {
107            store_file: file.clone(),
108            lock_file: file.with_extension("lock"),
109        };
110
111        // Ensure the workspace_store directory exists. We need this
112        // for repos that were created before workspace_store was added.
113        if !store_dir.exists() {
114            fs::create_dir(&store_dir)
115                .context(store_dir)
116                .map_err(SimpleWorkspaceStoreError::Path)?;
117
118            let _lock = store.lock()?;
119
120            store.write_store(simple_workspace_store::Workspaces::default())?;
121        }
122
123        Ok(store)
124    }
125
126    fn lock(&self) -> Result<FileLock, SimpleWorkspaceStoreError> {
127        Ok(FileLock::lock(self.lock_file.clone())?)
128    }
129
130    fn read_store(&self) -> Result<simple_workspace_store::Workspaces, SimpleWorkspaceStoreError> {
131        let workspace_data = fs::read(&self.store_file).context(&self.store_file)?;
132
133        let workspaces_proto = simple_workspace_store::Workspaces::decode(&*workspace_data)?;
134
135        Ok(workspaces_proto)
136    }
137
138    fn write_store(
139        &self,
140        workspaces_proto: simple_workspace_store::Workspaces,
141    ) -> Result<(), SimpleWorkspaceStoreError> {
142        // We had created the store dir in load(), so parent() must exist.
143        let store_file_parent = self.store_file.parent().unwrap();
144        let temp_file = NamedTempFile::new_in(store_file_parent).context(store_file_parent)?;
145
146        temp_file
147            .as_file()
148            .write_all(&workspaces_proto.encode_to_vec())
149            .context(temp_file.path())?;
150
151        persist_temp_file(temp_file, &self.store_file).context(&self.store_file)?;
152
153        Ok(())
154    }
155}
156
157impl WorkspaceStore for SimpleWorkspaceStore {
158    fn name(&self) -> &'static str {
159        "simple"
160    }
161
162    fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError> {
163        let _lock = self.lock()?;
164
165        let mut workspaces_proto = self.read_store()?;
166
167        // Delete any existing entry with the same name
168        workspaces_proto
169            .workspaces
170            .retain(|w| w.name.as_str() != workspace_name.as_str());
171
172        workspaces_proto
173            .workspaces
174            .push(simple_workspace_store::Workspace {
175                name: workspace_name.as_str().to_string(),
176                path: path_to_bytes(path)
177                    .map_err(SimpleWorkspaceStoreError::BadPathEncoding)?
178                    .to_owned(),
179            });
180
181        self.write_store(workspaces_proto)?;
182
183        Ok(())
184    }
185
186    fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError> {
187        let _lock = self.lock()?;
188
189        let mut workspaces_proto = self.read_store()?;
190
191        workspaces_proto.workspaces.retain(|w| {
192            !workspace_names
193                .iter()
194                .any(|name| w.name.as_str() == name.as_str())
195        });
196
197        self.write_store(workspaces_proto)?;
198
199        Ok(())
200    }
201
202    fn rename(
203        &self,
204        old_name: &WorkspaceName,
205        new_name: &WorkspaceName,
206    ) -> Result<(), WorkspaceStoreError> {
207        let _lock = self.lock()?;
208
209        let mut workspaces_proto = self.read_store()?;
210
211        for workspace in &mut workspaces_proto.workspaces {
212            if workspace.name.as_str() == old_name.as_str() {
213                workspace.name = new_name.as_str().to_string();
214            }
215        }
216
217        self.write_store(workspaces_proto)?;
218
219        Ok(())
220    }
221
222    fn get_workspace_path(
223        &self,
224        workspace_name: &WorkspaceName,
225    ) -> Result<Option<PathBuf>, WorkspaceStoreError> {
226        let workspace = self
227            .read_store()?
228            .workspaces
229            .iter()
230            .find(|w| w.name.as_str() == workspace_name.as_str())
231            .cloned();
232
233        Ok(workspace
234            .map(|w| {
235                path_from_bytes(&w.path)
236                    .map(|p| p.to_path_buf())
237                    .map_err(SimpleWorkspaceStoreError::BadPathEncoding)
238            })
239            .transpose()?)
240    }
241}