1use crate::context::file_system::FileSystemOperations;
2use crate::context::git_operations::GitOperations;
3use crate::storage::XdgDirectories;
4use chrono::{DateTime, Local};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8pub struct RepoManager {
9 xdg_directories: Arc<XdgDirectories>,
10 file_system: Arc<dyn FileSystemOperations>,
11 git_operations: Arc<dyn GitOperations>,
12}
13
14impl RepoManager {
15 pub fn new(
16 xdg_directories: Arc<XdgDirectories>,
17 file_system: Arc<dyn FileSystemOperations>,
18 git_operations: Arc<dyn GitOperations>,
19 ) -> Self {
20 Self {
21 xdg_directories,
22 file_system,
23 git_operations,
24 }
25 }
26
27 pub async fn copy_repo(
32 &self,
33 task_id: &str,
34 repo_root: &Path,
35 source_commit: Option<&str>,
36 ) -> Result<(PathBuf, String), String> {
37 let task_dir_name = task_id;
39 let branch_name = format!("tsk/{task_id}");
40
41 let repo_hash = crate::storage::get_repo_hash(repo_root);
43 let task_dir = self.xdg_directories.task_dir(task_dir_name, &repo_hash);
44 let repo_path = task_dir.join("repo");
45
46 self.file_system
48 .create_dir(&task_dir)
49 .await
50 .map_err(|e| format!("Failed to create task directory: {e}"))?;
51
52 if !self.git_operations.is_git_repository().await? {
54 return Err("Not in a git repository".to_string());
55 }
56
57 let current_dir = repo_root.to_path_buf();
59
60 let tracked_files = self.git_operations.get_tracked_files(¤t_dir).await?;
62
63 let untracked_files = self
65 .git_operations
66 .get_untracked_files(¤t_dir)
67 .await?;
68
69 let git_src = current_dir.join(".git");
71 let git_dst = repo_path.join(".git");
72 if self
73 .file_system
74 .exists(&git_src)
75 .await
76 .map_err(|e| format!("Failed to check if .git exists: {e}"))?
77 {
78 self.copy_directory(&git_src, &git_dst).await?;
79 }
80
81 for file_path in tracked_files {
83 let src_path = current_dir.join(&file_path);
84 let dst_path = repo_path.join(&file_path);
85
86 if let Some(parent) = dst_path.parent() {
88 self.file_system
89 .create_dir(parent)
90 .await
91 .map_err(|e| format!("Failed to create parent directory: {e}"))?;
92 }
93
94 self.file_system
96 .copy_file(&src_path, &dst_path)
97 .await
98 .map_err(|e| {
99 format!("Failed to copy tracked file {}: {}", file_path.display(), e)
100 })?;
101 }
102
103 for file_path in untracked_files {
105 let file_path_str = file_path.to_string_lossy();
107 let file_path_clean = if let Some(stripped) = file_path_str.strip_suffix('/') {
108 PathBuf::from(stripped)
109 } else {
110 file_path.clone()
111 };
112
113 let src_path = current_dir.join(&file_path_clean);
114 let dst_path = repo_path.join(&file_path_clean);
115
116 if self.file_system.read_dir(&src_path).await.is_ok() {
118 self.copy_directory(&src_path, &dst_path).await?;
120 } else {
121 if let Some(parent) = dst_path.parent() {
124 self.file_system
125 .create_dir(parent)
126 .await
127 .map_err(|e| format!("Failed to create parent directory: {e}"))?;
128 }
129
130 match self.file_system.copy_file(&src_path, &dst_path).await {
132 Ok(_) => {}
133 Err(e) => {
134 if !e.to_string().contains("Source file not found") {
137 return Err(format!(
138 "Failed to copy untracked file {}: {}",
139 file_path.display(),
140 e
141 ));
142 }
143 }
144 }
145 }
146 }
147
148 let tsk_src = current_dir.join(".tsk");
150 let tsk_dst = repo_path.join(".tsk");
151 if self
152 .file_system
153 .exists(&tsk_src)
154 .await
155 .map_err(|e| format!("Failed to check if .tsk exists: {e}"))?
156 {
157 self.copy_directory(&tsk_src, &tsk_dst).await?;
158 }
159
160 match source_commit {
162 Some(commit_sha) => {
163 self.git_operations
165 .create_branch_from_commit(&repo_path, &branch_name, commit_sha)
166 .await?;
167 println!("Created branch from commit: {commit_sha}");
168 }
169 None => {
170 self.git_operations
172 .create_branch(&repo_path, &branch_name)
173 .await?;
174 }
175 }
176
177 println!("Created repository copy at: {}", repo_path.display());
178 println!("Branch: {branch_name}");
179 Ok((repo_path, branch_name))
180 }
181
182 #[allow(clippy::only_used_in_recursion)]
184 async fn copy_directory(&self, src: &Path, dst: &Path) -> Result<(), String> {
185 self.file_system
186 .create_dir(dst)
187 .await
188 .map_err(|e| format!("Failed to create destination directory: {e}"))?;
189
190 let entries = self
191 .file_system
192 .read_dir(src)
193 .await
194 .map_err(|e| format!("Failed to read directory: {e}"))?;
195
196 for path in entries {
197 let file_name = path
198 .file_name()
199 .ok_or_else(|| "Invalid file name".to_string())?;
200
201 let dst_path = dst.join(file_name);
202
203 if self.file_system.read_dir(&path).await.is_ok() {
205 Box::pin(self.copy_directory(&path, &dst_path)).await?;
206 } else {
207 self.file_system
208 .copy_file(&path, &dst_path)
209 .await
210 .map_err(|e| format!("Failed to copy file {}: {}", path.display(), e))?;
211 }
212 }
213
214 Ok(())
215 }
216
217 pub async fn commit_changes(&self, repo_path: &Path, message: &str) -> Result<(), String> {
219 let status_output = self.git_operations.get_status(repo_path).await?;
221
222 if status_output.trim().is_empty() {
223 println!("No changes to commit");
224 return Ok(());
225 }
226
227 self.git_operations.add_all(repo_path).await?;
229
230 self.git_operations.commit(repo_path, message).await?;
232
233 println!("Committed changes: {message}");
234 Ok(())
235 }
236
237 pub async fn fetch_changes(
240 &self,
241 repo_path: &Path,
242 branch_name: &str,
243 repo_root: &Path,
244 ) -> Result<bool, String> {
245 let repo_path_str = repo_path
246 .to_str()
247 .ok_or_else(|| "Invalid repo path".to_string())?;
248
249 let main_repo = repo_root.to_path_buf();
251
252 let now: DateTime<Local> = Local::now();
254 let remote_name = format!("tsk-temp-{}", now.format("%Y-%m-%d-%H%M%S"));
255
256 self.git_operations
257 .add_remote(&main_repo, &remote_name, repo_path_str)
258 .await?;
259
260 match self
262 .git_operations
263 .fetch_branch(&main_repo, &remote_name, branch_name)
264 .await
265 {
266 Ok(_) => {
267 self.git_operations
269 .remove_remote(&main_repo, &remote_name)
270 .await?;
271 }
272 Err(e) => {
273 let _ = self
275 .git_operations
276 .remove_remote(&main_repo, &remote_name)
277 .await;
278 return Err(e);
279 }
280 }
281
282 let has_commits = self
284 .git_operations
285 .has_commits_not_in_base(&main_repo, branch_name, "main")
286 .await?;
287
288 if !has_commits {
289 println!("No new commits in branch {branch_name} - deleting branch");
290 if let Err(e) = self
292 .git_operations
293 .delete_branch(&main_repo, branch_name)
294 .await
295 {
296 eprintln!("Warning: Failed to delete branch {branch_name}: {e}");
297 }
298 return Ok(false);
299 }
300
301 println!("Fetched changes from copied repository");
302 Ok(true)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::context::git_operations::tests::MockGitOperations;
310 use std::collections::HashMap;
311 use tempfile::TempDir;
312
313 fn create_test_xdg_directories(temp_dir: &TempDir) -> Arc<XdgDirectories> {
314 unsafe {
315 std::env::set_var("XDG_DATA_HOME", temp_dir.path().join("data"));
316 }
317 unsafe {
318 std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path().join("runtime"));
319 }
320 let xdg = XdgDirectories::new().unwrap();
321 xdg.ensure_directories().unwrap();
322 Arc::new(xdg)
323 }
324
325 #[tokio::test]
326 async fn test_copy_repo_not_in_git_repo() {
327 let temp_dir = TempDir::new().expect("Failed to create temp dir");
328 let xdg_directories = create_test_xdg_directories(&temp_dir);
329
330 let mock_git_ops = Arc::new(MockGitOperations::new());
332 mock_git_ops.set_is_repo_result(Ok(false));
333
334 use crate::context::file_system::tests::MockFileSystem;
335 let fs = Arc::new(MockFileSystem::new());
336
337 let manager = RepoManager::new(xdg_directories, fs, mock_git_ops.clone());
338
339 let repo_root = temp_dir.path();
340 let result = manager
341 .copy_repo("2024-01-01-1200-generic-test-task", repo_root, None)
342 .await;
343
344 assert!(result.is_err());
345 assert_eq!(result.unwrap_err(), "Not in a git repository");
346 }
347
348 #[tokio::test]
349 async fn test_commit_changes_no_changes() {
350 let temp_dir = TempDir::new().expect("Failed to create temp dir");
351 let repo_path = temp_dir.path();
352
353 let mock_git_ops = Arc::new(MockGitOperations::new());
355 mock_git_ops.set_get_status_result(Ok("".to_string()));
356
357 use crate::context::file_system::tests::MockFileSystem;
358 let fs = Arc::new(MockFileSystem::new());
359
360 let xdg_directories = create_test_xdg_directories(&temp_dir);
361 let manager = RepoManager::new(xdg_directories, fs, mock_git_ops);
362
363 let result = manager.commit_changes(repo_path, "Test commit").await;
364
365 assert!(result.is_ok(), "Error: {result:?}");
366 }
367
368 #[tokio::test]
369 async fn test_commit_changes_with_changes() {
370 let temp_dir = TempDir::new().expect("Failed to create temp dir");
371 let repo_path = temp_dir.path();
372
373 let mock_git_ops = Arc::new(MockGitOperations::new());
375 mock_git_ops.set_get_status_result(Ok("M file.txt\n".to_string()));
376
377 use crate::context::file_system::tests::MockFileSystem;
378 let fs = Arc::new(MockFileSystem::new());
379
380 let xdg_directories = create_test_xdg_directories(&temp_dir);
381 let manager = RepoManager::new(xdg_directories, fs, mock_git_ops);
382
383 let result = manager.commit_changes(repo_path, "Test commit").await;
384
385 assert!(result.is_ok(), "Error: {result:?}");
386 }
387
388 #[tokio::test]
389 async fn test_fetch_changes_no_commits() {
390 let temp_dir = TempDir::new().expect("Failed to create temp dir");
391 let repo_path = temp_dir.path();
392
393 let mock_git_ops = Arc::new(MockGitOperations::new());
395 mock_git_ops.set_has_commits_not_in_base_result(Ok(false));
396
397 use crate::context::file_system::tests::MockFileSystem;
398 let fs = Arc::new(MockFileSystem::new());
399
400 unsafe {
402 std::env::set_var("XDG_DATA_HOME", temp_dir.path().join("data"));
403 }
404 unsafe {
405 std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path().join("runtime"));
406 }
407 let xdg = Arc::new(crate::storage::XdgDirectories::new().unwrap());
408
409 let manager = RepoManager::new(xdg, fs, mock_git_ops.clone());
410
411 let repo_root = temp_dir.path();
412 let result = manager
413 .fetch_changes(repo_path, "tsk/test-branch", repo_root)
414 .await;
415
416 assert!(result.is_ok(), "Error: {result:?}");
417 assert_eq!(result.unwrap(), false);
418
419 let delete_calls = mock_git_ops.get_delete_branch_calls();
421 assert_eq!(delete_calls.len(), 1);
422 assert_eq!(delete_calls[0].1, "tsk/test-branch");
423 }
424
425 #[tokio::test]
426 async fn test_fetch_changes_with_commits() {
427 let temp_dir = TempDir::new().expect("Failed to create temp dir");
428 let repo_path = temp_dir.path();
429
430 let mock_git_ops = Arc::new(MockGitOperations::new());
432 mock_git_ops.set_has_commits_not_in_base_result(Ok(true));
433
434 use crate::context::file_system::tests::MockFileSystem;
435 let fs = Arc::new(MockFileSystem::new());
436
437 unsafe {
439 std::env::set_var("XDG_DATA_HOME", temp_dir.path().join("data"));
440 }
441 unsafe {
442 std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path().join("runtime"));
443 }
444 let xdg = Arc::new(crate::storage::XdgDirectories::new().unwrap());
445
446 let manager = RepoManager::new(xdg, fs, mock_git_ops.clone());
447
448 let repo_root = temp_dir.path();
449 let result = manager
450 .fetch_changes(repo_path, "tsk/test-branch", repo_root)
451 .await;
452
453 assert!(result.is_ok(), "Error: {result:?}");
454 assert_eq!(result.unwrap(), true);
455
456 let delete_calls = mock_git_ops.get_delete_branch_calls();
458 assert_eq!(delete_calls.len(), 0);
459 }
460
461 #[tokio::test]
462 async fn test_copy_repo_with_source_commit() {
463 let temp_dir = TempDir::new().expect("Failed to create temp dir");
464 let xdg_directories = create_test_xdg_directories(&temp_dir);
465
466 let mock_git_ops = Arc::new(MockGitOperations::new());
468 mock_git_ops.set_is_repo_result(Ok(true));
469 mock_git_ops.set_get_tracked_files_result(Ok(vec![
470 PathBuf::from("src/main.rs"),
471 PathBuf::from("Cargo.toml"),
472 ]));
473
474 use crate::context::file_system::tests::MockFileSystem;
475 let fs = Arc::new(MockFileSystem::new());
476 let mut files = HashMap::new();
478 files.insert(temp_dir.path().join(".git"), "dir".to_string());
479 files.insert(
480 temp_dir.path().join("src/main.rs"),
481 "file content".to_string(),
482 );
483 files.insert(temp_dir.path().join("Cargo.toml"), "[package]".to_string());
484 fs.set_files(files);
485
486 let manager = RepoManager::new(xdg_directories, fs, mock_git_ops.clone());
487
488 let repo_root = temp_dir.path();
489 let source_commit = "abc123def456789012345678901234567890abcd";
490 let result = manager
491 .copy_repo(
492 "2024-01-01-1200-generic-test-task",
493 repo_root,
494 Some(source_commit),
495 )
496 .await;
497
498 assert!(result.is_ok(), "Error: {result:?}");
499 let (_, branch_name) = result.unwrap();
500 assert_eq!(branch_name, "tsk/2024-01-01-1200-generic-test-task");
501
502 let create_from_commit_calls = mock_git_ops.get_create_branch_from_commit_calls();
504 assert_eq!(create_from_commit_calls.len(), 1);
505 assert_eq!(
506 create_from_commit_calls[0].1,
507 "tsk/2024-01-01-1200-generic-test-task"
508 );
509 assert_eq!(create_from_commit_calls[0].2, source_commit);
510
511 let create_branch_calls = mock_git_ops.get_create_branch_calls();
513 assert_eq!(create_branch_calls.len(), 0);
514 }
515
516 #[tokio::test]
517 async fn test_copy_repo_without_source_commit() {
518 let temp_dir = TempDir::new().expect("Failed to create temp dir");
519 let xdg_directories = create_test_xdg_directories(&temp_dir);
520
521 let mock_git_ops = Arc::new(MockGitOperations::new());
523 mock_git_ops.set_is_repo_result(Ok(true));
524 mock_git_ops.set_get_tracked_files_result(Ok(vec![PathBuf::from("README.md")]));
525
526 use crate::context::file_system::tests::MockFileSystem;
527 let fs = Arc::new(MockFileSystem::new());
528 let mut files = HashMap::new();
530 files.insert(temp_dir.path().join(".git"), "dir".to_string());
531 files.insert(temp_dir.path().join("README.md"), "# README".to_string());
532 fs.set_files(files);
533
534 let manager = RepoManager::new(xdg_directories, fs, mock_git_ops.clone());
535
536 let repo_root = temp_dir.path();
537 let result = manager
538 .copy_repo("2024-01-01-1200-generic-test-task", repo_root, None)
539 .await;
540
541 assert!(result.is_ok(), "Error: {result:?}");
542 let (_, branch_name) = result.unwrap();
543 assert_eq!(branch_name, "tsk/2024-01-01-1200-generic-test-task");
544
545 let create_branch_calls = mock_git_ops.get_create_branch_calls();
547 assert_eq!(create_branch_calls.len(), 1);
548 assert_eq!(
549 create_branch_calls[0].1,
550 "tsk/2024-01-01-1200-generic-test-task"
551 );
552
553 let create_from_commit_calls = mock_git_ops.get_create_branch_from_commit_calls();
555 assert_eq!(create_from_commit_calls.len(), 0);
556 }
557
558 #[tokio::test]
559 async fn test_copy_repo_separates_tracked_and_untracked_files() {
560 let temp_dir = TempDir::new().expect("Failed to create temp dir");
561 let xdg_directories = create_test_xdg_directories(&temp_dir);
562
563 let mock_git_ops = Arc::new(MockGitOperations::new());
565 mock_git_ops.set_is_repo_result(Ok(true));
566 mock_git_ops.set_get_tracked_files_result(Ok(vec![
568 PathBuf::from("src/main.rs"),
569 PathBuf::from("Cargo.toml"),
570 ]));
571 mock_git_ops.set_get_untracked_files_result(Ok(vec![PathBuf::from("build.log")]));
573
574 use crate::context::file_system::tests::MockFileSystem;
575 let fs = Arc::new(MockFileSystem::new());
576 let mut files = HashMap::new();
578 files.insert(temp_dir.path().join(".git"), "dir".to_string());
579 files.insert(
580 temp_dir.path().join("src/main.rs"),
581 "fn main() {}".to_string(),
582 );
583 files.insert(temp_dir.path().join("Cargo.toml"), "[package]".to_string());
584 files.insert(
585 temp_dir.path().join("target/debug/app"),
586 "binary".to_string(),
587 ); files.insert(temp_dir.path().join("build.log"), "log content".to_string()); fs.set_files(files);
590
591 let manager = RepoManager::new(xdg_directories.clone(), fs.clone(), mock_git_ops.clone());
592
593 let repo_root = temp_dir.path();
594 let result = manager
595 .copy_repo("2024-01-01-1200-generic-test-task", repo_root, None)
596 .await;
597
598 assert!(result.is_ok(), "Error: {result:?}");
599 let (repo_path, _) = result.unwrap();
600
601 let copied_files = fs.get_files();
603 let repo_path_str = repo_path.to_string_lossy();
604
605 assert!(copied_files.contains_key(&format!("{repo_path_str}/src/main.rs")));
607 assert!(copied_files.contains_key(&format!("{repo_path_str}/Cargo.toml")));
608
609 assert!(copied_files.contains_key(&format!("{repo_path_str}/build.log")));
611
612 assert!(!copied_files.contains_key(&format!("{repo_path_str}/target/debug/app")));
614
615 let copied_dirs = fs.get_dirs();
617 assert!(
618 copied_dirs
619 .iter()
620 .any(|d| d == &format!("{repo_path_str}/.git"))
621 );
622 }
623
624 #[tokio::test]
625 async fn test_copy_repo_includes_untracked_files() {
626 let temp_dir = TempDir::new().expect("Failed to create temp dir");
627 let xdg_directories = create_test_xdg_directories(&temp_dir);
628
629 let mock_git_ops = Arc::new(MockGitOperations::new());
631 mock_git_ops.set_is_repo_result(Ok(true));
632 mock_git_ops.set_get_tracked_files_result(Ok(vec![
633 PathBuf::from("src/main.rs"),
634 PathBuf::from("Cargo.toml"),
635 ]));
636 mock_git_ops.set_get_untracked_files_result(Ok(vec![
637 PathBuf::from("notes.txt"),
638 PathBuf::from("test_output.log"),
639 PathBuf::from("debug/"), ]));
641
642 use crate::context::file_system::tests::MockFileSystem;
643 let fs = Arc::new(MockFileSystem::new());
644 let mut files = HashMap::new();
646 files.insert(temp_dir.path().join(".git"), "dir".to_string());
647 files.insert(temp_dir.path().join("src"), "dir".to_string());
648 files.insert(temp_dir.path().join("debug"), "dir".to_string());
649 files.insert(
650 temp_dir.path().join("src/main.rs"),
651 "fn main() {}".to_string(),
652 );
653 files.insert(temp_dir.path().join("Cargo.toml"), "[package]".to_string());
654 files.insert(temp_dir.path().join("notes.txt"), "Some notes".to_string());
655 files.insert(
656 temp_dir.path().join("test_output.log"),
657 "test output".to_string(),
658 );
659 files.insert(
660 temp_dir.path().join("debug/temp.txt"),
661 "temporary debug file".to_string(),
662 );
663 files.insert(
665 temp_dir.path().join("target/debug/app"),
666 "binary".to_string(),
667 );
668 fs.set_files(files);
669
670 let manager = RepoManager::new(xdg_directories.clone(), fs.clone(), mock_git_ops.clone());
671
672 let repo_root = temp_dir.path();
673 let result = manager
674 .copy_repo("2024-01-01-1200-generic-test-task", repo_root, None)
675 .await;
676
677 assert!(result.is_ok(), "Error: {result:?}");
678 let (repo_path, _) = result.unwrap();
679
680 let copied_files = fs.get_files();
682 let repo_path_str = repo_path.to_string_lossy();
683
684 assert!(copied_files.contains_key(&format!("{repo_path_str}/src/main.rs")));
686 assert!(copied_files.contains_key(&format!("{repo_path_str}/Cargo.toml")));
687
688 assert!(copied_files.contains_key(&format!("{repo_path_str}/notes.txt")));
690 assert!(copied_files.contains_key(&format!("{repo_path_str}/test_output.log")));
691 assert!(copied_files.contains_key(&format!("{repo_path_str}/debug/temp.txt")));
692
693 assert!(!copied_files.contains_key(&format!("{repo_path_str}/target/debug/app")));
695 }
696
697 #[tokio::test]
698 async fn test_copy_repo_includes_tsk_directory() {
699 let temp_dir = TempDir::new().expect("Failed to create temp dir");
700 let xdg_directories = create_test_xdg_directories(&temp_dir);
701
702 let mock_git_ops = Arc::new(MockGitOperations::new());
704 mock_git_ops.set_is_repo_result(Ok(true));
705 mock_git_ops.set_get_tracked_files_result(Ok(vec![
706 PathBuf::from("src/main.rs"),
707 PathBuf::from("Cargo.toml"),
708 ]));
709
710 use crate::context::file_system::tests::MockFileSystem;
711 let fs = Arc::new(MockFileSystem::new());
712 let mut files = HashMap::new();
714 files.insert(temp_dir.path().join(".git"), "dir".to_string());
715 files.insert(temp_dir.path().join(".tsk"), "dir".to_string());
716 files.insert(
717 temp_dir
718 .path()
719 .join(".tsk/dockerfiles/project/test-project/Dockerfile"),
720 "FROM ubuntu:22.04".to_string(),
721 );
722 files.insert(
723 temp_dir.path().join("src/main.rs"),
724 "fn main() {}".to_string(),
725 );
726 files.insert(temp_dir.path().join("Cargo.toml"), "[package]".to_string());
727 fs.set_files(files);
728
729 let manager = RepoManager::new(xdg_directories.clone(), fs.clone(), mock_git_ops.clone());
730
731 let repo_root = temp_dir.path();
732 let result = manager
733 .copy_repo("2024-01-01-1200-generic-test-task", repo_root, None)
734 .await;
735
736 assert!(result.is_ok(), "Error: {result:?}");
737 let (repo_path, _) = result.unwrap();
738
739 let copied_files = fs.get_files();
741 let copied_dirs = fs.get_dirs();
742 let repo_path_str = repo_path.to_string_lossy();
743
744 assert!(
746 copied_dirs
747 .iter()
748 .any(|d| d == &format!("{}/.tsk", repo_path_str))
749 );
750
751 assert!(
753 copied_dirs.iter().any(|d| d.contains(".tsk"))
754 || copied_files.keys().any(|f| f.contains(".tsk/dockerfiles"))
755 );
756
757 let copy_directory_exists = copied_dirs.iter().any(|d| d.contains(".tsk"))
760 || fs.get_files().keys().any(|k| k.contains(".tsk"));
761 assert!(
762 copy_directory_exists,
763 "Expected .tsk directory or its contents to be copied"
764 );
765 }
766}