use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
use patchworks::app::{PatchworksApp, StartupOptions};
use patchworks::cli::{self, OutputFormat};
use patchworks::db::snapshot::SnapshotStore;
#[derive(Debug, Parser)]
#[command(
name = "patchworks",
about = "Git-style diffs for SQLite databases.",
version,
after_help = "Run without a subcommand to launch the desktop GUI."
)]
struct Cli {
#[arg(long, hide = true)]
snapshot: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
files: Vec<PathBuf>,
}
#[derive(Debug, Subcommand)]
enum Command {
Inspect {
database: PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
},
Diff {
left: PathBuf,
right: PathBuf,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
},
Export {
left: PathBuf,
right: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
},
Snapshot {
#[command(subcommand)]
action: SnapshotAction,
},
}
#[derive(Debug, Subcommand)]
enum SnapshotAction {
Save {
database: PathBuf,
#[arg(long)]
name: Option<String>,
},
List {
#[arg(long)]
source: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
},
Delete {
id: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
enum Format {
Human,
Json,
}
impl From<Format> for OutputFormat {
fn from(format: Format) -> Self {
match format {
Format::Human => OutputFormat::Human,
Format::Json => OutputFormat::Json,
}
}
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let args = std::env::args()
.map(|arg| {
if arg == "-snapshot" {
"--snapshot".to_owned()
} else {
arg
}
})
.collect::<Vec<_>>();
let cli = Cli::parse_from(args);
if let Some(path) = cli.snapshot {
let store = SnapshotStore::new_default().context("create snapshot store")?;
let name = format!(
"{} snapshot",
path.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("database")
);
let snapshot = store
.save_snapshot(&path, &name)
.with_context(|| format!("save snapshot for {}", path.display()))?;
println!("Saved snapshot {} ({})", snapshot.name, snapshot.id);
return Ok(());
}
if let Some(command) = cli.command {
let mut stdout = std::io::stdout().lock();
let exit_code = match command {
Command::Inspect { database, format } => {
cli::run_inspect(&mut stdout, &database, format.into()).context("inspect failed")?
}
Command::Diff {
left,
right,
format,
} => cli::run_diff(&mut stdout, &left, &right, format.into()).context("diff failed")?,
Command::Export {
left,
right,
output,
} => {
if let Some(output_path) = output {
let file = std::fs::File::create(&output_path)
.with_context(|| format!("create output file {}", output_path.display()))?;
let mut writer = std::io::BufWriter::new(file);
cli::run_export(&mut writer, &left, &right).context("export failed")?
} else {
cli::run_export(&mut stdout, &left, &right).context("export failed")?
}
}
Command::Snapshot { action } => match action {
SnapshotAction::Save { database, name } => {
cli::run_snapshot_save(&mut stdout, &database, name.as_deref())
.context("snapshot save failed")?
}
SnapshotAction::List { source, format } => {
cli::run_snapshot_list(&mut stdout, source.as_deref(), format.into())
.context("snapshot list failed")?
}
SnapshotAction::Delete { id } => {
cli::run_snapshot_delete(&mut stdout, &id).context("snapshot delete failed")?
}
},
};
std::process::exit(exit_code);
}
let startup = match cli.files.as_slice() {
[] => StartupOptions::default(),
[one] => StartupOptions {
left: Some(one.clone()),
right: None,
},
[left, right] => StartupOptions {
left: Some(left.clone()),
right: Some(right.clone()),
},
_ => anyhow::bail!("expected at most two database file arguments"),
};
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"Patchworks",
native_options,
Box::new(move |_creation_context| {
let app = PatchworksApp::new(startup.clone())
.map_err(|error| -> Box<dyn std::error::Error + Send + Sync> { Box::new(error) })?;
Ok(Box::new(app))
}),
)
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
Ok(())
}