Skip to main content

jj_lib/
simple_workspace_store.rs

1// Copyright 2026 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//! Contains the implementation of a simple file-based `WorkspaceStore`.
16
17use std::fs;
18use std::io::Write as _;
19use std::path::Path;
20use std::path::PathBuf;
21
22use jj_core::backend::BackendInitError;
23use jj_core::backend::BackendLoadError;
24use prost::Message as _;
25use tempfile::NamedTempFile;
26use thiserror::Error;
27
28use crate::file_util::BadPathEncoding;
29use crate::file_util::IoResultExt as _;
30use crate::file_util::PathError;
31use crate::file_util::normalize_path;
32use crate::file_util::path_from_bytes;
33use crate::file_util::path_to_bytes;
34use crate::file_util::persist_temp_file;
35use crate::file_util::relative_path;
36use crate::file_util::slash_path;
37use crate::lock::FileLock;
38use crate::lock::FileLockError;
39use crate::protos::simple_workspace_store;
40use crate::ref_name::WorkspaceName;
41use crate::workspace_store::WorkspaceStore;
42use crate::workspace_store::WorkspaceStoreError;
43
44/// Errors specific to the `SimpleWorkspaceStore` implementation.
45#[derive(Error, Debug)]
46pub enum SimpleWorkspaceStoreError {
47    /// An I/O error related to a file path.
48    #[error(transparent)]
49    Path(#[from] PathError),
50    /// An error occurred while trying to lock the workspace store.
51    #[error("Failed to lock workspace store")]
52    Lock(#[from] FileLockError),
53    /// An error occurred while decoding Protobuf data.
54    #[error(transparent)]
55    ProstDecode(#[from] prost::DecodeError),
56    /// An error occurred due to bad path encoding.
57    #[error(transparent)]
58    BadPathEncoding(#[from] BadPathEncoding),
59}
60
61impl From<SimpleWorkspaceStoreError> for WorkspaceStoreError {
62    fn from(err: SimpleWorkspaceStoreError) -> Self {
63        Self::Other(Box::new(err))
64    }
65}
66
67/// A simple file-based implementation of `WorkspaceStore`.
68#[derive(Debug)]
69pub struct SimpleWorkspaceStore {
70    store_dir: PathBuf,
71    store_file: PathBuf,
72    lock_file: PathBuf,
73}
74
75impl SimpleWorkspaceStore {
76    /// The name of this WorkspaceStore implementation.
77    pub const NAME: &str = "simple_workspace_store";
78
79    fn new(store_dir: &Path) -> Self {
80        let store_dir = store_dir.to_path_buf();
81        let store_file = store_dir.join("index");
82        let lock_file = store_file.with_extension("lock");
83        Self {
84            store_dir,
85            store_file,
86            lock_file,
87        }
88    }
89
90    /// Loads the workspace store from the given store path and repo.
91    pub fn load(store_dir: &Path) -> Result<Self, BackendLoadError> {
92        let store = Self::new(store_dir);
93        if !store.store_file.exists() {
94            // TODO: Remove this in jj 0.57+: older repos do not have a workspace store index so we initialize it here.
95            store
96                .initialize()
97                .map_err(|err| BackendLoadError(err.into()))?;
98        }
99        Ok(store)
100    }
101
102    /// Initializes this SimpleWorkspaceStore with an empty index.
103    pub fn init(store_dir: &Path) -> Result<Self, BackendInitError> {
104        let store = Self::new(store_dir);
105        store.initialize()?;
106        Ok(store)
107    }
108
109    fn initialize(&self) -> Result<(), BackendInitError> {
110        let _lock = self.lock().map_err(|err| BackendInitError(err.into()))?;
111        self.write_store(simple_workspace_store::Workspaces::default())
112            .map_err(|err| BackendInitError(err.into()))?;
113        Ok(())
114    }
115
116    fn repo_path(&self) -> &Path {
117        self.store_dir
118            .parent()
119            .expect("store_dir must be under the repo_path")
120    }
121
122    fn lock(&self) -> Result<FileLock, FileLockError> {
123        FileLock::lock(self.lock_file.clone())
124    }
125
126    fn read_store(&self) -> Result<simple_workspace_store::Workspaces, SimpleWorkspaceStoreError> {
127        let workspace_data = fs::read(&self.store_file).context(&self.store_file)?;
128
129        let workspaces_proto = simple_workspace_store::Workspaces::decode(&*workspace_data)?;
130
131        Ok(workspaces_proto)
132    }
133
134    fn write_store(
135        &self,
136        workspaces_proto: simple_workspace_store::Workspaces,
137    ) -> Result<(), SimpleWorkspaceStoreError> {
138        // We had created the store dir in load(), so parent() must exist.
139        let store_file_parent = self.store_file.parent().unwrap();
140        let temp_file = NamedTempFile::new_in(store_file_parent).context(store_file_parent)?;
141
142        temp_file
143            .as_file()
144            .write_all(&workspaces_proto.encode_to_vec())
145            .context(temp_file.path())?;
146
147        persist_temp_file(temp_file, &self.store_file).context(&self.store_file)?;
148
149        Ok(())
150    }
151}
152
153impl WorkspaceStore for SimpleWorkspaceStore {
154    fn name(&self) -> &str {
155        Self::NAME
156    }
157
158    fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError> {
159        let _lock = self.lock().map_err(SimpleWorkspaceStoreError::Lock)?;
160
161        let mut workspaces_proto = self.read_store()?;
162
163        // Delete any existing entry with the same name
164        workspaces_proto
165            .workspaces
166            .retain(|w| w.name.as_str() != workspace_name.as_str());
167
168        let path_to_store = relative_path(self.repo_path(), path);
169        let path_to_store = if path_to_store.is_relative() {
170            slash_path(&path_to_store).into_owned()
171        } else {
172            path_to_store
173        };
174        workspaces_proto
175            .workspaces
176            .push(simple_workspace_store::Workspace {
177                name: workspace_name.as_str().to_string(),
178                path: path_to_bytes(&path_to_store)
179                    .map_err(SimpleWorkspaceStoreError::BadPathEncoding)?
180                    .to_owned(),
181            });
182
183        self.write_store(workspaces_proto)?;
184
185        Ok(())
186    }
187
188    fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError> {
189        let _lock = self.lock().map_err(SimpleWorkspaceStoreError::Lock)?;
190
191        let mut workspaces_proto = self.read_store()?;
192
193        workspaces_proto.workspaces.retain(|w| {
194            !workspace_names
195                .iter()
196                .any(|name| w.name.as_str() == name.as_str())
197        });
198
199        self.write_store(workspaces_proto)?;
200
201        Ok(())
202    }
203
204    fn rename(
205        &self,
206        old_name: &WorkspaceName,
207        new_name: &WorkspaceName,
208    ) -> Result<(), WorkspaceStoreError> {
209        let _lock = self.lock().map_err(SimpleWorkspaceStoreError::Lock)?;
210
211        let mut workspaces_proto = self.read_store()?;
212
213        for workspace in &mut workspaces_proto.workspaces {
214            if workspace.name.as_str() == old_name.as_str() {
215                workspace.name = new_name.as_str().to_string();
216            }
217        }
218
219        self.write_store(workspaces_proto)?;
220
221        Ok(())
222    }
223
224    fn get_workspace_path(
225        &self,
226        workspace_name: &WorkspaceName,
227    ) -> Result<Option<PathBuf>, WorkspaceStoreError> {
228        let workspaces = self.read_store()?.workspaces;
229        let Some(w) = workspaces
230            .iter()
231            .find(|w| w.name.as_str() == workspace_name.as_str())
232        else {
233            return Ok(None);
234        };
235
236        let path = path_from_bytes(&w.path).map_err(SimpleWorkspaceStoreError::BadPathEncoding)?;
237        // Resolve ".." components literally and convert to native separators
238        Ok(Some(normalize_path(&self.repo_path().join(path))))
239    }
240}