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        if let Some(file) = self.remote_file().cloned() {
33            self.discover_remote_git(file);
34            return;
35        }
36        // A container document discovers its repository inside the
37        // container (0037 DC1b) — the local cwd is never consulted.
38        if let crate::editor::document::DocumentSource::Container { container, path } =
39            &self.cur().source
40        {
41            self.discover_container_git(container.clone(), path.clone());
42            return;
43        }
44        let from = self
45            .buf()
46            .path
47            .as_deref()
48            .map(std::path::PathBuf::from)
49            .unwrap_or_else(|| self.cwd.clone());
50        // one request per origin while running; a resolved (Ready)
51        // discovery is re-derivable, so explicit switches refresh it
52        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == FileTarget::Local(from.clone()))
53        {
54            return;
55        }
56        let running = match &self.git_discovery {
57            Load::Running(current) => Some(current.request),
58            _ => None,
59        };
60        if let Some(request) = running {
61            self.cancel_git_worker(request, CancelReason::Superseded);
62        }
63        let Some(ticket) = self.git_ticket(ContextKey {
64            from: FileTarget::Local(from.clone()),
65        }) else {
66            return;
67        };
68        self.git_discovery = Load::Running(ticket.clone());
69        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
70            serde_json::json!({
71                "service":"git","request":"discover","target":"local","from":from.to_string_lossy(),
72            })
73        });
74        let args = ticket.clone();
75        self.launch_git_job(
76            "git-discover",
77            "git.discover",
78            ticket,
79            &args,
80            GitJob::Context,
81            move |cancel| {
82                if cancel.is_cancelled() {
83                    return Outcome::Cancelled(CancelReason::Superseded);
84                }
85                Outcome::Success(Repo::discover(&from).map(|repo| repo.context()))
86            },
87        );
88    }
89
90    /// Remote discovery: the repository containing the current remote
91    /// file, on its endpoint. `Ok(None)` (no repository there) is an
92    /// honest answer, published like any other context.
93    fn discover_remote_git(&mut self, file: strop_workspace::RemoteFile) {
94        let endpoint = file.endpoint().clone();
95        let from_dir = if self.remote_directory().is_some() {
96            file.path().to_owned()
97        } else {
98            file.path()
99                .parent()
100                .unwrap_or(std::path::Path::new("/"))
101                .to_owned()
102        };
103        let from = FileTarget::Remote(file.into());
104        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == from) {
105            return;
106        }
107        let running = match &self.git_discovery {
108            Load::Running(current) => Some(current.request),
109            _ => None,
110        };
111        if let Some(request) = running {
112            self.cancel_git_worker(request, CancelReason::Superseded);
113        }
114        let Some(ticket) = self.git_ticket(ContextKey { from }) else {
115            return;
116        };
117        self.git_discovery = Load::Running(ticket.clone());
118        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
119            serde_json::json!({
120                "service":"git","request":"discover","target":"remote",
121                "endpoint":endpoint.to_string(),"from":from_dir.to_string_lossy(),
122            })
123        });
124        let args = ticket.clone();
125        self.launch_git_job(
126            "git-discover-remote",
127            "git.discover",
128            ticket,
129            &args,
130            GitJob::Context,
131            move |cancel| {
132                if cancel.is_cancelled() {
133                    return Outcome::Cancelled(CancelReason::Superseded);
134                }
135                match strop_git::remote::discover(&endpoint, &from_dir, &cancel) {
136                    Ok(None) => Outcome::Success(None),
137                    Ok(Some(workdir)) => {
138                        match strop_git::remote::context(&endpoint, &workdir, &cancel) {
139                            Ok(context) => Outcome::Success(Some(context)),
140                            Err(error) => Outcome::Failed {
141                                failure: strop_core::worker::Failure::new(
142                                    strop_core::worker::FailureKind::Exit,
143                                    error.to_string(),
144                                ),
145                                partial: None,
146                            },
147                        }
148                    }
149                    Err(error) => Outcome::Failed {
150                        failure: strop_core::worker::Failure::new(
151                            strop_core::worker::FailureKind::Exit,
152                            error.to_string(),
153                        ),
154                        partial: None,
155                    },
156                }
157            },
158        );
159    }
160    /// Container discovery (0037 DC1b): the repository containing the
161    /// current container document, inside that container. `Ok(None)` (no
162    /// repository there) is an honest answer, published like any other.
163    fn discover_container_git(
164        &mut self,
165        container: strop_workspace::ContainerId,
166        path: std::path::PathBuf,
167    ) {
168        let from_dir = path
169            .parent()
170            .unwrap_or(std::path::Path::new("/"))
171            .to_owned();
172        let from = FileTarget::Container {
173            container: container.clone(),
174            path: from_dir.clone(),
175        };
176        if matches!(&self.git_discovery, Load::Running(current) if current.key.from == from) {
177            return;
178        }
179        let running = match &self.git_discovery {
180            Load::Running(current) => Some(current.request),
181            _ => None,
182        };
183        if let Some(request) = running {
184            self.cancel_git_worker(request, CancelReason::Superseded);
185        }
186        let Some(ticket) = self.git_ticket(ContextKey { from }) else {
187            return;
188        };
189        self.git_discovery = Load::Running(ticket.clone());
190        strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
191            serde_json::json!({
192                "service":"git","request":"discover","target":"container",
193                "container":container.to_string(),"from":from_dir.to_string_lossy(),
194            })
195        });
196        let args = ticket.clone();
197        self.launch_git_job(
198            "git-discover-container",
199            "git.discover",
200            ticket,
201            &args,
202            GitJob::Context,
203            move |cancel| {
204                if cancel.is_cancelled() {
205                    return Outcome::Cancelled(CancelReason::Superseded);
206                }
207                let fail = |error: &dyn ToString| Outcome::Failed {
208                    failure: strop_core::worker::Failure::new(
209                        strop_core::worker::FailureKind::Exit,
210                        error.to_string(),
211                    ),
212                    partial: None,
213                };
214                match strop_git::container::discover(&container, &from_dir, &cancel) {
215                    Ok(None) => Outcome::Success(None),
216                    Ok(Some(workdir)) => {
217                        match strop_git::container::context(&container, &workdir, &cancel) {
218                            Ok(context) => Outcome::Success(Some(context)),
219                            Err(error) => fail(&error),
220                        }
221                    }
222                    Err(error) => fail(&error),
223                }
224            },
225        );
226    }
227
228    pub(crate) fn git_context(&self) -> Option<&strop_git::GitContext> {
229        self.panes
230            .get(self.active_pane)
231            .and_then(|pane| self.docs.get(pane.doc))
232            .and_then(crate::editor::Document::git_context)
233            .or(self.git.as_ref())
234    }
235}