mj_controller/import/
safety.rs1use super::*;
2
3pub fn import_safety_issues(targets: &SessionEditTargets) -> Result<ImportSafetyIssues> {
4 let mut dirty_git_roots = Vec::new();
5 let mut has_untracked_files = false;
6 for root in &targets.git_roots {
7 let output = Command::new("git")
8 .args(["status", "--porcelain=v1", "--untracked-files=normal"])
9 .current_dir(root)
10 .output()
11 .with_context(|| format!("inspect Git status in {}", root.display()))?;
12 ensure!(
13 output.status.success(),
14 "could not inspect Git status in {}",
15 root.display()
16 );
17 let (tracked, untracked) = String::from_utf8_lossy(&output.stdout).lines().fold(
18 (0_usize, 0_usize),
19 |(tracked, untracked), line| {
20 if line.starts_with("??") {
21 (tracked, untracked + 1)
22 } else {
23 (tracked + 1, untracked)
24 }
25 },
26 );
27 has_untracked_files |= untracked > 0;
28 if tracked + untracked > 0 {
29 let mut parts = Vec::new();
30 if tracked > 0 {
31 parts.push(format!(
32 "{tracked} tracked change{}",
33 if tracked == 1 { "" } else { "s" }
34 ));
35 }
36 if untracked > 0 {
37 parts.push(format!(
38 "{untracked} untracked path{}",
39 if untracked == 1 { "" } else { "s" }
40 ));
41 }
42 dirty_git_roots.push((root.clone(), parts.join(" ยท ")));
43 }
44 }
45 Ok(ImportSafetyIssues {
46 dirty_git_roots,
47 omitted_non_git_dirs: targets.non_git_dirs.clone(),
48 scratch_git_roots: targets.scratch_git_roots.clone(),
49 has_untracked_files,
50 })
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ImportedClaudeSession {
55 pub session_id: String,
56 pub native_session_id: String,
57 pub source_jsonl: PathBuf,
58 pub source_cwd: PathBuf,
59 pub bundle_id: String,
60 pub archive_path: PathBuf,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ImportArchiveProgress {
65 Repository {
66 current: usize,
67 total: usize,
68 id: String,
69 },
70 UntrackedFile {
71 repository_id: String,
72 current: usize,
73 total: usize,
74 path: PathBuf,
75 },
76 WritingArchive,
77}
78
79pub struct ImportControl<'a> {
80 pub cancelled: &'a AtomicBool,
81 pub progress: &'a (dyn Fn(ImportArchiveProgress) + Sync),
82 pub include_untracked: bool,
83}
84
85impl ImportControl<'_> {
86 pub(super) fn check_cancelled(&self) -> Result<()> {
87 ensure!(!self.cancelled.load(Ordering::Acquire), "import cancelled");
88 Ok(())
89 }
90
91 pub(super) fn report(&self, progress: ImportArchiveProgress) -> Result<()> {
92 self.check_cancelled()?;
93 (self.progress)(progress);
94 Ok(())
95 }
96}
97
98pub fn harness_config_home(kind: HarnessKind) -> Result<PathBuf> {
103 let name = kind.display_name();
104 let home = std::env::var_os(kind.home_env())
105 .map(|path| kind.home_from_environment(path))
106 .or_else(|| dirs::home_dir().map(|home| home.join(kind.default_home_leaf())))
107 .with_context(|| format!("cannot determine {name} home; set {}", kind.home_env()))?;
108 ensure!(
109 home.is_dir(),
110 "{name} home is not a directory: {}",
111 home.display()
112 );
113 Ok(home)
114}