use crate::Paths;
use anyhow::{bail, Result};
use clap::Args;
use linkmarks_core::dedupe as core_dedupe;
use linkmarks_core::store;
use linkmarks_core::traits::BookmarkSource;
use std::path::PathBuf;
#[derive(Args, Debug)]
pub struct DedupeArgs {
#[arg(long, default_value = "store")]
pub source: String,
#[arg(long)]
pub path: Option<PathBuf>,
#[arg(long)]
pub apply: bool,
#[arg(long)]
pub refresh_canonical: bool,
}
pub fn run(args: DedupeArgs, format: crate::Format, paths: Paths) -> Result<i32> {
let bookmarks = match args.source.as_str() {
"store" => {
if !paths.store.exists() {
bail!(
"store not found at {}; run `linkmarks init` first",
paths.store.display()
);
}
let s = store::open(&paths.store)?;
let mut all = Vec::new();
let mut offset = 0usize;
loop {
let page = s.list(500, offset)?;
if page.is_empty() {
break;
}
let page_len = page.len();
offset += page_len;
all.extend(page);
if page_len < 500 {
break;
}
}
all
}
"chrome" => {
let path = args
.path
.clone()
.ok_or_else(|| anyhow::anyhow!("--path is required for --source=chrome"))?;
let src = linkmarks_bridge_chromium::ChromiumSource::open(&path)?;
src.list()?
}
other => bail!("unsupported --source '{other}' (try `store` or `chrome`)"),
};
let (canonical, report) = core_dedupe(&bookmarks);
match format {
crate::Format::Table => {
println!(
"canonical_count={} merged={} conflicts={}",
report.canonical_count,
report.merged_count,
report.conflicts.len()
);
for c in &report.conflicts {
println!(
"- {}\n chosen={}\n conflicting={:?}\n differing_fields={:?}",
c.canonical_url, c.chosen_id, c.conflicting_ids, c.differing_fields
);
}
}
crate::Format::Json => {
let mut out = serde_json::to_string_pretty(&report)?;
out.push('\n');
print!("{out}");
}
crate::Format::Yaml => {
let value = serde_yaml::to_string(&report)?;
print!("{value}");
}
}
if !args.apply {
eprintln!("(dry-run; pass --apply to write)");
if report.conflicts.is_empty() {
return Ok(crate::exit_codes::OK);
}
return Ok(crate::exit_codes::DEDUPE_CONFLICTS);
}
if args.source == "store" {
let mut s = store::open(&paths.store)?;
let mut rewritten = 0usize;
for bm in &canonical {
if let Err(e) = s.upsert(bm) {
tracing::warn!(error = %e, url = %bm.original_url, "dedupe upsert failed");
} else {
rewritten += 1;
}
}
eprintln!(
"(apply mode: {} canonical records, {} rows upserted back into the store)",
canonical.len(),
rewritten
);
} else {
eprintln!(
"(apply mode: {} canonical records; chrome source has no on-disk write)",
canonical.len()
);
}
let _ = args.refresh_canonical;
if report.conflicts.is_empty() {
Ok(crate::exit_codes::OK)
} else {
Ok(crate::exit_codes::DEDUPE_CONFLICTS)
}
}