├── .github └── workflows │ └── CI.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── 01_simple.rs └── 02_read_everytime.rs ├── src └── lib.rs └── tests ├── fixtures ├── bzr.diff ├── git.diff ├── hg.diff ├── sample0.diff ├── sample1.diff ├── sample2.diff ├── sample3.diff ├── sample4-plus.diff ├── sample4.diff ├── sample5.diff └── svn.diff ├── test_hunk.rs ├── test_patchedfile.rs └── test_patchset.rs /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | override: true 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: test 36 | 37 | fmt: 38 | name: Rustfmt 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | toolchain: stable 46 | override: true 47 | - run: rustup component add rustfmt 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: fmt 51 | args: --all -- --check 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["messense "] 3 | description = "Unified diff parsing/metadata extraction library for Rust" 4 | documentation = "https://messense.github.io/unidiff-rs" 5 | edition = "2018" 6 | homepage = "https://github.com/messense/unidiff-rs" 7 | keywords = ["diff", "git", "svn", "hg", "unified"] 8 | license = "MIT" 9 | name = "unidiff" 10 | readme = "README.md" 11 | repository = "https://github.com/messense/unidiff-rs" 12 | version = "0.3.3" 13 | 14 | [dependencies] 15 | lazy_static = "1.0" 16 | regex = "1.0" 17 | encoding_rs = { version = "0.8", optional = true } 18 | 19 | [features] 20 | default = ["encoding"] 21 | encoding = ["encoding_rs"] 22 | unstable = [] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 messense 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unidiff-rs 2 | 3 | [![GitHub Actions](https://github.com/messense/unidiff-rs/workflows/CI/badge.svg)](https://github.com/messense/unidiff-rs/actions?query=workflow%3ACI) 4 | [![Crates.io](https://img.shields.io/crates/v/unidiff.svg)](https://crates.io/crates/unidiff) 5 | [![docs.rs](https://docs.rs/unidiff/badge.svg)](https://docs.rs/unidiff/) 6 | 7 | Unified diff parsing/metadata extraction library for Rust 8 | 9 | ## Installation 10 | 11 | Add it to your ``Cargo.toml``: 12 | 13 | ```toml 14 | [dependencies] 15 | unidiff = "0.3" 16 | ``` 17 | 18 | Add ``extern crate unidiff`` to your crate root and your're good to go! 19 | 20 | ## License 21 | 22 | This work is released under the MIT license. A copy of the license is provided in the [LICENSE](./LICENSE) file. 23 | -------------------------------------------------------------------------------- /benches/01_simple.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | extern crate unidiff; 4 | 5 | use std::fs::File; 6 | use std::io::prelude::*; 7 | 8 | use test::Bencher; 9 | use unidiff::PatchSet; 10 | 11 | #[bench] 12 | fn bench_parse_diff_simple(b: &mut Bencher) { 13 | let mut buf = String::new(); 14 | File::open("tests/fixtures/sample0.diff") 15 | .and_then(|mut r| r.read_to_string(&mut buf)) 16 | .unwrap(); 17 | 18 | b.iter(|| { 19 | let mut patch = PatchSet::new(); 20 | patch.parse(&buf).unwrap(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /benches/02_read_everytime.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | extern crate unidiff; 4 | 5 | use std::fs::File; 6 | use std::io::prelude::*; 7 | 8 | use test::Bencher; 9 | use unidiff::PatchSet; 10 | 11 | #[bench] 12 | fn bench_parse_diff(b: &mut Bencher) { 13 | b.iter(|| { 14 | let mut buf = String::new(); 15 | File::open("tests/fixtures/sample0.diff") 16 | .and_then(|mut r| r.read_to_string(&mut buf)) 17 | .unwrap(); 18 | 19 | let mut patch = PatchSet::new(); 20 | patch.parse(&buf).unwrap(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Unified diff parsing/metadata extraction library for Rust 2 | //! 3 | //! # Examples 4 | //! 5 | //! ``` 6 | //! extern crate unidiff; 7 | //! 8 | //! use unidiff::PatchSet; 9 | //! 10 | //! fn main() { 11 | //! let diff_str = "diff --git a/added_file b/added_file 12 | //! new file mode 100644 13 | //! index 0000000..9b710f3 14 | //! --- /dev/null 15 | //! +++ b/added_file 16 | //! @@ -0,0 +1,4 @@ 17 | //! +This was missing! 18 | //! +Adding it now. 19 | //! + 20 | //! +Only for testing purposes."; 21 | //! let mut patch = PatchSet::new(); 22 | //! patch.parse(diff_str).ok().expect("Error parsing diff"); 23 | //! } 24 | //! ``` 25 | use lazy_static::lazy_static; 26 | 27 | use std::error; 28 | use std::fmt; 29 | use std::ops::{Index, IndexMut}; 30 | use std::str::FromStr; 31 | 32 | use regex::Regex; 33 | 34 | lazy_static! { 35 | static ref RE_SOURCE_FILENAME: Regex = Regex::new(r"^--- (?P[^\t\n]+)(?:\t(?P[^\n]+))?").unwrap(); 36 | static ref RE_TARGET_FILENAME: Regex = Regex::new(r"^\+\+\+ (?P[^\t\n]+)(?:\t(?P[^\n]+))?").unwrap(); 37 | static ref RE_HUNK_HEADER: Regex = Regex::new(r"^@@ -(?P\d+)(?:,(?P\d+))? \+(?P\d+)(?:,(?P\d+))? @@[ ]?(?P.*)").unwrap(); 38 | static ref RE_HUNK_BODY_LINE: Regex = Regex::new(r"^(?P[- \n\+\\]?)(?P.*)").unwrap(); 39 | } 40 | 41 | /// Diff line is added 42 | pub const LINE_TYPE_ADDED: &'static str = "+"; 43 | /// Diff line is removed 44 | pub const LINE_TYPE_REMOVED: &'static str = "-"; 45 | /// Diff line is context 46 | pub const LINE_TYPE_CONTEXT: &'static str = " "; 47 | /// Diff line is empty 48 | pub const LINE_TYPE_EMPTY: &'static str = "\n"; 49 | 50 | /// Error type 51 | #[derive(Debug, Clone)] 52 | pub enum Error { 53 | /// Target without source 54 | TargetWithoutSource(String), 55 | /// Unexpected hunk found 56 | UnexpectedHunk(String), 57 | /// Hunk line expected 58 | ExpectLine(String), 59 | } 60 | 61 | impl fmt::Display for Error { 62 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 63 | match *self { 64 | Error::TargetWithoutSource(ref l) => write!(f, "Target without source: {}", l), 65 | Error::UnexpectedHunk(ref l) => write!(f, "Unexpected hunk found: {}", l), 66 | Error::ExpectLine(ref l) => write!(f, "Hunk line expected: {}", l), 67 | } 68 | } 69 | } 70 | 71 | impl error::Error for Error { 72 | fn description(&self) -> &str { 73 | match *self { 74 | Error::TargetWithoutSource(..) => "Target without source", 75 | Error::UnexpectedHunk(..) => "Unexpected hunk found", 76 | Error::ExpectLine(..) => "Hunk line expected", 77 | } 78 | } 79 | } 80 | 81 | /// `unidiff::parse` result type 82 | pub type Result = ::std::result::Result; 83 | 84 | /// A diff line 85 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 86 | pub struct Line { 87 | /// Source file line number 88 | pub source_line_no: Option, 89 | /// Target file line number 90 | pub target_line_no: Option, 91 | /// Diff file line number 92 | pub diff_line_no: usize, 93 | /// Diff line type 94 | pub line_type: String, 95 | /// Diff line content value 96 | pub value: String, 97 | } 98 | 99 | impl Line { 100 | pub fn new>(value: T, line_type: T) -> Line { 101 | Line { 102 | source_line_no: Some(0usize), 103 | target_line_no: Some(0usize), 104 | diff_line_no: 0usize, 105 | line_type: line_type.into(), 106 | value: value.into(), 107 | } 108 | } 109 | 110 | /// Diff line type is added 111 | pub fn is_added(&self) -> bool { 112 | LINE_TYPE_ADDED == &self.line_type 113 | } 114 | 115 | /// Diff line type is removed 116 | pub fn is_removed(&self) -> bool { 117 | LINE_TYPE_REMOVED == &self.line_type 118 | } 119 | 120 | /// Diff line type is context 121 | pub fn is_context(&self) -> bool { 122 | LINE_TYPE_CONTEXT == &self.line_type 123 | } 124 | } 125 | 126 | impl fmt::Display for Line { 127 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 128 | write!(f, "{}{}", self.line_type, self.value) 129 | } 130 | } 131 | 132 | /// Each of the modified blocks of a file 133 | /// 134 | /// You can iterate over it to get ``Line``s. 135 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 136 | pub struct Hunk { 137 | /// Count of lines added 138 | added: usize, 139 | /// Count of lines removed 140 | removed: usize, 141 | /// Source file starting line number 142 | pub source_start: usize, 143 | /// Source file changes length 144 | pub source_length: usize, 145 | /// Target file starting line number 146 | pub target_start: usize, 147 | /// Target file changes length 148 | pub target_length: usize, 149 | /// Section header 150 | pub section_header: String, 151 | lines: Vec, 152 | source: Vec, 153 | target: Vec, 154 | } 155 | 156 | impl Hunk { 157 | pub fn new>( 158 | source_start: usize, 159 | source_length: usize, 160 | target_start: usize, 161 | target_length: usize, 162 | section_header: T, 163 | ) -> Hunk { 164 | Hunk { 165 | added: 0usize, 166 | removed: 0usize, 167 | source_start: source_start, 168 | source_length: source_length, 169 | target_start: target_start, 170 | target_length: target_length, 171 | section_header: section_header.into(), 172 | lines: vec![], 173 | source: vec![], 174 | target: vec![], 175 | } 176 | } 177 | 178 | /// Count of lines added 179 | pub fn added(&self) -> usize { 180 | self.added 181 | } 182 | 183 | /// Count of lines removed 184 | pub fn removed(&self) -> usize { 185 | self.removed 186 | } 187 | 188 | /// Is this hunk valid 189 | pub fn is_valid(&self) -> bool { 190 | self.source.len() == self.source_length && self.target.len() == self.target_length 191 | } 192 | 193 | /// Lines from source file 194 | pub fn source_lines(&self) -> Vec { 195 | self.lines 196 | .iter() 197 | .cloned() 198 | .filter(|l| l.is_context() || l.is_removed()) 199 | .collect() 200 | } 201 | 202 | /// Lines from target file 203 | pub fn target_lines(&self) -> Vec { 204 | self.lines 205 | .iter() 206 | .cloned() 207 | .filter(|l| l.is_context() || l.is_added()) 208 | .collect() 209 | } 210 | 211 | /// Append new line into hunk 212 | pub fn append(&mut self, line: Line) { 213 | if line.is_added() { 214 | self.added = self.added + 1; 215 | self.target 216 | .push(format!("{}{}", line.line_type, line.value)); 217 | } else if line.is_removed() { 218 | self.removed = self.removed + 1; 219 | self.source 220 | .push(format!("{}{}", line.line_type, line.value)); 221 | } else if line.is_context() { 222 | self.source 223 | .push(format!("{}{}", line.line_type, line.value)); 224 | self.target 225 | .push(format!("{}{}", line.line_type, line.value)); 226 | } 227 | self.lines.push(line); 228 | } 229 | 230 | /// Count of lines in this hunk 231 | pub fn len(&self) -> usize { 232 | self.lines.len() 233 | } 234 | 235 | /// Is this hunk empty 236 | pub fn is_empty(&self) -> bool { 237 | self.lines.is_empty() 238 | } 239 | 240 | /// Lines in this hunk 241 | pub fn lines(&self) -> &[Line] { 242 | &self.lines 243 | } 244 | 245 | pub fn lines_mut(&mut self) -> &mut [Line] { 246 | &mut self.lines 247 | } 248 | } 249 | 250 | impl fmt::Display for Hunk { 251 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 252 | let header = format!( 253 | "@@ -{},{} +{},{} @@ {}\n", 254 | self.source_start, 255 | self.source_length, 256 | self.target_start, 257 | self.target_length, 258 | self.section_header 259 | ); 260 | let content = self 261 | .lines 262 | .iter() 263 | .map(|l| l.to_string()) 264 | .collect::>() 265 | .join("\n"); 266 | write!(f, "{}{}", header, content) 267 | } 268 | } 269 | 270 | impl IntoIterator for Hunk { 271 | type Item = Line; 272 | type IntoIter = ::std::vec::IntoIter; 273 | 274 | fn into_iter(self) -> Self::IntoIter { 275 | self.lines.into_iter() 276 | } 277 | } 278 | 279 | impl Index for Hunk { 280 | type Output = Line; 281 | 282 | fn index(&self, idx: usize) -> &Line { 283 | &self.lines[idx] 284 | } 285 | } 286 | 287 | impl IndexMut for Hunk { 288 | fn index_mut(&mut self, index: usize) -> &mut Line { 289 | &mut self.lines[index] 290 | } 291 | } 292 | 293 | /// Patch updated file, contains a list of Hunks 294 | /// 295 | /// You can iterate over it to get ``Hunk``s. 296 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 297 | pub struct PatchedFile { 298 | /// Source file name 299 | pub source_file: String, 300 | /// Source file timestamp 301 | pub source_timestamp: Option, 302 | /// Target file name 303 | pub target_file: String, 304 | /// Target file timestamp 305 | pub target_timestamp: Option, 306 | hunks: Vec, 307 | } 308 | 309 | impl PatchedFile { 310 | /// Initialize a new PatchedFile instance 311 | pub fn new>(source_file: T, target_file: T) -> PatchedFile { 312 | PatchedFile { 313 | source_file: source_file.into(), 314 | target_file: target_file.into(), 315 | source_timestamp: None, 316 | target_timestamp: None, 317 | hunks: vec![], 318 | } 319 | } 320 | 321 | /// Initialize a new PatchedFile instance with hunks 322 | pub fn with_hunks>( 323 | source_file: T, 324 | target_file: T, 325 | hunks: Vec, 326 | ) -> PatchedFile { 327 | PatchedFile { 328 | source_file: source_file.into(), 329 | target_file: target_file.into(), 330 | source_timestamp: None, 331 | target_timestamp: None, 332 | hunks: hunks, 333 | } 334 | } 335 | 336 | /// Patched file relative path 337 | pub fn path(&self) -> String { 338 | if self.source_file.starts_with("a/") && self.target_file.starts_with("b/") { 339 | return self.source_file[2..].to_owned(); 340 | } 341 | if self.source_file.starts_with("a/") && "/dev/null" == &self.target_file { 342 | return self.source_file[2..].to_owned(); 343 | } 344 | if self.target_file.starts_with("b/") && "/dev/null" == &self.source_file { 345 | return self.target_file[2..].to_owned(); 346 | } 347 | self.source_file.clone() 348 | } 349 | 350 | /// Count of lines added 351 | pub fn added(&self) -> usize { 352 | self.hunks.iter().map(|h| h.added).fold(0, |acc, x| acc + x) 353 | } 354 | 355 | /// Count of lines removed 356 | pub fn removed(&self) -> usize { 357 | self.hunks 358 | .iter() 359 | .map(|h| h.removed) 360 | .fold(0, |acc, x| acc + x) 361 | } 362 | 363 | /// Is this file newly added 364 | pub fn is_added_file(&self) -> bool { 365 | self.hunks.len() == 1 && self.hunks[0].source_start == 0 && self.hunks[0].source_length == 0 366 | } 367 | 368 | /// Is this file removed 369 | pub fn is_removed_file(&self) -> bool { 370 | self.hunks.len() == 1 && self.hunks[0].target_start == 0 && self.hunks[0].target_length == 0 371 | } 372 | 373 | /// Is this file modified 374 | pub fn is_modified_file(&self) -> bool { 375 | !self.is_added_file() && !self.is_removed_file() 376 | } 377 | 378 | fn parse_hunk(&mut self, header: &str, diff: &[(usize, &str)]) -> Result<()> { 379 | let header_info = RE_HUNK_HEADER.captures(header).unwrap(); 380 | let source_start = header_info 381 | .name("source_start") 382 | .map_or("0", |s| s.as_str()) 383 | .parse::() 384 | .unwrap(); 385 | let source_length = header_info 386 | .name("source_length") 387 | .map_or("0", |s| s.as_str()) 388 | .parse::() 389 | .unwrap(); 390 | let target_start = header_info 391 | .name("target_start") 392 | .map_or("0", |s| s.as_str()) 393 | .parse::() 394 | .unwrap(); 395 | let target_length = header_info 396 | .name("target_length") 397 | .map_or("0", |s| s.as_str()) 398 | .parse::() 399 | .unwrap(); 400 | let section_header = header_info 401 | .name("section_header") 402 | .map_or("", |s| s.as_str()); 403 | let mut hunk = Hunk { 404 | added: 0usize, 405 | removed: 0usize, 406 | source: vec![], 407 | target: vec![], 408 | lines: vec![], 409 | source_start: source_start, 410 | source_length: source_length, 411 | target_start: target_start, 412 | target_length: target_length, 413 | section_header: section_header.to_owned(), 414 | }; 415 | let mut source_line_no = source_start; 416 | let mut target_line_no = target_start; 417 | let expected_source_end = source_start + source_length; 418 | let expected_target_end = target_start + target_length; 419 | for &(diff_line_no, line) in diff { 420 | if let Some(valid_line) = RE_HUNK_BODY_LINE.captures(line) { 421 | let mut line_type = valid_line.name("line_type").unwrap().as_str(); 422 | if line_type == LINE_TYPE_EMPTY || line_type == "" { 423 | line_type = LINE_TYPE_CONTEXT; 424 | } 425 | let value = valid_line.name("value").unwrap().as_str(); 426 | let mut original_line = Line { 427 | source_line_no: None, 428 | target_line_no: None, 429 | diff_line_no: diff_line_no + 1, 430 | line_type: line_type.to_owned(), 431 | value: value.to_owned(), 432 | }; 433 | match line_type { 434 | LINE_TYPE_ADDED => { 435 | original_line.target_line_no = Some(target_line_no); 436 | target_line_no = target_line_no + 1; 437 | } 438 | LINE_TYPE_REMOVED => { 439 | original_line.source_line_no = Some(source_line_no); 440 | source_line_no = source_line_no + 1; 441 | } 442 | LINE_TYPE_CONTEXT => { 443 | original_line.target_line_no = Some(target_line_no); 444 | target_line_no = target_line_no + 1; 445 | original_line.source_line_no = Some(source_line_no); 446 | source_line_no = source_line_no + 1; 447 | } 448 | _ => {} 449 | } 450 | hunk.append(original_line); 451 | if source_line_no >= expected_source_end && target_line_no >= expected_target_end { 452 | // FIXME: sync with upstream version 453 | break; 454 | } 455 | } else { 456 | return Err(Error::ExpectLine(line.to_owned())); 457 | } 458 | } 459 | self.hunks.push(hunk); 460 | Ok(()) 461 | } 462 | 463 | /// Count of hunks 464 | pub fn len(&self) -> usize { 465 | self.hunks.len() 466 | } 467 | 468 | pub fn is_empty(&self) -> bool { 469 | self.hunks.is_empty() 470 | } 471 | 472 | /// Hunks in this file 473 | pub fn hunks(&self) -> &[Hunk] { 474 | &self.hunks 475 | } 476 | 477 | pub fn hunks_mut(&mut self) -> &mut [Hunk] { 478 | &mut self.hunks 479 | } 480 | } 481 | 482 | impl fmt::Display for PatchedFile { 483 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 484 | let source = format!("--- {}\n", self.source_file); 485 | let target = format!("+++ {}\n", self.target_file); 486 | let hunks = self 487 | .hunks 488 | .iter() 489 | .map(|h| h.to_string()) 490 | .collect::>() 491 | .join("\n"); 492 | write!(f, "{}{}{}", source, target, hunks) 493 | } 494 | } 495 | 496 | impl IntoIterator for PatchedFile { 497 | type Item = Hunk; 498 | type IntoIter = ::std::vec::IntoIter; 499 | 500 | fn into_iter(self) -> Self::IntoIter { 501 | self.hunks.into_iter() 502 | } 503 | } 504 | 505 | impl Index for PatchedFile { 506 | type Output = Hunk; 507 | 508 | fn index(&self, idx: usize) -> &Hunk { 509 | &self.hunks[idx] 510 | } 511 | } 512 | 513 | impl IndexMut for PatchedFile { 514 | fn index_mut(&mut self, index: usize) -> &mut Hunk { 515 | &mut self.hunks[index] 516 | } 517 | } 518 | 519 | /// Unfied patchset 520 | /// 521 | /// You can iterate over it to get ``PatchedFile``s. 522 | /// 523 | /// ```ignore 524 | /// let mut patch = PatchSet::new(); 525 | /// patch.parse("some diff"); 526 | /// for patched_file in patch { 527 | /// // do something with patched_file 528 | /// for hunk in patched_file { 529 | /// // do something with hunk 530 | /// for line in hunk { 531 | /// // do something with line 532 | /// } 533 | /// } 534 | /// } 535 | /// ``` 536 | #[derive(Clone)] 537 | pub struct PatchSet { 538 | files: Vec, 539 | #[cfg(feature = "encoding")] 540 | encoding: &'static encoding_rs::Encoding, 541 | } 542 | 543 | impl fmt::Debug for PatchSet { 544 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 545 | fmt.debug_struct("PatchSet") 546 | .field("files", &self.files) 547 | .finish() 548 | } 549 | } 550 | 551 | impl Default for PatchSet { 552 | fn default() -> PatchSet { 553 | PatchSet::new() 554 | } 555 | } 556 | 557 | impl PatchSet { 558 | /// Added files vector 559 | pub fn added_files(&self) -> Vec { 560 | self.files 561 | .iter() 562 | .cloned() 563 | .filter(|f| f.is_added_file()) 564 | .collect() 565 | } 566 | 567 | /// Removed files vector 568 | pub fn removed_files(&self) -> Vec { 569 | self.files 570 | .iter() 571 | .cloned() 572 | .filter(|f| f.is_removed_file()) 573 | .collect() 574 | } 575 | 576 | /// Modified files vector 577 | pub fn modified_files(&self) -> Vec { 578 | self.files 579 | .iter() 580 | .cloned() 581 | .filter(|f| f.is_modified_file()) 582 | .collect() 583 | } 584 | 585 | /// Initialize a new PatchSet instance 586 | pub fn new() -> PatchSet { 587 | PatchSet { 588 | files: vec![], 589 | #[cfg(feature = "encoding")] 590 | encoding: encoding_rs::UTF_8, 591 | } 592 | } 593 | 594 | /// Initialize a new PatchedSet instance with encoding 595 | #[cfg(feature = "encoding")] 596 | pub fn with_encoding(coding: &'static encoding_rs::Encoding) -> PatchSet { 597 | PatchSet { 598 | files: vec![], 599 | encoding: coding, 600 | } 601 | } 602 | 603 | /// Initialize a new PatchedSet instance with encoding(string form) 604 | #[cfg(feature = "encoding")] 605 | pub fn from_encoding>(coding: T) -> PatchSet { 606 | let codec = encoding_rs::Encoding::for_label(coding.as_ref().as_bytes()); 607 | PatchSet { 608 | files: vec![], 609 | encoding: codec.unwrap_or(encoding_rs::UTF_8), 610 | } 611 | } 612 | 613 | /// Parse diff from bytes 614 | #[cfg(feature = "encoding")] 615 | pub fn parse_bytes(&mut self, input: &[u8]) -> Result<()> { 616 | let input = self.encoding.decode(input).0.to_string(); 617 | self.parse(input) 618 | } 619 | 620 | /// Parse diff from string 621 | pub fn parse>(&mut self, input: T) -> Result<()> { 622 | let input = input.as_ref(); 623 | let mut current_file: Option = None; 624 | let diff: Vec<(usize, &str)> = input.lines().enumerate().collect(); 625 | let mut source_file: Option = None; 626 | let mut source_timestamp: Option = None; 627 | 628 | for &(line_no, line) in &diff { 629 | // check for source file header 630 | if let Some(captures) = RE_SOURCE_FILENAME.captures(line) { 631 | source_file = match captures.name("filename") { 632 | Some(ref filename) => Some(filename.as_str().to_owned()), 633 | None => Some("".to_owned()), 634 | }; 635 | source_timestamp = match captures.name("timestamp") { 636 | Some(ref timestamp) => Some(timestamp.as_str().to_owned()), 637 | None => Some("".to_owned()), 638 | }; 639 | if let Some(patched_file) = current_file { 640 | self.files.push(patched_file.clone()); 641 | current_file = None; 642 | } 643 | continue; 644 | } 645 | // check for target file header 646 | if let Some(captures) = RE_TARGET_FILENAME.captures(line) { 647 | if current_file.is_some() { 648 | return Err(Error::TargetWithoutSource(line.to_owned())); 649 | } 650 | let target_file = match captures.name("filename") { 651 | Some(ref filename) => Some(filename.as_str().to_owned()), 652 | None => Some("".to_owned()), 653 | }; 654 | let target_timestamp = match captures.name("timestamp") { 655 | Some(ref timestamp) => Some(timestamp.as_str().to_owned()), 656 | None => Some("".to_owned()), 657 | }; 658 | 659 | // add current file to PatchSet 660 | current_file = Some(PatchedFile { 661 | source_file: source_file.clone().unwrap(), 662 | target_file: target_file.clone().unwrap(), 663 | source_timestamp: source_timestamp.clone(), 664 | target_timestamp: target_timestamp.clone(), 665 | hunks: Vec::new(), 666 | }); 667 | continue; 668 | } 669 | // check for hunk header 670 | if RE_HUNK_HEADER.is_match(line) { 671 | if let Some(ref mut patched_file) = current_file { 672 | patched_file.parse_hunk(line, &diff[line_no + 1..])?; 673 | } else { 674 | return Err(Error::UnexpectedHunk(line.to_owned())); 675 | } 676 | } 677 | } 678 | if let Some(patched_file) = current_file { 679 | self.files.push(patched_file.clone()); 680 | } 681 | Ok(()) 682 | } 683 | 684 | /// Count of patched files 685 | pub fn len(&self) -> usize { 686 | self.files.len() 687 | } 688 | 689 | pub fn is_empty(&self) -> bool { 690 | self.files.is_empty() 691 | } 692 | 693 | /// Files in this patch set 694 | pub fn files(&self) -> &[PatchedFile] { 695 | &self.files 696 | } 697 | 698 | pub fn files_mut(&mut self) -> &mut [PatchedFile] { 699 | &mut self.files 700 | } 701 | } 702 | 703 | impl fmt::Display for PatchSet { 704 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 705 | let diff = self 706 | .files 707 | .iter() 708 | .map(|f| f.to_string()) 709 | .collect::>() 710 | .join("\n"); 711 | write!(f, "{}", diff) 712 | } 713 | } 714 | 715 | impl IntoIterator for PatchSet { 716 | type Item = PatchedFile; 717 | type IntoIter = ::std::vec::IntoIter; 718 | 719 | fn into_iter(self) -> Self::IntoIter { 720 | self.files.into_iter() 721 | } 722 | } 723 | 724 | impl Index for PatchSet { 725 | type Output = PatchedFile; 726 | 727 | fn index(&self, idx: usize) -> &PatchedFile { 728 | &self.files[idx] 729 | } 730 | } 731 | 732 | impl IndexMut for PatchSet { 733 | fn index_mut(&mut self, index: usize) -> &mut PatchedFile { 734 | &mut self.files[index] 735 | } 736 | } 737 | 738 | impl FromStr for PatchSet { 739 | type Err = Error; 740 | 741 | fn from_str(s: &str) -> Result { 742 | let mut patch = PatchSet::new(); 743 | patch.parse(s)?; 744 | Ok(patch) 745 | } 746 | } 747 | -------------------------------------------------------------------------------- /tests/fixtures/bzr.diff: -------------------------------------------------------------------------------- 1 | === added file 'added_file' 2 | --- added_file 1970-01-01 00:00:00 +0000 3 | +++ added_file 2013-10-13 23:44:04 +0000 4 | @@ -0,0 +1,4 @@ 5 | +This was missing! 6 | +Adding it now. 7 | + 8 | +Only for testing purposes. 9 | \ No newline at end of file 10 | 11 | === modified file 'modified_file' 12 | --- modified_file 2013-10-13 23:53:13 +0000 13 | +++ modified_file 2013-10-13 23:53:26 +0000 14 | @@ -1,5 +1,7 @@ 15 | This is the original content. 16 | 17 | -This should be updated. 18 | +This is now updated. 19 | + 20 | +This is a new line. 21 | 22 | This will stay. 23 | \ No newline at end of file 24 | 25 | === removed file 'removed_file' 26 | --- removed_file 2013-10-13 23:53:13 +0000 27 | +++ removed_file 1970-01-01 00:00:00 +0000 28 | @@ -1,3 +0,0 @@ 29 | -This content shouldn't be here. 30 | - 31 | -This file will be removed. 32 | \ No newline at end of file 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/git.diff: -------------------------------------------------------------------------------- 1 | diff --git a/added_file b/added_file 2 | new file mode 100644 3 | index 0000000..9b710f3 4 | --- /dev/null 5 | +++ b/added_file 6 | @@ -0,0 +1,4 @@ 7 | +This was missing! 8 | +Adding it now. 9 | + 10 | +Only for testing purposes. 11 | \ No newline at end of file 12 | diff --git a/modified_file b/modified_file 13 | index c7921f5..8946660 100644 14 | --- a/modified_file 15 | +++ b/modified_file 16 | @@ -1,5 +1,7 @@ 17 | This is the original content. 18 | 19 | -This should be updated. 20 | +This is now updated. 21 | + 22 | +This is a new line. 23 | 24 | This will stay. 25 | \ No newline at end of file 26 | diff --git a/removed_file b/removed_file 27 | deleted file mode 100644 28 | index 1f38447..0000000 29 | --- a/removed_file 30 | +++ /dev/null 31 | @@ -1,3 +0,0 @@ 32 | -This content shouldn't be here. 33 | - 34 | -This file will be removed. 35 | \ No newline at end of file 36 | 37 | -------------------------------------------------------------------------------- /tests/fixtures/hg.diff: -------------------------------------------------------------------------------- 1 | diff -r 44299fd3d1a8 added_file 2 | --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3 | +++ b/added_file Sun Oct 13 20:51:40 2013 -0300 4 | @@ -0,0 +1,4 @@ 5 | +This was missing! 6 | +Adding it now. 7 | + 8 | +Only for testing purposes. 9 | \ No newline at end of file 10 | diff -r 44299fd3d1a8 modified_file 11 | --- a/modified_file Sun Oct 13 20:51:07 2013 -0300 12 | +++ b/modified_file Sun Oct 13 20:51:40 2013 -0300 13 | @@ -1,5 +1,7 @@ 14 | This is the original content. 15 | 16 | -This should be updated. 17 | +This is now updated. 18 | + 19 | +This is a new line. 20 | 21 | This will stay. 22 | \ No newline at end of file 23 | diff -r 44299fd3d1a8 removed_file 24 | --- a/removed_file Sun Oct 13 20:51:07 2013 -0300 25 | +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 26 | @@ -1,3 +0,0 @@ 27 | -This content shouldn't be here. 28 | - 29 | -This file will be removed. 30 | \ No newline at end of file 31 | -------------------------------------------------------------------------------- /tests/fixtures/sample0.diff: -------------------------------------------------------------------------------- 1 | --- /path/to/original ''timestamp'' 2 | +++ /path/to/new ''timestamp'' 3 | @@ -1,3 +1,9 @@ Section Header 4 | +This is an important 5 | +notice! It should 6 | +therefore be located at 7 | +the beginning of this 8 | +document! 9 | + 10 | This part of the 11 | document has stayed the 12 | same from version to 13 | @@ -5,16 +11,10 @@ 14 | be shown if it doesn't 15 | change. Otherwise, that 16 | would not be helping to 17 | -compress the size of the 18 | -changes. 19 | - 20 | -This paragraph contains 21 | -text that is outdated. 22 | -It will be deleted in the 23 | -near future. 24 | +compress anything. 25 | 26 | It is important to spell 27 | -check this dokument. On 28 | +check this document. On 29 | the other hand, a 30 | misspelled word isn't 31 | the end of the world. 32 | @@ -22,3 +22,7 @@ 33 | this paragraph needs to 34 | be changed. Things can 35 | be added after it. 36 | + 37 | +This paragraph contains 38 | +important new additions 39 | +to this document. 40 | --- /dev/null 41 | +++ /path/to/another_new 42 | @@ -0,0 +1,9 @@ 43 | +This is an important 44 | +notice! It should 45 | +therefore be located at 46 | +the beginning of this 47 | +document! 48 | + 49 | +This part of the 50 | +document has stayed the 51 | +same from version to 52 | --- /path/to/existing 53 | +++ /dev/null 54 | @@ -1,9 +0,0 @@ 55 | -This is an important 56 | -notice! It should 57 | -therefore be located at 58 | -the beginning of this 59 | -document! 60 | - 61 | -This part of the 62 | -document has stayed the 63 | -same from version to 64 | -------------------------------------------------------------------------------- /tests/fixtures/sample1.diff: -------------------------------------------------------------------------------- 1 | --- /path/to/original ''timestamp'' 2 | +++ /path/to/new ''timestamp'' 3 | @@ -1,3 +1,9 @@ 4 | +This is an important 5 | +notice! It should 6 | +therefore be located at 7 | +the beginning of this 8 | +document! 9 | + 10 | This part of the 11 | document has stayed the 12 | same from version to 13 | @@ -5,16 +11,13 @@ 14 | be shown if it doesn't 15 | change. Otherwise, that 16 | would not be helping to 17 | -compress the size of the 18 | -changes. 19 | - 20 | -This paragraph contains 21 | -text that is outdated. 22 | -It will be deleted in the 23 | -near future. 24 | +compress anything. 25 | 26 | It is important to spell 27 | -check this dokument. On 28 | +check this document. On 29 | the other hand, a 30 | misspelled word isn't 31 | the end of the world. 32 | @@ -22,3 +22,7 @@ 33 | this paragraph needs to 34 | be changed. Things can 35 | be added after it. 36 | + 37 | +This paragraph contains 38 | +important new additions 39 | +to this document. 40 | -------------------------------------------------------------------------------- /tests/fixtures/sample2.diff: -------------------------------------------------------------------------------- 1 | # HG changeset patch 2 | # Parent 13ba6cbdb304cd251fbc22466cadb21019ee817f 3 | # User Bill McCloskey 4 | 5 | diff --git a/content/base/src/nsContentUtils.cpp b/content/base/src/nsContentUtils.cpp 6 | --- a/content/base/src/nsContentUtils.cpp 7 | +++ b/content/base/src/nsContentUtils.cpp 8 | @@ -6369,17 +6369,17 @@ public: 9 | nsCycleCollectionParticipant* helper) 10 | { 11 | } 12 | 13 | NS_IMETHOD_(void) NoteNextEdgeName(const char* name) 14 | { 15 | } 16 | 17 | - NS_IMETHOD_(void) NoteWeakMapping(void* map, void* key, void* val) 18 | + NS_IMETHOD_(void) NoteWeakMapping(void* map, void* key, void* kdelegate, void* val) 19 | { 20 | } 21 | 22 | bool mFound; 23 | 24 | private: 25 | void* mWrapper; 26 | }; 27 | diff --git a/js/src/jsfriendapi.cpp b/js/src/jsfriendapi.cpp 28 | --- a/js/src/jsfriendapi.cpp 29 | +++ b/js/src/jsfriendapi.cpp 30 | @@ -527,16 +527,24 @@ js::VisitGrayWrapperTargets(JSCompartmen 31 | { 32 | for (WrapperMap::Enum e(comp->crossCompartmentWrappers); !e.empty(); e.popFront()) { 33 | gc::Cell *thing = e.front().key.wrapped; 34 | if (thing->isMarked(gc::GRAY)) 35 | callback(closure, thing); 36 | } 37 | } 38 | 39 | +JS_FRIEND_API(JSObject *) 40 | +js::GetWeakmapKeyDelegate(JSObject *key) 41 | +{ 42 | + if (JSWeakmapKeyDelegateOp op = key->getClass()->ext.weakmapKeyDelegateOp) 43 | + return op(key); 44 | + return NULL; 45 | +} 46 | + 47 | JS_FRIEND_API(void) 48 | JS_SetAccumulateTelemetryCallback(JSRuntime *rt, JSAccumulateTelemetryDataCallback callback) 49 | { 50 | rt->telemetryCallback = callback; 51 | } 52 | 53 | JS_FRIEND_API(JSObject *) -------------------------------------------------------------------------------- /tests/fixtures/sample3.diff: -------------------------------------------------------------------------------- 1 | === added file 'added_file' 2 | --- added_file 1970-01-01 00:00:00 +0000 3 | +++ added_file 2013-10-13 23:44:04 +0000 4 | @@ -0,0 +1,4 @@ 5 | +This was missing! 6 | +holá mundo! 7 | + 8 | +Only for testing purposes. 9 | \ No newline at end of file 10 | 11 | === modified file 'modified_file' 12 | --- modified_file 2013-10-13 23:53:13 +0000 13 | +++ modified_file 2013-10-13 23:53:26 +0000 14 | @@ -1,5 +1,7 @@ 15 | This is the original content. 16 | 17 | -This should be updated. 18 | +This is now updated. 19 | + 20 | +This is a new line. 21 | 22 | This will stay. 23 | \ No newline at end of file 24 | 25 | === removed file 'removed_file' 26 | --- removed_file 2013-10-13 23:53:13 +0000 27 | +++ removed_file 1970-01-01 00:00:00 +0000 28 | @@ -1,3 +0,0 @@ 29 | -This content shouldn't be here. 30 | - 31 | -This file will be removed. 32 | \ No newline at end of file 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/sample4-plus.diff: -------------------------------------------------------------------------------- 1 | diff --git a/sample.txt b/sample.txt 2 | new file mode 100644 3 | index 0000000..3b18e51 4 | --- /dev/null 5 | +++ b/sample.txt 6 | @@ -0,0 +1 @@ 7 | +hello world 8 | diff --git a/sample2.txt b/sample2.txt 9 | new file mode 100644 10 | index 0000000..3b18e51 11 | --- /dev/null 12 | +++ b/sample2.txt 13 | @@ -0,0 +1 @@ 14 | +hello world 15 | diff --git a/sample3.txt b/sample3.txt 16 | new file mode 100644 17 | index 0000000..3b18e51 18 | --- /dev/null 19 | +++ b/sample3.txt 20 | @@ -0,0 +1 @@ 21 | +hello world 22 | -------------------------------------------------------------------------------- /tests/fixtures/sample4.diff: -------------------------------------------------------------------------------- 1 | diff --git a/sample.txt b/sample.txt 2 | new file mode 100644 3 | index 0000000..3b18e51 4 | --- /dev/null 5 | +++ b/sample.txt 6 | @@ -0,0 +1 @@ 7 | +hello world 8 | -------------------------------------------------------------------------------- /tests/fixtures/sample5.diff: -------------------------------------------------------------------------------- 1 | diff --git a/sample.txt b/sample.txt 2 | index 3b18e51..e69de29 100644 3 | --- a/sample.txt 4 | +++ b/sample.txt 5 | @@ -1 +0,0 @@ 6 | -hello world 7 | -------------------------------------------------------------------------------- /tests/fixtures/svn.diff: -------------------------------------------------------------------------------- 1 | Index: modified_file 2 | =================================================================== 3 | --- modified_file (revision 191) 4 | +++ modified_file (working copy) 5 | @@ -1,5 +1,7 @@ 6 | This is the original content. 7 | 8 | -This should be updated. 9 | +This is now updated. 10 | 11 | +This is a new line. 12 | + 13 | This will stay. 14 | \ No newline at end of file 15 | Index: removed_file 16 | =================================================================== 17 | --- removed_file (revision 188) 18 | +++ removed_file (working copy) 19 | @@ -1,3 +0,0 @@ 20 | -This content shouldn't be here. 21 | - 22 | -This file will be removed. 23 | \ No newline at end of file 24 | Index: added_file 25 | =================================================================== 26 | --- added_file (revision 0) 27 | +++ added_file (revision 0) 28 | @@ -0,0 +1,4 @@ 29 | +This was missing! 30 | +Adding it now. 31 | + 32 | +Only for testing purposes. 33 | \ No newline at end of file 34 | -------------------------------------------------------------------------------- /tests/test_hunk.rs: -------------------------------------------------------------------------------- 1 | extern crate unidiff; 2 | 3 | use unidiff::{Hunk, Line}; 4 | 5 | #[test] 6 | fn test_default_is_valid() { 7 | let hunk = Hunk::new(0, 0, 0, 0, ""); 8 | assert!(hunk.is_valid()); 9 | } 10 | 11 | #[test] 12 | fn test_missing_data_is_not_valid() { 13 | let hunk = Hunk::new(0, 1, 0, 1, ""); 14 | assert!(!hunk.is_valid()); 15 | } 16 | 17 | #[test] 18 | fn test_append_context() { 19 | let mut hunk = Hunk::new(0, 1, 0, 1, ""); 20 | hunk.append(Line::new("sample line", " ")); 21 | assert!(hunk.is_valid()); 22 | assert_eq!(hunk.source_lines(), hunk.target_lines()); 23 | } 24 | 25 | #[test] 26 | fn test_append_added_line() { 27 | let mut hunk = Hunk::new(0, 0, 0, 1, ""); 28 | hunk.append(Line::new("sample line", "+")); 29 | assert!(hunk.is_valid()); 30 | assert_eq!(0, hunk.source_lines().len()); 31 | assert_eq!(1, hunk.target_lines().len()); 32 | } 33 | 34 | #[test] 35 | fn test_append_removed_line() { 36 | let mut hunk = Hunk::new(0, 1, 0, 0, ""); 37 | hunk.append(Line::new("sample line", "-")); 38 | assert!(hunk.is_valid()); 39 | assert_eq!(1, hunk.source_lines().len()); 40 | assert_eq!(0, hunk.target_lines().len()); 41 | } 42 | -------------------------------------------------------------------------------- /tests/test_patchedfile.rs: -------------------------------------------------------------------------------- 1 | extern crate unidiff; 2 | 3 | use unidiff::{Hunk, PatchedFile}; 4 | 5 | #[test] 6 | fn test_is_added_file() { 7 | let hunk = Hunk::new(0, 0, 0, 1, ""); 8 | let file = PatchedFile::with_hunks("a", "b", vec![hunk]); 9 | assert!(file.is_added_file()); 10 | } 11 | 12 | #[test] 13 | fn test_is_removed_file() { 14 | let hunk = Hunk::new(0, 1, 0, 0, ""); 15 | let file = PatchedFile::with_hunks("a", "b", vec![hunk]); 16 | assert!(file.is_removed_file()); 17 | } 18 | 19 | #[test] 20 | fn test_is_modified_file() { 21 | let hunk = Hunk::new(0, 1, 0, 1, ""); 22 | let file = PatchedFile::with_hunks("a", "b", vec![hunk]); 23 | assert!(file.is_modified_file()); 24 | } 25 | -------------------------------------------------------------------------------- /tests/test_patchset.rs: -------------------------------------------------------------------------------- 1 | extern crate unidiff; 2 | 3 | use unidiff::PatchSet; 4 | 5 | #[test] 6 | fn test_parse_sample0_diff() { 7 | let buf = include_str!("fixtures/sample0.diff"); 8 | 9 | let mut patch = PatchSet::new(); 10 | patch.parse(&buf).unwrap(); 11 | 12 | // three file in the patch 13 | assert_eq!(3, patch.len()); 14 | // three hunks 15 | assert_eq!(3, patch[0].len()); 16 | 17 | // first file is modified 18 | assert!(patch[0].is_modified_file()); 19 | assert!(!patch[0].is_added_file()); 20 | assert!(!patch[0].is_removed_file()); 21 | 22 | // Hunk 1: five additions, no deletions, a section header 23 | assert_eq!(6, patch[0][0].added()); 24 | assert_eq!(0, patch[0][0].removed()); 25 | assert_eq!("Section Header", &patch[0][0].section_header); 26 | 27 | // Hunk 2: 2 additions, 8 deletions, no section header 28 | assert_eq!(2, patch[0][1].added()); 29 | assert_eq!(8, patch[0][1].removed()); 30 | assert_eq!("", &patch[0][1].section_header); 31 | 32 | // Hunk 3: four additions, no deletions, no section header 33 | assert_eq!(4, patch[0][2].added()); 34 | assert_eq!(0, patch[0][2].removed()); 35 | assert_eq!("", &patch[0][2].section_header); 36 | 37 | // Check file totals 38 | assert_eq!(12, patch[0].added()); 39 | assert_eq!(8, patch[0].removed()); 40 | 41 | // second file is added 42 | assert!(!patch[1].is_modified_file()); 43 | assert!(patch[1].is_added_file()); 44 | assert!(!patch[1].is_removed_file()); 45 | 46 | // third file is removed 47 | assert!(!patch[2].is_modified_file()); 48 | assert!(!patch[2].is_added_file()); 49 | assert!(patch[2].is_removed_file()); 50 | } 51 | 52 | #[test] 53 | fn test_parse_git_diff() { 54 | let buf = include_str!("fixtures/git.diff"); 55 | 56 | let mut patch = PatchSet::new(); 57 | patch.parse(&buf).unwrap(); 58 | 59 | assert_eq!(3, patch.len()); 60 | 61 | let added_files = patch.added_files(); 62 | assert_eq!(1, added_files.len()); 63 | assert_eq!("added_file", added_files[0].path()); 64 | assert_eq!(4, added_files[0].added()); 65 | assert_eq!(0, added_files[0].removed()); 66 | 67 | let removed_files = patch.removed_files(); 68 | assert_eq!(1, removed_files.len()); 69 | assert_eq!("removed_file", removed_files[0].path()); 70 | assert_eq!(0, removed_files[0].added()); 71 | assert_eq!(3, removed_files[0].removed()); 72 | 73 | let modified_files = patch.modified_files(); 74 | assert_eq!(1, modified_files.len()); 75 | assert_eq!("modified_file", modified_files[0].path()); 76 | assert_eq!(3, modified_files[0].added()); 77 | assert_eq!(1, modified_files[0].removed()); 78 | } 79 | 80 | #[test] 81 | fn test_parse_bzr_diff() { 82 | let buf = include_str!("fixtures/bzr.diff"); 83 | 84 | let mut patch = PatchSet::new(); 85 | patch.parse(&buf).unwrap(); 86 | 87 | assert_eq!(3, patch.len()); 88 | 89 | let added_files = patch.added_files(); 90 | assert_eq!(1, added_files.len()); 91 | assert_eq!("added_file", added_files[0].path()); 92 | assert_eq!(4, added_files[0].added()); 93 | assert_eq!(0, added_files[0].removed()); 94 | 95 | let removed_files = patch.removed_files(); 96 | assert_eq!(1, removed_files.len()); 97 | assert_eq!("removed_file", removed_files[0].path()); 98 | assert_eq!(0, removed_files[0].added()); 99 | assert_eq!(3, removed_files[0].removed()); 100 | 101 | let modified_files = patch.modified_files(); 102 | assert_eq!(1, modified_files.len()); 103 | assert_eq!("modified_file", modified_files[0].path()); 104 | assert_eq!(3, modified_files[0].added()); 105 | assert_eq!(1, modified_files[0].removed()); 106 | } 107 | 108 | #[test] 109 | fn test_parse_hg_diff() { 110 | let buf = include_str!("fixtures/hg.diff"); 111 | 112 | let mut patch = PatchSet::new(); 113 | patch.parse(&buf).unwrap(); 114 | 115 | assert_eq!(3, patch.len()); 116 | 117 | let added_files = patch.added_files(); 118 | assert_eq!(1, added_files.len()); 119 | assert_eq!("added_file", added_files[0].path()); 120 | assert_eq!(4, added_files[0].added()); 121 | assert_eq!(0, added_files[0].removed()); 122 | 123 | let removed_files = patch.removed_files(); 124 | assert_eq!(1, removed_files.len()); 125 | assert_eq!("removed_file", removed_files[0].path()); 126 | assert_eq!(0, removed_files[0].added()); 127 | assert_eq!(3, removed_files[0].removed()); 128 | 129 | let modified_files = patch.modified_files(); 130 | assert_eq!(1, modified_files.len()); 131 | assert_eq!("modified_file", modified_files[0].path()); 132 | assert_eq!(3, modified_files[0].added()); 133 | assert_eq!(1, modified_files[0].removed()); 134 | } 135 | 136 | #[test] 137 | fn test_parse_svn_diff() { 138 | let buf = include_str!("fixtures/svn.diff"); 139 | 140 | let mut patch = PatchSet::new(); 141 | patch.parse(&buf).unwrap(); 142 | 143 | assert_eq!(3, patch.len()); 144 | 145 | let added_files = patch.added_files(); 146 | assert_eq!(1, added_files.len()); 147 | assert_eq!("added_file", added_files[0].path()); 148 | assert_eq!(4, added_files[0].added()); 149 | assert_eq!(0, added_files[0].removed()); 150 | 151 | let removed_files = patch.removed_files(); 152 | assert_eq!(1, removed_files.len()); 153 | assert_eq!("removed_file", removed_files[0].path()); 154 | assert_eq!(0, removed_files[0].added()); 155 | assert_eq!(3, removed_files[0].removed()); 156 | 157 | let modified_files = patch.modified_files(); 158 | assert_eq!(1, modified_files.len()); 159 | assert_eq!("modified_file", modified_files[0].path()); 160 | assert_eq!(3, modified_files[0].added()); 161 | assert_eq!(1, modified_files[0].removed()); 162 | } 163 | 164 | #[test] 165 | fn test_parse_line_numbers() { 166 | let buf = include_str!("fixtures/sample0.diff"); 167 | 168 | let mut patch = PatchSet::new(); 169 | patch.parse(&buf).unwrap(); 170 | 171 | let mut target_line_nos = vec![]; 172 | let mut source_line_nos = vec![]; 173 | let mut diff_line_nos = vec![]; 174 | 175 | for diff_file in patch { 176 | for hunk in diff_file { 177 | for line in hunk { 178 | source_line_nos.push(line.source_line_no.clone()); 179 | target_line_nos.push(line.target_line_no.clone()); 180 | diff_line_nos.push(line.diff_line_no); 181 | } 182 | } 183 | } 184 | 185 | let expected_target_line_nos = vec![ 186 | // File: 1, Hunk: 1 187 | Some(1), 188 | Some(2), 189 | Some(3), 190 | Some(4), 191 | Some(5), 192 | Some(6), 193 | Some(7), 194 | Some(8), 195 | Some(9), 196 | // File: 1, Hunk: 2 197 | Some(11), 198 | Some(12), 199 | Some(13), 200 | None, 201 | None, 202 | None, 203 | None, 204 | None, 205 | None, 206 | None, 207 | Some(14), 208 | Some(15), 209 | Some(16), 210 | None, 211 | Some(17), 212 | Some(18), 213 | Some(19), 214 | Some(20), 215 | // File: 1, Hunk: 3 216 | Some(22), 217 | Some(23), 218 | Some(24), 219 | Some(25), 220 | Some(26), 221 | Some(27), 222 | Some(28), 223 | // File: 2, Hunk 1 224 | Some(1), 225 | Some(2), 226 | Some(3), 227 | Some(4), 228 | Some(5), 229 | Some(6), 230 | Some(7), 231 | Some(8), 232 | Some(9), 233 | // File: 3, Hunk 1 234 | None, 235 | None, 236 | None, 237 | None, 238 | None, 239 | None, 240 | None, 241 | None, 242 | None, 243 | ]; 244 | let expected_source_line_nos = vec![ 245 | // File: 1, Hunk: 1 246 | None, 247 | None, 248 | None, 249 | None, 250 | None, 251 | None, 252 | Some(1), 253 | Some(2), 254 | Some(3), 255 | // File: 1, Hunk: 2 256 | Some(5), 257 | Some(6), 258 | Some(7), 259 | Some(8), 260 | Some(9), 261 | Some(10), 262 | Some(11), 263 | Some(12), 264 | Some(13), 265 | Some(14), 266 | None, 267 | Some(15), 268 | Some(16), 269 | Some(17), 270 | None, 271 | Some(18), 272 | Some(19), 273 | Some(20), 274 | // File: 1, Hunk: 3 275 | Some(22), 276 | Some(23), 277 | Some(24), 278 | None, 279 | None, 280 | None, 281 | None, 282 | // File: 2, Hunk 1 283 | None, 284 | None, 285 | None, 286 | None, 287 | None, 288 | None, 289 | None, 290 | None, 291 | None, 292 | // File: 3, Hunk 1 293 | Some(1), 294 | Some(2), 295 | Some(3), 296 | Some(4), 297 | Some(5), 298 | Some(6), 299 | Some(7), 300 | Some(8), 301 | Some(9), 302 | ]; 303 | let expected_diff_line_nos = vec![ 304 | // File: 1, Hunk: 1 305 | 4, 5, 6, 7, 8, 9, 10, 11, 12, // File: 1, Hunk: 2 306 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 307 | // File: 1, Hunk: 3 308 | 33, 34, 35, 36, 37, 38, 39, // File: 2, Hunk 1 309 | 43, 44, 45, 46, 47, 48, 49, 50, 51, // File: 3, Hunk 1 310 | 55, 56, 57, 58, 59, 60, 61, 62, 63, 311 | ]; 312 | 313 | assert_eq!(expected_source_line_nos, source_line_nos); 314 | assert_eq!(expected_target_line_nos, target_line_nos); 315 | assert_eq!(expected_diff_line_nos, diff_line_nos); 316 | } 317 | 318 | #[cfg(feature = "encoding")] 319 | #[test] 320 | fn test_parse_from_encoding() { 321 | let buf = include_bytes!("fixtures/sample3.diff"); 322 | 323 | let mut patch = PatchSet::from_encoding("utf-8"); 324 | patch.parse_bytes(buf).unwrap(); 325 | 326 | assert_eq!(3, patch.len()); 327 | assert_eq!("holá mundo!", patch[0][0][1].value); 328 | } 329 | 330 | #[test] 331 | fn test_single_line_diff() { 332 | { 333 | let buf = include_str!("fixtures/sample4.diff"); 334 | 335 | let mut patch = PatchSet::new(); 336 | patch.parse(&buf).unwrap(); 337 | 338 | assert_eq!(1, patch.len()); 339 | 340 | let added_files = patch.added_files(); 341 | assert_eq!(1, added_files.len()); 342 | assert_eq!("sample.txt", added_files[0].path()); 343 | assert_eq!(1, added_files[0].added()); 344 | assert_eq!(0, added_files[0].removed()); 345 | } 346 | { 347 | let buf = include_str!("fixtures/sample5.diff"); 348 | 349 | let mut patch = PatchSet::new(); 350 | patch.parse(&buf).unwrap(); 351 | 352 | assert_eq!(1, patch.len()); 353 | 354 | let removed_files = patch.removed_files(); 355 | assert_eq!(1, removed_files.len()); 356 | assert_eq!("sample.txt", removed_files[0].path()); 357 | assert_eq!(0, removed_files[0].added()); 358 | assert_eq!(1, removed_files[0].removed()); 359 | } 360 | } 361 | 362 | #[test] 363 | fn test_single_line_diff_with_trailer() { 364 | let buf = include_str!("fixtures/sample4-plus.diff"); 365 | 366 | let mut patch = PatchSet::new(); 367 | patch.parse(&buf).unwrap(); 368 | 369 | assert_eq!(3, patch.len()); 370 | 371 | let added_files = patch.added_files(); 372 | assert_eq!(3, added_files.len()); 373 | assert_eq!("sample.txt", added_files[0].path()); 374 | assert_eq!(1, added_files[0].added()); 375 | assert_eq!(0, added_files[0].removed()); 376 | 377 | assert_eq!("sample2.txt", added_files[1].path()); 378 | assert_eq!(1, added_files[1].added()); 379 | assert_eq!(0, added_files[1].removed()); 380 | 381 | assert_eq!("sample3.txt", added_files[2].path()); 382 | assert_eq!(1, added_files[2].added()); 383 | assert_eq!(0, added_files[2].removed()); 384 | } 385 | 386 | #[test] 387 | fn test_parse_patchset_from_str() { 388 | let buf = include_str!("fixtures/sample0.diff"); 389 | 390 | let patch: PatchSet = buf.parse().unwrap(); 391 | 392 | // three file in the patch 393 | assert_eq!(3, patch.len()); 394 | // three hunks 395 | assert_eq!(3, patch[0].len()); 396 | } 397 | --------------------------------------------------------------------------------