1use std::sync::Arc;
2
3use futures::channel::mpsc;
4use futures::StreamExt as _;
5use git::repository::{GitRepository, RepoPath};
6use gpui::{AppContext, SharedString};
7use settings::WorktreeId;
8use util::ResultExt as _;
9use worktree::RepositoryEntry;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum StatusAction {
13 Stage,
14 Unstage,
15}
16
17pub struct GitState {
18 /// The current commit message being composed.
19 pub commit_message: Option<SharedString>,
20
21 /// When a git repository is selected, this is used to track which repository's changes
22 /// are currently being viewed or modified in the UI.
23 pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
24
25 pub update_sender: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
26}
27
28impl GitState {
29 pub fn new(cx: &AppContext) -> Self {
30 let (tx, mut rx) =
31 mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
32 cx.spawn(|cx| async move {
33 while let Some((git_repo, paths, action)) = rx.next().await {
34 cx.background_executor()
35 .spawn(async move {
36 match action {
37 StatusAction::Stage => git_repo.stage_paths(&paths),
38 StatusAction::Unstage => git_repo.unstage_paths(&paths),
39 }
40 })
41 .await
42 .log_err();
43 }
44 })
45 .detach();
46 GitState {
47 commit_message: None,
48 active_repository: None,
49 update_sender: tx,
50 }
51 }
52
53 pub fn activate_repository(
54 &mut self,
55 worktree_id: WorktreeId,
56 active_repository: RepositoryEntry,
57 git_repo: Arc<dyn GitRepository>,
58 ) {
59 self.active_repository = Some((worktree_id, active_repository, git_repo));
60 }
61
62 pub fn active_repository(
63 &self,
64 ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
65 self.active_repository.as_ref()
66 }
67
68 pub fn commit_message(&mut self, message: Option<SharedString>) {
69 self.commit_message = message;
70 }
71
72 pub fn clear_commit_message(&mut self) {
73 self.commit_message = None;
74 }
75
76 fn act_on_entries(&self, entries: Vec<RepoPath>, action: StatusAction) {
77 if entries.is_empty() {
78 return;
79 }
80 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
81 let _ = self
82 .update_sender
83 .unbounded_send((git_repo.clone(), entries, action));
84 }
85 }
86
87 pub fn stage_entries(&self, entries: Vec<RepoPath>) {
88 self.act_on_entries(entries, StatusAction::Stage);
89 }
90
91 pub fn unstage_entries(&self, entries: Vec<RepoPath>) {
92 self.act_on_entries(entries, StatusAction::Unstage);
93 }
94
95 pub fn stage_all(&self) {
96 let Some((_, entry, _)) = self.active_repository.as_ref() else {
97 return;
98 };
99 let to_stage = entry
100 .status()
101 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
102 .map(|entry| entry.repo_path.clone())
103 .collect();
104 self.stage_entries(to_stage);
105 }
106
107 pub fn unstage_all(&self) {
108 let Some((_, entry, _)) = self.active_repository.as_ref() else {
109 return;
110 };
111 let to_unstage = entry
112 .status()
113 .filter(|entry| entry.status.is_staged().unwrap_or(true))
114 .map(|entry| entry.repo_path.clone())
115 .collect();
116 self.unstage_entries(to_unstage);
117 }
118
119 /// Get a count of all entries in the active repository, including
120 /// untracked files.
121 pub fn entry_count(&self) -> usize {
122 self.active_repository
123 .as_ref()
124 .map_or(0, |(_, entry, _)| entry.status_len())
125 }
126}