Add local file conflict metadata
This commit is contained in:
parent
e1f36ffafb
commit
20c88af800
13 changed files with 673 additions and 15 deletions
|
|
@ -133,6 +133,22 @@ impl Store {
|
|||
latest_tree_json TEXT,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_conflicts (
|
||||
conflict_id TEXT PRIMARY KEY,
|
||||
root_name TEXT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
base_tree_hash TEXT,
|
||||
local_tree_hash TEXT,
|
||||
remote_tree_hash TEXT,
|
||||
detail TEXT NOT NULL,
|
||||
resolution TEXT,
|
||||
resolution_note TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
resolved_at_ms INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS peer_cards (
|
||||
peer_id TEXT PRIMARY KEY,
|
||||
card_json TEXT NOT NULL,
|
||||
|
|
@ -450,6 +466,78 @@ impl Store {
|
|||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn upsert_file_conflict(&self, conflict: &StoredFileConflict) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO file_conflicts(
|
||||
conflict_id, root_name, resource_id, path, kind, status, base_tree_hash,
|
||||
local_tree_hash, remote_tree_hash, detail, resolution, resolution_note,
|
||||
created_at_ms, resolved_at_ms
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)"#,
|
||||
params![
|
||||
conflict.conflict_id,
|
||||
conflict.root_name,
|
||||
conflict.resource_id,
|
||||
conflict.path,
|
||||
conflict.kind,
|
||||
conflict.status,
|
||||
conflict.base_tree_hash,
|
||||
conflict.local_tree_hash,
|
||||
conflict.remote_tree_hash,
|
||||
conflict.detail,
|
||||
conflict.resolution,
|
||||
conflict.resolution_note,
|
||||
conflict.created_at_ms,
|
||||
conflict.resolved_at_ms
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_file_conflict(
|
||||
&self,
|
||||
conflict_id: &str,
|
||||
) -> Result<Option<StoredFileConflict>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT conflict_id, root_name, resource_id, path, kind, status, base_tree_hash,
|
||||
local_tree_hash, remote_tree_hash, detail, resolution, resolution_note,
|
||||
created_at_ms, resolved_at_ms
|
||||
FROM file_conflicts WHERE conflict_id = ?1"#,
|
||||
)?;
|
||||
let mut rows = stmt.query(params![conflict_id])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(stored_file_conflict_from_row(row)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_file_conflicts(
|
||||
&self,
|
||||
root_name: Option<&str>,
|
||||
) -> Result<Vec<StoredFileConflict>, StoreError> {
|
||||
let sql = if root_name.is_some() {
|
||||
r#"SELECT conflict_id, root_name, resource_id, path, kind, status, base_tree_hash,
|
||||
local_tree_hash, remote_tree_hash, detail, resolution, resolution_note,
|
||||
created_at_ms, resolved_at_ms
|
||||
FROM file_conflicts WHERE root_name = ?1
|
||||
ORDER BY status, created_at_ms, conflict_id"#
|
||||
} else {
|
||||
r#"SELECT conflict_id, root_name, resource_id, path, kind, status, base_tree_hash,
|
||||
local_tree_hash, remote_tree_hash, detail, resolution, resolution_note,
|
||||
created_at_ms, resolved_at_ms
|
||||
FROM file_conflicts
|
||||
ORDER BY status, root_name, created_at_ms, conflict_id"#
|
||||
};
|
||||
let mut stmt = self.conn.prepare(sql)?;
|
||||
let rows = if let Some(root_name) = root_name {
|
||||
stmt.query_map(params![root_name], stored_file_conflict_from_row)?
|
||||
} else {
|
||||
stmt.query_map([], stored_file_conflict_from_row)?
|
||||
};
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_resource_secret(&self, secret: &StoredResourceSecret) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO resource_secrets(
|
||||
|
|
@ -857,6 +945,27 @@ fn stored_ssh_cert_request_from_row(
|
|||
})
|
||||
}
|
||||
|
||||
fn stored_file_conflict_from_row(
|
||||
row: &rusqlite::Row<'_>,
|
||||
) -> Result<StoredFileConflict, rusqlite::Error> {
|
||||
Ok(StoredFileConflict {
|
||||
conflict_id: row.get(0)?,
|
||||
root_name: row.get(1)?,
|
||||
resource_id: row.get(2)?,
|
||||
path: row.get(3)?,
|
||||
kind: row.get(4)?,
|
||||
status: row.get(5)?,
|
||||
base_tree_hash: row.get(6)?,
|
||||
local_tree_hash: row.get(7)?,
|
||||
remote_tree_hash: row.get(8)?,
|
||||
detail: row.get(9)?,
|
||||
resolution: row.get(10)?,
|
||||
resolution_note: row.get(11)?,
|
||||
created_at_ms: row.get(12)?,
|
||||
resolved_at_ms: row.get(13)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredResource {
|
||||
pub resource_id: String,
|
||||
|
|
@ -908,6 +1017,24 @@ pub struct StoredFileRoot {
|
|||
pub updated_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredFileConflict {
|
||||
pub conflict_id: String,
|
||||
pub root_name: String,
|
||||
pub resource_id: String,
|
||||
pub path: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub base_tree_hash: Option<String>,
|
||||
pub local_tree_hash: Option<String>,
|
||||
pub remote_tree_hash: Option<String>,
|
||||
pub detail: String,
|
||||
pub resolution: Option<String>,
|
||||
pub resolution_note: Option<String>,
|
||||
pub created_at_ms: i64,
|
||||
pub resolved_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredResourceSecret {
|
||||
pub secret_id: String,
|
||||
|
|
@ -1204,6 +1331,62 @@ mod tests {
|
|||
assert_eq!(store.list_file_roots().expect("list roots"), vec![root]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_conflicts_roundtrip_and_resolve() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let conflict = StoredFileConflict {
|
||||
conflict_id: "file-conflict:1".to_owned(),
|
||||
root_name: "notes".to_owned(),
|
||||
resource_id: "resource:cas-tree:notes".to_owned(),
|
||||
path: "notes/todo.md".to_owned(),
|
||||
kind: "concurrent-edit".to_owned(),
|
||||
status: "open".to_owned(),
|
||||
base_tree_hash: None,
|
||||
local_tree_hash: Some(
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_owned(),
|
||||
),
|
||||
remote_tree_hash: Some(
|
||||
"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210".to_owned(),
|
||||
),
|
||||
detail: "local and remote edits touched the same path".to_owned(),
|
||||
resolution: None,
|
||||
resolution_note: None,
|
||||
created_at_ms: 10,
|
||||
resolved_at_ms: None,
|
||||
};
|
||||
store
|
||||
.upsert_file_conflict(&conflict)
|
||||
.expect("insert conflict");
|
||||
assert_eq!(
|
||||
store
|
||||
.get_file_conflict("file-conflict:1")
|
||||
.expect("get conflict"),
|
||||
Some(conflict.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.list_file_conflicts(Some("notes"))
|
||||
.expect("list by root"),
|
||||
vec![conflict.clone()]
|
||||
);
|
||||
|
||||
let resolved = StoredFileConflict {
|
||||
status: "resolved".to_owned(),
|
||||
resolution: Some("keep-local".to_owned()),
|
||||
resolution_note: Some("local file is authoritative".to_owned()),
|
||||
resolved_at_ms: Some(20),
|
||||
..conflict
|
||||
};
|
||||
store
|
||||
.upsert_file_conflict(&resolved)
|
||||
.expect("resolve conflict");
|
||||
|
||||
assert_eq!(
|
||||
store.list_file_conflicts(None).expect("list all"),
|
||||
vec![resolved]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kv_store_and_entries_roundtrip() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
|
|
|
|||
Loading…
Reference in a new issue