use crate::config::Config;
use crate::error::{Result, TixError};
use crate::storage::Store;
use crate::sync::{self, Tool};
use crate::triage::import_policy::ImportPolicy;
pub fn run_with_policy(
config: &Config,
store: &Store,
target: &str,
policy: ImportPolicy,
) -> Result<()> {
let report = run_core_with_policy(config, store, Some(target), policy)?;
print!("{report}");
Ok(())
}
pub fn run_core(config: &Config, store: &Store, target: Option<&str>) -> Result<String> {
run_core_with_policy(config, store, target, ImportPolicy::default())
}
pub fn run_core_with_policy(
config: &Config,
store: &Store,
target: Option<&str>,
policy: ImportPolicy,
) -> Result<String> {
let target = target
.ok_or_else(|| TixError::InvalidInput("missing --target — use gitlab or linear".into()))?;
if target == "speckit" {
return crate::speckit::import_core(config, store, policy);
}
if target == "beads" {
return import_beads(config, store);
}
let tool = Tool::from_target(target)?;
let mut report: Vec<u8> = Vec::new();
let outcome = sync::import(store, tool, config, &mut report, policy)?;
let handled = outcome.handled;
if handled == 0 {
report.extend_from_slice(b"nothing to import\n");
}
Ok(String::from_utf8(report).expect("sync reports contain only UTF-8 text"))
}
fn import_beads(config: &Config, store: &Store) -> Result<String> {
use crate::sync::beads;
beads::ensure_writes_allowed(config)?;
let root =
Config::project_root(&store.issues_dir).unwrap_or_else(|| std::path::PathBuf::from("."));
let mut state = beads::load_state(&root);
let rows = beads::fetch(&crate::sync::ProcessRunner, config)?;
let resolution = crate::workflow::resolution(config, config.default_config(), "default");
let report = beads::import_into(store, config, &resolution, &rows, &mut state)?;
beads::save_state(&root, &state)?;
let mut out = String::new();
for diagnostic in &report.diagnostics {
out.push_str(&format!("{diagnostic}\n"));
}
if report.created + report.updated == 0 {
out.push_str("nothing to import\n");
} else {
out.push_str(&format!(
"imported {} bead(s): {} created{}, {} updated, {} skipped\n",
rows.len(),
report.created,
if report.foreign > 0 {
format!(" ({} beads-originated)", report.foreign)
} else {
String::new()
},
report.updated,
report.skipped
));
}
Ok(out)
}
#[cfg(test)]
mod tests {
#[allow(clippy::wildcard_imports)]
use super::*;
use std::fs;
use tempfile::TempDir;
fn make_store() -> (TempDir, Store) {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("issues");
fs::create_dir_all(&dir).unwrap();
let config_path = tmp.path().join("tix.toml");
let config = crate::config::Config {
issues_dir: dir.clone(),
..crate::config::Config::default()
};
config.save(&config_path).unwrap();
let config = crate::config::Config::load_with_warning(&config_path, |_| {}).unwrap();
(
tmp,
Store {
issues_dir: dir,
config,
},
)
}
#[test]
fn run_core_missing_export_section_is_the_frozen_config_error() {
let (tmp, store) = make_store();
let _ = tmp;
let err = run_core(&store.config, &store, Some("gitlab")).unwrap_err();
assert_eq!(
err.to_string(),
"config error: no `[export.gitlab]` section in tix.toml — run `tix init --with gitlab` or edit tix.toml"
);
}
#[test]
fn run_core_unknown_target_is_the_frozen_error() {
let (tmp, store) = make_store();
let _ = tmp;
let err = run_core(&store.config, &store, Some("foobar")).unwrap_err();
assert_eq!(
err.to_string(),
"invalid input: unknown --target 'foobar' — use gitlab or linear"
);
}
#[test]
fn run_core_missing_target_is_an_invalid_input_error() {
let (tmp, store) = make_store();
let _ = tmp;
let err = run_core(&store.config, &store, None).unwrap_err();
assert!(
matches!(err, TixError::InvalidInput(_)),
"missing target must be a structured InvalidInput error, got {err}"
);
}
#[test]
fn the_beads_target_survives_the_dispatch_gate() {
let (tmp, store) = make_store();
let _ = tmp;
let mut config = store.config.clone();
config.beads.binary = "tix-no-such-bd-binary".to_string();
let command = crate::cli::Command::Import {
target: "beads".to_string(),
triage_policy: None,
};
let err = crate::commands::dispatch(&command, &config, &store)
.expect_err("a `bd` that does not exist must fail");
assert!(
!err.to_string().contains("unknown --target"),
"`beads` must not be refused as an unknown tracker: {err}"
);
}
#[test]
fn the_beads_export_target_survives_the_dispatch_gate() {
let (tmp, store) = make_store();
let _ = tmp;
let command = crate::cli::Command::Export {
target: "beads".to_string(),
dry_run: true,
project: None,
cycle: None,
output: None,
verbose: false,
};
crate::commands::dispatch(&command, &store.config, &store)
.expect("`tix export --target beads --dry-run` must be accepted");
}
}