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