1use ::settings::Settings;
2use collections::HashMap;
3use futures::channel::mpsc;
4use futures::StreamExt as _;
5use git::repository::{GitRepository, RepoPath};
6use git::status::FileStatus;
7use git_panel_settings::GitPanelSettings;
8use gpui::{actions, AppContext, Hsla, Model};
9use project::{Project, WorktreeId};
10use std::sync::Arc;
11use sum_tree::SumTree;
12use ui::{Color, Icon, IconName, IntoElement, SharedString};
13use util::ResultExt as _;
14use worktree::RepositoryEntry;
15
16pub mod git_panel;
17mod git_panel_settings;
18
19actions!(
20 git,
21 [
22 StageFile,
23 UnstageFile,
24 ToggleStaged,
25 // Revert actions are currently in the editor crate:
26 // editor::RevertFile,
27 // editor::RevertSelectedHunks
28 StageAll,
29 UnstageAll,
30 RevertAll,
31 CommitChanges,
32 CommitAllChanges,
33 ClearCommitMessage
34 ]
35);
36
37pub fn init(cx: &mut AppContext) {
38 GitPanelSettings::register(cx);
39}
40
41#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
42pub enum GitViewMode {
43 #[default]
44 List,
45 Tree,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49enum StatusAction {
50 Stage,
51 Unstage,
52}
53
54pub struct GitState {
55 /// The current commit message being composed.
56 commit_message: Option<SharedString>,
57
58 /// When a git repository is selected, this is used to track which repository's changes
59 /// are currently being viewed or modified in the UI.
60 active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
61
62 updater_tx: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
63
64 all_repositories: HashMap<WorktreeId, SumTree<RepositoryEntry>>,
65
66 list_view_mode: GitViewMode,
67}
68
69impl GitState {
70 pub fn new(cx: &AppContext) -> Self {
71 let (updater_tx, mut updater_rx) =
72 mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
73 cx.spawn(|cx| async move {
74 while let Some((git_repo, paths, action)) = updater_rx.next().await {
75 cx.background_executor()
76 .spawn(async move {
77 match action {
78 StatusAction::Stage => git_repo.stage_paths(&paths),
79 StatusAction::Unstage => git_repo.unstage_paths(&paths),
80 }
81 })
82 .await
83 .log_err();
84 }
85 })
86 .detach();
87 GitState {
88 commit_message: None,
89 active_repository: None,
90 updater_tx,
91 list_view_mode: GitViewMode::default(),
92 all_repositories: HashMap::default(),
93 }
94 }
95
96 pub fn activate_repository(
97 &mut self,
98 worktree_id: WorktreeId,
99 active_repository: RepositoryEntry,
100 git_repo: Arc<dyn GitRepository>,
101 ) {
102 self.active_repository = Some((worktree_id, active_repository, git_repo));
103 }
104
105 pub fn active_repository(
106 &self,
107 ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
108 self.active_repository.as_ref()
109 }
110
111 pub fn commit_message(&mut self, message: Option<SharedString>) {
112 self.commit_message = message;
113 }
114
115 pub fn clear_commit_message(&mut self) {
116 self.commit_message = None;
117 }
118
119 pub fn stage_entry(&mut self, repo_path: RepoPath) {
120 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
121 let _ = self.updater_tx.unbounded_send((
122 git_repo.clone(),
123 vec![repo_path],
124 StatusAction::Stage,
125 ));
126 }
127 }
128
129 pub fn unstage_entry(&mut self, repo_path: RepoPath) {
130 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
131 let _ = self.updater_tx.unbounded_send((
132 git_repo.clone(),
133 vec![repo_path],
134 StatusAction::Unstage,
135 ));
136 }
137 }
138
139 pub fn stage_entries(&mut self, entries: Vec<RepoPath>) {
140 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
141 let _ =
142 self.updater_tx
143 .unbounded_send((git_repo.clone(), entries, StatusAction::Stage));
144 }
145 }
146
147 fn act_on_all(&mut self, action: StatusAction) {
148 if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() {
149 let _ = self.updater_tx.unbounded_send((
150 git_repo.clone(),
151 active_repository
152 .status()
153 .map(|entry| entry.repo_path)
154 .collect(),
155 action,
156 ));
157 }
158 }
159
160 pub fn stage_all(&mut self) {
161 self.act_on_all(StatusAction::Stage);
162 }
163
164 pub fn unstage_all(&mut self) {
165 self.act_on_all(StatusAction::Unstage);
166 }
167}
168
169pub fn first_worktree_repository(
170 project: &Model<Project>,
171 worktree_id: WorktreeId,
172 cx: &mut AppContext,
173) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
174 project
175 .read(cx)
176 .worktree_for_id(worktree_id, cx)
177 .and_then(|worktree| {
178 let snapshot = worktree.read(cx).snapshot();
179 let repo = snapshot.repositories().iter().next()?.clone();
180 let git_repo = worktree
181 .read(cx)
182 .as_local()?
183 .get_local_repo(&repo)?
184 .repo()
185 .clone();
186 Some((repo, git_repo))
187 })
188}
189
190pub fn first_repository_in_project(
191 project: &Model<Project>,
192 cx: &mut AppContext,
193) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
194 project.read(cx).worktrees(cx).next().and_then(|worktree| {
195 let snapshot = worktree.read(cx).snapshot();
196 let repo = snapshot.repositories().iter().next()?.clone();
197 let git_repo = worktree
198 .read(cx)
199 .as_local()?
200 .get_local_repo(&repo)?
201 .repo()
202 .clone();
203 Some((snapshot.id(), repo, git_repo))
204 })
205}
206
207const ADDED_COLOR: Hsla = Hsla {
208 h: 142. / 360.,
209 s: 0.68,
210 l: 0.45,
211 a: 1.0,
212};
213const MODIFIED_COLOR: Hsla = Hsla {
214 h: 48. / 360.,
215 s: 0.76,
216 l: 0.47,
217 a: 1.0,
218};
219const REMOVED_COLOR: Hsla = Hsla {
220 h: 355. / 360.,
221 s: 0.65,
222 l: 0.65,
223 a: 1.0,
224};
225
226// TODO: Add updated status colors to theme
227pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
228 let (icon_name, color) = if status.is_conflicted() {
229 (IconName::Warning, REMOVED_COLOR)
230 } else if status.is_deleted() {
231 (IconName::SquareMinus, REMOVED_COLOR)
232 } else if status.is_modified() {
233 (IconName::SquareDot, MODIFIED_COLOR)
234 } else {
235 (IconName::SquarePlus, ADDED_COLOR)
236 };
237 Icon::new(icon_name).color(Color::Custom(color))
238}