Skip to main content

buntrail_engine/remote/
git.rs

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}