Something went wrong. Try again.
wip: was testing using yrs as a merge driver for git conflicts
Something went wrong. Try again.
5.1 kB · 152 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153//! `git yrs-merge init` — configure a git repo for Yrs merge driver.
use std::fs;use std::io;use std::path::Path;use std::process::Command;
/// Content file types that use the Yrs merge driver.const MERGE_PATTERNS: &[&str] = &[ "*.md", "*.html", "*.css", "*.js", "*.ts", "*.toml", "*.yaml", "*.yml", "*.json", "*.txt",];
/// Run `git yrs-merge init [--sidecar]`.pub fn run(sidecar: bool) -> io::Result<()> { // Verify we're in a git repo let git_dir = Path::new(".git"); if !git_dir.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, "not a git repository — run this from a git repo root", )); }
// Write .gitattributes write_gitattributes(sidecar)?;
// Configure merge driver in .git/config configure_merge_driver(sidecar)?;
if sidecar { // Create .yrs/ directory fs::create_dir_all(".yrs")?; eprintln!("git-yrs-merge: initialized with sidecar support (.yrs/)"); } else { eprintln!("git-yrs-merge: initialized (diff-based mode)"); }
Ok(())}
const DRIVER_HEADER: &str = "# Yrs CRDT merge driver — conflict-free merging";const SIDECAR_HEADER: &str = "# Sidecar files — always keep ours during merge (binary, no diff)";
/// Lines this tool owns and rewrites on every init.////// The headers have to count as ours, not just the entries: init rewrites its section/// on each run, so any of our lines left in place get duplicated by the next one.fn is_yrs_managed(line: &str) -> bool { line.contains("merge=yrs") || line.contains(".yrs/") || line == DRIVER_HEADER || line == SIDECAR_HEADER}
/// Write or update .gitattributes with merge driver entries.fn write_gitattributes(sidecar: bool) -> io::Result<()> { let mut content = String::new();
// Keep whatever the repo already had, minus our own section. if let Ok(existing) = fs::read_to_string(".gitattributes") { for line in existing.lines() { if !is_yrs_managed(line) { content.push_str(line); content.push('\n'); } } // Dropping our section leaves behind the blank line that separated it; without // this every init would add one more. let kept = content.trim_end_matches('\n').to_string(); content = if kept.is_empty() { String::new() } else { kept + "\n" }; }
// Add merge driver entries content.push_str(DRIVER_HEADER); content.push('\n'); for pattern in MERGE_PATTERNS { content.push_str(&format!("{} merge=yrs\n", pattern)); }
if sidecar { content.push('\n'); content.push_str(SIDECAR_HEADER); content.push('\n'); // NOTE: Cannot use `binary` macro here because it expands to `-diff -merge -text` // which would unset merge=ours. Spell out the individual attributes instead. content.push_str(".yrs/** -diff -text merge=ours\n"); }
fs::write(".gitattributes", content)?; Ok(())}
/// Configure the merge driver in .git/config.fn configure_merge_driver(sidecar: bool) -> io::Result<()> { git_config("merge.yrs.name", "Yrs CRDT merge")?; git_config("merge.yrs.driver", "git-yrs-merge merge %O %A %B --path=%P")?;
if sidecar { // Register the "ours" merge driver for .yrs/ sidecar files. // `driver = true` is a special git value meaning "always keep ours". git_config("merge.ours.name", "Keep ours")?; git_config("merge.ours.driver", "true")?; }
Ok(())}
fn git_config(key: &str, value: &str) -> io::Result<()> { let output = Command::new("git") .args(["config", key, value]) .output()?; if !output.status.success() { return Err(io::Error::new( io::ErrorKind::Other, format!("git config {} failed: {}", key, String::from_utf8_lossy(&output.stderr)), )); } Ok(())}
#[cfg(test)]mod tests { use super::*; use std::process::Command as StdCommand;
// These tests must run sequentially because they change process cwd. // We combine them into one test to avoid races. #[test] fn gitattributes_init() { // Test without sidecar let tmp1 = tempfile::tempdir().unwrap(); std::env::set_current_dir(tmp1.path()).unwrap(); StdCommand::new("git").args(["init"]).output().unwrap();
run(false).unwrap();
let attrs = fs::read_to_string(".gitattributes").unwrap(); assert!(attrs.contains("*.md merge=yrs")); assert!(attrs.contains("*.json merge=yrs")); assert!(!attrs.contains(".yrs/**"));
// Test with sidecar let tmp2 = tempfile::tempdir().unwrap(); std::env::set_current_dir(tmp2.path()).unwrap(); StdCommand::new("git").args(["init"]).output().unwrap();
run(true).unwrap();
let attrs = fs::read_to_string(".gitattributes").unwrap(); assert!(attrs.contains("*.md merge=yrs")); assert!(attrs.contains(".yrs/** -diff -text merge=ours")); assert!(Path::new(".yrs").exists()); }}