use crate::analyze::Analysis;
use crate::commands::{failed_hashes, InfoShow};
use crate::config::{Project, ProjectId, Size};
use crate::errors::{Context as _, Result};
use crate::github::Changes;
use crate::mono::ChangelogEntry;
use crate::mono::{Mono, Plan};
use crate::state::StateRead;
use crate::template::{construct_changelog_html, read_template};
use serde_json::json;
use std::path::{Path, PathBuf};
pub struct Output {}
impl Default for Output {
fn default() -> Output { Output::new() }
}
impl Output {
pub fn new() -> Output { Output {} }
pub fn check(&self) -> CheckOutput { CheckOutput::new() }
pub fn projects(&self, wide: bool, vers_only: bool) -> ProjOutput { ProjOutput::new(wide, vers_only) }
pub fn info(&self, show: InfoShow) -> ProjOutput { ProjOutput::info(show) }
pub fn diff(&self) -> DiffOutput { DiffOutput::new() }
pub fn files(&self) -> FilesOutput { FilesOutput::new() }
pub fn changes(&self) -> ChangesOutput { ChangesOutput::new() }
pub fn plan(&self) -> PlanOutput { PlanOutput::new() }
pub fn release(&self) -> ReleaseOutput { ReleaseOutput::new() }
pub fn resume(&self) -> ResumeOutput { ResumeOutput::new() }
}
pub struct CheckOutput {}
impl Default for CheckOutput {
fn default() -> CheckOutput { CheckOutput::new() }
}
impl CheckOutput {
pub fn new() -> CheckOutput { CheckOutput {} }
pub fn write_done(&mut self) -> Result<()> { Ok(()) }
pub fn commit(&mut self) -> Result<()> {
println!("Check complete.");
Ok(())
}
}
pub struct ResumeOutput {}
impl Default for ResumeOutput {
fn default() -> ResumeOutput { ResumeOutput::new() }
}
impl ResumeOutput {
pub fn new() -> ResumeOutput { ResumeOutput {} }
pub fn write_done(&mut self) -> Result<()> { Ok(()) }
pub fn commit(&mut self) -> Result<()> {
println!("Release complete.");
Ok(())
}
}
pub struct ProjOutput {
wide: bool,
vers_only: bool,
proj_lines: Vec<ProjLine>,
info_only: bool,
show: InfoShow
}
impl ProjOutput {
pub fn new(wide: bool, vers_only: bool) -> ProjOutput {
ProjOutput { show: InfoShow::new(), info_only: false, wide, vers_only, proj_lines: Vec::new() }
}
pub fn info(show: InfoShow) -> ProjOutput {
ProjOutput { info_only: true, show, wide: false, vers_only: false, proj_lines: Vec::new() }
}
pub fn write_projects<I: Iterator<Item = Result<ProjLine>>>(&mut self, lines: I) -> Result<()> {
self.proj_lines = lines.collect::<Result<_>>()?;
Ok(())
}
pub fn write_project(&mut self, line: ProjLine) -> Result<()> {
self.proj_lines = vec![line];
Ok(())
}
pub fn commit(&mut self) -> Result<()> {
let name_width = self.proj_lines.iter().map(|l| l.name.len()).max().unwrap_or(0);
if self.info_only {
let val = json!(self
.proj_lines
.iter()
.map(|line| {
let root = line.root.as_deref().unwrap_or(".");
let mut val = json!({});
if self.show.name() {
val["name"] = json!(line.name);
}
if self.show.root() {
val["root"] = json!(root);
}
if self.show.id() {
val["id"] = json!(line.id);
}
if self.show.full_version() {
val["full_version"] = json!(line.full_version);
}
if self.show.tag_prefix() {
val["tag_prefix"] = json!(line.tag_prefix);
}
if self.show.version() {
val["version"] = json!(line.version);
}
val
})
.collect::<Vec<_>>());
println!("{}", serde_json::to_string(&val)?);
} else {
for line in &self.proj_lines {
if self.vers_only {
println!("{}", line.version);
} else if self.wide {
println!("{:>6}. {:width$} : {}", line.id, line.name, line.version, width = name_width);
} else {
println!("{:width$} : {}", line.name, line.version, width = name_width);
}
}
}
Ok(())
}
}
pub struct ProjLine {
pub id: ProjectId,
pub name: String,
pub tag_prefix: Option<String>,
pub tag_prefix_separator: String,
pub version: String,
pub full_version: Option<String>,
pub root: Option<String>
}
impl ProjLine {
pub fn from<S: StateRead>(p: &Project, read: &S) -> Result<ProjLine> {
let id = p.id();
let name = p.name().to_string();
let version = p.get_value(read)?;
let tag_prefix = p.tag_prefix().clone();
let tag_prefix_separator = p.tag_prefix_separator().to_string();
let full_version = p.full_version(&version);
let root = p.root().cloned();
Ok(ProjLine { id: id.clone(), name, tag_prefix, tag_prefix_separator, version, full_version, root })
}
pub fn from_version(p: &Project, vers: String) -> Result<ProjLine> {
let id = p.id();
let name = p.name().to_string();
let version = vers;
let tag_prefix = p.tag_prefix().clone();
let tag_prefix_separator = p.tag_prefix_separator().to_string();
let full_version = p.full_version(&version);
let root = p.root().cloned();
Ok(ProjLine { id: id.clone(), name, tag_prefix, tag_prefix_separator, version, full_version, root })
}
}
pub struct DiffOutput {
analysis: Option<Analysis>
}
impl Default for DiffOutput {
fn default() -> DiffOutput { DiffOutput::new() }
}
impl DiffOutput {
pub fn new() -> DiffOutput { DiffOutput { analysis: None } }
pub fn write_analysis(&mut self, analysis: Analysis) -> Result<()> {
self.analysis = Some(analysis);
Ok(())
}
pub fn commit(&mut self) -> Result<()> {
if let Some(analysis) = &self.analysis {
println_analysis(analysis);
}
Ok(())
}
}
fn println_analysis(analysis: &Analysis) {
if !analysis.older().is_empty() {
println!("Removed projects:");
for mark in analysis.older() {
println!(" {} : {}", mark.name(), mark.mark());
}
}
if !analysis.newer().is_empty() {
println!("New projects:");
for mark in analysis.newer() {
println!(" {} : {}", mark.name(), mark.mark());
}
}
if analysis.changes().iter().any(|c| c.value().is_some()) {
println!("Changed versions:");
for change in analysis.changes().iter().filter(|c| c.value().is_some()) {
print!(" {}", change.new_mark().name());
if let Some((o, _)) = change.name().as_ref() {
print!(" (was \"{}\")", o);
}
if let Some((o, n)) = change.value().as_ref() {
print!(" : {} -> {}", o, n);
} else {
print!(" : {}", change.new_mark().mark());
}
println!();
}
}
if analysis.changes().iter().any(|c| c.value().is_none()) {
println!("Unchanged versions:");
for change in analysis.changes().iter().filter(|c| c.value().is_none()) {
print!(" {}", change.new_mark().name());
if let Some((o, _)) = change.name().as_ref() {
print!(" (was \"{}\")", o);
}
print!(" : {}", change.new_mark().mark());
println!();
}
}
}
pub struct FilesOutput {
files: Vec<(String, String)>
}
impl Default for FilesOutput {
fn default() -> FilesOutput { FilesOutput::new() }
}
impl FilesOutput {
pub fn new() -> FilesOutput { FilesOutput { files: Vec::new() } }
pub fn write_files(&mut self, files: impl Iterator<Item = Result<(String, String)>>) -> Result<()> {
self.files = files.collect::<std::result::Result<_, _>>()?;
Ok(())
}
pub fn commit(&mut self) -> Result<()> {
for (key, path) in &self.files {
println!("{} : {}", key, path);
}
Ok(())
}
}
pub struct ChangesOutput {
changes: Option<Changes>
}
impl Default for ChangesOutput {
fn default() -> ChangesOutput { ChangesOutput::new() }
}
impl ChangesOutput {
pub fn new() -> ChangesOutput { ChangesOutput { changes: None } }
pub fn write_changes(&mut self, changes: Changes) -> Result<()> {
self.changes = Some(changes);
Ok(())
}
pub fn commit(&mut self) {
if let Some(changes) = &self.changes {
println_changes(changes)
} else {
println!("No changes.");
}
}
}
fn println_changes(changes: &Changes) {
println!("\ngroups:");
for g in changes.groups().values() {
let head_oid = g.head_oid().as_ref().map(|o| o.to_string()).unwrap_or_else(|| "<not found>".to_string());
println!(" {}: {} ({} -> {})", g.number(), g.head_ref(), g.base_oid(), head_oid);
println!(" commits:");
for cmt in g.commits() {
println!(" {}", cmt.id());
}
println!(" excludes:");
for cmt in g.excludes() {
println!(" {}", cmt);
}
}
println!("\ncommits:");
for oid in changes.commits() {
println!(" {}", oid);
}
}
pub struct PlanOutput {
plan: Option<Plan>,
id: Option<ProjectId>,
template: Option<String>,
orig_dir: Option<PathBuf>
}
impl Default for PlanOutput {
fn default() -> PlanOutput { PlanOutput::new() }
}
impl PlanOutput {
pub fn new() -> PlanOutput { PlanOutput { plan: None, id: None, template: None, orig_dir: None } }
pub fn write_plan(
&mut self, plan: Plan, id: Option<ProjectId>, template: Option<&str>, orig_dir: &Path
) -> Result<()> {
self.plan = Some(plan);
self.id = id;
self.template = template.map(|s| s.to_string());
self.orig_dir = Some(orig_dir.to_path_buf());
Ok(())
}
pub async fn commit(&mut self, mono: &Mono) -> Result<()> {
if let Some(plan) = &self.plan {
self.println_plan(plan, mono).await
} else {
println!("No plan.");
Ok(())
}
}
async fn println_plan(&self, plan: &Plan, mono: &Mono) -> Result<()> {
self.println_plan_incrs(plan, mono).await?;
self.println_plan_ineff(plan);
Ok(())
}
async fn println_plan_incrs(&self, plan: &Plan, mono: &Mono) -> Result<()> {
if self.template.is_some() {
return self.println_template_plan(plan, mono).await;
}
if plan.incrs().is_empty() {
println!("(No projects)");
return Ok(());
}
for (id, (size, changelog)) in plan.incrs() {
if let Some(self_id) = self.id.as_ref() {
if id != self_id {
continue;
}
}
let curt_proj = mono.get_project(id).unwrap();
println!("{} : {}", curt_proj.name(), size);
let curt_config = mono.config();
let prev_config = curt_config.slice_to_prev(mono.repo())?;
let prev_vers = prev_config.get_value(id).with_context(|| format!("Unable to find prev {} value.", id))?;
let curt_vers = curt_config
.get_value(id)
.with_context(|| format!("Unable to find project {} value.", id))?
.unwrap_or_else(|| panic!("No such project {}.", id));
if let Some(prev_vers) = prev_vers {
if size.is_failure() {
println!(" ! Non-parseable conventional commits: {}", failed_hashes(plan));
} else if size != &Size::Empty {
let target = size.apply(&prev_vers)?;
if Size::less_than(&curt_vers, &target)? {
if curt_proj.verify_restrictions(&target).is_err() {
println!(" ! Illegal size change for restricted project {}.", curt_proj.id());
}
} else if curt_proj.verify_restrictions(&curt_vers).is_err() {
println!(" ! Illegal size change for restricted project {}.", curt_proj.id());
}
}
}
for entry in changelog.entries() {
match entry {
ChangelogEntry::Pr(pr, size) => {
if !pr.commits().iter().any(|c| c.included()) {
continue;
}
if pr.number() == 0 {
println!(" Other commits : {}", size);
} else {
println!(" PR {} : {}", pr.number(), size);
}
for c in pr.commits().iter().filter(|c| c.included()) {
let symbol = if c.duplicate() {
"."
} else if c.applies() {
"*"
} else {
" "
};
println!(" {} commit {} ({}) : {}", symbol, &c.oid()[.. 7], c.size(), c.message().trim());
}
}
ChangelogEntry::Dep(proj_id, proj_name) => {
println!(" Depends on: {} ({})", proj_name, proj_id);
}
}
}
}
Ok(())
}
fn println_plan_ineff(&self, plan: &Plan) {
for pr in plan.ineffective() {
if !pr.commits().iter().any(|c| c.included()) {
continue;
}
if pr.number() == 0 {
println!(" Unapplied commits");
} else {
println!(" Unapplied PR {}", pr.number());
}
for c in pr.commits().iter().filter(|c| c.included()) {
let symbol = if c.duplicate() {
"."
} else if c.applies() {
"*"
} else {
" "
};
println!(" {} commit {} ({}) : {}", symbol, &c.oid()[.. 7], c.size(), c.message());
}
}
}
async fn println_template_plan(&self, plan: &Plan, mono: &Mono) -> Result<()> {
let orig_dir = self.orig_dir.as_ref().ok_or_else(|| bad!("No orig dir for template format."))?;
let tmpl = self.template.as_ref().ok_or_else(|| bad!("No template for template format."))?;
let template = read_template(tmpl, Some(orig_dir), false).await?;
for (id, (_, changelog)) in plan.incrs() {
if let Some(self_id) = self.id.as_ref() {
if id != self_id {
continue;
}
}
let curt_config = mono.config();
let curt_vers = curt_config
.get_value(id)
.with_context(|| format!("Unable to find project {} value.", id))?
.unwrap_or_else(|| panic!("No such project {}.", id));
let proj = curt_config.get_project(id).ok_or_else(|| bad!("No such project ID {}", id))?;
let proj = ProjLine::from_version(proj, curt_vers.clone())?;
let html = construct_changelog_html(changelog, proj, &curt_vers, "".to_string(), template)?;
println!("{}", html);
break;
}
Ok(())
}
}
pub struct ReleaseOutput {
result: ReleaseResult
}
impl Default for ReleaseOutput {
fn default() -> ReleaseOutput { ReleaseOutput::new() }
}
impl ReleaseOutput {
pub fn new() -> ReleaseOutput { ReleaseOutput { result: ReleaseResult::Empty } }
pub fn write_empty(&mut self) -> Result<()> {
self.result = ReleaseResult::Empty;
Ok(())
}
pub fn write_logged(&mut self, path: PathBuf) { self.result.append_logged(path); }
pub fn write_done(&mut self) { self.result.append_done(); }
pub fn write_commit(&mut self) { self.result.append_commit(); }
pub fn write_pause(&mut self) { self.result.append_pause(); }
pub fn write_dry(&mut self) { self.result.append_dry(); }
pub fn write_wrote_changelogs(&mut self) { self.result.append_wrote_channgelogs(); }
pub fn write_changed(&mut self, name: String, prev: String, curt: String, targ: String) {
self.result.append_changed(name, prev, curt, targ);
}
pub fn write_forward(&mut self, all: bool, name: String, prev: String, curt: String, targ: String) {
self.result.append_forward(all, name, prev, curt, targ);
}
pub fn write_no_change(&mut self, all: bool, locked: bool, name: String, prev: Option<String>, curt: String) {
self.result.append_no_change(all, locked, name, prev, curt);
}
pub fn write_new(&mut self, all: bool, name: String, curt: String) { self.result.append_new(all, name, curt); }
pub fn commit(&mut self) { self.result.commit(); }
}
enum ReleaseResult {
Empty,
Wrote(WroteReleases)
}
impl ReleaseResult {
fn append_logged(&mut self, path: PathBuf) { self.append(ReleaseEvent::Logged(path)); }
fn append_done(&mut self) { self.append(ReleaseEvent::Done); }
fn append_commit(&mut self) { self.append(ReleaseEvent::Commit); }
fn append_pause(&mut self) { self.append(ReleaseEvent::Pause); }
fn append_dry(&mut self) { self.append(ReleaseEvent::Dry); }
fn append_wrote_channgelogs(&mut self) { self.append(ReleaseEvent::WroteChangelogs); }
fn append_changed(&mut self, name: String, prev: String, curt: String, targ: String) {
self.append(ReleaseEvent::Changed(name, prev, curt, targ));
}
fn append_forward(&mut self, all: bool, name: String, prev: String, curt: String, targ: String) {
self.append(ReleaseEvent::Forward(all, name, prev, curt, targ));
}
fn append_no_change(&mut self, all: bool, locked: bool, name: String, prev: Option<String>, curt: String) {
self.append(ReleaseEvent::NoChange(all, locked, name, prev, curt));
}
fn append_new(&mut self, all: bool, name: String, curt: String) { self.append(ReleaseEvent::New(all, name, curt)); }
fn append(&mut self, ev: ReleaseEvent) {
match self {
ReleaseResult::Empty => {
let mut releases = WroteReleases::new();
releases.push(ev);
*self = ReleaseResult::Wrote(releases);
}
ReleaseResult::Wrote(releases) => {
releases.push(ev);
}
}
}
fn commit(&mut self) {
match self {
ReleaseResult::Empty => println!("No release: no projects."),
ReleaseResult::Wrote(w) => w.commit()
}
}
}
struct WroteReleases {
events: Vec<ReleaseEvent>
}
impl WroteReleases {
pub fn new() -> WroteReleases { WroteReleases { events: Vec::new() } }
pub fn push(&mut self, path: ReleaseEvent) { self.events.push(path); }
pub fn commit(&mut self) {
for ev in &mut self.events {
ev.commit();
}
}
}
enum ReleaseEvent {
Logged(PathBuf),
Changed(String, String, String, String),
Forward(bool, String, String, String, String),
NoChange(bool, bool, String, Option<String>, String),
New(bool, String, String),
Commit,
Pause,
Dry,
WroteChangelogs,
Done
}
impl ReleaseEvent {
fn commit(&mut self) {
match self {
ReleaseEvent::Logged(p) => println!("Wrote changelog at {}.", p.to_string_lossy()),
ReleaseEvent::Done => println!("Release complete."),
ReleaseEvent::Commit => println!("Changes committed."),
ReleaseEvent::Pause => println!("Paused for commit: use --resume to continue."),
ReleaseEvent::Dry => println!("Dry run: no actual changes."),
ReleaseEvent::WroteChangelogs => println!("Changelogs only: only changelogs written."),
ReleaseEvent::Changed(name, prev, curt, targ) => {
if prev == curt {
println!(" {} : {} -> {}", name, prev, targ);
} else {
println!(" {} : {} -> {} instead of {}", name, prev, targ, curt);
}
}
ReleaseEvent::NoChange(all, locked, name, prev, curt) => {
if *all {
let lockmsg = if *locked { " (locked)" } else { "" };
if let Some(prev) = prev {
if prev == curt {
println!(" {} : untouched at {}{}", name, curt, lockmsg);
} else {
println!(" {} : untouched: {} -> {}{}", name, prev, curt, lockmsg);
}
} else {
println!(" {} : untouched non-existent at {}{}", name, curt, lockmsg);
}
}
}
ReleaseEvent::Forward(all, name, prev, curt, targ) => {
if *all {
if prev == curt {
println!(" {} : no change to {}", name, curt);
} else if curt == targ {
println!(" {} : no change: already {} -> {}", name, prev, curt);
} else {
println!(" {} : no change: {} -> {} exceeds {}", name, prev, curt, targ);
}
}
}
ReleaseEvent::New(all, name, curt) => {
if *all {
println!(" {} : no change: {} is new", name, curt);
}
}
}
}
}