git_iris/companion/
mod.rs1mod branch_memory;
7mod session;
8mod storage;
9mod watcher;
10
11pub use branch_memory::{BranchMemory, FileFocus};
12pub use session::{FileActivity, SessionState};
13pub use storage::CompanionStorage;
14pub use watcher::{CompanionEvent, FileWatcherService};
15
16use anyhow::Result;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use tokio::sync::mpsc;
20
21pub struct CompanionService {
23 repo_path: PathBuf,
25 session: Arc<parking_lot::RwLock<SessionState>>,
27 storage: CompanionStorage,
29 watcher: Option<FileWatcherService>,
31 event_rx: mpsc::UnboundedReceiver<CompanionEvent>,
33 _event_tx: mpsc::UnboundedSender<CompanionEvent>,
35}
36
37impl CompanionService {
38 pub fn new(repo_path: PathBuf, branch: &str) -> Result<Self> {
44 let (event_tx, event_rx) = mpsc::unbounded_channel();
45 let storage = CompanionStorage::new(&repo_path)?;
46 let session = Arc::new(parking_lot::RwLock::new(load_session(
47 &storage, &repo_path, branch,
48 )));
49 let watcher = start_file_watcher(&repo_path, event_tx.clone());
50
51 Ok(Self {
52 repo_path,
53 session,
54 storage,
55 watcher,
56 event_rx,
57 _event_tx: event_tx,
58 })
59 }
60
61 #[must_use]
63 pub fn session(&self) -> &Arc<parking_lot::RwLock<SessionState>> {
64 &self.session
65 }
66
67 pub fn load_branch_memory(&self, branch: &str) -> Result<Option<BranchMemory>> {
73 self.storage.load_branch_memory(branch)
74 }
75
76 pub fn save_branch_memory(&self, memory: &BranchMemory) -> Result<()> {
82 self.storage.save_branch_memory(memory)
83 }
84
85 pub fn save_session(&self) -> Result<()> {
91 let session = self.session.read();
92 self.storage.save_session(&session)
93 }
94
95 pub fn touch_file(&self, path: PathBuf) {
97 let mut session = self.session.write();
98 session.touch_file(path);
99 }
100
101 pub fn record_commit(&self, hash: String) {
103 let mut session = self.session.write();
104 session.record_commit(hash);
105 }
106
107 pub fn try_recv_event(&mut self) -> Option<CompanionEvent> {
109 self.event_rx.try_recv().ok()
110 }
111
112 #[must_use]
114 pub fn has_watcher(&self) -> bool {
115 self.watcher.is_some()
116 }
117
118 #[must_use]
120 pub fn repo_path(&self) -> &PathBuf {
121 &self.repo_path
122 }
123}
124
125fn load_session(storage: &CompanionStorage, repo_path: &Path, branch: &str) -> SessionState {
126 match storage.load_session() {
127 Ok(Some(mut session)) if session.branch == branch => {
128 repo_path.clone_into(&mut session.repo_path);
129 session
130 }
131 Ok(Some(session)) => {
132 tracing::info!(
133 "Ignoring session data for branch {} while starting on {}",
134 session.branch,
135 branch
136 );
137 SessionState::new(repo_path.to_path_buf(), branch.to_owned())
138 }
139 Ok(None) => SessionState::new(repo_path.to_path_buf(), branch.to_owned()),
140 Err(e) => {
141 tracing::warn!("Failed to load companion session; starting fresh: {}", e);
142 SessionState::new(repo_path.to_path_buf(), branch.to_owned())
143 }
144 }
145}
146
147fn start_file_watcher(
148 repo_path: &Path,
149 event_tx: mpsc::UnboundedSender<CompanionEvent>,
150) -> Option<FileWatcherService> {
151 match FileWatcherService::new(repo_path, event_tx) {
152 Ok(watcher) => {
153 tracing::info!("Companion file watcher started");
154 Some(watcher)
155 }
156 Err(e) => {
157 tracing::warn!(
158 "Failed to start file watcher: {}. Companion will run without live updates.",
159 e
160 );
161 None
162 }
163 }
164}
165
166impl Drop for CompanionService {
167 fn drop(&mut self) {
168 if let Err(e) = self.save_session() {
170 tracing::warn!("Failed to save session on shutdown: {}", e);
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests;