use crate::chunk::Chunk;
use crate::providers::vcs::{merge_icons, StatusIcon, VcsTrait};
use crate::style::to_superscript;
use crate::{cmd::CMD, options::Options};
use smallvec::SmallVec;
use smol_str::{format_smolstr, SmolStr, StrExt, ToSmolStr};
use std::convert::Infallible;
use std::env;
use std::path::Path;
use std::str::FromStr;
use tokio::join;
macro_rules! git {
( $( $x:expr ),* ) => {
CMD.exec("git", [$( $x ),*])
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Git;
impl VcsTrait for Git {
async fn branch(&self, opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
let icon = git_branch_icon(opts).await;
let info = git_branch_name(opts).await;
match (icon, info) {
(None, None) => None,
(Some(icon), None) => Some(Chunk::icon(icon)),
(None, Some(info)) => Some(Chunk::info(info)),
(Some(icon), Some(info)) => Some(Chunk::new(icon, info)),
}
}
async fn commit(&self, opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
let (name_rev, branch_name, descr) = join!(
git_name_rev(opts),
git_branch_name(opts),
git_describe_cmd(opts)
);
match (branch_name, descr, name_rev) {
(_, None, None) => None,
(None, None, Some(nr)) => Some(Chunk::info(nr)),
(Some(b), None, Some(nr)) if git_bidirectional_inclusion(&b, &nr) => None,
(Some(_), None, Some(nr)) => Some(Chunk::info(nr)),
(_, Some(c), _) => Some(Chunk::info(c)),
}
}
async fn status(&self, _opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
git!("status", "--porcelain")
.await
.filter(|s| !s.is_empty())
.map(|s| {
Chunk::info(merge_icons(
s.lines()
.map(|l| l.parse::<StatusIcon<Git>>().unwrap())
.collect::<SmallVec<[_; 8]>>(),
))
})
}
async fn worktree(&self, _opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
let path = env::current_dir().ok()?;
let output = git!("worktree", "list").await?;
output.lines().skip(1).find_map(|line| {
let mut parts = line.split_whitespace();
let worktree_path = parts.next()?;
if path.starts_with(worktree_path) {
parts.next()?; let name = parts.collect::<SmallVec<[_; 8]>>().join(" ");
Some(Chunk::new("⌂", name.into()))
} else {
None
}
})
}
async fn stash(&self, _opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
git!("stash", "list")
.await
.filter(|s| !s.is_empty())
.map(|s| {
let mut buffer = itoa::Buffer::new();
let n = buffer.format(s.lines().count());
Chunk::info(format_smolstr!("≡{}", to_superscript(n)))
})
}
async fn divergence(&self, _opts: &Options, _path: &Path) -> Option<Chunk<SmolStr>> {
let (ahead, behind) = join!(
git!("rev-list", "--count", "HEAD@{upstream}..HEAD"),
git!("rev-list", "--count", "HEAD..HEAD@{upstream}")
);
let ahead = ahead?;
let behind = behind?;
match (ahead.as_str(), behind.as_str()) {
("0" | "", "0" | "") => None,
("0" | "", behind) => Some(Chunk::info(format_smolstr!("↓{}", behind))),
(ahead, "0" | "") => Some(Chunk::info(format_smolstr!("↑{}", ahead))),
(ahead, behind) => Some(Chunk::info(format_smolstr!("↑{}↓{}", ahead, behind))),
}
}
}
impl FromStr for StatusIcon<Git> {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some('D'), Some('D')) => Ok(StatusIcon::new("✖")), (Some('A'), Some('A')) => Ok(StatusIcon::new("⧉")), (Some('U'), Some('U')) => Ok(StatusIcon::new("⚠")), (Some('A'), Some('U')) => Ok(StatusIcon::new("⊕")), (Some('U'), Some('A')) => Ok(StatusIcon::new("⊞")), (Some('D'), Some('U')) => Ok(StatusIcon::new("⊖")), (Some('U'), Some('D')) => Ok(StatusIcon::new("⊟")),
(Some('M'), Some(' ')) => Ok(StatusIcon::new("●")), (Some('M'), Some('M')) => Ok(StatusIcon::new("◉")), (Some('M'), Some('D')) => Ok(StatusIcon::new("◐")), (Some('M'), Some('T')) => Ok(StatusIcon::new("◑")),
(Some('A'), Some(' ')) => Ok(StatusIcon::new("✚")), (Some('A'), Some('M')) => Ok(StatusIcon::new("✛")), (Some('A'), Some('D')) => Ok(StatusIcon::new("⊕")), (Some('A'), Some('T')) => Ok(StatusIcon::new("⊛")),
(Some('D'), Some(' ')) => Ok(StatusIcon::new("−")), (Some('D'), Some('M')) => Ok(StatusIcon::new("∓")),
(Some('R'), Some(' ')) => Ok(StatusIcon::new("→")), (Some('R'), Some('M')) => Ok(StatusIcon::new("⇢")), (Some('R'), Some('D')) => Ok(StatusIcon::new("⇥")), (Some('R'), Some('T')) => Ok(StatusIcon::new("⤳")),
(Some('C'), Some(' ')) => Ok(StatusIcon::new("⊂")), (Some('C'), Some('M')) => Ok(StatusIcon::new("⊃")), (Some('C'), Some('D')) => Ok(StatusIcon::new("⊄")), (Some('C'), Some('T')) => Ok(StatusIcon::new("⊅")),
(Some('T'), Some(' ')) => Ok(StatusIcon::new("◈")), (Some('T'), Some('M')) => Ok(StatusIcon::new("◊")), (Some('T'), Some('D')) => Ok(StatusIcon::new("⬧")), (Some('T'), Some('T')) => Ok(StatusIcon::new("⬢")),
(Some(' '), Some('M')) => Ok(StatusIcon::new("○")), (Some(' '), Some('D')) => Ok(StatusIcon::new("x")), (Some(' '), Some('T')) => Ok(StatusIcon::new("◇")), (Some(' '), Some('R')) => Ok(StatusIcon::new("↻")), (Some(' '), Some('C')) => Ok(StatusIcon::new("⊆")), (Some(' '), Some('A')) => Ok(StatusIcon::new("⊹")),
(Some('?'), Some('?')) => Ok(StatusIcon::new("⁇")), (Some('!'), Some('!')) => Ok(StatusIcon::new("")),
_ => Ok(StatusIcon::new("")), }
}
}
async fn git_describe_cmd(_opts: &Options) -> Option<SmolStr> {
git!("describe", "--abbrev=7", "--always", "--tag", "--long")
.await
.map(|s| {
let output = s.trim().split('-').collect::<SmallVec<[_; 4]>>();
match output[..] {
[] => "".to_smolstr(),
[tag] => tag.to_smolstr(),
[tag, "0"] => tag.to_smolstr(),
[tag, n] => format_smolstr!("{tag}▴{n}"),
[tag, "0", hash] => format_smolstr!("{tag}|{hash}"),
[tag, n, hash, ..] => format_smolstr!("{tag}▴{n}|{hash}"),
}
})
}
async fn git_rev_parse(origin: bool) -> Option<SmolStr> {
git!(
"rev-parse",
"--abbrev-ref",
if origin { "origin/HEAD" } else { "HEAD" }
)
.await
.filter(|s| !s.is_empty())
.and_then(|s| s.trim().split('/').next_back().map(Into::into))
}
async fn git_name_rev(_opts: &Options) -> Option<SmolStr> {
let mut result = git!("name-rev", "--name-only", "HEAD").await?;
for (o, n) in &[
("remotes/origin/", "↪"),
("remotes/", "↪"),
("tags/", ""),
("~", "↓"),
] {
result = result.replace_smolstr(o, n);
}
Some(result)
}
#[inline]
fn git_bidirectional_inclusion(a: &SmolStr, b: &SmolStr) -> bool {
a.contains(b.as_str()) || b.contains(a.as_str())
}
#[inline]
async fn git_branch_icon(_: &Options) -> Option<&'static str> {
let (local, origin) = join!(git_rev_parse(false), git_rev_parse(true));
match local.as_deref() {
None => None,
Some("HEAD") => Some("⚠"),
Some(local) if Some(local) == origin.as_deref() => Some("⟝"),
_ => Some("⎇"),
}
}
#[inline]
async fn git_branch_name(_: &Options) -> Option<SmolStr> {
git!("branch", "--show")
.await
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_smolstr())
}