use super::App;
use crate::git::{SEP, git_capture};
pub struct Branch {
pub is_head: bool,
pub remote: bool,
pub name: String,
pub rel: String,
pub author: String,
pub has_upstream: bool,
pub track: String,
}
impl Branch {
pub fn unpushed(&self) -> bool {
!self.remote
&& (!self.has_upstream || self.track.contains("gone") || self.track.contains("ahead"))
}
pub fn status(&self) -> String {
if self.remote {
return String::new();
}
if !self.has_upstream {
return "no remote".to_string();
}
if self.track.contains("gone") {
return "gone".to_string();
}
let ahead = count(&self.track, "ahead");
let behind = count(&self.track, "behind");
match (ahead, behind) {
(Some(a), Some(b)) => format!("↑{a} ↓{b}"),
(Some(a), None) => format!("↑{a}"),
(None, Some(b)) => format!("↓{b}"),
(None, None) => "synced".to_string(),
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum Scope {
Local,
All,
Remote,
}
pub const SCOPES: [Scope; 3] = [Scope::Local, Scope::All, Scope::Remote];
pub const DEFAULT_SCOPE: usize = 0;
impl Scope {
pub fn label(self) -> &'static str {
match self {
Scope::Local => "local",
Scope::All => "all",
Scope::Remote => "remote",
}
}
pub(super) fn keeps(self, b: &Branch) -> bool {
match self {
Scope::Local => !b.remote,
Scope::Remote => b.remote,
Scope::All => true,
}
}
}
impl App {
pub(super) fn load_branches(&mut self) {
let fmt = format!(
"--format=%(HEAD){SEP}%(refname){SEP}%(refname:short){SEP}%(committerdate:relative){SEP}%(authorname){SEP}%(upstream){SEP}%(upstream:track)"
);
self.branches = git_capture(
&self.repo,
&[
"for-each-ref",
"--sort=-committerdate",
&fmt,
"refs/heads",
"refs/remotes",
],
)
.map(|out| {
out.lines()
.filter_map(|line| {
let mut f = line.split(SEP);
let head = f.next()?;
let refname = f.next()?;
let short = f.next()?;
let rel = f.next().unwrap_or("").to_string();
let author = f.next().unwrap_or("").to_string();
let upstream = f.next().unwrap_or("");
let track = f.next().unwrap_or("").to_string();
if refname.ends_with("/HEAD") {
return None;
}
Some(Branch {
is_head: head.trim() == "*",
remote: refname.starts_with("refs/remotes/"),
name: short.to_string(),
rel,
author,
has_upstream: !upstream.is_empty(),
track,
})
})
.collect()
})
.unwrap_or_default();
}
pub(super) fn rescope_branches(&mut self) {
let scope = SCOPES[self.bsel.scope];
let visible = (0..self.branches.len())
.filter(|&i| scope.keeps(&self.branches[i]))
.collect();
self.bsel.show(visible);
}
pub(super) fn enter_branch(&mut self) {
let Some(i) = self.bsel.idx() else {
self.commits.clear();
self.csel.show(Vec::new());
self.files.clear();
self.fsel.show(Vec::new());
return;
};
self.ensure_unpushed();
self.log_args = vec![self.branches[i].name.clone()];
self.limit = super::COMMITS_PER_BRANCH;
self.load_commits();
}
}
fn count(track: &str, key: &str) -> Option<u32> {
let rest = &track[track.find(key)? + key.len()..];
rest.split(|c: char| !c.is_ascii_digit())
.find(|t| !t.is_empty())
.and_then(|t| t.parse().ok())
}