Skip to content

Commit c90e139

Browse files
committed
backend: add methods for reading and writing copy objects
This patch implements the methods only in the test backend. That should enough for us to start implementing diffing and merging on top, including tests for that functionality. Support in the Git backend can come once we've seen that the model works.
1 parent 1c185a6 commit c90e139

File tree

7 files changed

+208
-2
lines changed

7 files changed

+208
-2
lines changed

cli/examples/custom-backend/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ use jj_lib::backend::Commit;
3232
use jj_lib::backend::CommitId;
3333
use jj_lib::backend::Conflict;
3434
use jj_lib::backend::ConflictId;
35+
use jj_lib::backend::CopyHistory;
36+
use jj_lib::backend::CopyId;
3537
use jj_lib::backend::CopyRecord;
3638
use jj_lib::backend::FileId;
3739
use jj_lib::backend::SigningFn;
@@ -171,6 +173,18 @@ impl Backend for JitBackend {
171173
self.inner.write_symlink(path, target).await
172174
}
173175

176+
async fn read_copy(&self, id: &CopyId) -> BackendResult<CopyHistory> {
177+
self.inner.read_copy(id).await
178+
}
179+
180+
async fn write_copy(&self, contents: &CopyHistory) -> BackendResult<CopyId> {
181+
self.inner.write_copy(contents).await
182+
}
183+
184+
async fn get_related_copies(&self, copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
185+
self.inner.get_related_copies(copy_id).await
186+
}
187+
174188
async fn read_tree(&self, path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
175189
self.inner.read_tree(path, id).await
176190
}

lib/src/backend.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ id_type!(pub TreeId { hex() });
5151
id_type!(pub FileId { hex() });
5252
id_type!(pub SymlinkId { hex() });
5353
id_type!(pub ConflictId { hex() });
54+
id_type!(pub CopyId { hex() });
5455

5556
impl ChangeId {
5657
/// Returns the hex string representation of this ID, which uses `z-k`
@@ -196,6 +197,24 @@ pub struct CopyRecord {
196197
pub source_commit: CommitId,
197198
}
198199

200+
/// Describes the copy history of a file. The copy object is unchanged when a
201+
/// file is modified.
202+
#[derive(ContentHash, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
203+
pub struct CopyHistory {
204+
/// The file's current path.
205+
pub current_path: RepoPathBuf,
206+
/// IDs of the files that became the current incarnation of this file.
207+
///
208+
/// A newly created file has no parents. A regular copy or rename has one
209+
/// parent. A merge of multiple files has multiple parents.
210+
pub parents: Vec<CopyId>,
211+
/// An optional piece of data to give the Copy object a different ID. May be
212+
/// randomly generated. This allows a commit to say that a file was replaced
213+
/// by a new incarnation of it, indicating a logically distinct file
214+
/// taking the place of the previous file at the path.
215+
pub salt: Vec<u8>,
216+
}
217+
199218
/// Error that may occur during backend initialization.
200219
#[derive(Debug, Error)]
201220
#[error(transparent)]
@@ -269,6 +288,8 @@ pub type BackendResult<T> = Result<T, BackendError>;
269288

270289
#[derive(ContentHash, Debug, PartialEq, Eq, Clone, Hash)]
271290
pub enum TreeValue {
291+
// TODO: When there's a CopyId here, the copy object's path must match
292+
// the path identified by the tree.
272293
File { id: FileId, executable: bool },
273294
Symlink(SymlinkId),
274295
Tree(TreeId),
@@ -440,6 +461,29 @@ pub trait Backend: Send + Sync + Debug {
440461

441462
async fn write_symlink(&self, path: &RepoPath, target: &str) -> BackendResult<SymlinkId>;
442463

464+
/// Read the specified `CopyHistory` object.
465+
///
466+
/// Backends that don't support copy tracking may return
467+
/// `BackendError::Unsupported`.
468+
async fn read_copy(&self, id: &CopyId) -> BackendResult<CopyHistory>;
469+
470+
/// Write the `CopyHistory` object and return its ID.
471+
///
472+
/// Backends that don't support copy tracking may return
473+
/// `BackendError::Unsupported`.
474+
async fn write_copy(&self, copy: &CopyHistory) -> BackendResult<CopyId>;
475+
476+
/// Find all copy histories that are related to the specified one. This is
477+
/// defined as those that are ancestors of the given specified one, plus
478+
/// their descendants. Children must be returned before parents.
479+
///
480+
/// It is valid (but wasteful) to include other copy histories, such as
481+
/// siblings, or even completely unrelated copy histories.
482+
///
483+
/// Backends that don't support copy tracking may return
484+
/// `BackendError::Unsupported`.
485+
async fn get_related_copies(&self, copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>>;
486+
443487
async fn read_tree(&self, path: &RepoPath, id: &TreeId) -> BackendResult<Tree>;
444488

445489
async fn write_tree(&self, path: &RepoPath, contents: &Tree) -> BackendResult<TreeId>;

lib/src/git_backend.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ use crate::backend::CommitId;
6060
use crate::backend::Conflict;
6161
use crate::backend::ConflictId;
6262
use crate::backend::ConflictTerm;
63+
use crate::backend::CopyHistory;
64+
use crate::backend::CopyId;
6365
use crate::backend::CopyRecord;
6466
use crate::backend::FileId;
6567
use crate::backend::MergedTreeId;
@@ -1022,6 +1024,24 @@ impl Backend for GitBackend {
10221024
Ok(SymlinkId::new(oid.as_bytes().to_vec()))
10231025
}
10241026

1027+
async fn read_copy(&self, _id: &CopyId) -> BackendResult<CopyHistory> {
1028+
Err(BackendError::Unsupported(
1029+
"The Git backend doesn't support tracked copies yet".to_string(),
1030+
))
1031+
}
1032+
1033+
async fn write_copy(&self, _contents: &CopyHistory) -> BackendResult<CopyId> {
1034+
Err(BackendError::Unsupported(
1035+
"The Git backend doesn't support tracked copies yet".to_string(),
1036+
))
1037+
}
1038+
1039+
async fn get_related_copies(&self, _copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
1040+
Err(BackendError::Unsupported(
1041+
"The Git backend doesn't support tracked copies yet".to_string(),
1042+
))
1043+
}
1044+
10251045
async fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
10261046
if id == &self.empty_tree_id {
10271047
return Ok(Tree::default());

lib/src/repo_path.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,15 @@ impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
199199
impl FusedIterator for RepoPathComponentsIter<'_> {}
200200

201201
/// Owned repository path.
202-
#[derive(Clone, Eq, Hash, PartialEq)]
202+
#[derive(ContentHash, Clone, Eq, Hash, PartialEq)]
203203
pub struct RepoPathBuf {
204204
// Don't add more fields. Eq, Hash, and Ord must be compatible with the
205205
// borrowed RepoPath type.
206206
value: String,
207207
}
208208

209209
/// Borrowed repository path.
210-
#[derive(Eq, Hash, PartialEq, RefCastCustom)]
210+
#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom)]
211211
#[repr(transparent)]
212212
pub struct RepoPath {
213213
value: str,

lib/src/secret_backend.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ use crate::backend::Commit;
3232
use crate::backend::CommitId;
3333
use crate::backend::Conflict;
3434
use crate::backend::ConflictId;
35+
use crate::backend::CopyHistory;
36+
use crate::backend::CopyId;
3537
use crate::backend::CopyRecord;
3638
use crate::backend::FileId;
3739
use crate::backend::SigningFn;
@@ -161,6 +163,24 @@ impl Backend for SecretBackend {
161163
self.inner.write_symlink(path, target).await
162164
}
163165

166+
async fn read_copy(&self, _id: &CopyId) -> BackendResult<CopyHistory> {
167+
Err(BackendError::Unsupported(
168+
"The secret backend doesn't support copies".to_string(),
169+
))
170+
}
171+
172+
async fn write_copy(&self, _contents: &CopyHistory) -> BackendResult<CopyId> {
173+
Err(BackendError::Unsupported(
174+
"The secret backend doesn't support copies".to_string(),
175+
))
176+
}
177+
178+
async fn get_related_copies(&self, _copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
179+
Err(BackendError::Unsupported(
180+
"The secret backend doesn't support copies".to_string(),
181+
))
182+
}
183+
164184
async fn read_tree(&self, path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
165185
self.inner.read_tree(path, id).await
166186
}

lib/src/simple_backend.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ use crate::backend::CommitId;
4747
use crate::backend::Conflict;
4848
use crate::backend::ConflictId;
4949
use crate::backend::ConflictTerm;
50+
use crate::backend::CopyHistory;
51+
use crate::backend::CopyId;
5052
use crate::backend::CopyRecord;
5153
use crate::backend::FileId;
5254
use crate::backend::MergedTreeId;
@@ -253,6 +255,24 @@ impl Backend for SimpleBackend {
253255
Ok(id)
254256
}
255257

258+
async fn read_copy(&self, _id: &CopyId) -> BackendResult<CopyHistory> {
259+
Err(BackendError::Unsupported(
260+
"The simple backend doesn't support copies".to_string(),
261+
))
262+
}
263+
264+
async fn write_copy(&self, _contents: &CopyHistory) -> BackendResult<CopyId> {
265+
Err(BackendError::Unsupported(
266+
"The simple backend doesn't support copies".to_string(),
267+
))
268+
}
269+
270+
async fn get_related_copies(&self, _copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
271+
Err(BackendError::Unsupported(
272+
"The simple backend doesn't support copies".to_string(),
273+
))
274+
}
275+
256276
async fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
257277
let path = self.tree_path(id);
258278
let buf = fs::read(path).map_err(|err| map_not_found_err(err, id))?;

lib/testutils/src/test_backend.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ use jj_lib::backend::Commit;
3838
use jj_lib::backend::CommitId;
3939
use jj_lib::backend::Conflict;
4040
use jj_lib::backend::ConflictId;
41+
use jj_lib::backend::CopyHistory;
42+
use jj_lib::backend::CopyId;
4143
use jj_lib::backend::CopyRecord;
4244
use jj_lib::backend::FileId;
4345
use jj_lib::backend::SecureSig;
4446
use jj_lib::backend::SigningFn;
4547
use jj_lib::backend::SymlinkId;
4648
use jj_lib::backend::Tree;
4749
use jj_lib::backend::TreeId;
50+
use jj_lib::dag_walk::topo_order_forward;
4851
use jj_lib::index::Index;
4952
use jj_lib::object_id::ObjectId as _;
5053
use jj_lib::repo_path::RepoPath;
@@ -68,6 +71,7 @@ pub struct TestBackendData {
6871
files: HashMap<RepoPathBuf, HashMap<FileId, Vec<u8>>>,
6972
symlinks: HashMap<RepoPathBuf, HashMap<SymlinkId, String>>,
7073
conflicts: HashMap<RepoPathBuf, HashMap<ConflictId, Conflict>>,
74+
copies: HashMap<CopyId, CopyHistory>,
7175
}
7276

7377
#[derive(Clone, Default)]
@@ -249,6 +253,47 @@ impl Backend for TestBackend {
249253
Ok(id)
250254
}
251255

256+
async fn read_copy(&self, id: &CopyId) -> BackendResult<CopyHistory> {
257+
let copy = self.locked_data().copies.get(id).cloned().ok_or_else(|| {
258+
BackendError::ObjectNotFound {
259+
object_type: "copy".to_string(),
260+
hash: id.hex(),
261+
source: "".into(),
262+
}
263+
})?;
264+
Ok(copy)
265+
}
266+
267+
async fn write_copy(&self, contents: &CopyHistory) -> BackendResult<CopyId> {
268+
let id = CopyId::new(get_hash(contents));
269+
self.locked_data()
270+
.copies
271+
.insert(id.clone(), contents.clone());
272+
Ok(id)
273+
}
274+
275+
async fn get_related_copies(&self, copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
276+
let copies = &self.locked_data().copies;
277+
if !copies.contains_key(copy_id) {
278+
return Err(BackendError::ObjectNotFound {
279+
object_type: "copy history".to_string(),
280+
hash: copy_id.hex(),
281+
source: "".into(),
282+
});
283+
}
284+
// Return all copy histories to test that the caller correctly ignores histories
285+
// that are not relevant to the trees they're working with.
286+
let mut histories = vec![];
287+
for id in topo_order_forward(
288+
copies.keys(),
289+
|id| *id,
290+
|id| copies.get(*id).unwrap().parents.iter(),
291+
) {
292+
histories.push(copies.get(id).unwrap().clone());
293+
}
294+
Ok(histories)
295+
}
296+
252297
async fn read_tree(&self, path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
253298
if id == &self.empty_tree_id {
254299
return Ok(Tree::default());
@@ -356,3 +401,46 @@ impl Backend for TestBackend {
356401
Ok(())
357402
}
358403
}
404+
405+
#[cfg(test)]
406+
mod tests {
407+
408+
use pollster::FutureExt as _;
409+
410+
use super::*;
411+
use crate::repo_path_buf;
412+
413+
fn copy_history(path: &str, parents: &[CopyId]) -> CopyHistory {
414+
CopyHistory {
415+
current_path: repo_path_buf(path),
416+
parents: parents.to_vec(),
417+
salt: vec![],
418+
}
419+
}
420+
421+
#[test]
422+
fn get_related_copies() {
423+
let backend = TestBackend::with_data(Arc::new(Mutex::new(TestBackendData::default())));
424+
425+
// Test with a single chain so the resulting order is deterministic
426+
let copy1 = copy_history("foo1", &[]);
427+
let copy1_id = backend.write_copy(&copy1).block_on().unwrap();
428+
let copy2 = copy_history("foo2", &[copy1_id.clone()]);
429+
let copy2_id = backend.write_copy(&copy2).block_on().unwrap();
430+
let copy3 = copy_history("foo3", &[copy2_id.clone()]);
431+
let copy3_id = backend.write_copy(&copy3).block_on().unwrap();
432+
433+
// Error when looking up by non-existent id
434+
assert!(backend
435+
.get_related_copies(&CopyId::from_hex("abcd"))
436+
.block_on()
437+
.is_err());
438+
439+
// Looking up by any id returns the related copies in the same order (children
440+
// before parents)
441+
let related = backend.get_related_copies(&copy1_id).block_on().unwrap();
442+
assert_eq!(related, vec![copy3.clone(), copy2.clone(), copy1.clone()]);
443+
let related: Vec<CopyHistory> = backend.get_related_copies(&copy3_id).block_on().unwrap();
444+
assert_eq!(related, vec![copy3.clone(), copy2.clone(), copy1.clone()]);
445+
}
446+
}

0 commit comments

Comments
 (0)