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, Context, Global, Hsla, Model, ModelContext};
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 let git_state = cx.new_model(GitState::new);
39 cx.set_global(GlobalGitState(git_state));
40}
41
42#[derive(Default, Debug, PartialEq, Eq, Clone)]
43pub enum GitViewMode {
44 #[default]
45 List,
46 Tree,
47}
48
49struct GlobalGitState(Model<GitState>);
50
51impl Global for GlobalGitState {}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54enum StatusAction {
55 Stage,
56 Unstage,
57}
58
59pub struct GitState {
60 /// The current commit message being composed.
61 commit_message: Option<SharedString>,
62
63 /// When a git repository is selected, this is used to track which repository's changes
64 /// are currently being viewed or modified in the UI.
65 active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
66
67 updater_tx: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
68
69 all_repositories: HashMap<WorktreeId, SumTree<RepositoryEntry>>,
70
71 list_view_mode: GitViewMode,
72}
73
74impl GitState {
75 pub fn new(cx: &mut ModelContext<'_, Self>) -> Self {
76 let (updater_tx, mut updater_rx) =
77 mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
78 cx.spawn(|_, cx| async move {
79 while let Some((git_repo, paths, action)) = updater_rx.next().await {
80 cx.background_executor()
81 .spawn(async move {
82 match action {
83 StatusAction::Stage => git_repo.stage_paths(&paths),
84 StatusAction::Unstage => git_repo.unstage_paths(&paths),
85 }
86 })
87 .await
88 .log_err();
89 }
90 })
91 .detach();
92 GitState {
93 commit_message: None,
94 active_repository: None,
95 updater_tx,
96 list_view_mode: GitViewMode::default(),
97 all_repositories: HashMap::default(),
98 }
99 }
100
101 pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
102 cx.global::<GlobalGitState>().0.clone()
103 }
104
105 pub fn activate_repository(
106 &mut self,
107 worktree_id: WorktreeId,
108 active_repository: RepositoryEntry,
109 git_repo: Arc<dyn GitRepository>,
110 ) {
111 self.active_repository = Some((worktree_id, active_repository, git_repo));
112 }
113
114 pub fn active_repository(
115 &self,
116 ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
117 self.active_repository.as_ref()
118 }
119
120 pub fn commit_message(&mut self, message: Option<SharedString>) {
121 self.commit_message = message;
122 }
123
124 pub fn clear_commit_message(&mut self) {
125 self.commit_message = None;
126 }
127
128 pub fn stage_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::Stage,
134 ));
135 }
136 }
137
138 pub fn unstage_entry(&mut self, repo_path: RepoPath) {
139 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
140 let _ = self.updater_tx.unbounded_send((
141 git_repo.clone(),
142 vec![repo_path],
143 StatusAction::Unstage,
144 ));
145 }
146 }
147
148 pub fn stage_entries(&mut self, entries: Vec<RepoPath>) {
149 if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
150 let _ =
151 self.updater_tx
152 .unbounded_send((git_repo.clone(), entries, StatusAction::Stage));
153 }
154 }
155
156 fn act_on_all(&mut self, action: StatusAction) {
157 if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() {
158 let _ = self.updater_tx.unbounded_send((
159 git_repo.clone(),
160 active_repository
161 .status()
162 .map(|entry| entry.repo_path)
163 .collect(),
164 action,
165 ));
166 }
167 }
168
169 pub fn stage_all(&mut self) {
170 self.act_on_all(StatusAction::Stage);
171 }
172
173 pub fn unstage_all(&mut self) {
174 self.act_on_all(StatusAction::Unstage);
175 }
176}
177
178pub fn first_worktree_repository(
179 project: &Model<Project>,
180 worktree_id: WorktreeId,
181 cx: &mut AppContext,
182) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
183 project
184 .read(cx)
185 .worktree_for_id(worktree_id, cx)
186 .and_then(|worktree| {
187 let snapshot = worktree.read(cx).snapshot();
188 let repo = snapshot.repositories().iter().next()?.clone();
189 let git_repo = worktree
190 .read(cx)
191 .as_local()?
192 .get_local_repo(&repo)?
193 .repo()
194 .clone();
195 Some((repo, git_repo))
196 })
197}
198
199pub fn first_repository_in_project(
200 project: &Model<Project>,
201 cx: &mut AppContext,
202) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
203 project.read(cx).worktrees(cx).next().and_then(|worktree| {
204 let snapshot = worktree.read(cx).snapshot();
205 let repo = snapshot.repositories().iter().next()?.clone();
206 let git_repo = worktree
207 .read(cx)
208 .as_local()?
209 .get_local_repo(&repo)?
210 .repo()
211 .clone();
212 Some((snapshot.id(), repo, git_repo))
213 })
214}
215
216const ADDED_COLOR: Hsla = Hsla {
217 h: 142. / 360.,
218 s: 0.68,
219 l: 0.45,
220 a: 1.0,
221};
222const MODIFIED_COLOR: Hsla = Hsla {
223 h: 48. / 360.,
224 s: 0.76,
225 l: 0.47,
226 a: 1.0,
227};
228const REMOVED_COLOR: Hsla = Hsla {
229 h: 355. / 360.,
230 s: 0.65,
231 l: 0.65,
232 a: 1.0,
233};
234
235// TODO: Add updated status colors to theme
236pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
237 match status {
238 GitFileStatus::Added | GitFileStatus::Untracked => {
239 Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
240 }
241 GitFileStatus::Modified => {
242 Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
243 }
244 GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
245 GitFileStatus::Deleted => {
246 Icon::new(IconName::SquareMinus).color(Color::Custom(REMOVED_COLOR))
247 }
248 }
249}