use crate::ids::WorkspaceId;
use std::fs;
use std::path::Path;
use anyhow::{Result, bail};
use aven_core::db::Database;
use serde::Serialize;
use crate::cli::{ConflictCommand, ConflictSubcommand};
use crate::input::read_required_text;
use crate::operations::ConflictDetail;
use crate::refs::DisplayRefContext;
use crate::render::{print_json_pretty, print_multiline_block, print_text_diff, quote};
use crate::types::Task;
use crate::workspaces::Workspace;
#[derive(Serialize)]
struct ConflictListJsonItem {
r#ref: String,
task_id: String,
title: String,
project: String,
field: String,
variants: Vec<String>,
}
#[derive(Serialize)]
struct ConflictDetailJson {
r#ref: String,
task_id: String,
field: String,
variants: Vec<ConflictVariantJson>,
}
#[derive(Serialize)]
struct ConflictVariantJson {
token: String,
value: String,
}
pub(crate) async fn cmd_conflict(
database: &Database,
workspace: &Workspace,
args: ConflictCommand,
) -> Result<()> {
match args.command {
ConflictSubcommand::List {
project,
field,
limit,
json,
} => {
let project_key =
resolve_conflict_project_filter(database, &workspace.id, project).await?;
let mut items = database
.list_conflicts(workspace, project_key.as_deref(), field.as_deref())
.await?;
if let Some(limit) = limit {
items.truncate(limit);
}
let display_refs = database.display_ref_context(&workspace.id).await?;
if json {
let mut json_items = Vec::new();
for item in items {
json_items.push(ConflictListJsonItem {
r#ref: display_refs.display_ref_for_id(
&workspace.id,
&item.project_prefix,
&item.task_id,
),
task_id: item.task_id.to_string(),
title: item.title,
project: item.project_key,
field: item.field,
variants: vec![item.variant_a, item.variant_b],
});
}
print_json_pretty(&json_items)?;
} else {
for item in items {
print_conflict_list_item(&display_refs, workspace, item);
}
}
}
ConflictSubcommand::Show {
task_ref,
field,
json,
} => {
let task = database.resolve_task_ref(workspace, &task_ref).await?;
let details = database
.task_conflicts(workspace, &task.id, field.as_deref())
.await?;
if json {
let mut json_details = Vec::new();
for detail in details {
let local_value = database
.conflict_display_value(
&task.workspace_id,
&detail.field,
&detail.local_value,
)
.await?;
let remote_value = database
.conflict_display_value(
&task.workspace_id,
&detail.field,
&detail.remote_value,
)
.await?;
json_details.push(ConflictDetailJson {
r#ref: task_ref.clone(),
task_id: task.id.to_string(),
field: detail.field,
variants: vec![
ConflictVariantJson {
token: detail.variant_a,
value: local_value,
},
ConflictVariantJson {
token: detail.variant_b,
value: remote_value,
},
],
});
}
print_json_pretty(&json_details)?;
} else {
let display_refs = database.display_ref_context(&workspace.id).await?;
for detail in details {
print_conflict_detail(database, &display_refs, &task, detail).await?;
}
}
}
ConflictSubcommand::Diff { task_ref, field } => {
let task = database.resolve_task_ref(workspace, &task_ref).await?;
let detail = load_single_conflict_detail(database, workspace, &task, &field).await?;
print_text_diff("local", &detail.local_value, "remote", &detail.remote_value);
}
ConflictSubcommand::Export {
task_ref,
field,
dir,
} => {
let task = database.resolve_task_ref(workspace, &task_ref).await?;
fs::create_dir_all(&dir)?;
let detail = load_single_conflict_detail(database, workspace, &task, &field).await?;
export_conflict_variant(&dir, &detail.field, &detail.variant_a, &detail.local_value)?;
export_conflict_variant(&dir, &detail.field, &detail.variant_b, &detail.remote_value)?;
}
ConflictSubcommand::Resolve {
task_ref,
field,
use_variant,
value,
value_file,
value_stdin,
} => {
let task = database.resolve_task_ref(workspace, &task_ref).await?;
let value = if let Some(token) = use_variant {
database
.conflict_variant_value(workspace, &task.id, &field, &token)
.await?
} else {
read_required_text(value, value_file.as_deref(), value_stdin, "value")?
};
let outcome = database
.resolve_conflict(workspace, &task.id, &field, &value)
.await?;
let display_refs = database.display_ref_context(&workspace.id).await?;
println!(
"resolved {} field={}",
display_refs.display_ref(&outcome.task),
outcome.field
);
}
}
Ok(())
}
async fn resolve_conflict_project_filter(
database: &Database,
workspace_id: &WorkspaceId,
project: Option<String>,
) -> Result<Option<String>> {
if let Some(project) = project {
return Ok(Some(
database
.resolve_existing_project(workspace_id, &project)
.await?
.key,
));
}
Ok(None)
}
fn print_conflict_list_item(
display_refs: &DisplayRefContext,
workspace: &Workspace,
item: crate::operations::ConflictListItem,
) {
let display =
display_refs.display_ref_for_id(&workspace.id, &item.project_prefix, &item.task_id);
println!(
"{} conflict field={} variants={},{} title={}",
display,
item.field,
item.variant_a,
item.variant_b,
quote(&item.title)
);
}
async fn print_conflict_detail(
database: &Database,
display_refs: &DisplayRefContext,
task: &Task,
detail: ConflictDetail,
) -> Result<()> {
println!(
"conflict {} field={}",
display_refs.display_ref(task),
detail.field
);
let local_value = database
.conflict_display_value(&task.workspace_id, &detail.field, &detail.local_value)
.await?;
let remote_value = database
.conflict_display_value(&task.workspace_id, &detail.field, &detail.remote_value)
.await?;
println!("variant {}", detail.variant_a);
print_multiline_block("value", &local_value);
println!("variant {}", detail.variant_b);
print_multiline_block("value", &remote_value);
Ok(())
}
async fn load_single_conflict_detail(
database: &Database,
workspace: &Workspace,
task: &Task,
field: &str,
) -> Result<ConflictDetail> {
single_conflict(
database
.task_conflicts(workspace, &task.id, Some(field))
.await?,
&task.id,
field,
)
}
fn export_conflict_variant(dir: &Path, field: &str, variant: &str, value: &str) -> Result<()> {
let path = dir.join(format!("{field}-{variant}.md"));
fs::write(&path, value)?;
println!(
"exported variant={} path={}",
variant,
quote(&path.display().to_string())
);
Ok(())
}
pub(super) fn single_conflict(
details: Vec<ConflictDetail>,
task_id: &str,
field: &str,
) -> Result<ConflictDetail> {
let mut iter = details.into_iter();
let Some(detail) = iter.next() else {
bail!("error conflict-not-found task_id={task_id} field={field}");
};
if iter.next().is_some() {
bail!(
"error multiple-conflicts task_id={task_id} field={field} hint=\"use export to view all variants\""
);
}
Ok(detail)
}