use std::path::{Path, PathBuf};
use balls::delivery_path::work_branch;
use crate::app::Snapshot;
use crate::binding::named_of;
use crate::files_view::{MAX_ENTRIES, Preview, classify};
use crate::git_tree::{file_patch, head_branch, numstat, rev_parse};
mod plan;
mod render;
pub(crate) mod wire;
pub use render::render;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Churn {
Text { added: u64, removed: u64 },
Binary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChurn {
pub path: String,
pub churn: Churn,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Unreadable,
Absent {
target: String,
source: String,
missing: Vec<String>,
},
Diff {
target: String,
source: String,
target_oid: String,
source_oid: String,
files: Vec<FileChurn>,
truncated: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attempt {
pub project: PathBuf,
pub ball_id: String,
pub change: Change,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkFile {
pub ball: String,
pub path: String,
}
impl Attempt {
pub fn range(&self) -> Option<String> {
match &self.change {
Change::Unreadable => None,
Change::Absent { target, source, .. } | Change::Diff { target, source, .. } => {
Some(format!("{target}..{source}"))
}
}
}
}
pub fn read(snap: &Snapshot, workspace: &Path) -> Vec<Attempt> {
let Some(name) = named_of(&snap.workspaces, workspace) else {
return Vec::new();
};
plan::plans(&snap.balls_by_project, &name)
.into_iter()
.map(resolve)
.collect()
}
fn resolve(plan: plan::Plan) -> Attempt {
let source = work_branch(&plan.ball_id);
let attempt = |change| Attempt {
project: plan.project.clone(),
ball_id: plan.ball_id.clone(),
change,
};
let Ok(Some(head)) = head_branch(&plan.project) else {
return attempt(Change::Unreadable);
};
let target = plan.target_ball.as_deref().map_or(head, work_branch);
let resolved = |spec: &str| rev_parse(&plan.project, spec).ok().flatten();
let (Some(target_oid), Some(source_oid)) = (resolved(&target), resolved(&source)) else {
let missing = [&target, &source]
.into_iter()
.filter(|spec| resolved(spec).is_none())
.cloned()
.collect();
return attempt(Change::Absent {
target,
source,
missing,
});
};
let Ok(out) = numstat(&plan.project, &format!("{target}..{source}")) else {
return attempt(Change::Unreadable);
};
let mut files = plan::parse_numstat(&out);
let truncated = files.len() > MAX_ENTRIES;
files.truncate(MAX_ENTRIES);
attempt(Change::Diff {
target,
source,
target_oid,
source_oid,
files,
truncated,
})
}
pub fn patch(attempts: &[Attempt], file: &WorkFile) -> Option<Preview> {
let attempt = attempts.iter().find(|a| a.ball_id == file.ball)?;
let range = attempt.range()?;
if matches!(attempt.change, Change::Absent { .. }) {
return None;
}
let bytes = file_patch(&attempt.project, &range, &file.path).unwrap_or_default();
let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
Some(classify(&bytes, size))
}
#[cfg(test)]
mod tests;