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 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/// Errors that can occur when interacting with a workspace store.
41#[derive(Error, Debug)]
42pub enum WorkspaceStoreError {
43    /// An unspecified error occurred.
44    #[error(transparent)]
45    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
46}
47
48/// A storage backend for workspace metadata.
49pub trait WorkspaceStore: Send + Sync + Debug {
50    /// Returns the name of this workspace store implementation.
51    fn name(&self) -> &str;
52
53    /// Adds a workspace with the given name and path to the store.
54    fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError>;
55
56    /// Forgets the workspaces with the given names.
57    fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError>;
58
59    /// Renames a workspace from `old_name` to `new_name`.
60    fn rename(
61        &self,
62        old_name: &WorkspaceName,
63        new_name: &WorkspaceName,
64    ) -> Result<(), WorkspaceStoreError>;
65
66    /// Gets the path of the workspace with the given name, if it exists.
67    fn get_workspace_path(
68        &self,
69        workspace_name: &WorkspaceName,
70    ) -> Result<Option<PathBuf>, WorkspaceStoreError>;
71}
72
73/// Errors specific to the `SimpleWorkspaceStore` implementation.
74#[derive(Error, Debug)]
75pub enum SimpleWorkspaceStoreError {
76    /// An I/O error related to a file path.
77    #[error(transparent)]
78    Path(#[from] PathError),
79    /// An error occurred while trying to lock the workspace store.
80    #[error("Failed to lock workspace store")]
81    Lock(#[from] FileLockError),
82    /// An error occurred while decoding Protobuf data.
83    #[error(transparent)]
84    ProstDecode(#[from] prost::DecodeError),
85    /// An error occurred due to bad path encoding.
86    #[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/// A simple file-based implementation of `WorkspaceStore`.
97#[derive(Debug)]
98pub struct SimpleWorkspaceStore {
99    repo_path: PathBuf,
100    store_file: PathBuf,
101    lock_file: PathBuf,
102}
103
104impl SimpleWorkspaceStore {
105    /// Loads the workspace store from the given repository path.
106    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        // Ensure the workspace_store directory exists. We need this
117        // for repos that were created before workspace_store was added.
118        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        // We had created the store dir in load(), so parent() must exist.
148        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        // Delete any existing entry with the same name
173        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}