1use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
2use crate::{Project, ProjectPath};
3use anyhow::{anyhow, Context as _};
4use client::ProjectId;
5use futures::channel::mpsc;
6use futures::{SinkExt as _, StreamExt as _};
7use git::{
8 repository::{GitRepository, RepoPath},
9 status::{GitSummary, TrackedSummary},
10};
11use gpui::{
12 App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
13};
14use language::{Buffer, LanguageRegistry};
15use rpc::{proto, AnyProtoClient};
16use settings::WorktreeId;
17use std::sync::Arc;
18use text::Rope;
19use util::maybe;
20use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
21
22pub struct GitState {
23 project_id: Option<ProjectId>,
24 client: Option<AnyProtoClient>,
25 repositories: Vec<RepositoryHandle>,
26 active_index: Option<usize>,
27 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
28 languages: Arc<LanguageRegistry>,
29 _subscription: Subscription,
30}
31
32#[derive(Clone)]
33pub struct RepositoryHandle {
34 git_state: WeakEntity<GitState>,
35 pub worktree_id: WorktreeId,
36 pub repository_entry: RepositoryEntry,
37 git_repo: Option<GitRepo>,
38 commit_message: Entity<Buffer>,
39 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
40}
41
42#[derive(Clone)]
43enum GitRepo {
44 Local(Arc<dyn GitRepository>),
45 Remote {
46 project_id: ProjectId,
47 client: AnyProtoClient,
48 worktree_id: WorktreeId,
49 work_directory_id: ProjectEntryId,
50 },
51}
52
53impl PartialEq<Self> for RepositoryHandle {
54 fn eq(&self, other: &Self) -> bool {
55 self.worktree_id == other.worktree_id
56 && self.repository_entry.work_directory_id()
57 == other.repository_entry.work_directory_id()
58 }
59}
60
61impl Eq for RepositoryHandle {}
62
63impl PartialEq<RepositoryEntry> for RepositoryHandle {
64 fn eq(&self, other: &RepositoryEntry) -> bool {
65 self.repository_entry.work_directory_id() == other.work_directory_id()
66 }
67}
68
69enum Message {
70 StageAndCommit(GitRepo, Rope, Vec<RepoPath>),
71 Commit(GitRepo, Rope),
72 Stage(GitRepo, Vec<RepoPath>),
73 Unstage(GitRepo, Vec<RepoPath>),
74}
75
76pub enum Event {
77 RepositoriesUpdated,
78}
79
80impl EventEmitter<Event> for GitState {}
81
82impl GitState {
83 pub fn new(
84 worktree_store: &Entity<WorktreeStore>,
85 languages: Arc<LanguageRegistry>,
86 client: Option<AnyProtoClient>,
87 project_id: Option<ProjectId>,
88 cx: &mut Context<'_, Self>,
89 ) -> Self {
90 let (update_sender, mut update_receiver) =
91 mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
92 cx.spawn(|_, cx| async move {
93 while let Some((msg, mut err_sender)) = update_receiver.next().await {
94 let result = cx
95 .background_executor()
96 .spawn(async move {
97 match msg {
98 Message::StageAndCommit(repo, message, paths) => {
99 match repo {
100 GitRepo::Local(repo) => {
101 repo.stage_paths(&paths)?;
102 repo.commit(&message.to_string())?;
103 }
104 GitRepo::Remote {
105 project_id,
106 client,
107 worktree_id,
108 work_directory_id,
109 } => {
110 client
111 .request(proto::Stage {
112 project_id: project_id.0,
113 worktree_id: worktree_id.to_proto(),
114 work_directory_id: work_directory_id.to_proto(),
115 paths: paths
116 .into_iter()
117 .map(|repo_path| repo_path.to_proto())
118 .collect(),
119 })
120 .await
121 .context("sending stage request")?;
122 client
123 .request(proto::Commit {
124 project_id: project_id.0,
125 worktree_id: worktree_id.to_proto(),
126 work_directory_id: work_directory_id.to_proto(),
127 message: message.to_string(),
128 })
129 .await
130 .context("sending commit request")?;
131 }
132 }
133
134 Ok(())
135 }
136 Message::Stage(repo, paths) => {
137 match repo {
138 GitRepo::Local(repo) => repo.stage_paths(&paths)?,
139 GitRepo::Remote {
140 project_id,
141 client,
142 worktree_id,
143 work_directory_id,
144 } => {
145 client
146 .request(proto::Stage {
147 project_id: project_id.0,
148 worktree_id: worktree_id.to_proto(),
149 work_directory_id: work_directory_id.to_proto(),
150 paths: paths
151 .into_iter()
152 .map(|repo_path| repo_path.to_proto())
153 .collect(),
154 })
155 .await
156 .context("sending stage request")?;
157 }
158 }
159 Ok(())
160 }
161 Message::Unstage(repo, paths) => {
162 match repo {
163 GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
164 GitRepo::Remote {
165 project_id,
166 client,
167 worktree_id,
168 work_directory_id,
169 } => {
170 client
171 .request(proto::Unstage {
172 project_id: project_id.0,
173 worktree_id: worktree_id.to_proto(),
174 work_directory_id: work_directory_id.to_proto(),
175 paths: paths
176 .into_iter()
177 .map(|repo_path| repo_path.to_proto())
178 .collect(),
179 })
180 .await
181 .context("sending unstage request")?;
182 }
183 }
184 Ok(())
185 }
186 Message::Commit(repo, message) => {
187 match repo {
188 GitRepo::Local(repo) => repo.commit(&message.to_string())?,
189 GitRepo::Remote {
190 project_id,
191 client,
192 worktree_id,
193 work_directory_id,
194 } => {
195 client
196 .request(proto::Commit {
197 project_id: project_id.0,
198 worktree_id: worktree_id.to_proto(),
199 work_directory_id: work_directory_id.to_proto(),
200 // TODO implement collaborative commit message buffer instead and use it
201 // If it works, remove `commit_with_message` method.
202 message: message.to_string(),
203 })
204 .await
205 .context("sending commit request")?;
206 }
207 }
208 Ok(())
209 }
210 }
211 })
212 .await;
213 if let Err(e) = result {
214 err_sender.send(e).await.ok();
215 }
216 }
217 })
218 .detach();
219
220 let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
221
222 GitState {
223 project_id,
224 languages,
225 client,
226 repositories: Vec::new(),
227 active_index: None,
228 update_sender,
229 _subscription,
230 }
231 }
232
233 pub fn active_repository(&self) -> Option<RepositoryHandle> {
234 self.active_index
235 .map(|index| self.repositories[index].clone())
236 }
237
238 fn on_worktree_store_event(
239 &mut self,
240 worktree_store: Entity<WorktreeStore>,
241 _event: &WorktreeStoreEvent,
242 cx: &mut Context<'_, Self>,
243 ) {
244 // TODO inspect the event
245
246 let mut new_repositories = Vec::new();
247 let mut new_active_index = None;
248 let this = cx.weak_entity();
249 let client = self.client.clone();
250 let project_id = self.project_id;
251
252 worktree_store.update(cx, |worktree_store, cx| {
253 for worktree in worktree_store.worktrees() {
254 worktree.update(cx, |worktree, cx| {
255 let snapshot = worktree.snapshot();
256 for repo in snapshot.repositories().iter() {
257 let git_repo = worktree
258 .as_local()
259 .and_then(|local_worktree| local_worktree.get_local_repo(repo))
260 .map(|local_repo| local_repo.repo().clone())
261 .map(GitRepo::Local)
262 .or_else(|| {
263 let client = client.clone()?;
264 let project_id = project_id?;
265 Some(GitRepo::Remote {
266 project_id,
267 client,
268 worktree_id: worktree.id(),
269 work_directory_id: repo.work_directory_id(),
270 })
271 });
272 let existing = self
273 .repositories
274 .iter()
275 .enumerate()
276 .find(|(_, existing_handle)| existing_handle == &repo);
277 let handle = if let Some((index, handle)) = existing {
278 if self.active_index == Some(index) {
279 new_active_index = Some(new_repositories.len());
280 }
281 // Update the statuses but keep everything else.
282 let mut existing_handle = handle.clone();
283 existing_handle.repository_entry = repo.clone();
284 existing_handle
285 } else {
286 let commit_message = cx.new(|cx| Buffer::local("", cx));
287 cx.spawn({
288 let commit_message = commit_message.downgrade();
289 let languages = self.languages.clone();
290 |_, mut cx| async move {
291 let markdown = languages.language_for_name("Markdown").await?;
292 commit_message.update(&mut cx, |commit_message, cx| {
293 commit_message.set_language(Some(markdown), cx);
294 })?;
295 anyhow::Ok(())
296 }
297 })
298 .detach_and_log_err(cx);
299 RepositoryHandle {
300 git_state: this.clone(),
301 worktree_id: worktree.id(),
302 repository_entry: repo.clone(),
303 git_repo,
304 commit_message,
305 update_sender: self.update_sender.clone(),
306 }
307 };
308 new_repositories.push(handle);
309 }
310 })
311 }
312 });
313
314 if new_active_index == None && new_repositories.len() > 0 {
315 new_active_index = Some(0);
316 }
317
318 self.repositories = new_repositories;
319 self.active_index = new_active_index;
320
321 cx.emit(Event::RepositoriesUpdated);
322 }
323
324 pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
325 self.repositories.clone()
326 }
327}
328
329impl RepositoryHandle {
330 pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
331 maybe!({
332 let path = self.unrelativize(&"".into())?;
333 Some(
334 project
335 .absolute_path(&path, cx)?
336 .file_name()?
337 .to_string_lossy()
338 .to_string()
339 .into(),
340 )
341 })
342 .unwrap_or("".into())
343 }
344
345 pub fn activate(&self, cx: &mut App) {
346 let Some(git_state) = self.git_state.upgrade() else {
347 return;
348 };
349 git_state.update(cx, |git_state, cx| {
350 let Some((index, _)) = git_state
351 .repositories
352 .iter()
353 .enumerate()
354 .find(|(_, handle)| handle == &self)
355 else {
356 return;
357 };
358 git_state.active_index = Some(index);
359 cx.emit(Event::RepositoriesUpdated);
360 });
361 }
362
363 pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
364 self.repository_entry.status()
365 }
366
367 pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
368 let path = self.repository_entry.unrelativize(path)?;
369 Some((self.worktree_id, path).into())
370 }
371
372 pub fn commit_message(&self) -> Entity<Buffer> {
373 self.commit_message.clone()
374 }
375
376 pub fn stage_entries(
377 &self,
378 entries: Vec<RepoPath>,
379 err_sender: mpsc::Sender<anyhow::Error>,
380 ) -> anyhow::Result<()> {
381 if entries.is_empty() {
382 return Ok(());
383 }
384 let Some(git_repo) = self.git_repo.clone() else {
385 return Ok(());
386 };
387 self.update_sender
388 .unbounded_send((Message::Stage(git_repo, entries), err_sender))
389 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
390 Ok(())
391 }
392
393 pub fn unstage_entries(
394 &self,
395 entries: Vec<RepoPath>,
396 err_sender: mpsc::Sender<anyhow::Error>,
397 ) -> anyhow::Result<()> {
398 if entries.is_empty() {
399 return Ok(());
400 }
401 let Some(git_repo) = self.git_repo.clone() else {
402 return Ok(());
403 };
404 self.update_sender
405 .unbounded_send((Message::Unstage(git_repo, entries), err_sender))
406 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
407 Ok(())
408 }
409
410 pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
411 let to_stage = self
412 .repository_entry
413 .status()
414 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
415 .map(|entry| entry.repo_path.clone())
416 .collect();
417 self.stage_entries(to_stage, err_sender)?;
418 Ok(())
419 }
420
421 pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
422 let to_unstage = self
423 .repository_entry
424 .status()
425 .filter(|entry| entry.status.is_staged().unwrap_or(true))
426 .map(|entry| entry.repo_path.clone())
427 .collect();
428 self.unstage_entries(to_unstage, err_sender)?;
429 Ok(())
430 }
431
432 /// Get a count of all entries in the active repository, including
433 /// untracked files.
434 pub fn entry_count(&self) -> usize {
435 self.repository_entry.status_len()
436 }
437
438 fn have_changes(&self) -> bool {
439 self.repository_entry.status_summary() != GitSummary::UNCHANGED
440 }
441
442 fn have_staged_changes(&self) -> bool {
443 self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
444 }
445
446 pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
447 return self
448 .commit_message
449 .read(cx)
450 .chars()
451 .any(|c| !c.is_ascii_whitespace())
452 && self.have_changes()
453 && (commit_all || self.have_staged_changes());
454 }
455
456 pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
457 let Some(git_repo) = self.git_repo.clone() else {
458 return;
459 };
460 let message = self.commit_message.read(cx).as_rope().clone();
461 let result = self
462 .update_sender
463 .unbounded_send((Message::Commit(git_repo, message), err_sender.clone()));
464 if result.is_err() {
465 cx.spawn(|_| async move {
466 err_sender
467 .send(anyhow!("Failed to submit commit operation"))
468 .await
469 .ok();
470 })
471 .detach();
472 return;
473 }
474 self.commit_message.update(cx, |commit_message, cx| {
475 commit_message.set_text("", cx);
476 });
477 }
478
479 pub fn commit_with_message(
480 &self,
481 message: String,
482 err_sender: mpsc::Sender<anyhow::Error>,
483 ) -> anyhow::Result<()> {
484 let Some(git_repo) = self.git_repo.clone() else {
485 return Ok(());
486 };
487 let result = self
488 .update_sender
489 .unbounded_send((Message::Commit(git_repo, message.into()), err_sender));
490 anyhow::ensure!(result.is_ok(), "Failed to submit commit operation");
491 Ok(())
492 }
493
494 pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut App) {
495 let Some(git_repo) = self.git_repo.clone() else {
496 return;
497 };
498 let to_stage = self
499 .repository_entry
500 .status()
501 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
502 .map(|entry| entry.repo_path.clone())
503 .collect::<Vec<_>>();
504 let message = self.commit_message.read(cx).as_rope().clone();
505 let result = self.update_sender.unbounded_send((
506 Message::StageAndCommit(git_repo, message, to_stage),
507 err_sender.clone(),
508 ));
509 if result.is_err() {
510 cx.spawn(|_| async move {
511 err_sender
512 .send(anyhow!("Failed to submit commit all operation"))
513 .await
514 .ok();
515 })
516 .detach();
517 return;
518 }
519 self.commit_message.update(cx, |commit_message, cx| {
520 commit_message.set_text("", cx);
521 });
522 }
523}