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