//! `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. Load more
//! `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 commits use 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)) };