1use std::sync::mpsc::{self, Sender};
2use std::thread::JoinHandle;
3
4use termesh_core::{GitEvent, GitRequest};
5
6use crate::GitService;
7
8enum WorkerMessage {
9 Request(GitRequest),
10 Shutdown,
11}
12
13pub struct GitWorker {
15 tx: Sender<WorkerMessage>,
16 handle: Option<JoinHandle<()>>,
17}
18
19impl GitWorker {
20 pub fn spawn<S, F>(mut service: S, sink: F) -> Self
21 where
22 S: GitService,
23 F: Fn(GitEvent) + Send + 'static,
24 {
25 let (tx, rx) = mpsc::channel();
26 let handle = std::thread::Builder::new()
27 .name("termesh-git".into())
28 .spawn(move || {
29 while let Ok(message) = rx.recv() {
30 let WorkerMessage::Request(request) = message else {
31 break;
32 };
33 run_request(&mut service, request, &sink);
34 }
35 })
36 .expect("spawning the Git worker thread");
37 Self { tx, handle: Some(handle) }
38 }
39
40 pub fn request(&self, request: GitRequest) -> bool {
42 self.tx.send(WorkerMessage::Request(request)).is_ok()
43 }
44}
45
46impl Drop for GitWorker {
47 fn drop(&mut self) {
48 let _ = self.tx.send(WorkerMessage::Shutdown);
49 if let Some(handle) = self.handle.take() {
50 let _ = handle.join();
51 }
52 }
53}
54
55fn run_request<S, F>(service: &mut S, request: GitRequest, sink: &F)
56where
57 S: GitService,
58 F: Fn(GitEvent),
59{
60 let id = match &request {
61 GitRequest::Refresh { id, .. }
62 | GitRequest::Diff { id, .. }
63 | GitRequest::Branches { id, .. }
64 | GitRequest::Execute { id, .. } => *id,
65 };
66 sink(GitEvent::Started { id });
67 match request {
68 GitRequest::Refresh { root, .. } => match service.snapshot(&root) {
69 Ok(snapshot) => sink(GitEvent::SnapshotLoaded { id, snapshot }),
70 Err(failure) => sink(GitEvent::Failed { id, operation_applied: false, failure }),
71 },
72 GitRequest::Diff { root, path, target, .. } => match service.diff(&root, &path, target) {
73 Ok(diff) => sink(GitEvent::DiffLoaded { id, diff }),
74 Err(failure) => sink(GitEvent::Failed { id, operation_applied: false, failure }),
75 },
76 GitRequest::Branches { root, .. } => match service.branches(&root) {
77 Ok(branches) => sink(GitEvent::BranchesLoaded { id, branches }),
78 Err(failure) => sink(GitEvent::Failed { id, operation_applied: false, failure }),
79 },
80 GitRequest::Execute { root, operation, .. } => match service.execute(&root, &operation) {
81 Ok(message) => match service.snapshot(&root) {
82 Ok(snapshot) => {
83 sink(GitEvent::OperationFinished { id, operation, message, snapshot });
84 }
85 Err(failure) => sink(GitEvent::Failed { id, operation_applied: true, failure }),
86 },
87 Err(failure) => sink(GitEvent::Failed { id, operation_applied: false, failure }),
88 },
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use std::path::Path;
95 use std::sync::{Arc, Mutex};
96
97 use termesh_core::{
98 GitBranch, GitBranchStatus, GitContextDiff, GitDiffTarget, GitEvent, GitFailure,
99 GitFailureKind, GitFileDiff, GitOperation, GitRepositorySnapshot, GitRequest, GitRequestId,
100 GitResult,
101 };
102
103 use crate::{GitService, GitWorker};
104
105 struct RecordingGitService {
106 execute: GitResult<String>,
107 snapshot: GitResult<GitRepositorySnapshot>,
108 calls: Arc<Mutex<Vec<&'static str>>>,
109 }
110
111 impl RecordingGitService {
112 fn scripted(
113 execute: GitResult<String>,
114 snapshot: GitResult<GitRepositorySnapshot>,
115 ) -> (Self, Arc<Mutex<Vec<&'static str>>>) {
116 let calls = Arc::new(Mutex::new(Vec::new()));
117 (Self { execute, snapshot, calls: calls.clone() }, calls)
118 }
119 }
120
121 impl GitService for RecordingGitService {
122 fn snapshot(&mut self, _root: &Path) -> GitResult<GitRepositorySnapshot> {
123 self.calls.lock().unwrap().push("snapshot");
124 self.snapshot.clone()
125 }
126
127 fn diff(
128 &mut self,
129 _root: &Path,
130 _path: &Path,
131 _target: GitDiffTarget,
132 ) -> GitResult<GitFileDiff> {
133 unreachable!()
134 }
135
136 fn branches(&mut self, _root: &Path) -> GitResult<Vec<GitBranch>> {
137 unreachable!()
138 }
139
140 fn execute(&mut self, _root: &Path, _operation: &GitOperation) -> GitResult<String> {
141 self.calls.lock().unwrap().push("execute");
142 self.execute.clone()
143 }
144 }
145
146 fn snapshot() -> GitRepositorySnapshot {
147 GitRepositorySnapshot {
148 repository_root: "/repo".into(),
149 workspace_root: "/repo".into(),
150 branch: GitBranchStatus::default(),
151 files: Vec::new(),
152 context_diff: GitContextDiff::default(),
153 }
154 }
155
156 fn failure(message: &str) -> GitFailure {
157 GitFailure { kind: GitFailureKind::Command, message: message.into() }
158 }
159
160 fn execute_request() -> GitRequest {
161 GitRequest::Execute {
162 id: GitRequestId::new(7),
163 root: "/repo".into(),
164 operation: GitOperation::Commit { message: "message".into() },
165 }
166 }
167
168 #[test]
169 fn execute_refreshes_before_reporting_completion() {
170 let (service, calls) =
171 RecordingGitService::scripted(Ok("committed".into()), Ok(snapshot()));
172 let (tx, rx) = std::sync::mpsc::channel();
173 let worker = GitWorker::spawn(service, move |event| tx.send(event).unwrap());
174 assert!(worker.request(execute_request()));
175 assert!(matches!(rx.recv().unwrap(), GitEvent::Started { .. }));
176 assert!(matches!(rx.recv().unwrap(), GitEvent::OperationFinished { .. }));
177 assert_eq!(*calls.lock().unwrap(), vec!["execute", "snapshot"]);
178 }
179
180 #[test]
181 fn successful_operation_with_failed_refresh_reports_applied_but_stale() {
182 let (service, calls) =
183 RecordingGitService::scripted(Ok("committed".into()), Err(failure("refresh failed")));
184 let (tx, rx) = std::sync::mpsc::channel();
185 let worker = GitWorker::spawn(service, move |event| tx.send(event).unwrap());
186 assert!(worker.request(execute_request()));
187 assert!(matches!(rx.recv().unwrap(), GitEvent::Started { .. }));
188 assert!(matches!(rx.recv().unwrap(), GitEvent::Failed { operation_applied: true, .. }));
189 assert_eq!(*calls.lock().unwrap(), vec!["execute", "snapshot"]);
190 }
191
192 #[test]
193 fn failed_operation_does_not_refresh_and_reports_not_applied() {
194 let (service, calls) =
195 RecordingGitService::scripted(Err(failure("commit failed")), Ok(snapshot()));
196 let (tx, rx) = std::sync::mpsc::channel();
197 let worker = GitWorker::spawn(service, move |event| tx.send(event).unwrap());
198 assert!(worker.request(execute_request()));
199 assert!(matches!(rx.recv().unwrap(), GitEvent::Started { .. }));
200 assert!(matches!(rx.recv().unwrap(), GitEvent::Failed { operation_applied: false, .. }));
201 assert_eq!(*calls.lock().unwrap(), vec!["execute"]);
202 }
203}