1use std::{
2 collections::HashMap,
3 convert::TryInto,
4 io::{self, BufReader},
5 path::{Path, PathBuf},
6 rc::Rc,
7 string::ToString,
8};
9
10use gen_core::{HashId, Workspace, calculate_hash, traits::Capnp};
11use gen_graph::{OperationGraph, all_simple_paths};
12use petgraph::{Direction, graphmap::UnGraphMap};
13use rusqlite::{Result as SQLResult, Row, params, types::Value};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16use thiserror::Error;
17
18use crate::{
19 changesets::{
20 DatabaseChangeset, get_changeset_dependencies_from_path, get_changeset_from_path,
21 },
22 db::OperationsConnection,
23 errors::{BranchError, FileAdditionError, RemoteError},
24 file_types::FileTypes,
25 gen_models_capnp::operation,
26 session_operations::DependencyModels,
27 traits::*,
28};
29
30#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
31pub struct Operation {
32 pub hash: HashId,
33 pub parent_hash: Option<HashId>,
34 pub change_type: String,
35 pub created_on: i64,
36}
37
38impl<'a> Capnp<'a> for Operation {
39 type Builder = operation::Builder<'a>;
40 type Reader = operation::Reader<'a>;
41
42 fn write_capnp(&self, builder: &mut Self::Builder) {
43 builder.set_hash(&self.hash.0).unwrap();
44 match &self.parent_hash {
45 None => {
46 builder.reborrow().get_parent_hash().set_none(());
47 }
48 Some(n) => {
49 builder.reborrow().get_parent_hash().set_some(&n.0).unwrap();
50 }
51 }
52 builder.set_change_type(&self.change_type);
53 builder.set_created_on(self.created_on);
54 }
55
56 fn read_capnp(reader: Self::Reader) -> Self {
57 let hash = reader
58 .get_hash()
59 .unwrap()
60 .as_slice()
61 .unwrap()
62 .try_into()
63 .unwrap();
64 let parent_hash = match reader.get_parent_hash().which().unwrap() {
65 operation::parent_hash::None(()) => None,
66 operation::parent_hash::Some(n) => {
67 Some(n.unwrap().as_slice().unwrap().try_into().unwrap())
68 }
69 };
70 let change_type = reader.get_change_type().unwrap().to_string().unwrap();
71 let created_on = reader.get_created_on();
72
73 Operation {
74 hash,
75 parent_hash,
76 change_type,
77 created_on,
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub struct HashRange {
84 pub from: Option<HashId>,
85 pub to: Option<HashId>,
86}
87
88#[derive(Debug, Error)]
89pub enum HashParseError {
90 #[error("No current branch is checked out.")]
91 NoCurrentBranch,
92 #[error("Branch '{0}' not found.")]
93 BranchNotFound(String),
94 #[error("Branch '{0}' has no operations.")]
95 EmptyBranch(String),
96 #[error("Reference '{0}' is not a valid HEAD shorthand.")]
97 InvalidHead(String),
98 #[error("HEAD offset {0} is out of range for the current branch.")]
99 HeadOffsetOutOfRange(usize),
100 #[error("Reference '{0}' did not match any operation.")]
101 OperationNotFound(String),
102 #[error("Reference '{0}' matches multiple operations.")]
103 OperationAmbiguous(String),
104}
105
106impl Operation {
107 pub fn create(
108 conn: &OperationsConnection,
109 change_type: &str,
110 hash: &HashId,
111 ) -> SQLResult<Operation> {
112 let current_op = OperationState::get_operation(conn);
113 let current_branch_id =
114 OperationState::get_current_branch(conn).expect("No branch is checked out.");
115
116 let timestamp = chrono::Utc::now().timestamp_nanos_opt().unwrap();
117 let query = "INSERT INTO operations (hash, change_type, parent_hash, created_on) VALUES (?1, ?2, ?3, ?4);";
118 let mut stmt = conn.prepare(query).unwrap();
119 stmt.execute(params![hash, change_type, current_op, timestamp])?;
120 let operation = Operation {
121 hash: *hash,
122 parent_hash: current_op,
123 change_type: change_type.to_string(),
124 created_on: timestamp,
125 };
126 OperationState::set_operation(conn, &operation.hash);
128 Branch::set_current_operation(conn, current_branch_id, &operation.hash);
129 Ok(operation)
130 }
131
132 pub fn create_without_tracking(
133 conn: &OperationsConnection,
134 hash: &HashId,
135 change_type: &str,
136 parent_hash: Option<HashId>,
137 created_on: Option<i64>,
138 ) -> SQLResult<Operation> {
139 let timestamp = created_on.unwrap_or(chrono::Utc::now().timestamp_nanos_opt().unwrap());
140 let query = "INSERT INTO operations (hash, change_type, parent_hash, created_on) VALUES (?1, ?2, ?3, ?4);";
141 let mut stmt = conn.prepare(query).unwrap();
142 stmt.execute(params![hash, change_type, parent_hash, timestamp])?;
143 let operation = Operation {
144 hash: *hash,
145 parent_hash,
146 change_type: change_type.to_string(),
147 created_on: timestamp,
148 };
149 Ok(operation)
150 }
151
152 pub fn add_file(
153 conn: &OperationsConnection,
154 operation_hash: &HashId,
155 file_addition_id: &HashId,
156 ) -> SQLResult<()> {
157 let query =
158 "INSERT INTO operation_files (operation_hash, file_addition_id) VALUES (?1, ?2)";
159 let mut stmt = conn.prepare(query).unwrap();
160 stmt.execute(params![operation_hash, file_addition_id])?;
161 Ok(())
162 }
163
164 pub fn add_database(
165 conn: &OperationsConnection,
166 operation_hash: &HashId,
167 db_uuid: &str,
168 ) -> SQLResult<()> {
169 let query =
170 "INSERT INTO operation_databases (operation_hash, database_uuid) VALUES (?1, ?2)";
171 let mut stmt = conn.prepare(query).unwrap();
172 stmt.execute(params![operation_hash, db_uuid])?;
173 Ok(())
174 }
175
176 pub fn get_upstream(conn: &OperationsConnection, operation_hash: &HashId) -> Vec<HashId> {
177 let query = "WITH RECURSIVE r_operations(operation_hash, depth) AS ( \
178 select ?1, 0 UNION \
179 select parent_hash, depth + 1 from r_operations join operations ON hash=operation_hash \
180 ) SELECT operation_hash, depth from r_operations where operation_hash is not null order by depth desc;";
181 let mut stmt = conn.prepare(query).unwrap();
182 stmt.query_map([operation_hash], |row| row.get(0))
183 .unwrap()
184 .map(|id| id.unwrap())
185 .collect::<Vec<HashId>>()
186 }
187
188 pub fn get_operation_graph(conn: &OperationsConnection) -> OperationGraph {
189 let mut graph = OperationGraph::new();
190 let operations = Operation::query(conn, "select * from operations;", rusqlite::params![]);
191 for op in operations.iter() {
192 graph.add_node(op.hash);
193 if let Some(v) = op.parent_hash {
194 graph.add_node(v);
195 graph.add_edge(v, op.hash, ());
196 }
197 }
198 graph
199 }
200
201 pub fn get_path_between(
202 conn: &OperationsConnection,
203 source_node: HashId,
204 target_node: HashId,
205 ) -> Vec<(HashId, Direction, HashId)> {
206 let directed_graph = Operation::get_operation_graph(conn);
207 let mut undirected_graph: UnGraphMap<HashId, ()> = Default::default();
208
209 for node in directed_graph.nodes() {
210 undirected_graph.add_node(node);
211 }
212 for (source, target, _weight) in directed_graph.all_edges() {
213 undirected_graph.add_edge(source, target, ());
214 }
215 let mut patch_path: Vec<(HashId, Direction, HashId)> = vec![];
216 for path in all_simple_paths(&undirected_graph, source_node, target_node) {
217 let mut last_node = source_node;
218 for node in &path[1..] {
219 if *node != source_node {
220 for (_edge_src, edge_target, _edge_weight) in
221 directed_graph.edges_directed(last_node, Direction::Outgoing)
222 {
223 if edge_target == *node {
224 patch_path.push((last_node, Direction::Outgoing, *node));
225 break;
226 }
227 }
228 for (edge_src, _edge_target, _edge_weight) in
229 directed_graph.edges_directed(last_node, Direction::Incoming)
230 {
231 if edge_src == *node {
232 patch_path.push((last_node, Direction::Incoming, *node));
233 break;
234 }
235 }
236 }
237 last_node = *node;
238 }
239 }
240 patch_path
241 }
242
243 pub fn search_hash(
244 conn: &OperationsConnection,
245 op_hash: &str,
246 ) -> Result<Operation, HashParseError> {
247 let matches = Operation::search_hashes(conn, op_hash);
248 match matches.len() {
249 0 => Err(HashParseError::OperationNotFound(op_hash.to_string())),
250 1 => Ok(matches[0].clone()),
251 _ => Err(HashParseError::OperationAmbiguous(op_hash.to_string())),
252 }
253 }
254
255 pub fn search_hashes(conn: &OperationsConnection, op_hash: &str) -> Vec<Operation> {
256 Operation::query(
257 conn,
258 "select * from operations where hex(hash) LIKE ?1",
259 params![format!("{op_hash}%")],
260 )
261 }
262
263 pub fn get_changeset_path(&self, workspace: &Workspace) -> PathBuf {
264 workspace.changeset_path(&self.hash).join("changeset")
265 }
266
267 pub fn get_changeset_dependencies_path(&self, workspace: &Workspace) -> PathBuf {
268 workspace.changeset_path(&self.hash).join("dependencies")
269 }
270
271 pub fn get_changeset(&self, workspace: &Workspace) -> DatabaseChangeset {
272 let path = self.get_changeset_path(workspace);
273 get_changeset_from_path(path)
274 }
275
276 pub fn get_changeset_dependencies(&self, workspace: &Workspace) -> DependencyModels {
277 let path = self.get_changeset_dependencies_path(workspace);
278 get_changeset_dependencies_from_path(path)
279 }
280}
281
282pub fn parse_hash(conn: &OperationsConnection, input: &str) -> Result<HashRange, HashParseError> {
283 if input.contains("..") {
284 let mut it = input.split("..");
285 let from_ref = it.next().unwrap_or_default();
286 let to_ref = it.next().unwrap_or_default();
287 return Ok(HashRange {
288 from: Some(resolve_reference(conn, from_ref)?),
289 to: Some(resolve_reference(conn, to_ref)?),
290 });
291 }
292
293 Ok(HashRange {
294 from: None,
295 to: Some(resolve_reference(conn, input)?),
296 })
297}
298
299fn resolve_reference(
300 conn: &OperationsConnection,
301 reference: &str,
302) -> Result<HashId, HashParseError> {
303 if reference.starts_with("HEAD") {
304 return resolve_head(conn, reference);
305 }
306
307 if let Some(branch) = Branch::get_by_name(conn, reference) {
308 if let Some(hash) = branch.current_operation_hash {
309 return Ok(hash);
310 }
311 return Err(HashParseError::EmptyBranch(branch.name));
312 }
313
314 let operation = Operation::search_hash(conn, reference)?;
315 Ok(operation.hash)
316}
317
318fn resolve_head(conn: &OperationsConnection, reference: &str) -> Result<HashId, HashParseError> {
319 let branch_id =
320 OperationState::get_current_branch(conn).ok_or(HashParseError::NoCurrentBranch)?;
321 let branch = Branch::get_by_id(conn, branch_id)
322 .ok_or_else(|| HashParseError::BranchNotFound(branch_id.to_string()))?;
323 let operations = Branch::get_operations(conn, branch.id);
324 if operations.is_empty() {
325 return Err(HashParseError::EmptyBranch(branch.name));
326 }
327 if reference == "HEAD" {
328 return Ok(operations.last().unwrap().hash);
329 }
330 if let Some(offset) = reference.strip_prefix("HEAD~") {
331 let offset: usize = offset
332 .parse()
333 .map_err(|_| HashParseError::InvalidHead(reference.to_string()))?;
334 let head_index = operations.len() - 1;
335 let target_index = head_index
336 .checked_sub(offset)
337 .ok_or(HashParseError::HeadOffsetOutOfRange(offset))?;
338 return Ok(operations[target_index].hash);
339 }
340
341 Err(HashParseError::InvalidHead(reference.to_string()))
342}
343
344impl Query for Operation {
345 type Model = Operation;
346
347 const PRIMARY_KEY: &'static str = "hash";
348 const TABLE_NAME: &'static str = "operations";
349
350 fn process_row(row: &Row) -> Self::Model {
351 Operation {
352 hash: row.get(0).unwrap(),
353 parent_hash: row.get(1).unwrap(),
354 change_type: row.get(2).unwrap(),
355 created_on: row.get(3).unwrap(),
356 }
357 }
358}
359
360pub struct OperationFile {
361 pub file_path: String,
362 pub file_type: FileTypes,
363}
364
365pub struct OperationInfo {
366 pub files: Vec<OperationFile>,
367 pub description: String,
368}
369
370pub fn calculate_file_checksum<P: AsRef<Path>>(file_path: P) -> Result<HashId, std::io::Error> {
371 let file = std::fs::File::open(file_path)?;
372 let reader = BufReader::new(file);
373 let hash_bytes = calculate_stream_hash(reader)?;
374 Ok(HashId(hash_bytes))
375}
376
377fn calculate_stream_hash<R: std::io::Read>(mut reader: R) -> Result<[u8; 32], std::io::Error> {
378 let mut hasher = Sha256::new();
379 io::copy(&mut reader, &mut hasher)?;
380 let result = hasher.finalize();
381 let mut hash_array = [0u8; 32];
382 hash_array.copy_from_slice(&result);
383 Ok(hash_array)
384}
385
386#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
387pub struct FileAddition {
388 pub id: HashId,
389 pub file_path: String,
390 pub file_type: FileTypes,
391 pub checksum: HashId,
392}
393
394impl Query for FileAddition {
395 type Model = FileAddition;
396
397 const TABLE_NAME: &'static str = "file_additions";
398
399 fn process_row(row: &Row) -> Self::Model {
400 Self::Model {
401 id: row.get(0).unwrap(),
402 file_path: row.get(1).unwrap(),
403 file_type: row.get(2).unwrap(),
404 checksum: row.get(3).unwrap(),
405 }
406 }
407}
408
409impl FileAddition {
410 pub fn generate_file_addition_id(checksum: &HashId, file_path: &str) -> HashId {
411 let combined = format!("{checksum};{file_path}");
412 HashId(calculate_hash(&combined))
413 }
414
415 fn normalize_file_paths(workspace: &Workspace, file_path: &str) -> (String, String) {
416 if file_path.is_empty() {
417 return (String::new(), String::new());
418 }
419 let repo_root = workspace.repo_root().unwrap();
420
421 let provided_path = Path::new(file_path);
422
423 if provided_path.is_absolute() {
424 if provided_path.starts_with(&repo_root) {
425 let absolute = provided_path.to_string_lossy().to_string();
426 let relative = provided_path
427 .strip_prefix(&repo_root)
428 .unwrap()
429 .to_string_lossy()
430 .to_string();
431 return (absolute, relative);
432 }
433 } else {
434 let absolute = repo_root.join(provided_path);
435 if absolute.exists() {
436 let relative = absolute
437 .strip_prefix(&repo_root)
438 .unwrap()
439 .to_string_lossy()
440 .to_string();
441 return (absolute.to_string_lossy().to_string(), relative);
442 }
443 };
444
445 let fallback = file_path.to_string();
446 (fallback.clone(), fallback)
447 }
448
449 pub fn get_or_create(
450 workspace: &Workspace,
451 conn: &OperationsConnection,
452 file_path: &str,
453 file_type: FileTypes,
454 checksum_override: Option<HashId>,
455 ) -> Result<FileAddition, FileAdditionError> {
456 let (absolute_file_path, relative_file_path) =
457 FileAddition::normalize_file_paths(workspace, file_path);
458
459 let checksum = if let Some(checksum_override) = checksum_override {
461 checksum_override
462 } else {
463 let absolute_path = Path::new(&absolute_file_path);
464 let checksum_path = if absolute_path.is_file() {
465 absolute_file_path.as_str()
466 } else {
467 relative_file_path.as_str()
468 };
469 match calculate_file_checksum(checksum_path) {
470 Ok(checksum) => checksum,
471 Err(e) => match e.kind() {
472 std::io::ErrorKind::NotFound => HashId::convert_str("non-existent"),
473 std::io::ErrorKind::PermissionDenied => {
474 return Err(FileAdditionError::FilePermissionDenied(
475 file_path.to_string(),
476 ));
477 }
478 _ => {
479 return Err(FileAdditionError::FileReadError(e));
480 }
481 },
482 }
483 };
484
485 let id = FileAddition::generate_file_addition_id(&checksum, &relative_file_path);
486
487 let query = "INSERT INTO file_additions (id, file_path, file_type, checksum) VALUES (?1, ?2, ?3, ?4);";
488 let mut stmt = conn.prepare(query).unwrap();
489
490 let addition = FileAddition {
491 id,
492 file_path: relative_file_path.clone(),
493 file_type,
494 checksum,
495 };
496
497 match stmt.execute((&id, &relative_file_path, file_type, &checksum)) {
498 Ok(_) => Ok(addition),
499 Err(err) => match &err {
500 rusqlite::Error::SqliteFailure(suberr, _details) => {
501 if suberr.code == rusqlite::ErrorCode::ConstraintViolation {
502 Ok(addition)
503 } else {
504 Err(FileAdditionError::DatabaseError(err))
505 }
506 }
507 _ => Err(FileAdditionError::DatabaseError(err)),
508 },
509 }
510 }
511
512 pub fn get_files_for_operation(
513 conn: &OperationsConnection,
514 operation_hash: &HashId,
515 ) -> Vec<FileAddition> {
516 let query = "select fa.* from file_additions fa left join operation_files of on (fa.id = of.file_addition_id) where of.operation_hash = ?1";
517 let mut stmt = conn.prepare(query).unwrap();
518 let rows = stmt
519 .query_map(params![operation_hash], |row| {
520 Ok(FileAddition::process_row(row))
521 })
522 .unwrap();
523 rows.map(|row| row.unwrap()).collect()
524 }
525
526 pub fn query_by_operations(
527 conn: &OperationsConnection,
528 operations: &[HashId],
529 ) -> Result<HashMap<HashId, Vec<FileAddition>>, FileAdditionError> {
530 let query = "select fa.*, of.operation_hash from file_additions fa left join operation_files of on (fa.id = of.file_addition_id) where of.operation_hash in rarray(?1)";
531 let mut stmt = conn.prepare(query).unwrap();
532 let rows = stmt
533 .query_map(
534 params![Rc::new(
535 operations
536 .iter()
537 .map(|h| Value::from(*h))
538 .collect::<Vec<Value>>()
539 )],
540 |row| Ok((FileAddition::process_row(row), row.get::<_, HashId>(4)?)),
541 )
542 .unwrap();
543 rows.into_iter()
544 .try_fold(HashMap::new(), |mut acc: HashMap<_, Vec<_>>, row| {
545 let (item, hash) = row?;
546 acc.entry(hash).or_default().push(item);
547 Ok(acc)
548 })
549 }
550
551 pub fn hashed_filename(self) -> String {
552 format!(
553 "{}.{}",
554 self.checksum.clone(),
555 &FileTypes::suffix(self.file_type)
556 )
557 }
558}
559
560#[derive(Debug, Error)]
561pub enum OperationSummaryError {
562 #[error("Database error: {0}")]
563 DatabaseError(#[from] rusqlite::Error),
564}
565
566#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
567pub struct OperationSummary {
568 pub id: i64,
569 pub operation_hash: HashId,
570 pub summary: String,
571}
572
573impl Query for OperationSummary {
574 type Model = OperationSummary;
575
576 const TABLE_NAME: &'static str = "operation_summaries";
577
578 fn process_row(row: &Row) -> Self::Model {
579 Self::Model {
580 id: row.get(0).unwrap(),
581 operation_hash: row.get(1).unwrap(),
582 summary: row.get(2).unwrap(),
583 }
584 }
585}
586
587impl OperationSummary {
588 pub fn create(
589 conn: &OperationsConnection,
590 operation_hash: &HashId,
591 summary: &str,
592 ) -> OperationSummary {
593 let query = "INSERT INTO operation_summaries (operation_hash, summary) VALUES (?1, ?2) RETURNING (id)";
594 let mut stmt = conn.prepare(query).unwrap();
595 let mut rows = stmt
596 .query_map(params![operation_hash, summary], |row| {
597 Ok(OperationSummary {
598 id: row.get(0)?,
599 operation_hash: *operation_hash,
600 summary: summary.to_string(),
601 })
602 })
603 .unwrap();
604 rows.next().unwrap().unwrap()
605 }
606
607 pub fn set_message(conn: &OperationsConnection, id: i64, message: &str) -> SQLResult<()> {
608 let query = "UPDATE operation_summaries SET summary = ?2 where id = ?1";
609 let mut stmt = conn.prepare(query).unwrap();
610 stmt.execute(params![id, message])?;
611 Ok(())
612 }
613
614 pub fn query_by_operations(
615 conn: &OperationsConnection,
616 operations: &[HashId],
617 ) -> Result<HashMap<HashId, Vec<Self>>, OperationSummaryError> {
618 let query = "select * from operation_summaries where operation_hash in rarray(?1)";
619 let mut stmt = conn.prepare(query).unwrap();
620 let rows = stmt
621 .query_map(
622 params![Rc::new(
623 operations
624 .iter()
625 .map(|h| Value::from(*h))
626 .collect::<Vec<Value>>()
627 )],
628 |row| Ok(Self::process_row(row)),
629 )
630 .unwrap();
631 rows.into_iter()
632 .try_fold(HashMap::new(), |mut acc: HashMap<_, Vec<_>>, row| {
633 let item = row?;
634 acc.entry(item.operation_hash).or_default().push(item);
635 Ok(acc)
636 })
637 }
638}
639
640impl<'a> Capnp<'a> for FileAddition {
641 type Builder = crate::gen_models_capnp::file_addition::Builder<'a>;
642 type Reader = crate::gen_models_capnp::file_addition::Reader<'a>;
643
644 fn write_capnp(&self, builder: &mut Self::Builder) {
645 builder.set_id(&self.id.0).unwrap();
646 builder.set_file_path(&self.file_path);
647 builder.set_file_type(self.file_type.into());
648 builder.set_checksum(&self.checksum.0).unwrap();
649 }
650
651 fn read_capnp(reader: Self::Reader) -> Self {
652 Self {
653 id: reader
654 .get_id()
655 .unwrap()
656 .as_slice()
657 .unwrap()
658 .try_into()
659 .unwrap(),
660 file_path: reader.get_file_path().unwrap().to_string().unwrap(),
661 file_type: reader.get_file_type().unwrap().into(),
662 checksum: reader
663 .get_checksum()
664 .unwrap()
665 .as_slice()
666 .unwrap()
667 .try_into()
668 .unwrap(),
669 }
670 }
671}
672
673impl<'a> Capnp<'a> for OperationSummary {
674 type Builder = crate::gen_models_capnp::operation_summary::Builder<'a>;
675 type Reader = crate::gen_models_capnp::operation_summary::Reader<'a>;
676
677 fn write_capnp(&self, builder: &mut Self::Builder) {
678 builder.set_id(self.id);
679 builder.set_operation_hash(&self.operation_hash.0).unwrap();
680 builder.set_summary(&self.summary);
681 }
682
683 fn read_capnp(reader: Self::Reader) -> Self {
684 Self {
685 id: reader.get_id(),
686 operation_hash: reader
687 .get_operation_hash()
688 .unwrap()
689 .as_slice()
690 .unwrap()
691 .try_into()
692 .unwrap(),
693 summary: reader.get_summary().unwrap().to_string().unwrap(),
694 }
695 }
696}
697
698#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
699pub struct Remote {
700 pub name: String,
701 pub url: String,
702}
703
704impl Query for Remote {
705 type Model = Remote;
706
707 const TABLE_NAME: &'static str = "remotes";
708
709 fn process_row(row: &Row) -> Self::Model {
710 Remote {
711 name: row.get(0).unwrap(),
712 url: row.get(1).unwrap(),
713 }
714 }
715}
716
717impl Remote {
718 pub fn validate_name(name: &str) -> Result<(), RemoteError> {
720 if name.is_empty() {
721 return Err(RemoteError::EmptyName);
722 }
723
724 if name
725 .chars()
726 .any(|c| !c.is_alphanumeric() && c != '-' && c != '_')
727 {
728 return Err(RemoteError::InvalidNameCharacters);
729 }
730
731 Ok(())
732 }
733
734 pub fn validate_url(url: &str) -> Result<(), RemoteError> {
736 if url.is_empty() {
737 return Err(RemoteError::EmptyUrl);
738 }
739
740 if url.contains("://") {
742 match url::Url::parse(url) {
743 Ok(parsed_url) => {
744 match parsed_url.scheme() {
746 "http" | "https" | "ssh" | "file" => Ok(()),
747 _ => Err(RemoteError::UnsupportedUrlScheme),
748 }
749 }
750 Err(_) => Err(RemoteError::InvalidUrl("Invalid URL format".to_string())),
751 }
752 } else if url.starts_with('/') || url.contains(':') {
753 Ok(())
755 } else {
756 Err(RemoteError::UnsupportedUrlScheme)
757 }
758 }
759
760 pub fn create(
763 conn: &OperationsConnection,
764 name: &str,
765 url: &str,
766 ) -> Result<Remote, RemoteError> {
767 Self::validate_name(name)?;
769 Self::validate_url(url)?;
770
771 let query = "INSERT INTO remotes (name, url) VALUES (?1, ?2)";
772 let mut stmt = conn.prepare(query)?;
773
774 match stmt.execute(params![name, url]) {
775 Ok(_) => Ok(Remote {
776 name: name.to_string(),
777 url: url.to_string(),
778 }),
779 Err(rusqlite::Error::SqliteFailure(err, _))
780 if err.code == rusqlite::ErrorCode::ConstraintViolation =>
781 {
782 Err(RemoteError::RemoteAlreadyExists(name.to_string()))
783 }
784 Err(e) => Err(RemoteError::DatabaseError(e)),
785 }
786 }
787
788 pub fn get_by_name(conn: &OperationsConnection, name: &str) -> Result<Remote, RemoteError> {
790 let query = "SELECT name, url FROM remotes WHERE name = ?1";
791 match Remote::get(conn, query, params![name]) {
792 Ok(remote) => Ok(remote),
793 Err(rusqlite::Error::QueryReturnedNoRows) => {
794 Err(RemoteError::RemoteNotFound(name.to_string()))
795 }
796 Err(e) => Err(RemoteError::DatabaseError(e)),
797 }
798 }
799
800 pub fn get_by_name_optional(conn: &OperationsConnection, name: &str) -> Option<Remote> {
802 Self::get_by_name(conn, name).ok()
803 }
804
805 pub fn list_all(conn: &OperationsConnection) -> Vec<Remote> {
807 Remote::query(
808 conn,
809 "SELECT name, url FROM remotes ORDER BY name",
810 params![],
811 )
812 }
813
814 pub fn delete(conn: &OperationsConnection, name: &str) -> Result<(), RemoteError> {
816 Self::get_by_name(conn, name)?;
818
819 let query = "DELETE FROM remotes WHERE name = ?1";
820 let mut stmt = conn.prepare(query)?;
821 stmt.execute(params![name])?;
822 Ok(())
823 }
824
825 pub fn exists(conn: &OperationsConnection, name: &str) -> bool {
827 Self::get_by_name_optional(conn, name).is_some()
828 }
829}
830
831#[derive(Clone, Debug)]
832pub struct Branch {
833 pub id: i64,
834 pub name: String,
835 pub current_operation_hash: Option<HashId>,
836 pub remote_name: Option<String>,
837}
838
839impl Query for Branch {
840 type Model = Branch;
841
842 const TABLE_NAME: &'static str = "branches";
843
844 fn process_row(row: &Row) -> Self::Model {
845 Branch {
846 id: row.get(0).unwrap(),
847 name: row.get(1).unwrap(),
848 current_operation_hash: row.get(2).unwrap(),
849 remote_name: row.get(3).unwrap(),
850 }
851 }
852}
853
854impl Branch {
855 pub fn get_or_create(conn: &OperationsConnection, branch_name: &str) -> Branch {
856 match Branch::create_with_remote(conn, branch_name, None) {
857 Ok(res) => res,
858 Err(rusqlite::Error::SqliteFailure(err, details)) => {
859 if err.code == rusqlite::ErrorCode::ConstraintViolation {
860 Branch::get_by_name(conn, branch_name)
861 .unwrap_or_else(|| panic!("No branch named {branch_name}."))
862 } else {
863 panic!("something bad happened querying the database {err:?} {details:?}");
864 }
865 }
866 Err(_) => {
867 panic!("something bad happened querying the database");
868 }
869 }
870 }
871
872 pub fn create_with_remote(
873 conn: &OperationsConnection,
874 branch_name: &str,
875 remote_name: Option<&str>,
876 ) -> SQLResult<Branch> {
877 let current_operation_hash = OperationState::get_operation(conn);
878 let mut stmt = conn.prepare_cached("insert into branch (name, current_operation_hash, remote_name) values (?1, ?2, ?3) returning (id);").unwrap();
879
880 let mut rows = stmt
881 .query_map((branch_name, current_operation_hash, remote_name), |row| {
882 Ok(Branch {
883 id: row.get(0)?,
884 name: branch_name.to_string(),
885 current_operation_hash,
886 remote_name: remote_name.map(|s| s.to_string()),
887 })
888 })
889 .unwrap();
890 rows.next().unwrap()
891 }
892
893 pub fn delete(conn: &OperationsConnection, branch_id: i64) -> Result<(), BranchError> {
894 if let Some(current_branch) = OperationState::get_current_branch(conn)
895 && current_branch == branch_id
896 {
897 return Err(BranchError::CannotDelete(
898 "Unable to delete the branch that is currently active.".to_string(),
899 ));
900 }
901 conn.execute("delete from branch where id = ?1", (branch_id,))?;
902 Ok(())
903 }
904
905 pub fn all(conn: &OperationsConnection) -> Vec<Branch> {
906 Branch::query(conn, "select * from branch;", params![])
907 }
908
909 pub fn get_by_name(conn: &OperationsConnection, branch_name: &str) -> Option<Branch> {
910 let mut branch: Option<Branch> = None;
911 let results = Branch::query(
912 conn,
913 "select * from branch where name = ?1",
914 params![branch_name],
915 );
916 for result in results.iter() {
917 branch = Some(result.clone());
918 }
919 branch
920 }
921
922 pub fn get_by_id(conn: &OperationsConnection, branch_id: i64) -> Option<Branch> {
923 let mut branch: Option<Branch> = None;
924 for result in Branch::query(
925 conn,
926 "select * from branch where id = ?1",
927 params![Value::from(branch_id)],
928 )
929 .iter()
930 {
931 branch = Some(result.clone());
932 }
933 branch
934 }
935
936 pub fn set_current_operation(
937 conn: &OperationsConnection,
938 branch_id: i64,
939 operation_hash: &HashId,
940 ) {
941 conn.execute(
942 "UPDATE branch set current_operation_hash = ?2 where id = ?1",
943 params![branch_id, operation_hash],
944 )
945 .unwrap();
946 }
947
948 pub fn get_operations(conn: &OperationsConnection, branch_id: i64) -> Vec<Operation> {
949 let branch = Branch::get_by_id(conn, branch_id)
950 .unwrap_or_else(|| panic!("No branch with id {branch_id}."));
951 if let Some(hash) = branch.current_operation_hash {
952 let hashes = Operation::get_upstream(conn, &hash);
953 hashes
954 .iter()
955 .map(|hash| Operation::get_by_id(conn, hash).unwrap())
956 .collect::<Vec<Operation>>()
957 } else {
958 vec![]
959 }
960 }
961
962 pub fn set_remote(
964 conn: &OperationsConnection,
965 branch_id: i64,
966 remote_name: Option<&str>,
967 ) -> SQLResult<()> {
968 let query = "UPDATE branch SET remote_name = ?1 WHERE id = ?2";
969 let mut stmt = conn.prepare(query)?;
970 stmt.execute(params![remote_name, branch_id])?;
971 Ok(())
972 }
973
974 pub fn set_remote_validated(
976 conn: &OperationsConnection,
977 branch_id: i64,
978 remote_name: Option<&str>,
979 ) -> Result<(), RemoteError> {
980 if let Some(name) = remote_name {
982 Remote::get_by_name(conn, name)?;
983 }
984
985 let query = "UPDATE branch SET remote_name = ?1 WHERE id = ?2";
986 let mut stmt = conn.prepare(query)?;
987 stmt.execute(params![remote_name, branch_id])?;
988 Ok(())
989 }
990
991 pub fn get_remote(conn: &OperationsConnection, branch_id: i64) -> Option<String> {
993 let query = "SELECT remote_name FROM branch WHERE id = ?1";
994 let mut stmt = conn.prepare(query).ok()?;
995 let mut rows = stmt
996 .query_map(params![branch_id], |row| row.get::<_, Option<String>>(0))
997 .ok()?;
998
999 if let Some(Ok(remote_name)) = rows.next() {
1000 remote_name
1001 } else {
1002 None
1003 }
1004 }
1005}
1006
1007#[derive(Clone, Debug, Serialize, Deserialize)]
1008pub struct Defaults {
1009 pub id: i64,
1010 pub db_name: Option<String>,
1011 pub collection_name: Option<String>,
1012 pub remote_name: Option<String>,
1013}
1014
1015impl Query for Defaults {
1016 type Model = Defaults;
1017
1018 const TABLE_NAME: &'static str = "defaults";
1019
1020 fn process_row(row: &Row) -> Self::Model {
1021 Defaults {
1022 id: row.get(0).unwrap(),
1023 db_name: row.get(1).unwrap(),
1024 collection_name: row.get(2).unwrap(),
1025 remote_name: row.get(3).unwrap(),
1026 }
1027 }
1028}
1029
1030impl Defaults {
1031 pub fn set_default_remote(
1033 conn: &OperationsConnection,
1034 remote_name: Option<&str>,
1035 ) -> Result<(), RemoteError> {
1036 if let Some(name) = remote_name {
1038 Remote::get_by_name(conn, name)?;
1039 }
1040
1041 let query = "UPDATE defaults SET remote_name = ?1 WHERE id = 1";
1042 let mut stmt = conn.prepare(query)?;
1043 stmt.execute(params![remote_name])?;
1044 Ok(())
1045 }
1046
1047 pub fn set_default_remote_compat(
1048 conn: &OperationsConnection,
1049 remote_name: Option<&str>,
1050 ) -> SQLResult<()> {
1051 let query = "UPDATE defaults SET remote_name = ?1 WHERE id = 1";
1052 let mut stmt = conn.prepare(query)?;
1053 stmt.execute(params![remote_name])?;
1054 Ok(())
1055 }
1056
1057 pub fn get_default_remote(conn: &OperationsConnection) -> Option<String> {
1059 let query = "SELECT remote_name FROM defaults WHERE id = 1";
1060 let mut stmt = conn.prepare(query).ok()?;
1061 let mut rows = stmt
1062 .query_map(params![], |row| row.get::<_, Option<String>>(0))
1063 .ok()?;
1064
1065 if let Some(Ok(remote_name)) = rows.next() {
1066 remote_name
1067 } else {
1068 None
1069 }
1070 }
1071
1072 pub fn get_default_remote_url(conn: &OperationsConnection) -> Option<String> {
1074 if let Some(remote_name) = Self::get_default_remote(conn) {
1075 if let Some(remote) = Remote::get_by_name_optional(conn, &remote_name) {
1076 Some(remote.url)
1077 } else {
1078 None
1079 }
1080 } else {
1081 None
1082 }
1083 }
1084
1085 pub fn get(conn: &OperationsConnection) -> Option<Defaults> {
1087 let query = "SELECT id, db_name, collection_name, remote_name FROM defaults WHERE id = 1";
1088 Self::get_single(conn, query, params![]).ok()
1089 }
1090
1091 fn get_single(
1093 conn: &OperationsConnection,
1094 query: &str,
1095 params: &[&dyn rusqlite::ToSql],
1096 ) -> SQLResult<Defaults> {
1097 let mut stmt = conn.prepare(query)?;
1098 let mut rows = stmt.query_map(params, |row| Ok(Self::process_row(row)))?;
1099
1100 if let Some(row) = rows.next() {
1101 row
1102 } else {
1103 Err(rusqlite::Error::QueryReturnedNoRows)
1104 }
1105 }
1106}
1107
1108pub struct OperationState {}
1109
1110impl OperationState {
1111 pub fn set_operation(conn: &OperationsConnection, op_hash: &HashId) {
1112 let mut stmt = conn
1113 .prepare(
1114 "INSERT INTO operation_state (id, operation_hash)
1115 VALUES (1, ?1)
1116 ON CONFLICT (id) DO
1117 UPDATE SET operation_hash=excluded.operation_hash;",
1118 )
1119 .unwrap();
1120 stmt.execute([op_hash]).unwrap();
1121 let branch_id = OperationState::get_current_branch(conn).expect("No current branch set.");
1122 Branch::set_current_operation(conn, branch_id, op_hash);
1123 }
1124
1125 pub fn get_operation(conn: &OperationsConnection) -> Option<HashId> {
1126 let mut hash: Option<HashId> = None;
1127 let mut stmt = conn
1128 .prepare("SELECT operation_hash from operation_state where id = 1;")
1129 .unwrap();
1130 let rows = stmt.query_map((), |row| row.get(0)).unwrap();
1131 for row in rows {
1132 hash = row.unwrap();
1133 }
1134 hash
1135 }
1136
1137 pub fn set_branch(conn: &OperationsConnection, branch_name: &str) {
1138 let branch = Branch::get_by_name(conn, branch_name)
1139 .unwrap_or_else(|| panic!("No branch named {branch_name}."));
1140 let mut stmt = conn
1141 .prepare(
1142 "INSERT INTO operation_state (id, branch_id)
1143 VALUES (1, ?1)
1144 ON CONFLICT (id) DO
1145 UPDATE SET branch_id=excluded.branch_id;",
1146 )
1147 .unwrap();
1148 stmt.execute(params![branch.id]).unwrap();
1149 if let Some(current_branch_id) = OperationState::get_current_branch(conn) {
1150 if current_branch_id != branch.id {
1151 panic!("Failed to set branch to {branch_name}");
1152 }
1153 } else {
1154 panic!("Failed to set branch.");
1155 }
1156 }
1157
1158 pub fn get_current_branch(conn: &OperationsConnection) -> Option<i64> {
1159 let mut id: Option<i64> = None;
1160 let mut stmt = conn
1161 .prepare("SELECT branch_id from operation_state where id = 1;")
1162 .unwrap();
1163 let rows = stmt.query_map((), |row| row.get(0)).unwrap();
1164 for row in rows {
1165 id = row.unwrap();
1166 }
1167 id
1168 }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use std::{
1174 collections::HashSet,
1175 fs,
1176 io::{Cursor, Write},
1177 path::PathBuf,
1178 };
1179
1180 use tempfile::NamedTempFile;
1181
1182 use super::*;
1183 use crate::{
1184 files::GenDatabase,
1185 test_helpers::{create_operation, setup_gen},
1186 };
1187
1188 #[cfg(test)]
1189 mod defaults {
1190 use super::*;
1191
1192 #[test]
1193 fn test_writes_operation_hash() {
1194 let context = setup_gen();
1195 let op_conn = context.operations().conn();
1196
1197 let operation =
1198 Operation::create(op_conn, "test", &HashId::convert_str("some-hash")).unwrap();
1199 OperationState::set_operation(op_conn, &operation.hash);
1200 assert_eq!(
1201 OperationState::get_operation(op_conn).unwrap(),
1202 operation.hash
1203 );
1204 }
1205
1206 #[test]
1207 fn test_default_remote_functionality() {
1208 let context = setup_gen();
1209 let op_conn = context.operations().conn();
1210
1211 Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
1213 Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
1214
1215 assert_eq!(Defaults::get_default_remote(op_conn), None);
1217 assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1218
1219 Defaults::set_default_remote(op_conn, Some("origin")).unwrap();
1221 assert_eq!(
1222 Defaults::get_default_remote(op_conn),
1223 Some("origin".to_string())
1224 );
1225 assert_eq!(
1226 Defaults::get_default_remote_url(op_conn),
1227 Some("https://example.com/repo.gen".to_string())
1228 );
1229
1230 Defaults::set_default_remote(op_conn, Some("upstream")).unwrap();
1232 assert_eq!(
1233 Defaults::get_default_remote(op_conn),
1234 Some("upstream".to_string())
1235 );
1236 assert_eq!(
1237 Defaults::get_default_remote_url(op_conn),
1238 Some("https://upstream.com/repo.gen".to_string())
1239 );
1240
1241 Defaults::set_default_remote(op_conn, None).unwrap();
1243 assert_eq!(Defaults::get_default_remote(op_conn), None);
1244 assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1245
1246 Defaults::set_default_remote_compat(op_conn, Some("nonexistent")).unwrap();
1248 assert_eq!(
1249 Defaults::get_default_remote(op_conn),
1250 Some("nonexistent".to_string())
1251 );
1252 assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1253 }
1254
1255 #[test]
1256 fn test_defaults_get() {
1257 let context = setup_gen();
1258 let op_conn = context.operations().conn();
1259
1260 let defaults = Defaults::get(op_conn).unwrap();
1262 assert_eq!(defaults.id, 1);
1263 assert_eq!(defaults.db_name, None);
1264 assert_eq!(defaults.collection_name, None);
1265 assert_eq!(defaults.remote_name, None);
1266
1267 Defaults::set_default_remote_compat(op_conn, Some("test-remote")).unwrap();
1269 let defaults = Defaults::get(op_conn).unwrap();
1270 assert_eq!(defaults.remote_name, Some("test-remote".to_string()));
1271 }
1272 }
1273
1274 #[cfg(test)]
1275 mod remote {
1276 use super::*;
1277
1278 #[test]
1279 fn test_validate_remote_name() {
1280 assert!(Remote::validate_name("origin").is_ok());
1282 assert!(Remote::validate_name("my-remote").is_ok());
1283 assert!(Remote::validate_name("remote_1").is_ok());
1284 assert!(Remote::validate_name("test123").is_ok());
1285
1286 assert!(Remote::validate_name("").is_err());
1288 assert!(Remote::validate_name("remote with spaces").is_err());
1289 assert!(Remote::validate_name("remote@special").is_err());
1290 assert!(Remote::validate_name("remote.dot").is_err());
1291 }
1292
1293 #[test]
1294 fn test_validate_url() {
1295 assert!(Remote::validate_url("https://genhub.bio/user/repo.gen").is_ok());
1297 assert!(Remote::validate_url("http://example.com/repo").is_ok());
1298 assert!(Remote::validate_url("ssh://git@genhub.bio/user/repo.gen").is_ok());
1299 assert!(Remote::validate_url("/path/to/local/repo").is_ok());
1300 assert!(Remote::validate_url("user@host:path/to/repo").is_ok());
1301
1302 assert!(Remote::validate_url("").is_err());
1304 assert!(Remote::validate_url("not-a-url").is_err());
1305
1306 assert!(Remote::validate_url("ftp://invalid-protocol.com").is_err());
1307 }
1308 }
1309
1310 mod branch {
1311 use super::*;
1312
1313 #[test]
1314 fn test_branch_set_remote_valid() {
1315 let context = setup_gen();
1316 let op_conn = context.operations().conn();
1317
1318 Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1322
1323 let branch = Branch::get_or_create(op_conn, "test_branch");
1325
1326 assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1328
1329 let result = Branch::set_remote(op_conn, branch.id, Some("origin"));
1331 assert!(result.is_ok());
1332
1333 assert_eq!(
1335 Branch::get_remote(op_conn, branch.id),
1336 Some("origin".to_string())
1337 );
1338 }
1339
1340 #[test]
1341 fn test_branch_set_remote_nonexistent() {
1342 let context = setup_gen();
1343 let op_conn = context.operations().conn();
1344
1345 let branch = Branch::get_or_create(op_conn, "test_branch");
1349
1350 let result = Branch::set_remote(op_conn, branch.id, Some("nonexistent"));
1352 assert!(result.is_err());
1353
1354 assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1356 }
1357
1358 #[test]
1359 fn test_branch_clear_remote() {
1360 let context = setup_gen();
1361 let op_conn = context.operations().conn();
1362
1363 Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1367
1368 let branch = Branch::get_or_create(op_conn, "test_branch");
1370
1371 Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
1373 assert_eq!(
1374 Branch::get_remote(op_conn, branch.id),
1375 Some("origin".to_string())
1376 );
1377
1378 Branch::set_remote(op_conn, branch.id, None).unwrap();
1380 assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1381 }
1382
1383 #[test]
1384 fn test_branch_remote_cascade_on_remote_delete() {
1385 let context = setup_gen();
1386 let op_conn = context.operations().conn();
1387
1388 Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1392
1393 let branch = Branch::get_or_create(op_conn, "test_branch_cascade");
1395 Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
1396
1397 assert_eq!(
1399 Branch::get_remote(op_conn, branch.id),
1400 Some("origin".to_string())
1401 );
1402
1403 Remote::delete(op_conn, "origin").unwrap();
1405
1406 assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1408
1409 let branch_from_db = Branch::get_by_id(op_conn, branch.id);
1411 assert!(branch_from_db.is_some());
1412 assert_eq!(branch_from_db.unwrap().remote_name, None);
1413 }
1414 }
1415
1416 mod parse_hash {
1417 use super::*;
1418
1419 #[test]
1420 fn test_parse_hash_head_and_range() {
1421 let context = setup_gen();
1422 let op_conn = context.operations().conn();
1423
1424 let branch = Branch::get_or_create(op_conn, "main");
1425 OperationState::set_branch(op_conn, &branch.name);
1426
1427 let op_1 =
1428 Operation::create(op_conn, "add", &HashId::convert_str("op-1-abc-123")).unwrap();
1429 let op_2 =
1430 Operation::create(op_conn, "add", &HashId::convert_str("op-2-abc-123")).unwrap();
1431
1432 let head = parse_hash(op_conn, "HEAD").unwrap();
1433 assert_eq!(
1434 head,
1435 HashRange {
1436 from: None,
1437 to: Some(op_2.hash),
1438 }
1439 );
1440
1441 let range = parse_hash(op_conn, "HEAD~1..HEAD").unwrap();
1442 assert_eq!(
1443 range,
1444 HashRange {
1445 from: Some(op_1.hash),
1446 to: Some(op_2.hash),
1447 }
1448 );
1449 }
1450
1451 #[test]
1452 fn test_parse_hash_branch_and_partial() {
1453 let context = setup_gen();
1454 let op_conn = context.operations().conn();
1455
1456 let branch = Branch::get_or_create(op_conn, "main");
1457 OperationState::set_branch(op_conn, &branch.name);
1458
1459 let op_1 =
1460 Operation::create(op_conn, "add", &HashId::convert_str("op-1-xyz-123")).unwrap();
1461 let op_2 =
1462 Operation::create(op_conn, "add", &HashId::convert_str("op-2-xyz-123")).unwrap();
1463
1464 let branch_ref = parse_hash(op_conn, "main").unwrap();
1465 assert_eq!(
1466 branch_ref,
1467 HashRange {
1468 from: None,
1469 to: Some(op_2.hash),
1470 }
1471 );
1472
1473 let partial = format!("{}", op_1.hash);
1474 let prefix = &partial[..6];
1475 let resolved = parse_hash(op_conn, prefix).unwrap();
1476 assert_eq!(
1477 resolved,
1478 HashRange {
1479 from: None,
1480 to: Some(op_1.hash),
1481 }
1482 );
1483 }
1484
1485 #[test]
1486 fn test_parse_hash_head_offset_out_of_range() {
1487 let context = setup_gen();
1488 let op_conn = context.operations().conn();
1489
1490 let branch = Branch::get_or_create(op_conn, "main");
1491 OperationState::set_branch(op_conn, &branch.name);
1492
1493 let _op = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1494 let result = parse_hash(op_conn, "HEAD~1");
1495 assert!(matches!(
1496 result,
1497 Err(HashParseError::HeadOffsetOutOfRange(1))
1498 ));
1499 }
1500 }
1501
1502 mod search_hash {
1503 use super::*;
1504
1505 #[test]
1506 fn test_search_hashes_returns_matches() {
1507 let context = setup_gen();
1508 let op_conn = context.operations().conn();
1509
1510 let branch = Branch::get_or_create(op_conn, "main");
1511 OperationState::set_branch(op_conn, &branch.name);
1512
1513 let _op_1 = Operation::create(
1514 op_conn,
1515 "add",
1516 &HashId::pad_str(
1517 "abc0000000000000000000000000000000000000000000000000000000000001",
1518 ),
1519 )
1520 .unwrap();
1521 let _op_2 = Operation::create(
1522 op_conn,
1523 "add",
1524 &HashId::pad_str(
1525 "abc0000000000000000000000000000000000000000000000000000000000002",
1526 ),
1527 )
1528 .unwrap();
1529 let _op_3 = Operation::create(
1530 op_conn,
1531 "add",
1532 &HashId::pad_str(
1533 "def0000000000000000000000000000000000000000000000000000000000003",
1534 ),
1535 )
1536 .unwrap();
1537
1538 let matches = Operation::search_hashes(op_conn, "abc");
1539 assert_eq!(matches.len(), 2);
1540 }
1541
1542 #[test]
1543 fn test_search_hash_resolves_and_errors() {
1544 let context = setup_gen();
1545 let op_conn = context.operations().conn();
1546
1547 let branch = Branch::get_or_create(op_conn, "main");
1548 OperationState::set_branch(op_conn, &branch.name);
1549
1550 let op_unique = Operation::create(
1551 op_conn,
1552 "add",
1553 &HashId::pad_str(
1554 "def0000000000000000000000000000000000000000000000000000000000001",
1555 ),
1556 )
1557 .unwrap();
1558 let _op_ambiguous = Operation::create(
1559 op_conn,
1560 "add",
1561 &HashId::pad_str(
1562 "abc0000000000000000000000000000000000000000000000000000000000001",
1563 ),
1564 )
1565 .unwrap();
1566 let _op_ambiguous_2 = Operation::create(
1567 op_conn,
1568 "add",
1569 &HashId::pad_str(
1570 "abc0000000000000000000000000000000000000000000000000000000000002",
1571 ),
1572 )
1573 .unwrap();
1574
1575 let resolved = Operation::search_hash(op_conn, "def").unwrap();
1576 assert_eq!(resolved.hash, op_unique.hash);
1577
1578 let ambiguous = Operation::search_hash(op_conn, "abc");
1579 assert!(matches!(
1580 ambiguous,
1581 Err(HashParseError::OperationAmbiguous(_))
1582 ));
1583 }
1584 }
1585
1586 mod resolve_reference {
1587 use super::*;
1588
1589 #[test]
1590 fn test_resolve_reference_branch_and_hash() {
1591 let context = setup_gen();
1592 let op_conn = context.operations().conn();
1593
1594 let branch = Branch::get_or_create(op_conn, "main");
1595 OperationState::set_branch(op_conn, &branch.name);
1596
1597 let op_unique = Operation::create(
1598 op_conn,
1599 "add",
1600 &HashId::pad_str(
1601 "def0000000000000000000000000000000000000000000000000000000000001",
1602 ),
1603 )
1604 .unwrap();
1605
1606 let branch_hash = resolve_reference(op_conn, "main").unwrap();
1607 assert_eq!(branch_hash, op_unique.hash);
1608
1609 let hash_ref = resolve_reference(op_conn, "def").unwrap();
1610 assert_eq!(hash_ref, op_unique.hash);
1611 }
1612
1613 #[test]
1614 fn test_resolve_reference_ambiguous() {
1615 let context = setup_gen();
1616 let op_conn = context.operations().conn();
1617
1618 let branch = Branch::get_or_create(op_conn, "main");
1619 OperationState::set_branch(op_conn, &branch.name);
1620
1621 let _op_1 = Operation::create(
1622 op_conn,
1623 "add",
1624 &HashId::pad_str(
1625 "abc0000000000000000000000000000000000000000000000000000000000001",
1626 ),
1627 )
1628 .unwrap();
1629 let _op_2 = Operation::create(
1630 op_conn,
1631 "add",
1632 &HashId::pad_str(
1633 "abc0000000000000000000000000000000000000000000000000000000000002",
1634 ),
1635 )
1636 .unwrap();
1637
1638 let result = resolve_reference(op_conn, "abc");
1639 assert!(matches!(result, Err(HashParseError::OperationAmbiguous(_))));
1640 }
1641 }
1642
1643 mod resolve_head {
1644 use super::*;
1645
1646 #[test]
1647 fn test_resolve_head_variants() {
1648 let context = setup_gen();
1649 let op_conn = context.operations().conn();
1650
1651 let branch = Branch::get_or_create(op_conn, "main");
1652 OperationState::set_branch(op_conn, &branch.name);
1653
1654 let op_1 = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1655 let op_2 = Operation::create(op_conn, "add", &HashId::convert_str("op-2")).unwrap();
1656
1657 let head = resolve_head(op_conn, "HEAD").unwrap();
1658 assert_eq!(head, op_2.hash);
1659
1660 let head_prev = resolve_head(op_conn, "HEAD~1").unwrap();
1661 assert_eq!(head_prev, op_1.hash);
1662 }
1663
1664 #[test]
1665 fn test_resolve_head_invalid() {
1666 let context = setup_gen();
1667 let op_conn = context.operations().conn();
1668
1669 let branch = Branch::get_or_create(op_conn, "main");
1670 OperationState::set_branch(op_conn, &branch.name);
1671
1672 let _op = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1673 let result = resolve_head(op_conn, "HEAD~2");
1674 assert!(matches!(
1675 result,
1676 Err(HashParseError::HeadOffsetOutOfRange(2))
1677 ));
1678 }
1679 }
1680
1681 #[test]
1682 fn test_create_operation_adds_database() {
1683 let context = setup_gen();
1684 let conn = context.graph().conn();
1685 let op_conn = context.operations().conn();
1686 let db_uuid = crate::metadata::get_db_uuid(conn);
1687 let gen_db = GenDatabase::create(op_conn, &db_uuid, "foo.db", "/foo.db").unwrap();
1688
1689 let op = create_operation(
1690 &context,
1691 "something.fa",
1692 FileTypes::Fasta,
1693 "foo",
1694 HashId::convert_str("op-1"),
1695 );
1696
1697 let databases = GenDatabase::query_by_operations(op_conn, &[op.hash]).unwrap();
1698 assert_eq!(databases[&op.hash], vec![gen_db]);
1699 }
1700
1701 #[test]
1702 fn test_gets_operations_of_branch() {
1703 let context = setup_gen();
1704 let conn = context.graph().conn();
1705 let op_conn = context.operations().conn();
1706
1707 let db_uuid = crate::metadata::get_db_uuid(conn);
1708 crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
1709
1710 create_operation(
1711 &context,
1712 "test.fasta",
1713 FileTypes::Fasta,
1714 "foo",
1715 HashId::convert_str("op-1"),
1716 );
1717 create_operation(
1738 &context,
1739 "test.fasta",
1740 FileTypes::Fasta,
1741 "foo",
1742 HashId::convert_str("op-2"),
1743 );
1744 create_operation(
1745 &context,
1746 "test.fasta",
1747 FileTypes::Fasta,
1748 "foo",
1749 HashId::convert_str("op-3"),
1750 );
1751 create_operation(
1752 &context,
1753 "test.fasta",
1754 FileTypes::Fasta,
1755 "foo",
1756 HashId::convert_str("op-4"),
1757 );
1758 create_operation(
1759 &context,
1760 "test.fasta",
1761 FileTypes::Fasta,
1762 "foo",
1763 HashId::convert_str("op-5"),
1764 );
1765 OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
1766 create_operation(
1767 &context,
1768 "test.fasta",
1769 FileTypes::Fasta,
1770 "foo",
1771 HashId::convert_str("op-6"),
1772 );
1773 let _branch_2_midpoint = create_operation(
1774 &context,
1775 "test.fasta",
1776 FileTypes::Fasta,
1777 "foo",
1778 HashId::convert_str("op-7"),
1779 );
1780 create_operation(
1781 &context,
1782 "test.fasta",
1783 FileTypes::Fasta,
1784 "foo",
1785 HashId::convert_str("op-8"),
1786 );
1787 create_operation(
1788 &context,
1789 "test.fasta",
1790 FileTypes::Fasta,
1791 "foo",
1792 HashId::convert_str("op-9"),
1793 );
1794 create_operation(
1795 &context,
1796 "test.fasta",
1797 FileTypes::Fasta,
1798 "foo",
1799 HashId::convert_str("op-10"),
1800 );
1801 create_operation(
1802 &context,
1803 "test.fasta",
1804 FileTypes::Fasta,
1805 "foo",
1806 HashId::convert_str("op-11"),
1807 );
1808 OperationState::set_operation(op_conn, &HashId::convert_str("op-7"));
1809 create_operation(
1810 &context,
1811 "test.fasta",
1812 FileTypes::Fasta,
1813 "foo",
1814 HashId::convert_str("op-12"),
1815 );
1816 create_operation(
1817 &context,
1818 "test.fasta",
1819 FileTypes::Fasta,
1820 "foo",
1821 HashId::convert_str("op-13"),
1822 );
1823
1824 OperationState::set_operation(op_conn, &HashId::convert_str("op-3"));
1825 let branch_1 = Branch::get_or_create(op_conn, "branch-1");
1826 OperationState::set_operation(op_conn, &HashId::convert_str("op-8"));
1827 let branch_2 = Branch::get_or_create(op_conn, "branch-2");
1828 OperationState::set_operation(op_conn, &HashId::convert_str("op-5"));
1829 let branch_1_sub_1 = Branch::get_or_create(op_conn, "branch-1-sub-1");
1830 OperationState::set_operation(op_conn, &HashId::convert_str("op-11"));
1831 let branch_2_sub_1 = Branch::get_or_create(op_conn, "branch-2-sub-1");
1832 OperationState::set_operation(op_conn, &HashId::convert_str("op-13"));
1833 let branch_2_midpoint_1 = Branch::get_or_create(op_conn, "branch-2-midpoint-1");
1834
1835 let ops = Branch::get_operations(op_conn, branch_2_midpoint_1.id)
1836 .iter()
1837 .map(|f| f.hash)
1838 .collect::<Vec<_>>();
1839 assert_eq!(
1840 ops,
1841 vec![
1842 HashId::convert_str("op-1"),
1843 HashId::convert_str("op-6"),
1844 HashId::convert_str("op-7"),
1845 HashId::convert_str("op-12"),
1846 HashId::convert_str("op-13")
1847 ]
1848 );
1849
1850 let ops = Branch::get_operations(op_conn, branch_1.id)
1851 .iter()
1852 .map(|f| f.hash)
1853 .collect::<Vec<_>>();
1854 assert_eq!(
1855 ops,
1856 vec![
1857 HashId::convert_str("op-1"),
1858 HashId::convert_str("op-2"),
1859 HashId::convert_str("op-3")
1860 ]
1861 );
1862
1863 let ops = Branch::get_operations(op_conn, branch_2.id)
1864 .iter()
1865 .map(|f| f.hash)
1866 .collect::<Vec<_>>();
1867 assert_eq!(
1868 ops,
1869 vec![
1870 HashId::convert_str("op-1"),
1871 HashId::convert_str("op-6"),
1872 HashId::convert_str("op-7"),
1873 HashId::convert_str("op-8")
1874 ]
1875 );
1876
1877 let ops = Branch::get_operations(op_conn, branch_1_sub_1.id)
1878 .iter()
1879 .map(|f| f.hash)
1880 .collect::<Vec<_>>();
1881 assert_eq!(
1882 ops,
1883 vec![
1884 HashId::convert_str("op-1"),
1885 HashId::convert_str("op-2"),
1886 HashId::convert_str("op-3"),
1887 HashId::convert_str("op-4"),
1888 HashId::convert_str("op-5")
1889 ]
1890 );
1891
1892 let ops = Branch::get_operations(op_conn, branch_2_sub_1.id)
1893 .iter()
1894 .map(|f: &Operation| f.hash)
1895 .collect::<Vec<_>>();
1896 assert_eq!(
1897 ops,
1898 vec![
1899 HashId::convert_str("op-1"),
1900 HashId::convert_str("op-6"),
1901 HashId::convert_str("op-7"),
1902 HashId::convert_str("op-8"),
1903 HashId::convert_str("op-9"),
1904 HashId::convert_str("op-10"),
1905 HashId::convert_str("op-11")
1906 ]
1907 );
1908 }
1909
1910 #[test]
1911 fn test_graph_representation() {
1912 let context = setup_gen();
1913 let op_conn = context.operations().conn();
1914
1915 let mut expected_graph = OperationGraph::new();
1926 expected_graph.add_edge(HashId::convert_str("op-1"), HashId::convert_str("op-2"), ());
1927 expected_graph.add_edge(HashId::convert_str("op-2"), HashId::convert_str("op-3"), ());
1928 expected_graph.add_edge(HashId::convert_str("op-3"), HashId::convert_str("op-4"), ());
1929 expected_graph.add_edge(HashId::convert_str("op-4"), HashId::convert_str("op-5"), ());
1930 expected_graph.add_edge(HashId::convert_str("op-4"), HashId::convert_str("op-6"), ());
1931 expected_graph.add_edge(HashId::convert_str("op-1"), HashId::convert_str("op-7"), ());
1932
1933 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-1")).unwrap();
1934 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-2")).unwrap();
1935 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-3")).unwrap();
1936 Branch::get_or_create(op_conn, "branch-1");
1937 OperationState::set_branch(op_conn, "branch-1");
1938 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-4")).unwrap();
1939 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-5")).unwrap();
1940 OperationState::set_operation(op_conn, &HashId::convert_str("op-4"));
1941 Branch::get_or_create(op_conn, "branch-2");
1942 OperationState::set_branch(op_conn, "branch-2");
1943 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-6")).unwrap();
1944 OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
1945 Branch::get_or_create(op_conn, "branch-3");
1946 OperationState::set_branch(op_conn, "branch-3");
1947 let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-7")).unwrap();
1948 let graph = Operation::get_operation_graph(op_conn);
1949
1950 assert_eq!(
1951 graph.nodes().collect::<HashSet<_>>(),
1952 expected_graph.nodes().collect::<HashSet<_>>()
1953 );
1954 assert_eq!(
1955 graph.all_edges().collect::<HashSet<_>>(),
1956 expected_graph.all_edges().collect::<HashSet<_>>()
1957 );
1958 }
1959
1960 #[test]
1961 fn test_path_between() {
1962 let context = setup_gen();
1963 let conn = context.graph().conn();
1964 let op_conn = context.operations().conn();
1965
1966 let db_uuid = crate::metadata::get_db_uuid(conn);
1967 crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
1968
1969 create_operation(
1980 &context,
1981 "test.fasta",
1982 FileTypes::Fasta,
1983 "foo",
1984 HashId::convert_str("op-1"),
1985 );
1986 create_operation(
1987 &context,
1988 "test.fasta",
1989 FileTypes::Fasta,
1990 "foo",
1991 HashId::convert_str("op-2"),
1992 );
1993 create_operation(
1994 &context,
1995 "test.fasta",
1996 FileTypes::Fasta,
1997 "foo",
1998 HashId::convert_str("op-3"),
1999 );
2000 Branch::get_or_create(op_conn, "branch-1");
2001 OperationState::set_branch(op_conn, "branch-1");
2002 create_operation(
2003 &context,
2004 "test.fasta",
2005 FileTypes::Fasta,
2006 "foo",
2007 HashId::convert_str("op-4"),
2008 );
2009 create_operation(
2010 &context,
2011 "test.fasta",
2012 FileTypes::Fasta,
2013 "foo",
2014 HashId::convert_str("op-5"),
2015 );
2016 OperationState::set_operation(op_conn, &HashId::convert_str("op-4"));
2017 Branch::get_or_create(op_conn, "branch-2");
2018 OperationState::set_branch(op_conn, "branch-2");
2019 create_operation(
2020 &context,
2021 "test.fasta",
2022 FileTypes::Fasta,
2023 "foo",
2024 HashId::convert_str("op-6"),
2025 );
2026 OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
2027 Branch::get_or_create(op_conn, "branch-3");
2028 OperationState::set_branch(op_conn, "branch-3");
2029 create_operation(
2030 &context,
2031 "test.fasta",
2032 FileTypes::Fasta,
2033 "foo",
2034 HashId::convert_str("op-7"),
2035 );
2036 assert_eq!(
2037 Operation::get_path_between(
2038 op_conn,
2039 HashId::convert_str("op-1"),
2040 HashId::convert_str("op-6")
2041 ),
2042 vec![
2043 (
2044 HashId::convert_str("op-1"),
2045 Direction::Outgoing,
2046 HashId::convert_str("op-2")
2047 ),
2048 (
2049 HashId::convert_str("op-2"),
2050 Direction::Outgoing,
2051 HashId::convert_str("op-3")
2052 ),
2053 (
2054 HashId::convert_str("op-3"),
2055 Direction::Outgoing,
2056 HashId::convert_str("op-4")
2057 ),
2058 (
2059 HashId::convert_str("op-4"),
2060 Direction::Outgoing,
2061 HashId::convert_str("op-6")
2062 ),
2063 ]
2064 );
2065
2066 assert_eq!(
2067 Operation::get_path_between(
2068 op_conn,
2069 HashId::convert_str("op-7"),
2070 HashId::convert_str("op-1")
2071 ),
2072 vec![(
2073 HashId::convert_str("op-7"),
2074 Direction::Incoming,
2075 HashId::convert_str("op-1")
2076 ),]
2077 );
2078
2079 assert_eq!(
2080 Operation::get_path_between(
2081 op_conn,
2082 HashId::convert_str("op-3"),
2083 HashId::convert_str("op-7")
2084 ),
2085 vec![
2086 (
2087 HashId::convert_str("op-3"),
2088 Direction::Incoming,
2089 HashId::convert_str("op-2")
2090 ),
2091 (
2092 HashId::convert_str("op-2"),
2093 Direction::Incoming,
2094 HashId::convert_str("op-1")
2095 ),
2096 (
2097 HashId::convert_str("op-1"),
2098 Direction::Outgoing,
2099 HashId::convert_str("op-7")
2100 ),
2101 ]
2102 );
2103 }
2104
2105 #[test]
2106 fn test_remote_create() {
2107 let context = setup_gen();
2108 let op_conn = context.operations().conn();
2109
2110 let remote = Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2112 assert_eq!(remote.name, "origin");
2113 assert_eq!(remote.url, "https://example.com/repo.gen");
2114
2115 let result = Remote::create(op_conn, "origin", "https://different.com/repo.gen");
2117 assert!(result.is_err());
2118 }
2119
2120 #[test]
2121 fn test_remote_get_by_name() {
2122 let context = setup_gen();
2123 let op_conn = context.operations().conn();
2124
2125 let result = Remote::get_by_name_optional(op_conn, "nonexistent");
2127 assert!(result.is_none());
2128
2129 Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2131 let result = Remote::get_by_name_optional(op_conn, "upstream");
2132 assert!(result.is_some());
2133 let remote = result.unwrap();
2134 assert_eq!(remote.name, "upstream");
2135 assert_eq!(remote.url, "https://upstream.com/repo.gen");
2136 }
2137
2138 #[test]
2139 fn test_remote_list_all() {
2140 let context = setup_gen();
2141 let op_conn = context.operations().conn();
2142
2143 let remotes = Remote::list_all(op_conn);
2145 assert!(remotes.is_empty());
2146
2147 Remote::create(op_conn, "origin", "https://origin.com/repo.gen").unwrap();
2149 Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2150 Remote::create(op_conn, "fork", "https://fork.com/repo.gen").unwrap();
2151
2152 let remotes = Remote::list_all(op_conn);
2154 assert_eq!(remotes.len(), 3);
2155 assert_eq!(remotes[0].name, "fork");
2156 assert_eq!(remotes[1].name, "origin");
2157 assert_eq!(remotes[2].name, "upstream");
2158 }
2159
2160 #[test]
2161 fn test_remote_delete() {
2162 let context = setup_gen();
2163 let op_conn = context.operations().conn();
2164
2165 Remote::create(op_conn, "temp", "https://temp.com/repo.gen").unwrap();
2167
2168 let remote = Remote::get_by_name_optional(op_conn, "temp");
2170 assert!(remote.is_some());
2171
2172 let result = Remote::delete(op_conn, "temp");
2174 assert!(result.is_ok());
2175
2176 let remote = Remote::get_by_name_optional(op_conn, "temp");
2178 assert!(remote.is_none());
2179
2180 let result = Remote::delete(op_conn, "nonexistent");
2182 assert!(result.is_err());
2183 }
2184
2185 #[test]
2186 fn test_remote_delete_with_branch_associations() {
2187 let context = setup_gen();
2188 let op_conn = context.operations().conn();
2189
2190 Remote::create(op_conn, "test_remote", "https://test.com/repo.gen").unwrap();
2192
2193 let branch = Branch::get_or_create(op_conn, "test_branch");
2195
2196 op_conn
2198 .execute(
2199 "UPDATE branch SET remote_name = ?1 WHERE id = ?2",
2200 params!["test_remote", branch.id],
2201 )
2202 .unwrap();
2203
2204 let remote_name: Option<String> = op_conn
2206 .query_row(
2207 "SELECT remote_name FROM branch WHERE id = ?1",
2208 params![branch.id],
2209 |row| row.get(0),
2210 )
2211 .unwrap();
2212 assert_eq!(remote_name, Some("test_remote".to_string()));
2213
2214 let result = Remote::delete(op_conn, "test_remote");
2216 assert!(result.is_ok());
2217
2218 let remote_name_after_delete: Option<String> = op_conn
2220 .query_row(
2221 "SELECT remote_name FROM branch WHERE id = ?1",
2222 params![branch.id],
2223 |row| row.get(0),
2224 )
2225 .unwrap();
2226 assert_eq!(remote_name_after_delete, None);
2227
2228 let remote = Remote::get_by_name_optional(op_conn, "test_remote");
2230 assert!(remote.is_none());
2231 }
2232
2233 #[test]
2234 fn test_branch_set_remote() {
2235 let context = setup_gen();
2236 let op_conn = context.operations().conn();
2237
2238 Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2240
2241 let branch = Branch::get_or_create(op_conn, "test_branch");
2243
2244 let remote = Branch::get_remote(op_conn, branch.id);
2246 assert_eq!(remote, None);
2247
2248 Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
2250
2251 let remote = Branch::get_remote(op_conn, branch.id);
2253 assert_eq!(remote, Some("origin".to_string()));
2254
2255 Branch::set_remote(op_conn, branch.id, None).unwrap();
2257
2258 let remote = Branch::get_remote(op_conn, branch.id);
2260 assert_eq!(remote, None);
2261 }
2262
2263 #[test]
2264 fn test_branch_get_remote() {
2265 let context = setup_gen();
2266 let op_conn = context.operations().conn();
2267
2268 Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2270 Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2271
2272 let branch1 = Branch::get_or_create(op_conn, "branch1");
2274 let branch2 = Branch::get_or_create(op_conn, "branch2");
2275
2276 Branch::set_remote(op_conn, branch1.id, Some("origin")).unwrap();
2278 Branch::set_remote(op_conn, branch2.id, Some("upstream")).unwrap();
2279
2280 assert_eq!(
2282 Branch::get_remote(op_conn, branch1.id),
2283 Some("origin".to_string())
2284 );
2285 assert_eq!(
2286 Branch::get_remote(op_conn, branch2.id),
2287 Some("upstream".to_string())
2288 );
2289
2290 assert_eq!(Branch::get_remote(op_conn, 99999), None);
2292 }
2293
2294 #[test]
2295 fn test_branch_create_with_remote() {
2296 let context = setup_gen();
2297 let op_conn = context.operations().conn();
2298
2299 Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2301
2302 let branch = Branch::create_with_remote(op_conn, "test_branch", Some("origin")).unwrap();
2304
2305 assert_eq!(branch.remote_name, Some("origin".to_string()));
2307 assert_eq!(
2308 Branch::get_remote(op_conn, branch.id),
2309 Some("origin".to_string())
2310 );
2311
2312 let branch2 = Branch::create_with_remote(op_conn, "test_branch2", None).unwrap();
2314 assert_eq!(branch2.remote_name, None);
2315 assert_eq!(Branch::get_remote(op_conn, branch2.id), None);
2316 }
2317
2318 #[test]
2319 fn test_branch_process_row_with_remote() {
2320 let context = setup_gen();
2321 let op_conn = context.operations().conn();
2322
2323 Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2325
2326 let branch = Branch::create_with_remote(op_conn, "test_branch", Some("origin")).unwrap();
2328
2329 let branches = Branch::query(
2331 op_conn,
2332 "SELECT * FROM branch WHERE id = ?1",
2333 params![branch.id],
2334 );
2335 assert_eq!(branches.len(), 1);
2336
2337 let queried_branch = &branches[0];
2338 assert_eq!(queried_branch.id, branch.id);
2339 assert_eq!(queried_branch.name, "test_branch");
2340 assert_eq!(queried_branch.remote_name, Some("origin".to_string()));
2341 }
2342
2343 #[test]
2344 fn test_branch_set_remote_foreign_key_constraint() {
2345 let context = setup_gen();
2346 let op_conn = context.operations().conn();
2347
2348 let branch = Branch::get_or_create(op_conn, "test_branch");
2350
2351 let result = Branch::set_remote(op_conn, branch.id, Some("nonexistent_remote"));
2353 assert!(result.is_err());
2354
2355 let remote = Branch::get_remote(op_conn, branch.id);
2357 assert_eq!(remote, None);
2358 }
2359
2360 #[test]
2361 fn operation_capnp_serialization() {
2362 use capnp::message::TypedBuilder;
2363
2364 let model = Operation {
2365 hash: HashId::convert_str("test"),
2366 parent_hash: Some(HashId::convert_str("parent")),
2367 change_type: "foo".to_string(),
2368 created_on: 0,
2369 };
2370
2371 let mut message = TypedBuilder::<operation::Owned>::new_default();
2372 let mut root = message.init_root();
2373 model.write_capnp(&mut root);
2374
2375 let deserialized = Operation::read_capnp(root.into_reader());
2376 assert_eq!(model, deserialized);
2377 }
2378
2379 #[test]
2380 fn operation_capnp_serialization_no_parent() {
2381 use capnp::message::TypedBuilder;
2382
2383 let model = Operation {
2384 hash: HashId::convert_str("test"),
2385 parent_hash: None,
2386 change_type: "foo".to_string(),
2387 created_on: 1,
2388 };
2389
2390 let mut message = TypedBuilder::<operation::Owned>::new_default();
2391 let mut root = message.init_root();
2392 model.write_capnp(&mut root);
2393
2394 let deserialized = Operation::read_capnp(root.into_reader());
2395 assert_eq!(model, deserialized);
2396 }
2397
2398 #[test]
2399 fn file_addition_capnp_serialization() {
2400 use capnp::message::TypedBuilder;
2401
2402 let file_addition = FileAddition {
2403 id: HashId([42u8; 32]),
2404 file_path: "test/path.fasta".to_string(),
2405 file_type: FileTypes::Fasta,
2406 checksum: HashId([24u8; 32]),
2407 };
2408
2409 let mut message =
2410 TypedBuilder::<crate::gen_models_capnp::file_addition::Owned>::new_default();
2411 let mut root = message.init_root();
2412 file_addition.write_capnp(&mut root);
2413
2414 let deserialized = FileAddition::read_capnp(root.into_reader());
2415 assert_eq!(file_addition, deserialized);
2416 }
2417
2418 #[test]
2419 fn operation_summary_capnp_serialization() {
2420 use capnp::message::TypedBuilder;
2421
2422 let operation_summary = OperationSummary {
2423 id: 123,
2424 operation_hash: HashId::convert_str("op-hash-123"),
2425 summary: "Added new sequences from FASTA file".to_string(),
2426 };
2427
2428 let mut message =
2429 TypedBuilder::<crate::gen_models_capnp::operation_summary::Owned>::new_default();
2430 let mut root = message.init_root();
2431 operation_summary.write_capnp(&mut root);
2432
2433 let deserialized = OperationSummary::read_capnp(root.into_reader());
2434 assert_eq!(operation_summary, deserialized);
2435 }
2436
2437 #[test]
2438 fn test_calculate_stream_hash() {
2439 let content = b"Hello, World!";
2440 let cursor = Cursor::new(content);
2441 let hash = calculate_stream_hash(cursor).unwrap();
2442
2443 assert_eq!(hash.len(), 32);
2444
2445 let cursor2 = Cursor::new(content);
2447 let hash2 = calculate_stream_hash(cursor2).unwrap();
2448 assert_eq!(hash, hash2);
2449
2450 let different_content = b"Hello, World!!";
2452 let cursor3 = Cursor::new(different_content);
2453 let hash3 = calculate_stream_hash(cursor3).unwrap();
2454 assert_ne!(hash, hash3);
2455 }
2456
2457 #[test]
2458 fn test_calculate_file_checksum() {
2459 let mut temp_file = NamedTempFile::new().unwrap();
2460 let content = b"Test file content for checksum calculation";
2461 temp_file.write_all(content).unwrap();
2462 temp_file.flush().unwrap();
2463
2464 let checksum = calculate_file_checksum(temp_file.path()).unwrap();
2465
2466 assert_eq!(checksum.0.len(), 32);
2467
2468 let checksum2 = calculate_file_checksum(temp_file.path()).unwrap();
2470 assert_eq!(checksum, checksum2);
2471
2472 let mut temp_file2 = NamedTempFile::new().unwrap();
2474 let different_content = b"Different test file content";
2475 temp_file2.write_all(different_content).unwrap();
2476 temp_file2.flush().unwrap();
2477
2478 let checksum3 = calculate_file_checksum(temp_file2.path()).unwrap();
2479 assert_ne!(checksum, checksum3);
2480 }
2481
2482 #[test]
2483 fn test_calculate_file_checksum_nonexistent_file() {
2484 let result = calculate_file_checksum("/nonexistent/file/path");
2485 assert!(result.is_err());
2486 assert!(matches!(
2487 result.unwrap_err().kind(),
2488 std::io::ErrorKind::NotFound
2489 ));
2490 }
2491
2492 #[test]
2493 fn test_generate_file_addition_id_consistency() {
2494 let checksum = HashId([1u8; 32]);
2495 let file_path = "/path/to/file.txt";
2496
2497 let id1 = FileAddition::generate_file_addition_id(&checksum, file_path);
2498 let id2 = FileAddition::generate_file_addition_id(&checksum, file_path);
2499
2500 assert_eq!(id1, id2);
2501 }
2502
2503 #[test]
2504 fn test_generate_file_addition_id_uniqueness_different_paths() {
2505 let checksum = HashId([1u8; 32]);
2506 let file_path1 = "/path/to/file1.txt";
2507 let file_path2 = "/path/to/file2.txt";
2508
2509 let id1 = FileAddition::generate_file_addition_id(&checksum, file_path1);
2510 let id2 = FileAddition::generate_file_addition_id(&checksum, file_path2);
2511
2512 assert_ne!(id1, id2);
2513 }
2514
2515 #[test]
2516 fn test_generate_file_addition_id_uniqueness_different_checksums() {
2517 let checksum1 = HashId([1u8; 32]);
2518 let checksum2 = HashId([2u8; 32]);
2519 let file_path = "/path/to/file.txt";
2520
2521 let id1 = FileAddition::generate_file_addition_id(&checksum1, file_path);
2522 let id2 = FileAddition::generate_file_addition_id(&checksum2, file_path);
2523
2524 assert_ne!(id1, id2);
2525 }
2526
2527 #[test]
2528 fn test_normalize_file_paths_absolute_path_in_repo() {
2529 let context = setup_gen();
2530 let workspace = context.workspace();
2531 let repo_root = workspace.base_dir();
2532
2533 let absolute_path = repo_root.join("inputs").join("absolute.txt");
2534 fs::create_dir_all(absolute_path.parent().unwrap()).unwrap();
2535 fs::write(&absolute_path, b"absolute").unwrap();
2536 let absolute_string = absolute_path.to_string_lossy().to_string();
2537 let relative_string = absolute_path
2538 .strip_prefix(repo_root)
2539 .unwrap()
2540 .to_string_lossy()
2541 .to_string();
2542
2543 let (absolute, relative) =
2544 FileAddition::normalize_file_paths(workspace, absolute_string.as_str());
2545
2546 assert_eq!(absolute, absolute_string);
2547 assert_eq!(relative, relative_string);
2548 }
2549
2550 #[test]
2551 fn test_normalize_file_paths_relative_path_in_repo() {
2552 let context = setup_gen();
2553 let workspace = context.workspace();
2554 let repo_root = workspace.repo_root().unwrap();
2555
2556 let relative_path = PathBuf::from("relative/path/file.txt");
2557 let absolute_path = repo_root.join(&relative_path);
2558 fs::create_dir_all(absolute_path.parent().unwrap()).unwrap();
2559 fs::write(&absolute_path, b"relative").unwrap();
2560 let relative_string = relative_path.to_string_lossy().to_string();
2561 let absolute_string = absolute_path.to_string_lossy().to_string();
2562
2563 let (absolute, relative) =
2564 FileAddition::normalize_file_paths(workspace, relative_string.as_str());
2565
2566 assert_eq!(absolute, absolute_string);
2567 assert_eq!(relative, relative_string);
2568 }
2569
2570 #[test]
2571 fn test_normalize_file_paths_outside_repo_fallbacks() {
2572 let context = setup_gen();
2573 let workspace = context.workspace();
2574
2575 let outside_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
2576 let outside_string = outside_path.to_string_lossy().to_string();
2577
2578 let (absolute, relative) =
2579 FileAddition::normalize_file_paths(workspace, outside_string.as_str());
2580
2581 assert_eq!(absolute, outside_string);
2582 assert_eq!(relative, outside_string);
2583 }
2584
2585 #[test]
2586 fn test_normalize_file_paths_without_connection_path() {
2587 let context = setup_gen();
2588 let workspace = context.workspace();
2589
2590 let (absolute, relative) =
2591 FileAddition::normalize_file_paths(workspace, "detached/file.txt");
2592 assert_eq!(absolute, "detached/file.txt");
2593 assert_eq!(relative, "detached/file.txt");
2594
2595 let (absolute_empty, relative_empty) = FileAddition::normalize_file_paths(workspace, "");
2596 assert_eq!(absolute_empty, "");
2597 assert_eq!(relative_empty, "");
2598 }
2599
2600 #[test]
2601 fn test_file_addition_get_or_create() {
2602 let context = setup_gen();
2603 let op_conn = context.operations().conn();
2604 let repo_root = context.workspace().repo_root().unwrap();
2605
2606 let file1_path = repo_root.join("test_file.txt");
2607 fs::write(&file1_path, b"Test file content").unwrap();
2608 let file1_path_str = file1_path.to_string_lossy().to_string();
2609 let relative1 = file1_path
2610 .strip_prefix(&repo_root)
2611 .unwrap()
2612 .to_string_lossy()
2613 .to_string();
2614
2615 let fa1 = FileAddition::get_or_create(
2616 context.workspace(),
2617 op_conn,
2618 &file1_path_str,
2619 FileTypes::Fasta,
2620 None,
2621 )
2622 .expect("Failed to create FileAddition");
2623
2624 assert_eq!(fa1.file_path, relative1);
2625
2626 let checksum = calculate_file_checksum(&file1_path_str).unwrap();
2627 let relative1_id = FileAddition::generate_file_addition_id(&checksum, &relative1);
2628
2629 assert_eq!(fa1.id, relative1_id);
2630
2631 let fa2 = FileAddition::get_or_create(
2633 context.workspace(),
2634 op_conn,
2635 &file1_path_str,
2636 FileTypes::Fasta,
2637 None,
2638 )
2639 .expect("Failed to get existing FileAddition");
2640
2641 assert_eq!(fa1, fa2);
2642
2643 let file2_path = repo_root.join("nested").join("file2.txt");
2644 fs::create_dir_all(file2_path.parent().unwrap()).unwrap();
2645 fs::write(&file2_path, b"Test file content").unwrap();
2646 let file2_path_str = file2_path.to_string_lossy().to_string();
2647
2648 let fa3 = FileAddition::get_or_create(
2649 context.workspace(),
2650 op_conn,
2651 &file2_path_str,
2652 FileTypes::Fasta,
2653 None,
2654 )
2655 .expect("Failed to create different FileAddition");
2656
2657 assert_ne!(fa1.id, fa3.id);
2658
2659 fs::write(&file1_path, b"new content").unwrap();
2660 let fa1_new = FileAddition::get_or_create(
2661 context.workspace(),
2662 op_conn,
2663 &file1_path_str,
2664 FileTypes::Fasta,
2665 None,
2666 )
2667 .expect("Failed to create FileAddition");
2668
2669 assert_ne!(fa1.id, fa1_new.id);
2670 }
2671}