use std::collections::HashSet;
use crate::core::graph::{self, Section};
use crate::core::repo::RemoteStatus;
use crate::core::shortid::IdAllocator;
pub(crate) const LOCAL_CHANGES_KEY: &str = "local";
pub(crate) const PENDING_COMMIT_OID: git2::Oid = git2::Oid::ZERO_SHA1;
pub(crate) fn branch_key(name: &str) -> String {
format!("br:{}", name)
}
pub(crate) fn working_file_key(path: &str) -> String {
format!("wf:{}", path)
}
pub(crate) fn commit_file_key(oid: git2::Oid, index: usize) -> String {
format!("{}:{}", oid, index)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum RowMark {
None,
Selected,
Source,
Covered,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum SelectionClass {
LocalChanges,
WorkingFile,
Branch,
Commit,
CommitFile,
}
impl SelectionClass {
pub fn label(self) -> &'static str {
match self {
SelectionClass::LocalChanges => "local changes",
SelectionClass::WorkingFile => "files",
SelectionClass::Branch => "branches",
SelectionClass::Commit => "commits",
SelectionClass::CommitFile => "commit files",
}
}
}
pub(crate) enum RowKind {
LocalChanges { count: usize },
WorkingFile {
path: String,
index: char,
worktree: char,
},
BranchName {
name: String,
remote: Option<RemoteStatus>,
connector: &'static str,
range: Option<(String, String)>,
},
Commit {
oid: git2::Oid,
message: String,
dot_color: Option<usize>,
file_count: usize,
},
CommitFile {
oid: git2::Oid,
path: String,
index: char,
worktree: char,
on_branch: bool,
},
Upstream {
label: String,
base_short_id: String,
base_message: String,
commits_ahead: usize,
},
Context {
short_hash: String,
date: String,
message: String,
},
Spacer(&'static str),
}
pub(crate) struct Row {
pub kind: RowKind,
pub sid: String,
pub target: Option<String>,
pub key: String,
pub focusable: bool,
pub expandable: bool,
pub expanded: bool,
}
impl Row {
pub fn selection_class(&self) -> Option<SelectionClass> {
Some(match self.kind {
RowKind::LocalChanges { .. } => SelectionClass::LocalChanges,
RowKind::WorkingFile { .. } => SelectionClass::WorkingFile,
RowKind::BranchName { .. } => SelectionClass::Branch,
RowKind::Commit { .. } => SelectionClass::Commit,
RowKind::CommitFile { .. } => SelectionClass::CommitFile,
RowKind::Upstream { .. } | RowKind::Context { .. } | RowKind::Spacer(_) => {
return None;
}
})
}
fn structural(kind: RowKind) -> Self {
Row {
kind,
sid: String::new(),
target: None,
key: String::new(),
focusable: false,
expandable: false,
expanded: false,
}
}
fn info(kind: RowKind, key: String) -> Self {
Row {
key,
focusable: true,
..Row::structural(kind)
}
}
}
pub(crate) fn build_rows(
sections: &[Section],
ids: &IdAllocator,
expanded: &HashSet<String>,
) -> Vec<Row> {
let mut rows: Vec<Row> = Vec::new();
let last_idx = sections.len().saturating_sub(1);
let mut branch_color_idx: usize = 0;
for (idx, section) in sections.iter().enumerate() {
match section {
Section::WorkingChanges(changes) => {
let key = LOCAL_CHANGES_KEY.to_string();
let is_expanded = expanded.contains(&key);
rows.push(Row {
kind: RowKind::LocalChanges {
count: changes.len(),
},
sid: ids.get_unstaged().to_string(),
target: Some(ids.get_unstaged().to_string()),
key,
focusable: true,
expandable: !changes.is_empty(),
expanded: is_expanded,
});
if is_expanded {
for change in changes {
rows.push(Row {
kind: RowKind::WorkingFile {
path: change.path.clone(),
index: change.index,
worktree: change.worktree,
},
sid: ids.get_file(&change.path).to_string(),
target: Some(ids.get_file(&change.path).to_string()),
key: working_file_key(&change.path),
focusable: true,
expandable: false,
expanded: false,
});
}
}
rows.push(Row::structural(RowKind::Spacer("│")));
}
Section::Branch { names, commits } => {
let dot_color = branch_color_idx;
branch_color_idx += 1;
let prev_stacked = idx > 0 && graph::is_stacked_with_next(sections, idx - 1);
let next_stacked = graph::is_stacked_with_next(sections, idx);
const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
let range = match (commits.first(), commits.last()) {
(Some(first), Some(last)) => Some((
last.parent_oid
.map_or_else(|| EMPTY_TREE.to_string(), |p| p.to_string()),
first.oid.to_string(),
)),
_ => None,
};
for (i, (name, remote)) in names.iter().enumerate() {
let connector = if i == 0 && !prev_stacked {
"│╭─"
} else {
"│├─"
};
rows.push(Row {
kind: RowKind::BranchName {
name: name.clone(),
remote: remote.clone(),
connector,
range: range.clone(),
},
sid: ids.get_branch(name).to_string(),
target: Some(name.clone()),
key: branch_key(name),
focusable: true,
expandable: false,
expanded: false,
});
}
push_commit_rows(&mut rows, commits, Some(dot_color), ids, expanded);
if next_stacked {
rows.push(Row::structural(RowKind::Spacer("││")));
} else {
rows.push(Row::structural(RowKind::Spacer("├╯")));
if idx < last_idx {
rows.push(Row::structural(RowKind::Spacer("│")));
}
}
}
Section::Loose(commits) => {
push_commit_rows(&mut rows, commits, None, ids, expanded);
if idx < last_idx {
rows.push(Row::structural(RowKind::Spacer("│")));
}
}
Section::Upstream(info) => {
rows.push(Row::info(
RowKind::Upstream {
label: info.label.clone(),
base_short_id: info.base_short_id.clone(),
base_message: info.base_message.clone(),
commits_ahead: info.commits_ahead,
},
format!("up:{}", info.merge_base_oid),
));
}
Section::Context(commits) => {
for commit in commits {
rows.push(Row::info(
RowKind::Context {
short_hash: commit.short_hash.clone(),
date: commit.date.clone(),
message: commit.message.clone(),
},
format!("ctx:{}", commit.short_hash),
));
}
}
}
}
rows
}
fn push_commit_rows(
rows: &mut Vec<Row>,
commits: &[crate::core::repo::CommitInfo],
dot_color: Option<usize>,
ids: &IdAllocator,
expanded: &HashSet<String>,
) {
for commit in commits {
let sid = ids.get_commit(commit.oid).to_string();
let key = commit.oid.to_string();
let is_expanded = expanded.contains(&key);
rows.push(Row {
kind: RowKind::Commit {
oid: commit.oid,
message: commit.message.clone(),
dot_color,
file_count: commit.files.len(),
},
sid: sid.clone(),
target: (commit.oid != PENDING_COMMIT_OID).then(|| commit.oid.to_string()),
key,
focusable: true,
expandable: !commit.files.is_empty(),
expanded: is_expanded,
});
if is_expanded {
for (i, file) in commit.files.iter().enumerate() {
let file_sid = crate::core::shortid::commit_file_id(&sid, i);
rows.push(Row {
kind: RowKind::CommitFile {
oid: commit.oid,
path: file.path.clone(),
index: file.index,
worktree: file.worktree,
on_branch: dot_color.is_some(),
},
sid: file_sid.clone(),
target: Some(file_sid.clone()),
key: commit_file_key(commit.oid, i),
focusable: true,
expandable: false,
expanded: false,
});
}
}
}
}
#[cfg(test)]
#[path = "status_tree_test.rs"]
mod tests;