git_worktree_manager/
session.rs1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::constants::{sanitize_branch_name, CLAUDE_SESSION_PREFIX_LENGTH};
6use crate::error::Result;
7use crate::git::normalize_branch_name;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SessionMetadata {
12 pub branch: String,
13 pub ai_tool: String,
14 pub worktree_path: String,
15 pub created_at: String,
16 pub updated_at: String,
17}
18
19pub fn get_sessions_dir() -> PathBuf {
21 let dir = dirs::home_dir()
22 .unwrap_or_else(|| PathBuf::from("."))
23 .join(".config")
24 .join("git-worktree-manager")
25 .join("sessions");
26 let _ = std::fs::create_dir_all(&dir);
27 dir
28}
29
30pub fn get_session_dir(branch_name: &str) -> PathBuf {
32 let branch = normalize_branch_name(branch_name);
33 let safe = sanitize_branch_name(branch);
34 let dir = get_sessions_dir().join(safe);
35 let _ = std::fs::create_dir_all(&dir);
36 dir
37}
38
39pub fn claude_native_session_exists(worktree_path: &Path) -> bool {
41 let path_str = worktree_path
42 .canonicalize()
43 .unwrap_or_else(|_| worktree_path.to_path_buf())
44 .to_string_lossy()
45 .to_string();
46
47 let encoded: String = path_str
49 .chars()
50 .map(|c| if c.is_alphanumeric() { c } else { '-' })
51 .collect();
52
53 let claude_projects_dir = dirs::home_dir()
54 .unwrap_or_else(|| PathBuf::from("."))
55 .join(".claude")
56 .join("projects");
57
58 if !claude_projects_dir.exists() {
59 return false;
60 }
61
62 if encoded.len() <= 255 {
64 let project_dir = claude_projects_dir.join(&encoded);
65 if project_dir.is_dir() && has_jsonl_files(&project_dir) {
66 return true;
67 }
68 }
69
70 if encoded.len() > CLAUDE_SESSION_PREFIX_LENGTH {
72 let prefix = &encoded[..CLAUDE_SESSION_PREFIX_LENGTH];
73 if let Ok(entries) = std::fs::read_dir(&claude_projects_dir) {
74 for entry in entries.flatten() {
75 let name = entry.file_name();
76 let name_str = name.to_string_lossy();
77 if entry.path().is_dir()
78 && name_str.starts_with(prefix)
79 && has_jsonl_files(&entry.path())
80 {
81 return true;
82 }
83 }
84 }
85 }
86
87 false
88}
89
90fn has_jsonl_files(dir: &Path) -> bool {
91 std::fs::read_dir(dir)
92 .map(|entries| {
93 entries.flatten().any(|e| {
94 e.path()
95 .extension()
96 .map(|ext| ext == "jsonl")
97 .unwrap_or(false)
98 })
99 })
100 .unwrap_or(false)
101}
102
103pub fn save_session_metadata(branch_name: &str, ai_tool: &str, worktree_path: &str) -> Result<()> {
105 let session_dir = get_session_dir(branch_name);
106 let metadata_file = session_dir.join("metadata.json");
107
108 let now = chrono_now_iso();
109
110 let mut metadata = SessionMetadata {
111 branch: branch_name.to_string(),
112 ai_tool: ai_tool.to_string(),
113 worktree_path: worktree_path.to_string(),
114 created_at: now.clone(),
115 updated_at: now,
116 };
117
118 if metadata_file.exists() {
120 if let Ok(content) = std::fs::read_to_string(&metadata_file) {
121 if let Ok(existing) = serde_json::from_str::<SessionMetadata>(&content) {
122 metadata.created_at = existing.created_at;
123 }
124 }
125 }
126
127 let content = serde_json::to_string_pretty(&metadata)?;
128 std::fs::write(&metadata_file, content)?;
129 Ok(())
130}
131
132pub fn load_session_metadata(branch_name: &str) -> Option<SessionMetadata> {
134 let session_dir = get_session_dir(branch_name);
135 let metadata_file = session_dir.join("metadata.json");
136
137 if !metadata_file.exists() {
138 return None;
139 }
140
141 let content = std::fs::read_to_string(&metadata_file).ok()?;
142 serde_json::from_str(&content).ok()
143}
144
145pub fn delete_session(branch_name: &str) {
147 let session_dir = get_session_dir(branch_name);
148 if session_dir.exists() {
149 let _ = std::fs::remove_dir_all(session_dir);
150 }
151}
152
153pub fn list_sessions() -> Vec<SessionMetadata> {
155 let sessions_dir = get_sessions_dir();
156 let mut sessions = Vec::new();
157
158 if let Ok(entries) = std::fs::read_dir(&sessions_dir) {
159 for entry in entries.flatten() {
160 if entry.path().is_dir() {
161 let metadata_file = entry.path().join("metadata.json");
162 if metadata_file.exists() {
163 if let Ok(content) = std::fs::read_to_string(&metadata_file) {
164 if let Ok(meta) = serde_json::from_str::<SessionMetadata>(&content) {
165 sessions.push(meta);
166 }
167 }
168 }
169 }
170 }
171 }
172
173 sessions
174}
175
176pub fn save_context(branch_name: &str, context: &str) -> Result<()> {
178 let session_dir = get_session_dir(branch_name);
179 let context_file = session_dir.join("context.txt");
180 std::fs::write(&context_file, context)?;
181 Ok(())
182}
183
184pub fn load_context(branch_name: &str) -> Option<String> {
186 let session_dir = get_session_dir(branch_name);
187 let context_file = session_dir.join("context.txt");
188 if !context_file.exists() {
189 return None;
190 }
191 std::fs::read_to_string(&context_file).ok()
192}
193
194pub fn chrono_now_iso_pub() -> String {
196 chrono_now_iso()
197}
198
199fn chrono_now_iso() -> String {
201 use std::time::SystemTime;
202 let now = SystemTime::now()
203 .duration_since(SystemTime::UNIX_EPOCH)
204 .unwrap_or_default();
205 let secs = now.as_secs();
207 let days = secs / 86400;
209 let time_secs = secs % 86400;
210 let hours = time_secs / 3600;
211 let minutes = (time_secs % 3600) / 60;
212 let seconds = time_secs % 60;
213
214 let mut year = 1970u64;
216 let mut remaining_days = days;
217 loop {
218 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
219 if remaining_days < days_in_year {
220 break;
221 }
222 remaining_days -= days_in_year;
223 year += 1;
224 }
225 let mut month = 1u64;
226 let month_days = if is_leap_year(year) {
227 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
228 } else {
229 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
230 };
231 for &md in &month_days {
232 if remaining_days < md {
233 break;
234 }
235 remaining_days -= md;
236 month += 1;
237 }
238 let day = remaining_days + 1;
239
240 format!(
241 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
242 year, month, day, hours, minutes, seconds
243 )
244}
245
246fn is_leap_year(year: u64) -> bool {
247 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
248}