git_ui.rs

  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}