1use anyhow::{Result, bail};
4use mj_core::config::{Config, TargetTemplate, is_bare_project_target};
5use mj_core::state::{Checkout, ManagedWorktreeTarget, SessionRecord};
6
7pub use mj_core::config::DEFAULT_CONTAINER_IMAGE as DEFAULT_IMAGE;
12
13pub fn managed_worktree_target(template: &TargetTemplate) -> Result<ManagedWorktreeTarget> {
16 match template {
17 TargetTemplate::LocalBare => Ok(ManagedWorktreeTarget::Local),
18 TargetTemplate::SshBare { ssh, .. } => {
19 let destination = match &ssh.user {
20 Some(user) => format!("{user}@{}", ssh.host),
21 None => ssh.host.clone(),
22 };
23 let ssh_args = mj_core::targets::ssh_args_with_identity(
27 &ssh.extra_args,
28 ssh.identity_file.as_deref(),
29 );
30 Ok(ManagedWorktreeTarget::Ssh {
31 destination,
32 ssh_args,
33 })
34 }
35 _ => bail!("managed raw worktrees require a bare target"),
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ResumePlan {
42 InPlace,
44 RawToWorkspace,
49 WorkspaceToRaw,
51}
52
53pub fn resume_compatibility(
60 session: &SessionRecord,
61 config: &Config,
62 target_id: &str,
63) -> Result<ResumePlan, String> {
64 let checkout = session.checkout();
65 resume_compatibility_with_checkout(session, &checkout, config, target_id)
66}
67
68pub fn resume_compatibility_with_checkout(
71 session: &SessionRecord,
72 checkout: &Checkout<'_>,
73 config: &Config,
74 target_id: &str,
75) -> Result<ResumePlan, String> {
76 let Some(target) = config.targets.get(target_id) else {
77 return Err(format!("target {target_id} is no longer configured"));
78 };
79 let project_directory = checkout.project_directory();
80 let Some(project_directory) = project_directory else {
81 if matches!(target, TargetTemplate::LocalBare) {
82 return workspace_to_raw_compatibility(session, config);
83 }
84 return Ok(ResumePlan::InPlace);
85 };
86 let directory = project_directory.display();
87 let worktree = match checkout {
88 Checkout::ManagedWorktree { worktree, .. } => Some(*worktree),
89 Checkout::Attached { .. } | Checkout::Borrowed { .. } => None,
90 Checkout::ManagedWorkspace => None,
91 };
92 let Some(worktree) = worktree else {
93 let Some(previous) = config.targets.get(&session.target_template_id) else {
94 return Err(
95 "the bare target this session last used is no longer configured".to_owned(),
96 );
97 };
98 if is_bare_project_target(target) {
99 if matches!(previous, TargetTemplate::LocalBare)
100 == matches!(target, TargetTemplate::LocalBare)
101 {
102 return Ok(ResumePlan::InPlace);
103 }
104 return Err(format!(
105 "this session opens {directory} directly on its host; resume it on the same kind of bare target"
106 ));
107 }
108 if matches!(previous, TargetTemplate::LocalBare) {
113 return Ok(ResumePlan::RawToWorkspace);
114 }
115 return Err(format!(
116 "this session opens {directory} on an SSH host; resume it on a bare target there"
117 ));
118 };
119 match managed_worktree_target(target) {
120 Ok(resume_target) if resume_target.same_location(&worktree.target) => {
121 Ok(ResumePlan::InPlace)
122 }
123 Ok(_) => Err(format!(
124 "this session's working tree lives on {}; resume it there",
125 managed_worktree_location(&worktree.target)
126 )),
127 Err(_) if worktree.target != ManagedWorktreeTarget::Local => Err(format!(
128 "this session works directly in {directory} on {}; resume it on a bare target there",
129 managed_worktree_location(&worktree.target)
130 )),
131 Err(_) if worktree.worktree_root == project_directory => Ok(ResumePlan::RawToWorkspace),
134 Err(_) => Err(format!(
135 "this session opens {directory}, a subdirectory of its checkout; resume it on a bare target"
136 )),
137 }
138}
139
140const BUNDLE_ON_LOCAL_BARE: &str = "this session was created from a project bundle; a local bare target only hosts raw project sessions — resume it on a container, SSH, or EC2 target";
143
144fn workspace_to_raw_compatibility(
147 session: &SessionRecord,
148 config: &Config,
149) -> Result<ResumePlan, String> {
150 let Some(bundle) = session.project_bundle(config) else {
151 return Err(BUNDLE_ON_LOCAL_BARE.to_owned());
152 };
153 let [repository] = bundle.repositories.as_slice() else {
154 return Err(format!(
155 "this session's project has {} repositories; a local bare target holds one checkout — resume it on a container, SSH, or EC2 target",
156 bundle.repositories.len()
157 ));
158 };
159 if repository.local.is_none() {
160 return Err(
161 "this session's project came from GitHub; resume it on a container, SSH, or EC2 target"
162 .to_owned(),
163 );
164 }
165 Ok(ResumePlan::WorkspaceToRaw)
166}
167
168fn managed_worktree_location(target: &ManagedWorktreeTarget) -> String {
171 match target {
172 ManagedWorktreeTarget::Local => "this machine".to_owned(),
173 ManagedWorktreeTarget::Ssh { destination, .. } => destination.clone(),
174 }
175}