1use super::*;
2impl RemoteServices {
3 pub(crate) fn require_repository(&self) -> Result<(), CoreError> {
4 if !self
5 .negotiated_capabilities()
6 .iter()
7 .any(|v| v == "git.repository.v1")
8 {
9 return Err(CoreError::new(
10 ErrorCode::Unavailable,
11 "Active helper lacks repository support; preserve retained sessions and update after they end",
12 ));
13 }
14 Ok(())
15 }
16}
17impl GitService for RemoteServices {
18 fn repository(
19 &self,
20 worktree: WorktreeIdentity,
21 ) -> ServiceFuture<'_, Result<GitRepositorySnapshot, CoreError>> {
22 Box::pin(async move {
23 self.check(&worktree.target)?;
24 self.require_repository()?;
25 match self.call(
26 w::Call::Repository {
27 worktree: w::Worktree::from_core(&worktree)?,
28 },
29 None,
30 )? {
31 w::Reply::Repository { snapshot } if snapshot.worktree == worktree => Ok(*snapshot),
32 _ => Err(w::invalid()),
33 }
34 })
35 }
36 fn commit(&self, query: CommitQuery) -> ServiceFuture<'_, Result<CommitPage, CoreError>> {
37 Box::pin(async move {
38 self.check(&query.worktree.target)?;
39 self.require_repository()?;
40 match self.call(
41 w::Call::Commit {
42 worktree: w::Worktree::from_core(&query.worktree)?,
43 oid: query.oid.clone(),
44 cursor: query.cursor,
45 },
46 None,
47 )? {
48 w::Reply::Commit { page } if page.oid == query.oid => Ok(page),
49 _ => Err(w::invalid()),
50 }
51 })
52 }
53
54 fn read_query(
55 &self,
56 worktree: WorktreeIdentity,
57 query: GitRead,
58 ) -> ServiceFuture<'_, Result<GitReadOutput, CoreError>> {
59 Box::pin(async move {
60 self.check(&worktree.target)?;
61 if !self
62 .negotiated_capabilities()
63 .iter()
64 .any(|c| c == "git.read")
65 {
66 return Err(CoreError::new(
67 ErrorCode::Unavailable,
68 "Active helper lacks typed Git inspection; preserve its retained sessions and update after they end",
69 ));
70 }
71 match self.call(
72 w::Call::GitRead {
73 worktree: w::Worktree::from_core(&worktree)?,
74 query,
75 },
76 None,
77 )? {
78 w::Reply::GitRead {
79 stdout_base64,
80 stderr_base64,
81 success,
82 } => Ok(GitReadOutput {
83 stdout: B64.decode(stdout_base64).map_err(|_| w::invalid())?,
84 stderr: B64.decode(stderr_base64).map_err(|_| w::invalid())?,
85 success,
86 }),
87 _ => Err(w::invalid()),
88 }
89 })
90 }
91
92 fn existing_receipt(
93 &self,
94 r: GitMutationRequest,
95 ) -> ServiceFuture<'_, Result<Option<OperationReceipt>, CoreError>> {
96 Box::pin(async move {
97 if matches!(r.command, GitMutation::Repository(_)) {
98 self.require_repository()?;
99 }
100 if self
101 .negotiated_capabilities()
102 .iter()
103 .any(|c| c == "git.original_lookup")
104 {
105 return self.existing_original(&r);
106 }
107 self.check(&r.worktree.target)?;
108 if !self
109 .negotiated_capabilities()
110 .iter()
111 .any(|c| c == "git.existing")
112 {
113 return Err(CoreError::new(
114 ErrorCode::Unavailable,
115 "active helper cannot verify the original Git request; preserve the existing supervisor",
116 ));
117 }
118 match self.call(
119 w::Call::GitExisting {
120 worktree: w::Worktree::from_core(&r.worktree)?,
121 expected: w::GitState::from_core(&r.expected),
122 command: w::GitCommand::from_core(&r.command)?,
123 },
124 Some(r.operation),
125 )? {
126 w::Reply::GitExisting { receipt } => receipt
127 .map(|receipt| {
128 let receipt = receipt.core(&r.worktree.target)?;
129 if receipt.id != r.operation {
130 return Err(w::invalid());
131 }
132 Ok(receipt)
133 })
134 .transpose(),
135 _ => Err(w::invalid()),
136 }
137 })
138 }
139 fn discover(
140 &self,
141 file: FileRef,
142 ) -> ServiceFuture<'_, Result<Option<WorktreeIdentity>, CoreError>> {
143 Box::pin(async move {
144 self.check(&file.target)?;
145 match self.call(
146 w::Call::Discover {
147 file: w::File::from_core(&file)?,
148 },
149 None,
150 )? {
151 w::Reply::Discovered { worktree } => {
152 worktree.map(|w| w.core(&self.target)).transpose()
153 }
154 _ => Err(w::invalid()),
155 }
156 })
157 }
158 fn status(
159 &self,
160 worktree: WorktreeIdentity,
161 ) -> ServiceFuture<'_, Result<GitSnapshot, CoreError>> {
162 Box::pin(async move {
163 self.check(&worktree.target)?;
164 match self.call(
165 w::Call::Status {
166 worktree: w::Worktree::from_core(&worktree)?,
167 },
168 None,
169 )? {
170 w::Reply::Status {
171 worktree,
172 state,
173 staged,
174 unstaged,
175 untracked,
176 conflicts,
177 } => {
178 let entries = |v: Vec<w::GitEntry>| {
179 v.iter().map(w::GitEntry::core).collect::<Result<_, _>>()
180 };
181 Ok(GitSnapshot {
182 worktree: worktree.core(&self.target)?,
183 state: state.core()?,
184 staged: entries(staged)?,
185 unstaged: entries(unstaged)?,
186 untracked: entries(untracked)?,
187 conflicts: entries(conflicts)?,
188 })
189 }
190 _ => Err(w::invalid()),
191 }
192 })
193 }
194 fn diff(&self, r: DiffRequest) -> ServiceFuture<'_, Result<DiffSnapshot, CoreError>> {
195 Box::pin(async move {
196 self.check(&r.worktree.target)?;
197 if matches!(r.source, DiffSource::Commit(_)) {
198 self.require_repository()?;
199 }
200 match self.call(
201 w::Call::Diff {
202 worktree: w::Worktree::from_core(&r.worktree)?,
203 path_base64: w::path(&r.path)?,
204 source: w::DiffSource::from_core(&r.source),
205 },
206 None,
207 )? {
208 w::Reply::Diff {
209 source,
210 content,
211 unavailable,
212 change,
213 previous_path,
214 expected,
215 bytes_base64,
216 } => Ok(DiffSnapshot {
217 source: source.core(),
218 content: match content.as_str() {
219 "Text" => DiffContent::Text,
220 "Binary" => DiffContent::Binary,
221 "Unavailable" => {
222 DiffContent::Unavailable(unavailable.ok_or_else(w::invalid)?)
223 }
224 _ => return Err(w::invalid()),
225 },
226 change: match change.as_str() {
227 "Modified" => DiffChange::Modified,
228 "Added" => DiffChange::Added,
229 "Deleted" => DiffChange::Deleted,
230 "Renamed" => DiffChange::Renamed,
231 "Conflicted" => DiffChange::Conflicted,
232 _ => return Err(w::invalid()),
233 },
234 previous_path: previous_path.as_deref().map(w::target_path).transpose()?,
235 expected: expected.core()?,
236 bytes: B64.decode(bytes_base64).map_err(|_| w::invalid())?,
237 }),
238 _ => Err(w::invalid()),
239 }
240 })
241 }
242 fn history(&self, r: HistoryQuery) -> ServiceFuture<'_, Result<GraphPage, CoreError>> {
243 Box::pin(async move {
244 self.check(&r.worktree.target)?;
245 let call = if let Some(filter) = &r.filter {
246 self.require_repository()?;
247 w::Call::FilteredHistory {
248 worktree: w::Worktree::from_core(&r.worktree)?,
249 refs: r.ref_filter.iter().map(|v| B64.encode(v)).collect(),
250 cursor: r.cursor.as_ref().map(|v| B64.encode(v)),
251 filter: filter.clone(),
252 }
253 } else {
254 w::Call::History {
255 worktree: w::Worktree::from_core(&r.worktree)?,
256 refs: r.ref_filter.iter().map(|v| B64.encode(v)).collect(),
257 cursor: r.cursor.as_ref().map(|v| B64.encode(v)),
258 }
259 };
260 match self.call(call, None)? {
261 w::Reply::History {
262 commits,
263 continuation_edges,
264 next_cursor,
265 } => Ok(GraphPage {
266 commits: commits
267 .iter()
268 .map(w::Commit::core)
269 .collect::<Result<_, _>>()?,
270 continuation_edges,
271 next_cursor: next_cursor
272 .map(|c| B64.decode(c).map_err(|_| w::invalid()))
273 .transpose()?,
274 }),
275 _ => Err(w::invalid()),
276 }
277 })
278 }
279 fn mutate(&self, r: GitMutationRequest) -> ServiceFuture<'_, OperationReceipt> {
280 Box::pin(async move {
281 if matches!(r.command, GitMutation::Repository(_))
282 && let Err(error) = self.require_repository()
283 {
284 return OperationReceipt {
285 id: r.operation,
286 state: OperationState::Failed(OperationFailure {
287 error,
288 target: r.worktree.target.clone(),
289 observed: ObservedState::Unobserved,
290 effect: FailureEffect::ProvenNoEffect,
291 }),
292 };
293 }
294 let id = r.operation;
295 let target = r.worktree.target.clone();
296 let result = (|| -> Result<OperationReceipt, CoreError> {
297 self.check(&target)?;
298 Ok(self.mutation_call(
299 w::Call::GitMutate {
300 worktree: w::Worktree::from_core(&r.worktree)?,
301 expected: w::GitState::from_core(&r.expected),
302 command: w::GitCommand::from_core(&r.command)?,
303 },
304 id,
305 &target,
306 ))
307 })();
308 result.unwrap_or_else(|e| buntrail_services::files::failure(id, target, e))
309 })
310 }
311}