///|
/// Git passes three temporary files. Refuse aliases/symlinks; only replace
/// OURS after a complete validated merge and successful temporary-file write.
async fn git_driver(args : Array[String]) -> Int {
guard args.length() >= 6 && args[4] == "--key" else {
fail(
"Expected: git-driver BASE OURS THEIRS --key NAME[,NAME] [--row-order ORDER] [--columns FILE] [--rows FILE] [--column-evolution FILE] [--format csv|tsv]",
)
}
let mut order : @tablemerge.RowOrder? = None
let mut columns : String? = None
let mut rows_path : String? = None
let mut format : Format? = None
let mut evolution_path : String? = None
let mut i = 6
while i < args.length() {
if i + 1 >= args.length() {
fail("Git driver options require a value")
}
match args[i] {
"--format" if format == None => format = Some(Format::parse(args[i + 1]))
"--row-order" if order == None =>
order = Some(@tablemerge.RowOrder::parse(args[i + 1]))
"--columns" if columns == None => columns = Some(args[i + 1])
"--rows" if rows_path == None => rows_path = Some(args[i + 1])
"--column-evolution" if evolution_path == None =>
evolution_path = Some(args[i + 1])
_ => fail("Unknown or duplicate Git driver option: \{args[i]}")
}
i += 2
}
let lineage = match columns {
None => []
Some(path) => @tablemerge.parse_column_lineage(read_text(path))
}
let rows = match rows_path {
None => []
Some(path) => @tablemerge.parse_row_lineage(read_text(path))
}
let evolution = match evolution_path {
None => []
Some(path) => @tablemerge.parse_column_evolution(read_text(path))
}
let paths = args[1:4].to_owned()
let real = []
for path in paths {
if @fs.kind(path, follow_symlink=false) != Regular {
fail("Git driver inputs must be regular files, not symlinks: \{path}")
}
real.push(@fs.realpath(path))
}
if real[0] == real[1] || real[0] == real[2] || real[1] == real[2] {
fail("Git driver requires three distinct input files")
}
let transport = format.unwrap_or(Csv)
let base = transport.read(read_text(paths[0]))
let ours = transport.read(read_text(paths[1]))
let theirs = transport.read(read_text(paths[2]))
let keys = args[5].split(",").map(s => s.to_owned()).to_array()
let plan = @tablemerge.merge(
base,
ours,
theirs,
key=keys,
row_order=order.unwrap_or(@tablemerge.Lexical),
column_lineage=lineage,
row_lineage=rows,
column_evolution=evolution,
)
if !plan.conflicts().is_empty() {
@stdio.stderr.write(plan.report().stringify() + "\n")
@stdio.stderr.write(
"Table merge requires review. OURS is unchanged; Git index stages retain all three inputs. See docs/git-driver.md.\n",
)
return 1
}
let csv = transport.write(plan.finish().table())
let temp_path = paths[1] + ".mntablemerge.tmp"
// Exclusive creation never truncates an existing sidecar. It is beside OURS
// so replacement stays on the same filesystem.
let file = @fs.open(temp_path, mode=WriteOnly, create_mode=CreateNew)
errdefer {
file.close()
@fs.remove(temp_path) catch {
_ => ()
}
}
file.write(csv)
file.sync()
file.close()
@fs.rename(temp_path, paths[1], replace=true)
0
}