1use std::fmt::Write as _;
8use std::path::Path;
9
10use anyhow::{anyhow, Context, Result};
11
12use crate::common::{is_in_path, set_current_dir};
13use crate::io::execute_and_output_no_log;
14
15#[derive(Default)]
16struct GitStatus {
17 branch: Option<String>,
18 ahead: i64,
19 behind: i64,
20
21 staged: i64,
22 modified: i64,
23 deleted: i64,
24 unmerged: i64,
25 untracked: i64,
26}
27
28impl GitStatus {
29 fn parse_porcelain2(porcerlain2_output: String) -> Option<GitStatus> {
30 let mut status = GitStatus::default();
31 for entry in porcerlain2_output.split('\0') {
33 let mut entry = entry.split(' ');
34 match entry.next() {
35 Some("#") => match entry.next()? {
37 "branch.head" => {
38 let head = entry.next()?;
39 if head != "(detached)" {
40 status.branch = Some(String::from(head));
41 }
42 }
43 "branch.ab" => {
44 let a = entry.next()?;
45 let b = entry.next()?;
46 status.ahead = a.parse::<i64>().ok()?.abs();
47 status.behind = b.parse::<i64>().ok()?.abs();
48 }
49 _ => {}
50 },
51 Some("1") | Some("2") => {
53 let mut xy = entry.next()?.chars();
54 let x = xy.next()?;
55 let y = xy.next()?;
56 if x != '.' {
57 status.staged += 1;
58 }
59 match y {
60 'M' => status.modified += 1,
61 'D' => status.deleted += 1,
62 _ => {}
63 }
64 }
65 Some("u") => status.unmerged += 1,
66 Some("?") => status.untracked += 1,
67 _ => {}
68 }
69 }
70 Some(status)
71 }
72
73 fn is_modified(&self) -> bool {
74 self.untracked + self.modified + self.deleted + self.unmerged + self.staged > 0
75 }
76
77 fn format_git_string(&self) -> Result<String> {
78 let mut git_string = String::new();
79
80 git_string.push('(');
81
82 if let Some(branch) = &self.branch {
83 git_string.push_str(branch);
84 } else {
85 git_string.push_str(":HEAD");
87 }
88
89 if self.ahead != 0 {
91 write!(git_string, "↑{}", self.ahead)?;
92 }
93 if self.behind != 0 {
94 write!(git_string, "↓{}", self.behind)?;
95 }
96
97 if self.is_modified() {
98 git_string.push('|');
99
100 if self.untracked != 0 {
101 write!(git_string, "+{}", self.untracked)?;
102 }
103 if self.modified != 0 {
104 write!(git_string, "~{}", self.modified)?;
105 }
106 if self.deleted != 0 {
107 write!(git_string, "-{}", self.deleted)?;
108 }
109 if self.unmerged != 0 {
110 write!(git_string, "x{}", self.unmerged)?;
111 }
112 if self.staged != 0 {
113 write!(git_string, "•{}", self.staged)?;
114 }
115 }
116
117 git_string.push(')');
118
119 Ok(git_string)
120 }
121}
122
123fn porcelain2() -> Result<std::process::Output> {
124 execute_and_output_no_log(
125 "git",
126 [
127 "status",
128 "--porcelain=v2",
129 "-z",
130 "--branch",
131 "--untracked-files=all",
132 ],
133 )
134}
135
136pub fn git(path: &Path) -> Result<String> {
139 if !is_in_path("git") {
140 return Ok("".to_owned());
141 }
142 if set_current_dir(path).is_err() {
143 return Ok("".to_owned());
145 }
146 let output = porcelain2()?;
147 if !output.status.success() {
148 return Ok("".to_owned());
150 }
151 let porcerlain_output = String::from_utf8(output.stdout)?;
152
153 GitStatus::parse_porcelain2(porcerlain_output)
154 .context("Error while parsing Git output")?
155 .format_git_string()
156}
157
158pub fn git_root() -> Result<String> {
161 let output = execute_and_output_no_log("git", ["rev-parse", "--show-toplevel"])?;
162 if !output.status.success() {
163 return Err(anyhow!("git root: git command returned an error"));
165 }
166 Ok(String::from_utf8(output.stdout)?.trim().to_owned())
167}