use std::collections::HashMap;
use serde::Serialize;
use crate::core::graph::{self, FileGroup, Section};
use crate::core::repo::{self, CommitInfo, ContextCommit, FileChange, RemoteStatus, UpstreamInfo};
use crate::core::shortid::{self, IdAllocator};
const SCHEMA: u32 = 1;
#[derive(Serialize, Debug)]
pub struct StatusGraph {
pub schema: u32,
pub integration_branch: String,
pub cwd_prefix: String,
pub local_changes: LocalChanges,
pub branches: Vec<BranchGroup>,
pub loose_commits: Vec<Commit>,
pub upstream: Upstream,
pub context_commits: Vec<ContextEntry>,
}
#[derive(Serialize, Debug)]
pub struct LocalChanges {
pub id: String,
pub files: Vec<WorkingFile>,
}
#[derive(Serialize, Debug)]
pub struct WorkingFile {
pub id: String,
pub path: String,
pub index: char,
pub worktree: char,
pub state: FileState,
}
#[derive(Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FileState {
Conflicted,
Tracked,
Untracked,
}
#[derive(Serialize, Debug)]
pub struct BranchGroup {
pub names: Vec<BranchName>,
pub stacked_on: Option<String>,
pub stacked_on_hidden: bool,
pub commits: Vec<Commit>,
}
#[derive(Serialize, Debug)]
pub struct BranchName {
pub id: String,
pub name: String,
pub remote: Option<Remote>,
}
#[derive(Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Remote {
Synced,
Different,
Gone,
}
#[derive(Serialize, Debug)]
pub struct Commit {
pub id: String,
pub hash: String,
pub oid: String,
pub subject: String,
pub change_id: Option<String>,
pub files: Vec<CommitFile>,
}
#[derive(Serialize, Debug)]
pub struct CommitFile {
pub id: String,
pub path: String,
pub index: char,
pub worktree: char,
}
#[derive(Serialize, Debug)]
pub struct Upstream {
pub label: String,
pub base_hash: String,
pub base_oid: String,
pub base_subject: String,
pub base_date: String,
pub commits_ahead: usize,
}
#[derive(Serialize, Debug)]
pub struct ContextEntry {
pub hash: String,
pub date: String,
pub subject: String,
}
#[derive(Debug, Default, Clone)]
pub struct StackEdge {
pub below: Option<String>,
pub below_hidden: bool,
}
pub fn build(
sections: &[Section],
integration_branch: &str,
stacks: &HashMap<String, StackEdge>,
ids: &IdAllocator,
cwd_prefix: &str,
) -> StatusGraph {
let mut local_changes = LocalChanges {
id: ids.get_unstaged().to_string(),
files: Vec::new(),
};
let mut branches = Vec::new();
let mut loose_commits = Vec::new();
let mut context_commits = Vec::new();
let mut upstream = None;
for section in sections {
match section {
Section::WorkingChanges(changes) => {
local_changes.files = changes
.iter()
.map(|f| working_file(f, ids, cwd_prefix))
.collect();
}
Section::Branch { names, commits } => {
let edge = names
.first()
.and_then(|(n, _)| stacks.get(n))
.cloned()
.unwrap_or_default();
branches.push(BranchGroup {
names: names.iter().map(|(n, r)| branch_name(n, r, ids)).collect(),
stacked_on: edge.below,
stacked_on_hidden: edge.below_hidden,
commits: commits.iter().map(|c| commit(c, ids, cwd_prefix)).collect(),
});
}
Section::Loose(commits) => {
loose_commits.extend(commits.iter().map(|c| commit(c, ids, cwd_prefix)));
}
Section::Context(entries) => {
context_commits.extend(entries.iter().map(context_entry));
}
Section::Upstream(up) => upstream = Some(upstream_dto(up)),
}
}
StatusGraph {
schema: SCHEMA,
integration_branch: integration_branch.to_string(),
cwd_prefix: cwd_prefix.to_string(),
local_changes,
branches,
loose_commits,
upstream: upstream.expect("build_sections always emits the upstream"),
context_commits,
}
}
fn working_file(f: &FileChange, ids: &IdAllocator, cwd_prefix: &str) -> WorkingFile {
WorkingFile {
id: ids.get_file(&f.path).to_string(),
path: repo::cwd_relative_path(&f.path, cwd_prefix),
index: f.index,
worktree: f.worktree,
state: file_state(f),
}
}
fn file_state(f: &FileChange) -> FileState {
match graph::file_group(f.index, f.worktree) {
FileGroup::Conflicted => FileState::Conflicted,
FileGroup::Tracked => FileState::Tracked,
FileGroup::Untracked => FileState::Untracked,
}
}
fn branch_name(name: &str, remote: &Option<RemoteStatus>, ids: &IdAllocator) -> BranchName {
BranchName {
id: ids.get_branch(name).to_string(),
name: name.to_string(),
remote: remote.as_ref().map(|r| match r {
RemoteStatus::Synced => Remote::Synced,
RemoteStatus::Different => Remote::Different,
RemoteStatus::Gone => Remote::Gone,
}),
}
}
fn commit(c: &CommitInfo, ids: &IdAllocator, cwd_prefix: &str) -> Commit {
let sid = ids.get_commit(c.oid);
Commit {
id: sid.to_string(),
hash: c.short_id.clone(),
oid: c.oid.to_string(),
subject: c.message.clone(),
change_id: c.change_id.clone(),
files: c
.files
.iter()
.enumerate()
.map(|(i, f)| CommitFile {
id: shortid::commit_file_id(sid, i),
path: repo::cwd_relative_path(&f.path, cwd_prefix),
index: f.index,
worktree: f.worktree,
})
.collect(),
}
}
fn upstream_dto(up: &UpstreamInfo) -> Upstream {
Upstream {
label: up.label.clone(),
base_hash: up.base_short_id.clone(),
base_oid: up.merge_base_oid.to_string(),
base_subject: up.base_message.clone(),
base_date: up.base_date.clone(),
commits_ahead: up.commits_ahead,
}
}
fn context_entry(c: &ContextCommit) -> ContextEntry {
ContextEntry {
hash: c.short_hash.clone(),
date: c.date.clone(),
subject: c.message.clone(),
}
}
#[cfg(test)]
#[path = "status_json_test.rs"]
mod tests;