subversion 0.1.10

Rust bindings for Subversion
Documentation
use std::path::Path;
use subversion::{
    client::{CheckoutOptions, Context},
    conflict::{ConflictChoice, ConflictDescription, ConflictResolver, ConflictResult},
    merge::{merge_peg, MergeOptions},
    Depth, Error, Revision,
};

/// A conflict resolver that always picks the same choice.
struct FixedChoiceResolver(ConflictChoice);

impl ConflictResolver for FixedChoiceResolver {
    fn resolve(
        &mut self,
        _conflict: &ConflictDescription,
    ) -> Result<ConflictResult, Error<'static>> {
        Ok(ConflictResult {
            choice: self.0,
            merged_file: None,
            save_merged: false,
        })
    }
}

/// A custom conflict resolver that logs conflicts and makes decisions based on file type.
struct SmartConflictResolver {
    log_file: std::fs::File,
}

impl SmartConflictResolver {
    fn new() -> std::io::Result<Self> {
        Ok(Self {
            log_file: std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open("conflict_log.txt")?,
        })
    }
}

impl ConflictResolver for SmartConflictResolver {
    fn resolve(
        &mut self,
        conflict: &ConflictDescription,
    ) -> Result<ConflictResult, Error<'static>> {
        use std::io::Write;

        // Log the conflict
        writeln!(
            self.log_file,
            "Conflict in {}: {:?} (action: {:?}, reason: {:?})",
            conflict.local_abspath, conflict.kind, conflict.action, conflict.reason
        )
        .unwrap();

        // Make decisions based on file type and conflict type
        let choice = if conflict.is_binary {
            // For binary files, prefer the repository version
            ConflictChoice::TheirsFull
        } else if let Some(ref mime) = conflict.mime_type {
            if mime.starts_with("text/") {
                // For text files, try to use theirs for conflicts only
                ConflictChoice::TheirsConflict
            } else {
                // Other files, postpone for manual review
                ConflictChoice::Postpone
            }
        } else if conflict.local_abspath.ends_with(".generated") {
            // Generated files should use repository version
            ConflictChoice::TheirsFull
        } else {
            // Default: postpone for manual resolution
            ConflictChoice::Postpone
        };

        writeln!(self.log_file, "  Resolution: {:?}", choice).unwrap();

        Ok(ConflictResult {
            choice,
            merged_file: None,
            save_merged: false,
        })
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse command line arguments
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 3 {
        eprintln!(
            "Usage: {} <repository_url> <working_copy_path> [--theirs|--mine|--smart]",
            args[0]
        );
        std::process::exit(1);
    }

    let repo_url = &args[1];
    let wc_path = Path::new(&args[2]);
    let resolver_type = args.get(3).map(|s| s.as_str()).unwrap_or("--postpone");

    // Create a client context
    let mut ctx = Context::new()?;

    // Set up authentication if needed
    // ctx.set_auth(&mut auth_baton);

    // Set up conflict resolver based on command line option
    match resolver_type {
        "--theirs" => {
            println!("Using automatic resolver: always choose theirs");
            ctx.set_conflict_resolver(FixedChoiceResolver(ConflictChoice::TheirsFull));
        }
        "--mine" => {
            println!("Using automatic resolver: always choose mine");
            ctx.set_conflict_resolver(FixedChoiceResolver(ConflictChoice::MineFull));
        }
        "--smart" => {
            println!("Using smart conflict resolver");
            ctx.set_conflict_resolver(SmartConflictResolver::new()?);
        }
        _ => {
            println!("Using default resolver: postpone all conflicts");
            ctx.set_conflict_resolver(FixedChoiceResolver(ConflictChoice::Postpone));
        }
    }

    // If working copy doesn't exist, check it out first
    if !wc_path.exists() {
        println!("Checking out {} to {:?}", repo_url, wc_path);
        ctx.checkout(repo_url.as_str(), wc_path, &CheckoutOptions::default())?;
    }

    // Perform a merge from the repository
    // This example does an automatic merge from the URL
    println!("Performing merge...");
    let merge_options = MergeOptions {
        dry_run: false,
        record_only: false,
        force_delete: false,
        allow_mixed_rev: true,
        ..Default::default()
    };

    // Merge from trunk to working copy
    // Using peg merge for automatic range detection
    match merge_peg(
        repo_url.as_str(),
        None, // automatic range detection
        Revision::Head,
        wc_path,
        Depth::Infinity,
        &merge_options,
        &mut ctx,
    ) {
        Ok(()) => {
            println!("Merge completed successfully!");
        }
        Err(e) => {
            eprintln!("Merge failed: {}", e);
            return Err(Box::new(e));
        }
    }

    // Check status to see if there are any remaining conflicts
    println!("\nChecking working copy status...");
    // Note: status API in this example is simplified - actual usage may vary
    println!("(Status checking would iterate through working copy)");

    println!("\nMerge operation complete!");
    if resolver_type == "--smart" {
        println!("Check conflict_log.txt for details about resolved conflicts");
    }

    Ok(())
}