1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
2use crate::{Project, ProjectPath};
3use anyhow::{anyhow, Context as _};
4use client::ProjectId;
5use futures::channel::{mpsc, oneshot};
6use futures::StreamExt as _;
7use git::{
8 repository::{GitRepository, RepoPath},
9 status::{GitSummary, TrackedSummary},
10};
11use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
12use rpc::{proto, AnyProtoClient};
13use settings::WorktreeId;
14use std::sync::Arc;
15use util::{maybe, ResultExt};
16use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
17
18pub struct GitState {
19 project_id: Option<ProjectId>,
20 client: Option<AnyProtoClient>,
21 repositories: Vec<RepositoryHandle>,
22 active_index: Option<usize>,
23 update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
24 _subscription: Subscription,
25}
26
27#[derive(Clone)]
28pub struct RepositoryHandle {
29 git_state: WeakEntity<GitState>,
30 pub worktree_id: WorktreeId,
31 pub repository_entry: RepositoryEntry,
32 pub git_repo: GitRepo,
33 update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
34}
35
36#[derive(Clone)]
37pub enum GitRepo {
38 Local(Arc<dyn GitRepository>),
39 Remote {
40 project_id: ProjectId,
41 client: AnyProtoClient,
42 worktree_id: WorktreeId,
43 work_directory_id: ProjectEntryId,
44 },
45}
46
47impl PartialEq<Self> for RepositoryHandle {
48 fn eq(&self, other: &Self) -> bool {
49 self.worktree_id == other.worktree_id
50 && self.repository_entry.work_directory_id()
51 == other.repository_entry.work_directory_id()
52 }
53}
54
55impl Eq for RepositoryHandle {}
56
57impl PartialEq<RepositoryEntry> for RepositoryHandle {
58 fn eq(&self, other: &RepositoryEntry) -> bool {
59 self.repository_entry.work_directory_id() == other.work_directory_id()
60 }
61}
62
63enum Message {
64 Commit {
65 git_repo: GitRepo,
66 name_and_email: Option<(SharedString, SharedString)>,
67 },
68 Stage(GitRepo, Vec<RepoPath>),
69 Unstage(GitRepo, Vec<RepoPath>),
70}
71
72pub enum GitEvent {
73 ActiveRepositoryChanged,
74 FileSystemUpdated,
75 GitStateUpdated,
76}
77
78impl EventEmitter<GitEvent> for GitState {}
79
80impl GitState {
81 pub fn new(
82 worktree_store: &Entity<WorktreeStore>,
83 client: Option<AnyProtoClient>,
84 project_id: Option<ProjectId>,
85 cx: &mut Context<'_, Self>,
86 ) -> Self {
87 let update_sender = Self::spawn_git_worker(cx);
88 let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
89
90 GitState {
91 project_id,
92 client,
93 repositories: Vec::new(),
94 active_index: None,
95 update_sender,
96 _subscription,
97 }
98 }
99
100 pub fn active_repository(&self) -> Option<RepositoryHandle> {
101 self.active_index
102 .map(|index| self.repositories[index].clone())
103 }
104
105 fn on_worktree_store_event(
106 &mut self,
107 worktree_store: Entity<WorktreeStore>,
108 event: &WorktreeStoreEvent,
109 cx: &mut Context<'_, Self>,
110 ) {
111 // TODO inspect the event
112
113 let mut new_repositories = Vec::new();
114 let mut new_active_index = None;
115 let this = cx.weak_entity();
116 let client = self.client.clone();
117 let project_id = self.project_id;
118
119 worktree_store.update(cx, |worktree_store, cx| {
120 for worktree in worktree_store.worktrees() {
121 worktree.update(cx, |worktree, _| {
122 let snapshot = worktree.snapshot();
123 for repo in snapshot.repositories().iter() {
124 let git_repo = worktree
125 .as_local()
126 .and_then(|local_worktree| local_worktree.get_local_repo(repo))
127 .map(|local_repo| local_repo.repo().clone())
128 .map(GitRepo::Local)
129 .or_else(|| {
130 let client = client.clone()?;
131 let project_id = project_id?;
132 Some(GitRepo::Remote {
133 project_id,
134 client,
135 worktree_id: worktree.id(),
136 work_directory_id: repo.work_directory_id(),
137 })
138 });
139 let Some(git_repo) = git_repo else {
140 continue;
141 };
142 let existing = self
143 .repositories
144 .iter()
145 .enumerate()
146 .find(|(_, existing_handle)| existing_handle == &repo);
147 let handle = if let Some((index, handle)) = existing {
148 if self.active_index == Some(index) {
149 new_active_index = Some(new_repositories.len());
150 }
151 // Update the statuses but keep everything else.
152 let mut existing_handle = handle.clone();
153 existing_handle.repository_entry = repo.clone();
154 existing_handle
155 } else {
156 RepositoryHandle {
157 git_state: this.clone(),
158 worktree_id: worktree.id(),
159 repository_entry: repo.clone(),
160 git_repo,
161 update_sender: self.update_sender.clone(),
162 }
163 };
164 new_repositories.push(handle);
165 }
166 })
167 }
168 });
169
170 if new_active_index == None && new_repositories.len() > 0 {
171 new_active_index = Some(0);
172 }
173
174 self.repositories = new_repositories;
175 self.active_index = new_active_index;
176
177 match event {
178 WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
179 cx.emit(GitEvent::GitStateUpdated);
180 }
181 _ => {
182 cx.emit(GitEvent::FileSystemUpdated);
183 }
184 }
185 }
186
187 pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
188 self.repositories.clone()
189 }
190
191 fn spawn_git_worker(
192 cx: &mut Context<'_, GitState>,
193 ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)> {
194 let (update_sender, mut update_receiver) =
195 mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
196 cx.spawn(|_, cx| async move {
197 while let Some((msg, respond)) = update_receiver.next().await {
198 let result = cx
199 .background_executor()
200 .spawn(Self::process_git_msg(msg))
201 .await;
202 respond.send(result).ok();
203 }
204 })
205 .detach();
206 update_sender
207 }
208
209 async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> {
210 match msg {
211 Message::Stage(repo, paths) => {
212 match repo {
213 GitRepo::Local(repo) => repo.stage_paths(&paths)?,
214 GitRepo::Remote {
215 project_id,
216 client,
217 worktree_id,
218 work_directory_id,
219 } => {
220 client
221 .request(proto::Stage {
222 project_id: project_id.0,
223 worktree_id: worktree_id.to_proto(),
224 work_directory_id: work_directory_id.to_proto(),
225 paths: paths
226 .into_iter()
227 .map(|repo_path| repo_path.to_proto())
228 .collect(),
229 })
230 .await
231 .context("sending stage request")?;
232 }
233 }
234 Ok(())
235 }
236 Message::Unstage(repo, paths) => {
237 match repo {
238 GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
239 GitRepo::Remote {
240 project_id,
241 client,
242 worktree_id,
243 work_directory_id,
244 } => {
245 client
246 .request(proto::Unstage {
247 project_id: project_id.0,
248 worktree_id: worktree_id.to_proto(),
249 work_directory_id: work_directory_id.to_proto(),
250 paths: paths
251 .into_iter()
252 .map(|repo_path| repo_path.to_proto())
253 .collect(),
254 })
255 .await
256 .context("sending unstage request")?;
257 }
258 }
259 Ok(())
260 }
261 Message::Commit {
262 git_repo,
263 name_and_email,
264 } => {
265 match git_repo {
266 GitRepo::Local(repo) => repo.commit(
267 name_and_email
268 .as_ref()
269 .map(|(name, email)| (name.as_ref(), email.as_ref())),
270 )?,
271 GitRepo::Remote {
272 project_id,
273 client,
274 worktree_id,
275 work_directory_id,
276 } => {
277 let (name, email) = name_and_email.unzip();
278 client
279 .request(proto::Commit {
280 project_id: project_id.0,
281 worktree_id: worktree_id.to_proto(),
282 work_directory_id: work_directory_id.to_proto(),
283 name: name.map(String::from),
284 email: email.map(String::from),
285 })
286 .await
287 .context("sending commit request")?;
288 }
289 }
290 Ok(())
291 }
292 }
293 }
294}
295
296impl RepositoryHandle {
297 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
298 maybe!({
299 let path = self.repo_path_to_project_path(&"".into())?;
300 Some(
301 project
302 .absolute_path(&path, cx)?
303 .file_name()?
304 .to_string_lossy()
305 .to_string()
306 .into(),
307 )
308 })
309 .unwrap_or("".into())
310 }
311
312 pub fn activate(&self, cx: &mut App) {
313 let Some(git_state) = self.git_state.upgrade() else {
314 return;
315 };
316 git_state.update(cx, |git_state, cx| {
317 let Some((index, _)) = git_state
318 .repositories
319 .iter()
320 .enumerate()
321 .find(|(_, handle)| handle == &self)
322 else {
323 return;
324 };
325 git_state.active_index = Some(index);
326 cx.emit(GitEvent::ActiveRepositoryChanged);
327 });
328 }
329
330 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
331 self.repository_entry.status()
332 }
333
334 pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
335 let path = self.repository_entry.unrelativize(path)?;
336 Some((self.worktree_id, path).into())
337 }
338
339 pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
340 if path.worktree_id != self.worktree_id {
341 return None;
342 }
343 self.repository_entry.relativize(&path.path).log_err()
344 }
345
346 pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
347 if entries.is_empty() {
348 return Ok(());
349 }
350 let (result_tx, result_rx) = futures::channel::oneshot::channel();
351 self.update_sender
352 .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
353 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
354
355 result_rx.await?
356 }
357
358 pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
359 if entries.is_empty() {
360 return Ok(());
361 }
362 let (result_tx, result_rx) = futures::channel::oneshot::channel();
363 self.update_sender
364 .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
365 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
366 result_rx.await?
367 }
368
369 pub async fn stage_all(&self) -> anyhow::Result<()> {
370 let to_stage = self
371 .repository_entry
372 .status()
373 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
374 .map(|entry| entry.repo_path.clone())
375 .collect();
376 self.stage_entries(to_stage).await
377 }
378
379 pub async fn unstage_all(&self) -> anyhow::Result<()> {
380 let to_unstage = self
381 .repository_entry
382 .status()
383 .filter(|entry| entry.status.is_staged().unwrap_or(true))
384 .map(|entry| entry.repo_path.clone())
385 .collect();
386 self.unstage_entries(to_unstage).await
387 }
388
389 /// Get a count of all entries in the active repository, including
390 /// untracked files.
391 pub fn entry_count(&self) -> usize {
392 self.repository_entry.status_len()
393 }
394
395 fn have_changes(&self) -> bool {
396 self.repository_entry.status_summary() != GitSummary::UNCHANGED
397 }
398
399 fn have_staged_changes(&self) -> bool {
400 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
401 }
402
403 pub fn can_commit(&self, commit_all: bool) -> bool {
404 return self.have_changes() && (commit_all || self.have_staged_changes());
405 }
406
407 pub async fn commit(
408 &self,
409 name_and_email: Option<(SharedString, SharedString)>,
410 ) -> anyhow::Result<()> {
411 let (result_tx, result_rx) = futures::channel::oneshot::channel();
412 self.update_sender.unbounded_send((
413 Message::Commit {
414 git_repo: self.git_repo.clone(),
415 name_and_email,
416 },
417 result_tx,
418 ))?;
419 result_rx.await?
420 }
421}