1use std::sync::Arc;
2
3use anyhow::anyhow;
4use futures::channel::mpsc;
5use futures::{SinkExt as _, StreamExt as _};
6use git::{
7 repository::{GitRepository, RepoPath},
8 status::{GitSummary, TrackedSummary},
9};
10use gpui::{AppContext, SharedString};
11use settings::WorktreeId;
12use worktree::RepositoryEntry;
13
14pub struct GitState {
15 /// The current commit message being composed.
16 pub commit_message: SharedString,
17
18 /// When a git repository is selected, this is used to track which repository's changes
19 /// are currently being viewed or modified in the UI.
20 pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
21
22 update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
23}
24
25enum Message {
26 StageAndCommit(Arc<dyn GitRepository>, SharedString, Vec<RepoPath>),
27 Commit(Arc<dyn GitRepository>, SharedString),
28 Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
29 Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
30}
31
32impl GitState {
33 pub fn new(cx: &AppContext) -> Self {
34 let (update_sender, mut update_receiver) =
35 mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
36 cx.spawn(|cx| async move {
37 while let Some((msg, mut err_sender)) = update_receiver.next().await {
38 let result = cx
39 .background_executor()
40 .spawn(async move {
41 match msg {
42 Message::StageAndCommit(repo, message, paths) => {
43 repo.stage_paths(&paths)?;
44 repo.commit(&message)?;
45 Ok(())
46 }
47 Message::Stage(repo, paths) => repo.stage_paths(&paths),
48 Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
49 Message::Commit(repo, message) => repo.commit(&message),
50 }
51 })
52 .await;
53 if let Err(e) = result {
54 err_sender.send(e).await.ok();
55 }
56 }
57 })
58 .detach();
59 GitState {
60 commit_message: SharedString::default(),
61 active_repository: None,
62 update_sender,
63 }
64 }
65
66 pub fn activate_repository(
67 &mut self,
68 worktree_id: WorktreeId,
69 active_repository: RepositoryEntry,
70 git_repo: Arc<dyn GitRepository>,
71 ) {
72 self.active_repository = Some((worktree_id, active_repository, git_repo));
73 }
74
75 pub fn active_repository(
76 &self,
77 ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
78 self.active_repository.as_ref()
79 }
80
81 pub fn stage_entries(
82 &self,
83 entries: Vec<RepoPath>,
84 err_sender: mpsc::Sender<anyhow::Error>,
85 ) -> anyhow::Result<()> {
86 if entries.is_empty() {
87 return Ok(());
88 }
89 let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
90 return Err(anyhow!("No active repository"));
91 };
92 self.update_sender
93 .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
94 .map_err(|_| anyhow!("Failed to submit stage operation"))?;
95 Ok(())
96 }
97
98 pub fn unstage_entries(
99 &self,
100 entries: Vec<RepoPath>,
101 err_sender: mpsc::Sender<anyhow::Error>,
102 ) -> anyhow::Result<()> {
103 if entries.is_empty() {
104 return Ok(());
105 }
106 let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
107 return Err(anyhow!("No active repository"));
108 };
109 self.update_sender
110 .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
111 .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
112 Ok(())
113 }
114
115 pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
116 let Some((_, entry, _)) = self.active_repository.as_ref() else {
117 return Err(anyhow!("No active repository"));
118 };
119 let to_stage = entry
120 .status()
121 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
122 .map(|entry| entry.repo_path.clone())
123 .collect();
124 self.stage_entries(to_stage, err_sender)?;
125 Ok(())
126 }
127
128 pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
129 let Some((_, entry, _)) = self.active_repository.as_ref() else {
130 return Err(anyhow!("No active repository"));
131 };
132 let to_unstage = entry
133 .status()
134 .filter(|entry| entry.status.is_staged().unwrap_or(true))
135 .map(|entry| entry.repo_path.clone())
136 .collect();
137 self.unstage_entries(to_unstage, err_sender)?;
138 Ok(())
139 }
140
141 /// Get a count of all entries in the active repository, including
142 /// untracked files.
143 pub fn entry_count(&self) -> usize {
144 self.active_repository
145 .as_ref()
146 .map_or(0, |(_, entry, _)| entry.status_len())
147 }
148
149 fn have_changes(&self) -> bool {
150 let Some((_, entry, _)) = self.active_repository.as_ref() else {
151 return false;
152 };
153 entry.status_summary() != GitSummary::UNCHANGED
154 }
155
156 fn have_staged_changes(&self) -> bool {
157 let Some((_, entry, _)) = self.active_repository.as_ref() else {
158 return false;
159 };
160 entry.status_summary().index != TrackedSummary::UNCHANGED
161 }
162
163 pub fn can_commit(&self, commit_all: bool) -> bool {
164 return !self.commit_message.trim().is_empty()
165 && self.have_changes()
166 && (commit_all || self.have_staged_changes());
167 }
168
169 pub fn commit(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
170 if !self.can_commit(false) {
171 return Err(anyhow!("Unable to commit"));
172 }
173 let Some((_, _, git_repo)) = self.active_repository() else {
174 return Err(anyhow!("No active repository"));
175 };
176 let git_repo = git_repo.clone();
177 let message = std::mem::take(&mut self.commit_message);
178 self.update_sender
179 .unbounded_send((Message::Commit(git_repo, message), err_sender))
180 .map_err(|_| anyhow!("Failed to submit commit operation"))?;
181 Ok(())
182 }
183
184 pub fn commit_all(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
185 if !self.can_commit(true) {
186 return Err(anyhow!("Unable to commit"));
187 }
188 let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
189 return Err(anyhow!("No active repository"));
190 };
191 let to_stage = entry
192 .status()
193 .filter(|entry| !entry.status.is_staged().unwrap_or(false))
194 .map(|entry| entry.repo_path.clone())
195 .collect::<Vec<_>>();
196 let message = std::mem::take(&mut self.commit_message);
197 self.update_sender
198 .unbounded_send((
199 Message::StageAndCommit(git_repo.clone(), message, to_stage),
200 err_sender,
201 ))
202 .map_err(|_| anyhow!("Failed to submit commit operation"))?;
203 Ok(())
204 }
205}