1use std::path::Path;
6use std::sync::Arc;
7use tokio::sync::mpsc::UnboundedSender;
8
9use crate::config::{Config, WorkspaceConfig, WorkspaceProvider};
10use crate::git::{FetchResult, GitOperations, PullResult, ShellGit};
11use crate::operations::clone::CloneProgress;
12use crate::operations::sync::SyncProgress;
13use crate::provider::DiscoveryProgress;
14use crate::types::{OpSummary, OwnedRepo};
15use crate::workflows::status_scan::scan_workspace_status;
16use crate::workflows::sync_workspace::{
17 execute_prepared_sync, prepare_sync_workspace, SyncWorkspaceRequest,
18};
19
20use super::app::{App, Operation};
21use super::event::{AppEvent, BackendMessage};
22
23struct TuiDiscoveryProgress {
26 tx: UnboundedSender<AppEvent>,
27}
28
29impl DiscoveryProgress for TuiDiscoveryProgress {
30 fn on_orgs_discovered(&self, count: usize) {
31 let _ = self
32 .tx
33 .send(AppEvent::Backend(BackendMessage::OrgsDiscovered(count)));
34 }
35
36 fn on_org_started(&self, org_name: &str) {
37 let _ = self.tx.send(AppEvent::Backend(BackendMessage::OrgStarted(
38 org_name.to_string(),
39 )));
40 }
41
42 fn on_org_complete(&self, org_name: &str, repo_count: usize) {
43 let _ = self.tx.send(AppEvent::Backend(BackendMessage::OrgComplete(
44 org_name.to_string(),
45 repo_count,
46 )));
47 }
48
49 fn on_personal_repos_started(&self) {}
50
51 fn on_personal_repos_complete(&self, _count: usize) {}
52
53 fn on_error(&self, message: &str) {
54 let _ = self
55 .tx
56 .send(AppEvent::Backend(BackendMessage::DiscoveryError(
57 message.to_string(),
58 )));
59 }
60}
61
62struct TuiCloneProgress {
63 tx: UnboundedSender<AppEvent>,
64}
65
66impl CloneProgress for TuiCloneProgress {
67 fn on_start(&self, repo: &OwnedRepo, _index: usize, _total: usize) {
68 let _ = self.tx.send(AppEvent::Backend(BackendMessage::RepoStarted {
69 repo_name: repo.full_name().to_string(),
70 }));
71 }
72
73 fn on_complete(&self, repo: &OwnedRepo, _index: usize, _total: usize) {
74 let _ = self
75 .tx
76 .send(AppEvent::Backend(BackendMessage::RepoProgress {
77 repo_name: repo.full_name().to_string(),
78 success: true,
79 skipped: false,
80 message: "cloned".to_string(),
81 had_updates: true,
82 is_clone: true,
83 new_commits: None,
84 skip_reason: None,
85 }));
86 }
87
88 fn on_error(&self, repo: &OwnedRepo, error: &str, _index: usize, _total: usize) {
89 let _ = self
90 .tx
91 .send(AppEvent::Backend(BackendMessage::RepoProgress {
92 repo_name: repo.full_name().to_string(),
93 success: false,
94 skipped: false,
95 message: error.to_string(),
96 had_updates: false,
97 is_clone: true,
98 new_commits: None,
99 skip_reason: None,
100 }));
101 }
102
103 fn on_skip(&self, repo: &OwnedRepo, reason: &str, _index: usize, _total: usize) {
104 let _ = self
105 .tx
106 .send(AppEvent::Backend(BackendMessage::RepoProgress {
107 repo_name: repo.full_name().to_string(),
108 success: true,
109 skipped: true,
110 message: format!("skipped: {}", reason),
111 had_updates: false,
112 is_clone: true,
113 new_commits: None,
114 skip_reason: Some(reason.to_string()),
115 }));
116 }
117}
118
119struct TuiSyncProgress {
120 tx: UnboundedSender<AppEvent>,
121}
122
123impl SyncProgress for TuiSyncProgress {
124 fn on_start(&self, repo: &OwnedRepo, _path: &Path, _index: usize, _total: usize) {
125 let _ = self.tx.send(AppEvent::Backend(BackendMessage::RepoStarted {
126 repo_name: repo.full_name().to_string(),
127 }));
128 }
129
130 fn on_fetch_complete(
131 &self,
132 repo: &OwnedRepo,
133 result: &FetchResult,
134 _index: usize,
135 _total: usize,
136 ) {
137 let status = if result.updated {
138 "updated"
139 } else {
140 "up to date"
141 };
142 let _ = self
143 .tx
144 .send(AppEvent::Backend(BackendMessage::RepoProgress {
145 repo_name: repo.full_name().to_string(),
146 success: true,
147 skipped: false,
148 message: status.to_string(),
149 had_updates: result.updated,
150 is_clone: false,
151 new_commits: result.new_commits,
152 skip_reason: None,
153 }));
154 }
155
156 fn on_pull_complete(
157 &self,
158 repo: &OwnedRepo,
159 result: &PullResult,
160 _index: usize,
161 _total: usize,
162 ) {
163 let status = if result.fast_forward {
164 "fast-forward"
165 } else {
166 "pulled"
167 };
168 let _ = self
169 .tx
170 .send(AppEvent::Backend(BackendMessage::RepoProgress {
171 repo_name: repo.full_name().to_string(),
172 success: result.success,
173 skipped: false,
174 message: status.to_string(),
175 had_updates: result.success,
176 is_clone: false,
177 new_commits: None,
178 skip_reason: None,
179 }));
180 }
181
182 fn on_error(&self, repo: &OwnedRepo, error: &str, _index: usize, _total: usize) {
183 let _ = self
184 .tx
185 .send(AppEvent::Backend(BackendMessage::RepoProgress {
186 repo_name: repo.full_name().to_string(),
187 success: false,
188 skipped: false,
189 message: error.to_string(),
190 had_updates: false,
191 is_clone: false,
192 new_commits: None,
193 skip_reason: None,
194 }));
195 }
196
197 fn on_skip(&self, repo: &OwnedRepo, reason: &str, _index: usize, _total: usize) {
198 let _ = self
199 .tx
200 .send(AppEvent::Backend(BackendMessage::RepoProgress {
201 repo_name: repo.full_name().to_string(),
202 success: true,
203 skipped: true,
204 message: format!("skipped: {}", reason),
205 had_updates: false,
206 is_clone: false,
207 new_commits: None,
208 skip_reason: Some(reason.to_string()),
209 }));
210 }
211}
212
213pub fn spawn_commit_fetch(
217 repo_path: std::path::PathBuf,
218 repo_name: String,
219 tx: UnboundedSender<AppEvent>,
220) {
221 tokio::spawn(async move {
222 let commits = tokio::task::spawn_blocking(move || {
223 let git = ShellGit::new();
224 git.recent_commits(&repo_path, 30).unwrap_or_default()
225 })
226 .await
227 .unwrap_or_default();
228
229 let _ = tx.send(AppEvent::Backend(BackendMessage::RepoCommitLog {
230 repo_name,
231 commits,
232 }));
233 });
234}
235
236pub fn spawn_changelog_fetch(
238 repos: Vec<(String, std::path::PathBuf)>,
239 tx: UnboundedSender<AppEvent>,
240) {
241 for (repo_name, repo_path) in repos {
242 let tx = tx.clone();
243 tokio::spawn(async move {
244 let commits = tokio::task::spawn_blocking(move || {
245 let git = ShellGit::new();
246 git.recent_commits(&repo_path, 30).unwrap_or_default()
247 })
248 .await
249 .unwrap_or_default();
250
251 let _ = tx.send(AppEvent::Backend(BackendMessage::RepoCommitLog {
252 repo_name,
253 commits,
254 }));
255 });
256 }
257}
258
259pub fn spawn_setup_org_discovery(
261 ws_provider: WorkspaceProvider,
262 token: String,
263 tx: UnboundedSender<AppEvent>,
264) {
265 tokio::spawn(async move {
266 match crate::setup::handler::discover_org_entries(ws_provider, token).await {
267 Ok(orgs) => {
268 let _ = tx.send(AppEvent::Backend(BackendMessage::SetupOrgsDiscovered(orgs)));
269 }
270 Err(err) => {
271 let _ = tx.send(AppEvent::Backend(BackendMessage::SetupOrgsError(err)));
272 }
273 }
274 });
275}
276
277pub fn spawn_operation(operation: Operation, app: &App, tx: UnboundedSender<AppEvent>) {
279 let config = app.config.clone();
280 let workspace = app.active_workspace.clone();
281 let sync_pull = app.sync_pull;
282
283 match operation {
284 Operation::Sync => {
285 tokio::spawn(async move {
286 run_sync_operation(config, workspace, tx, sync_pull).await;
287 });
288 }
289 Operation::Status => {
290 let workspace = app.active_workspace.clone();
291 let config = app.config.clone();
292 tokio::spawn(async move {
293 run_status_scan(config, workspace, tx).await;
294 });
295 }
296 }
297}
298
299async fn run_sync_operation(
301 config: Config,
302 workspace: Option<WorkspaceConfig>,
303 tx: UnboundedSender<AppEvent>,
304 pull_mode: bool,
305) {
306 let workspace = match workspace {
307 Some(ws) => ws,
308 None => {
309 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationError(
310 "No workspace selected. Run 'gisa setup' to configure one.".to_string(),
311 )));
312 return;
313 }
314 };
315
316 let discovery_progress = TuiDiscoveryProgress { tx: tx.clone() };
317 let prepared = match prepare_sync_workspace(
318 SyncWorkspaceRequest {
319 config: &config,
320 workspace: &workspace,
321 refresh: true,
322 skip_uncommitted: true,
323 pull: pull_mode,
324 concurrency_override: None,
325 create_base_path: true,
326 },
327 &discovery_progress,
328 )
329 .await
330 {
331 Ok(p) => p,
332 Err(e) => {
333 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationError(format!(
334 "{}",
335 e
336 ))));
337 return;
338 }
339 };
340
341 let _ = tx.send(AppEvent::Backend(BackendMessage::DiscoveryComplete(
343 prepared.repos.clone(),
344 )));
345
346 if prepared.repos.is_empty() {
347 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationComplete(
348 OpSummary::new(),
349 )));
350 return;
351 }
352
353 let clone_count = prepared.plan.to_clone.len();
355 let sync_count = prepared.to_sync.len();
356 let total = clone_count + sync_count;
357 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationStarted {
358 operation: Operation::Sync,
359 total,
360 to_clone: clone_count,
361 to_sync: sync_count,
362 }));
363
364 let clone_progress: Arc<dyn CloneProgress> = Arc::new(TuiCloneProgress { tx: tx.clone() });
365 let sync_progress: Arc<dyn SyncProgress> = Arc::new(TuiSyncProgress { tx: tx.clone() });
366 let outcome = execute_prepared_sync(&prepared, false, clone_progress, sync_progress).await;
367
368 let mut combined_summary = OpSummary::new();
369 if let Some(summary) = outcome.clone_summary {
370 combined_summary.success += summary.success;
371 combined_summary.failed += summary.failed;
372 combined_summary.skipped += summary.skipped;
373 }
374 if let Some(summary) = outcome.sync_summary {
375 combined_summary.success += summary.success;
376 combined_summary.failed += summary.failed;
377 combined_summary.skipped += summary.skipped;
378 }
379
380 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationComplete(
381 combined_summary,
382 )));
383}
384
385async fn run_status_scan(
387 config: Config,
388 workspace: Option<WorkspaceConfig>,
389 tx: UnboundedSender<AppEvent>,
390) {
391 let workspace = match workspace {
392 Some(ws) => ws,
393 None => {
394 let _ = tx.send(AppEvent::Backend(BackendMessage::OperationError(
395 "No workspace selected.".to_string(),
396 )));
397 return;
398 }
399 };
400
401 let entries = tokio::task::spawn_blocking(move || scan_workspace_status(&config, &workspace))
402 .await
403 .unwrap_or_default();
404
405 let _ = tx.send(AppEvent::Backend(BackendMessage::StatusResults(entries)));
406}
407
408#[cfg(test)]
409#[path = "backend_tests.rs"]
410mod tests;