git_ui.rs

  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}