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