2610 lines · 83.5 KB · Rust
RawDownload
//! `ep split-commit` implementation.//!//! This command generates a single evaluation example JSON object from a//! chronologically-ordered unified diff (a "commit").//!//! TODO: Port Python code to generate chronologically-ordered commitsuse crate::FailedHandling;use crate::reorder_patch::{    EditLocation, Patch, PatchLine, edit_locations, extract_edits, locate_edited_line,};use crate::word_diff::tokenize; /// Find the largest valid UTF-8 char boundary at or before `index` in `s`.fn floor_char_boundary(s: &str, index: usize) -> usize {    if index >= s.len() {        s.len()    } else if s.is_char_boundary(index) {        index    } else {        // Find the nearest valid character boundary at or before index        (0..index)            .rev()            .find(|&i| s.is_char_boundary(i))            .unwrap_or(0)    }}use anyhow::{Context as _, Result};use clap::Args;use edit_prediction::example_spec::ExampleSpec;use rand::Rng;use rand::SeedableRng;use rand::seq::SliceRandom;use serde::Deserialize;use similar::{DiffTag, TextDiff};use std::collections::BTreeSet;use std::fs;use std::io::{self, Write};use std::path::Path;use std::path::PathBuf; const MAX_SPLIT_POINT_SAMPLING_ATTEMPTS: usize = 10;const SAME_FILE_NEAR_LINE_THRESHOLD: usize = 30; /// A commit has no split point matching the requested kind. This is an/// expected outcome when filtering by kind, so such commits are skipped/// rather than treated as failures.#[derive(Debug)]pub struct NoMatchingSplitPointError {    kind: SplitPointKind,} impl std::fmt::Display for NoMatchingSplitPointError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "no split point found matching {}", self.kind)    }} impl std::error::Error for NoMatchingSplitPointError {} /// `ep split-commit` CLI args.#[derive(Debug, Args, Clone)]pub struct SplitCommitArgs {    /// Split point (float 0.0-1.0 for fraction, integer for index, or one of: fim, same-file-near, same-file-far, cross-file; append :<index-or-fraction> to validate a specific split)    #[arg(long, short = 's')]    pub split_point: Option<String>,     /// Random seed for reproducibility    #[arg(long)]    pub seed: Option<u64>,     /// Pretty-print JSON output    #[arg(long, short = 'p')]    pub pretty: bool,     /// Number of samples to generate per commit (samples random split points)    #[arg(long, short = 'n')]    pub num_samples: Option<usize>,} /// Input format for annotated commits (JSON Lines).#[derive(Debug, Clone, Deserialize)]#[allow(dead_code)]pub struct AnnotatedCommit {    /// Repository path (e.g., "repos/zed")    pub repo: String,    /// Repository URL (e.g., "https://github.com/zed-industries/zed")    pub repo_url: String,    /// Commit SHA    pub commit_sha: String,    /// Chronologically reordered commit diff    pub reordered_commit: String,    /// Original commit diff    pub original_commit: String,    /// Whether diff stats match between original and reordered    pub diff_stats_match: bool,} /// Cursor position in a file.#[derive(Debug, Clone, PartialEq, Eq)]pub struct CursorPosition {    pub file: String,    pub line: usize,    pub column: usize,    pub line_length: usize,} impl std::fmt::Display for CursorPosition {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}:{}:{}", self.file, self.line, self.column)    }} /// Represents a split commit with source and target patches.#[derive(Debug, Clone)]pub struct SplitCommit {    pub source_patch: String,    pub target_patch: String,} /// Split point specification for evaluation generation.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum SplitPointKind {    Fim,    SameFileNear,    SameFileFar,    CrossFile,} impl std::fmt::Display for SplitPointKind {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            SplitPointKind::Fim => write!(f, "fim"),            SplitPointKind::SameFileNear => write!(f, "same-file-near"),            SplitPointKind::SameFileFar => write!(f, "same-file-far"),            SplitPointKind::CrossFile => write!(f, "cross-file"),        }    }} impl std::str::FromStr for SplitPointKind {    type Err = anyhow::Error;     fn from_str(value: &str) -> Result<Self> {        match value {            "fim" => Ok(Self::Fim),            "same-file-near" => Ok(Self::SameFileNear),            "same-file-far" => Ok(Self::SameFileFar),            "cross-file" => Ok(Self::CrossFile),            _ => anyhow::bail!(                "invalid split point kind '{value}' (expected fim, same-file-near, same-file-far, or cross-file)"            ),        }    }} #[derive(Debug, Clone, PartialEq)]pub enum SplitPoint {    /// Fraction of total edits (0.0 to 1.0)    Fraction(f64),    /// Absolute index    Index(usize),    /// Random split point matching the requested kind.    Kind(SplitPointKind),    /// Explicit split point that must match the requested kind.    KindWithSplit {        kind: SplitPointKind,        split_point: SplitPointValue,    },} #[derive(Debug, Clone, PartialEq)]pub enum SplitPointValue {    Fraction(f64),    Index(usize),} fn parse_split_point_value(value: &str) -> Result<SplitPointValue> {    if value.contains('.') {        value            .parse::<f64>()            .map(SplitPointValue::Fraction)            .with_context(|| format!("invalid split point fraction '{value}'"))    } else {        value            .parse::<usize>()            .map(SplitPointValue::Index)            .with_context(|| format!("invalid split point index '{value}'"))    }} fn parse_split_point(value: &str) -> Result<SplitPoint> {    if let Some((kind, split_point)) = value.split_once(':') {        let kind = kind.parse::<SplitPointKind>()?;        anyhow::ensure!(            !split_point.is_empty(),            "missing split point after kind '{kind}:'"        );        return Ok(SplitPoint::KindWithSplit {            kind,            split_point: parse_split_point_value(split_point)?,        });    }     if let Ok(kind) = value.parse::<SplitPointKind>() {        return Ok(SplitPoint::Kind(kind));    }     match parse_split_point_value(value)? {        SplitPointValue::Fraction(value) => Ok(SplitPoint::Fraction(value)),        SplitPointValue::Index(value) => Ok(SplitPoint::Index(value)),    }} fn is_service_file(path: &str) -> bool {    let path = path.trim();    let path = path        .strip_prefix("a/")        .or_else(|| path.strip_prefix("b/"))        .unwrap_or(path)        .trim_start_matches("./");     if path.is_empty() || path == "/dev/null" {        return true;    }     let file_name = path.rsplit('/').next().unwrap_or(path);    if matches!(        file_name,        "package.json"            | "package-lock.json"            | "pnpm-lock.yaml"            | "Cargo.lock"            | "yarn.lock"            | "bun.lock"            | "bun.lockb"            | "go.sum"            | "composer.lock"            | "Gemfile.lock"            | "Pipfile.lock"            | "poetry.lock"            | "uv.lock"            | ".gitlab-ci.yml"            | ".travis.yml"            | "azure-pipelines.yml"            | "Jenkinsfile"    ) {        return true;    }     if file_name.ends_with(".min.js")        || file_name.ends_with(".bundle.js")        || file_name.contains(".generated.")        || file_name.ends_with(".pb.go")    {        return true;    }     if path == ".github/workflows"        || path.starts_with(".github/workflows/")        || path == ".circleci"        || path.starts_with(".circleci/")    {        return true;    }     path.split('/').any(|component| {        matches!(            component,            "dist" | "build" | "coverage" | "node_modules" | "vendor"        )    })} fn edit_starts_on_service_file(patch: &Patch, split_pos: usize) -> bool {    locate_edited_line(patch, split_pos as isize)        .is_some_and(|edit_location| is_service_file(&edit_location.filename))} fn has_submodule_gitlink_hunk(commit: &str) -> bool {    commit.lines().any(line_indicates_submodule_gitlink)} fn line_indicates_submodule_gitlink(line: &str) -> bool {    let line = line.trim();     matches!(        line,        "new file mode 160000" | "deleted file mode 160000" | "old mode 160000" | "new mode 160000"    ) || line        .strip_prefix("index ")        .and_then(|line| line.split_whitespace().last())        .is_some_and(|mode| mode == "160000")        || line            .strip_prefix('+')            .or_else(|| line.strip_prefix('-'))            .is_some_and(|line| line.starts_with("Subproject commit "))} fn sample_split_point(patch: &Patch, rng: &mut dyn rand::RngCore) -> usize {    let stats = patch.stats();    let num_edits = stats.added + stats.removed;    if num_edits == 0 {        return 0;    }     let mut split = rng.random_range(1..=num_edits);    for _ in 1..MAX_SPLIT_POINT_SAMPLING_ATTEMPTS {        if !edit_starts_on_service_file(patch, split) {            break;        }        split = rng.random_range(1..=num_edits);    }     split} fn resolve_split_point_value(split_point: SplitPointValue, num_edits: usize) -> usize {    match split_point {        SplitPointValue::Fraction(fraction) => {            let split = (fraction * num_edits as f64).floor() as usize;            split.min(num_edits)        }        SplitPointValue::Index(index) => index.min(num_edits),    }} #[derive(Debug, Clone)]struct GeneratedSplitCommit {    split: usize,    split_commit: SplitCommit,    cursor: CursorPosition,    cursor_from_human_edit: bool,} fn generate_split_commit_at_split(    patch: &Patch,    split: usize,    rng: &mut dyn rand::RngCore,) -> Result<GeneratedSplitCommit> {    let (prefix, suffix) = split_ordered_patch(patch, split);     let mut split_commit = SplitCommit {        source_patch: prefix,        target_patch: suffix,    };     let human_edit_seed = rng.random_range(1..=10000u64);    let (src_patch, tgt_patch, cursor_opt) = imitate_human_edits(        &split_commit.source_patch,        &split_commit.target_patch,        human_edit_seed,    );    split_commit.source_patch = src_patch;    split_commit.target_patch = tgt_patch;     let cursor_from_human_edit = cursor_opt.is_some();    let cursor = match cursor_opt {        Some(cursor) => cursor,        None => sample_cursor_position(&split_commit, rng)            .context("failed to sample cursor position")?,    };     Ok(GeneratedSplitCommit {        split,        split_commit,        cursor,        cursor_from_human_edit,    })} fn classify_generated_split_commit(    generated_split_commit: &GeneratedSplitCommit,) -> Option<SplitPointKind> {    let target_patch = Patch::parse_unified_diff(&generated_split_commit.split_commit.target_patch);    let next_edit = locate_edited_line(&target_patch, 0)?;     if next_edit.filename != generated_split_commit.cursor.file {        return Some(SplitPointKind::CrossFile);    }     if generated_split_commit.cursor_from_human_edit        && next_edit.target_line_number == generated_split_commit.cursor.line    {        return Some(SplitPointKind::Fim);    }     let line_distance = next_edit        .target_line_number        .abs_diff(generated_split_commit.cursor.line);    if line_distance <= SAME_FILE_NEAR_LINE_THRESHOLD {        Some(SplitPointKind::SameFileNear)    } else {        Some(SplitPointKind::SameFileFar)    }} /// Cheap necessary condition for a split to be classifiable as `kind`,/// computed from the full patch without generating the split.////// The cursor ends up either at the first target edit (or, via/// `imitate_human_edits`, on its line), or at the last source edit. So the/// edits adjacent to the split bound what classifications are reachable./// Line numbers here are in full-patch coordinates, which can drift slightly/// from split-patch coordinates, so this is a heuristic pre-filter; the final/// classification is always verified on the generated split.fn split_can_match_kind(    edit_locations: &[EditLocation],    split: usize,    kind: SplitPointKind,) -> bool {    let (Some(previous_edit), Some(next_edit)) = (        split.checked_sub(1).and_then(|i| edit_locations.get(i)),        edit_locations.get(split),    ) else {        return false;    };     match kind {        SplitPointKind::Fim => matches!(next_edit.patch_line, PatchLine::Addition(_)),        SplitPointKind::SameFileNear => true,        SplitPointKind::SameFileFar => {            previous_edit.filename == next_edit.filename                && previous_edit                    .target_line_number                    .abs_diff(next_edit.target_line_number)                    > SAME_FILE_NEAR_LINE_THRESHOLD        }        SplitPointKind::CrossFile => previous_edit.filename != next_edit.filename,    }} fn sample_split_commit_of_kind(    patch: &Patch,    kind: SplitPointKind,    rng: &mut dyn rand::RngCore,) -> Result<GeneratedSplitCommit> {    let edit_locations = edit_locations(patch);    let num_edits = edit_locations.len();     let mut candidate_splits: Vec<usize> = (1..num_edits)        .filter(|&split| {            !edit_locations                .get(split)                .is_some_and(|next_edit| is_service_file(&next_edit.filename))                && split_can_match_kind(&edit_locations, split, kind)        })        .collect();    candidate_splits.shuffle(rng);     for split in candidate_splits {        for _ in 0..MAX_SPLIT_POINT_SAMPLING_ATTEMPTS {            let Ok(generated_split_commit) = generate_split_commit_at_split(patch, split, rng)            else {                continue;            };             if classify_generated_split_commit(&generated_split_commit) == Some(kind) {                return Ok(generated_split_commit);            }        }    }     Err(NoMatchingSplitPointError { kind }.into())} /// Entry point for the `ep split-commit` subcommand.////// This runs synchronously and outputs JSON Lines (one output per input line).pub fn run_split_commit(    args: &SplitCommitArgs,    inputs: &[PathBuf],    output_path: Option<&PathBuf>,    failed: FailedHandling,) -> Result<()> {    use std::collections::HashSet;    use std::io::BufRead;     let stdin_path = PathBuf::from("-");    let inputs = if inputs.is_empty() {        std::slice::from_ref(&stdin_path)    } else {        inputs    };     let split_point = args        .split_point        .as_deref()        .map(parse_split_point)        .transpose()?;    let mut output_lines = Vec::new();    let mut processed_commits = 0usize;     for input_path in inputs {        let input: Box<dyn BufRead> = if input_path.as_os_str() == "-" {            Box::new(io::BufReader::new(io::stdin()))        } else {            let file = fs::File::open(input_path)                .with_context(|| format!("failed to open input file {}", input_path.display()))?;            Box::new(io::BufReader::new(file))        };

Lines 1–500 of 2610.