diff options
Diffstat (limited to 'src/io/git.rs')
-rw-r--r-- | src/io/git.rs | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/src/io/git.rs b/src/io/git.rs new file mode 100644 index 0000000..7e9c786 --- /dev/null +++ b/src/io/git.rs @@ -0,0 +1,170 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copied and modified from https://github.com/9ary/gitprompt-rs/blob/master/src/main.rs +// Couldn't use without forking and I'm lazy. + +use std::fmt::Write as _; +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; + +use crate::common::is_program_in_path; + +use crate::io::execute_and_output_no_log; + +#[derive(Default)] +struct GitStatus { + branch: Option<String>, + ahead: i64, + behind: i64, + + staged: i64, + modified: i64, + deleted: i64, + unmerged: i64, + untracked: i64, +} + +impl GitStatus { + fn parse_porcelain2(porcerlain2_output: String) -> Option<GitStatus> { + let mut status = GitStatus::default(); + // Simple parser for the porcelain v2 format + for entry in porcerlain2_output.split('\0') { + let mut entry = entry.split(' '); + match entry.next() { + // Header lines + Some("#") => match entry.next()? { + "branch.head" => { + let head = entry.next()?; + if head != "(detached)" { + status.branch = Some(String::from(head)); + } + } + "branch.ab" => { + let a = entry.next()?; + let b = entry.next()?; + status.ahead = a.parse::<i64>().ok()?.abs(); + status.behind = b.parse::<i64>().ok()?.abs(); + } + _ => {} + }, + // File entries + Some("1") | Some("2") => { + let mut xy = entry.next()?.chars(); + let x = xy.next()?; + let y = xy.next()?; + if x != '.' { + status.staged += 1; + } + match y { + 'M' => status.modified += 1, + 'D' => status.deleted += 1, + _ => {} + } + } + Some("u") => status.unmerged += 1, + Some("?") => status.untracked += 1, + _ => {} + } + } + Some(status) + } + + fn is_modified(&self) -> bool { + self.untracked + self.modified + self.deleted + self.unmerged + self.staged > 0 + } + + fn format_git_string(&self) -> Result<String> { + let mut git_string = String::new(); + + git_string.push('('); + + if let Some(branch) = &self.branch { + git_string.push_str(branch); + } else { + // Detached head + git_string.push_str(":HEAD"); + } + + // Divergence with remote branch + if self.ahead != 0 { + write!(git_string, "↑{}", self.ahead)?; + } + if self.behind != 0 { + write!(git_string, "↓{}", self.behind)?; + } + + if self.is_modified() { + git_string.push('|'); + + if self.untracked != 0 { + write!(git_string, "+{}", self.untracked)?; + } + if self.modified != 0 { + write!(git_string, "~{}", self.modified)?; + } + if self.deleted != 0 { + write!(git_string, "-{}", self.deleted)?; + } + if self.unmerged != 0 { + write!(git_string, "x{}", self.unmerged)?; + } + if self.staged != 0 { + write!(git_string, "•{}", self.staged)?; + } + } + + git_string.push(')'); + + Ok(git_string) + } +} + +fn porcelain2() -> Result<std::process::Output> { + execute_and_output_no_log( + "git", + [ + "status", + "--porcelain=v2", + "-z", + "--branch", + "--untracked-files=all", + ], + ) +} + +/// Returns a string representation of the git status of this path. +/// Will return an empty string if we're not in a git repository. +pub fn git(path: &Path) -> Result<String> { + if !is_program_in_path("git") { + return Ok("".to_owned()); + } + if std::env::set_current_dir(path).is_err() { + // The path may not exist. It should never happen. + return Ok("".to_owned()); + } + let output = porcelain2()?; + if !output.status.success() { + // We're most likely not in a Git repo + return Ok("".to_owned()); + } + let porcerlain_output = String::from_utf8(output.stdout) + .ok() + .context("Invalid UTF-8 while decoding Git output")?; + + GitStatus::parse_porcelain2(porcerlain_output) + .context("Error while parsing Git output")? + .format_git_string() +} + +/// Returns the git root. +/// Returns an error outside of a git repository. +pub fn git_root() -> Result<String> { + let output = execute_and_output_no_log("git", ["rev-parse", "--show-toplevel"])?; + if !output.status.success() { + // We're most likely not in a Git repo + return Err(anyhow!("git root: git command returned an error")); + } + Ok(String::from_utf8(output.stdout)?.trim().to_owned()) +} |