summaryrefslogtreecommitdiffstats
path: root/src/shell/inputs/git.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/shell/inputs/git.rs')
-rw-r--r--src/shell/inputs/git.rs274
1 files changed, 274 insertions, 0 deletions
diff --git a/src/shell/inputs/git.rs b/src/shell/inputs/git.rs
new file mode 100644
index 0000000..dbae1c4
--- /dev/null
+++ b/src/shell/inputs/git.rs
@@ -0,0 +1,274 @@
+use crate::shell::prelude::*;
+
+use notify::Watcher as _;
+
+pub struct Handler {
+ git_w: tokio::sync::mpsc::UnboundedSender<std::path::PathBuf>,
+}
+
+impl Handler {
+ pub fn new(event_w: crate::shell::event::Writer) -> Self {
+ let (git_w, git_r) = tokio::sync::mpsc::unbounded_channel();
+ tokio::spawn(Self::task(git_r, event_w));
+ Self { git_w }
+ }
+
+ pub fn new_dir(&self, path: std::path::PathBuf) {
+ self.git_w.send(path).unwrap();
+ }
+
+ async fn task(
+ mut git_r: tokio::sync::mpsc::UnboundedReceiver<std::path::PathBuf>,
+ event_w: crate::shell::event::Writer,
+ ) {
+ // clippy can't tell that we assign to this later
+ #[allow(clippy::no_effect_underscore_binding)]
+ let mut _active_watcher = None;
+ while let Some(mut dir) = git_r.recv().await {
+ while let Ok(newer_dir) = git_r.try_recv() {
+ dir = newer_dir;
+ }
+ let repo = git2::Repository::discover(&dir).ok();
+ if repo.is_some() {
+ let (sync_watch_w, sync_watch_r) = std::sync::mpsc::channel();
+ let (watch_w, mut watch_r) =
+ tokio::sync::mpsc::unbounded_channel();
+ let mut watcher =
+ notify::recommended_watcher(sync_watch_w).unwrap();
+ watcher
+ .watch(&dir, notify::RecursiveMode::Recursive)
+ .unwrap();
+ tokio::task::spawn_blocking(move || {
+ while let Ok(event) = sync_watch_r.recv() {
+ if watch_w.send(event).is_err() {
+ break;
+ }
+ }
+ });
+ let event_w = event_w.clone();
+ tokio::spawn(async move {
+ while watch_r.recv().await.is_some() {
+ let repo = git2::Repository::discover(&dir).ok();
+ let info = tokio::task::spawn_blocking(|| {
+ repo.map(|repo| Info::new(&repo))
+ })
+ .await
+ .unwrap();
+ event_w.send(Event::GitInfo(info));
+ }
+ });
+ _active_watcher = Some(watcher);
+ } else {
+ _active_watcher = None;
+ }
+ let info = tokio::task::spawn_blocking(|| {
+ repo.map(|repo| Info::new(&repo))
+ })
+ .await
+ .unwrap();
+ event_w.send(Event::GitInfo(info));
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Info {
+ modified_files: bool,
+ staged_files: bool,
+ new_files: bool,
+ commits: bool,
+ active_operation: ActiveOperation,
+ branch: Option<String>,
+ remote_branch_diff: Option<(usize, usize)>,
+}
+
+const MODIFIED: git2::Status = git2::Status::WT_DELETED
+ .union(git2::Status::WT_MODIFIED)
+ .union(git2::Status::WT_RENAMED)
+ .union(git2::Status::WT_TYPECHANGE)
+ .union(git2::Status::CONFLICTED);
+const STAGED: git2::Status = git2::Status::INDEX_DELETED
+ .union(git2::Status::INDEX_MODIFIED)
+ .union(git2::Status::INDEX_NEW)
+ .union(git2::Status::INDEX_RENAMED)
+ .union(git2::Status::INDEX_TYPECHANGE);
+const NEW: git2::Status = git2::Status::WT_NEW;
+
+impl Info {
+ pub fn new(git: &git2::Repository) -> Self {
+ let mut status_options = git2::StatusOptions::new();
+ status_options.include_untracked(true);
+ status_options.update_index(true);
+
+ let statuses = git.statuses(Some(&mut status_options));
+
+ let mut modified_files = false;
+ let mut staged_files = false;
+ let mut new_files = false;
+ if let Ok(statuses) = statuses {
+ for file in statuses.iter() {
+ if file.status().intersects(MODIFIED) {
+ modified_files = true;
+ }
+ if file.status().intersects(STAGED) {
+ staged_files = true;
+ }
+ if file.status().intersects(NEW) {
+ new_files = true;
+ }
+ }
+ }
+
+ let head = git.head();
+ let mut commits = false;
+ let mut branch = None;
+ let mut remote_branch_diff = None;
+
+ if let Ok(head) = head {
+ commits = true;
+ if head.is_branch() {
+ branch = head.shorthand().map(ToString::to_string);
+ remote_branch_diff =
+ head.resolve()
+ .ok()
+ .map(|head| {
+ (
+ head.target(),
+ head.shorthand().map(ToString::to_string),
+ )
+ })
+ .and_then(|(head_id, name)| {
+ head_id.and_then(|head_id| {
+ name.and_then(|name| {
+ git.refname_to_id(&format!(
+ "refs/remotes/origin/{}",
+ name
+ ))
+ .ok()
+ .and_then(|remote_id| {
+ git.graph_ahead_behind(
+ head_id, remote_id,
+ )
+ .ok()
+ })
+ })
+ })
+ });
+ } else {
+ branch =
+ head.resolve().ok().and_then(|head| head.target()).map(
+ |oid| {
+ let mut sha: String = oid
+ .as_bytes()
+ .iter()
+ .take(4)
+ .map(|b| format!("{:02x}", b))
+ .collect();
+ sha.truncate(7);
+ sha
+ },
+ );
+ }
+ }
+
+ let active_operation = match git.state() {
+ git2::RepositoryState::Merge => ActiveOperation::Merge,
+ git2::RepositoryState::Revert
+ | git2::RepositoryState::RevertSequence => {
+ ActiveOperation::Revert
+ }
+ git2::RepositoryState::CherryPick
+ | git2::RepositoryState::CherryPickSequence => {
+ ActiveOperation::CherryPick
+ }
+ git2::RepositoryState::Bisect => ActiveOperation::Bisect,
+ git2::RepositoryState::Rebase
+ | git2::RepositoryState::RebaseInteractive
+ | git2::RepositoryState::RebaseMerge => ActiveOperation::Rebase,
+ _ => ActiveOperation::None,
+ };
+
+ Self {
+ modified_files,
+ staged_files,
+ new_files,
+ commits,
+ active_operation,
+ branch,
+ remote_branch_diff,
+ }
+ }
+}
+
+impl std::fmt::Display for Info {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "g")?;
+
+ if self.modified_files {
+ write!(f, "*")?;
+ }
+ if self.staged_files {
+ write!(f, "+")?;
+ }
+ if self.new_files {
+ write!(f, "?")?;
+ }
+ if !self.commits {
+ write!(f, "!")?;
+ return Ok(());
+ }
+
+ let branch = self.branch.as_ref().map_or("???", |branch| {
+ if branch == "master" {
+ ""
+ } else {
+ branch
+ }
+ });
+ if !branch.is_empty() {
+ write!(f, ":")?;
+ }
+ write!(f, "{}", branch)?;
+
+ if let Some((local, remote)) = self.remote_branch_diff {
+ if local > 0 || remote > 0 {
+ write!(f, ":")?;
+ }
+ if local > 0 {
+ write!(f, "+{}", local)?;
+ }
+ if remote > 0 {
+ write!(f, "-{}", remote)?;
+ }
+ } else {
+ write!(f, ":-")?;
+ }
+
+ write!(f, "{}", self.active_operation)?;
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum ActiveOperation {
+ None,
+ Merge,
+ Revert,
+ CherryPick,
+ Bisect,
+ Rebase,
+}
+
+impl std::fmt::Display for ActiveOperation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ActiveOperation::None => Ok(()),
+ ActiveOperation::Merge => write!(f, "(m)"),
+ ActiveOperation::Revert => write!(f, "(v)"),
+ ActiveOperation::CherryPick => write!(f, "(c)"),
+ ActiveOperation::Bisect => write!(f, "(b)"),
+ ActiveOperation::Rebase => write!(f, "(r)"),
+ }
+ }
+}