Skip to main content

strop_engine/editor/git/
context.rs

1//! Repository discovery and source-owned context; all native work is a job.
2use crate::editor::git_memory::{ContextKey, GitJob};
3use crate::editor::Editor;
4use crate::files::FileTarget;
5use strop_core::worker::{CancelReason, Load, Outcome};
6use strop_git::Repo;
7
8impl Editor {
9    /// Discover the repository for the current buffer. Native work
10    /// runs on a worker (R6); the pure cached context lands through
11    /// `GitJob::Context` and invalidates the git view only when it
12    /// actually changed. A remote buffer discovers on its endpoint —
13    /// `rev-parse --show-toplevel` from the file's directory, then the
14    /// context reads — through the bounded remote-execution boundary;
15    /// the local cwd is never consulted for it (0036 RW8).
16    pub fn discover_git(&mut self) {
17        if self.docs.is_empty() || self.finishing {
18            return;
19        }
20        if let Some(context) = self.cur().git_context().cloned() {
21            if let Load::Running(ticket) = &self.git_discovery {
22                self.cancel_git_worker(ticket.request, CancelReason::Superseded);
23            }
24            self.git_discovery = Load::Idle;
25            if self.git.as_ref() != Some(&context) {
26                self.git = Some(context);
27                self.invalidate_git_view();
28            }
29            return;
30        }
31        self.git_discovery.retry_failed();
32        let directory = self.directory().map(|source| source.location.clone());
33        if let Some(directory) = &directory {
34            match &directory.filesystem {
35                strop_workspace::Filesystem::Remote(endpoint) => {
36                    match strop_workspace::RemoteFile::from_path(
37                        endpoint.clone(),
38                        directory.path.clone(),
39                    ) {
40                        Ok(file) => self.discover_remote_git(file),
41                        Err(error) => self.message = error.to_string(),
42                    }
43                    return;
44                }
45                strop_workspace::Filesystem::Container(container) => {
46                    self.discover_container_git(container.clone(), directory.path.clone());
47                    return;
48                }
49                strop_workspace::Filesystem::Local => {}
50            }
51        }
52        if let Some(file) = self.remote_file().cloned() {
53            self.discover_remote_git(file);
54            return;
55        }
56        // A container document discovers its repository inside the
57        // container (0037 DC1b) — the local cwd is never consulted.
58        if let crate::editor::document::DocumentSource::Container { container, path } =
59            &self.cur().source
60        {
61            self.discover_container_git(container.clone(), path.clone());
62            return;
63        }
64        let from = directory
65            .map(|directory| directory.path)
66            .or_else(|| self.buf().path.clone())
67            .unwrap_or_else(|| self.cwd.clone());
68        // one request per origin while running; a resolved (Ready)
69        // discovery is re-derivable, so explicit switches refresh it
70        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == FileTarget::Local(from.clone()))
71        {
72            return;
73        }
74        let running = match &self.git_discovery {
75            Load::Running(current) => Some(current.request),
76            _ => None,
77        };
78        if let Some(request) = running {
79            self.cancel_git_worker(request, CancelReason::Superseded);
80        }
81        let Some(ticket) = self.git_ticket(ContextKey {
82            from: FileTarget::Local(from.clone()),
83        }) else {
84            return;
85        };
86        self.git_discovery = Load::Running(ticket.clone());
87        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
88            serde_json::json!({
89                "service":"git","request":"discover","target":"local","from":from.to_string_lossy(),
90            })
91        });
92        let args = ticket.clone();
93        self.launch_git_job(
94            "git-discover",
95            "git.discover",
96            ticket,
97            &args,
98            GitJob::Context,
99            move |cancel| {
100                if cancel.is_cancelled() {
101                    return Outcome::Cancelled(CancelReason::Superseded);
102                }
103                Outcome::Success(Repo::discover(&from).map(|repo| repo.context()))
104            },
105        );
106    }
107
108    /// Remote discovery: the repository containing the current remote
109    /// file, on its endpoint. `Ok(None)` (no repository there) is an
110    /// honest answer, published like any other context.
111    fn discover_remote_git(&mut self, file: strop_workspace::RemoteFile) {
112        let endpoint = file.endpoint().clone();
113        let from_dir = if self.directory().is_some() {
114            file.path().to_owned()
115        } else {
116            file.path()
117                .parent()
118                .unwrap_or(std::path::Path::new("/"))
119                .to_owned()
120        };
121        let from = FileTarget::Remote(file.into());
122        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == from) {
123            return;
124        }
125        let running = match &self.git_discovery {
126            Load::Running(current) => Some(current.request),
127            _ => None,
128        };
129        if let Some(request) = running {
130            self.cancel_git_worker(request, CancelReason::Superseded);
131        }
132        let Some(ticket) = self.git_ticket(ContextKey { from }) else {
133            return;
134        };
135        self.git_discovery = Load::Running(ticket.clone());
136        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
137            serde_json::json!({
138                "service":"git","request":"discover","target":"remote",
139                "endpoint":endpoint.to_string(),"from":from_dir.to_string_lossy(),
140            })
141        });
142        let args = ticket.clone();
143        self.launch_git_job(
144            "git-discover-remote",
145            "git.discover",
146            ticket,
147            &args,
148            GitJob::Context,
149            move |cancel| {
150                if cancel.is_cancelled() {
151                    return Outcome::Cancelled(CancelReason::Superseded);
152                }
153                match strop_git::remote::discover(&endpoint, &from_dir, &cancel) {
154                    Ok(None) => Outcome::Success(None),
155                    Ok(Some(workdir)) => {
156                        match strop_git::remote::context(&endpoint, &workdir, &cancel) {
157                            Ok(context) => Outcome::Success(Some(context)),
158                            Err(error) => Outcome::Failed {
159                                failure: strop_core::worker::Failure::new(
160                                    strop_core::worker::FailureKind::Exit,
161                                    error.to_string(),
162                                ),
163                                partial: None,
164                            },
165                        }
166                    }
167                    Err(error) => Outcome::Failed {
168                        failure: strop_core::worker::Failure::new(
169                            strop_core::worker::FailureKind::Exit,
170                            error.to_string(),
171                        ),
172                        partial: None,
173                    },
174                }
175            },
176        );
177    }
178    /// Container discovery (0037 DC1b): the repository containing the
179    /// current container document, inside that container. `Ok(None)` (no
180    /// repository there) is an honest answer, published like any other.
181    fn discover_container_git(
182        &mut self,
183        container: strop_workspace::ContainerId,
184        path: std::path::PathBuf,
185    ) {
186        let from_dir = if self.directory().is_some() {
187            path.clone()
188        } else {
189            path.parent()
190                .unwrap_or(std::path::Path::new("/"))
191                .to_owned()
192        };
193        let from = FileTarget::Container {
194            container: container.clone(),
195            path: from_dir.clone(),
196        };
197        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == from) {
198            return;
199        }
200        let running = match &self.git_discovery {
201            Load::Running(current) => Some(current.request),
202            _ => None,
203        };
204        if let Some(request) = running {
205            self.cancel_git_worker(request, CancelReason::Superseded);
206        }
207        let Some(ticket) = self.git_ticket(ContextKey { from }) else {
208            return;
209        };
210        self.git_discovery = Load::Running(ticket.clone());
211        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
212            serde_json::json!({
213                "service":"git","request":"discover","target":"container",
214                "container":container.to_string(),"from":from_dir.to_string_lossy(),
215            })
216        });
217        let args = ticket.clone();
218        self.launch_git_job(
219            "git-discover-container",
220            "git.discover",
221            ticket,
222            &args,
223            GitJob::Context,
224            move |cancel| {
225                if cancel.is_cancelled() {
226                    return Outcome::Cancelled(CancelReason::Superseded);
227                }
228                let fail = |error: &dyn ToString| Outcome::Failed {
229                    failure: strop_core::worker::Failure::new(
230                        strop_core::worker::FailureKind::Exit,
231                        error.to_string(),
232                    ),
233                    partial: None,
234                };
235                match strop_git::container::discover(&container, &from_dir, &cancel) {
236                    Ok(None) => Outcome::Success(None),
237                    Ok(Some(workdir)) => {
238                        match strop_git::container::context(&container, &workdir, &cancel) {
239                            Ok(context) => Outcome::Success(Some(context)),
240                            Err(error) => fail(&error),
241                        }
242                    }
243                    Err(error) => fail(&error),
244                }
245            },
246        );
247    }
248
249    pub(crate) fn git_context(&self) -> Option<&strop_git::GitContext> {
250        self.panes
251            .get(self.active_pane)
252            .and_then(|pane| self.docs.get(pane.doc))
253            .and_then(crate::editor::Document::git_context)
254            .or(self.git.as_ref())
255    }
256}