parsePatch function

List<FileDiff> parsePatch(
  1. String patch
)

Parses a unified diff, such as the output of git diff or diff -u, into one FileDiff per file.

Git headers are understood: added, deleted, renamed and copied files, quoted paths, binary files, and \ No newline at end of file. Text before the first file, such as a commit message, is ignored. Malformed input never throws; whatever could be read is returned.

Transport CRLF is distinguished from source CRLF. Display text excludes carriage returns; source terminators remain available for serialization.

Implementation

List<FileDiff> parsePatch(String patch) {
  final rawLines = patch.split('\n');
  final header = rawLines
      .where(
        (line) => line.startsWith('diff --git ') || line.startsWith('--- '),
      )
      .firstOrNull;
  final transport = header?.endsWith('\r') == true ? '\r\n' : '\n';
  final lines = [
    for (final line in rawLines)
      transport == '\r\n' && line.endsWith('\r')
          ? line.substring(0, line.length - 1)
          : line,
  ];
  // The final line break ends the last line; it doesn't start a blank one.
  if (patch.endsWith('\n')) lines.removeLast();
  final files = <FileDiff>[];
  _FileBuilder? file;

  void finish() {
    if (file != null) {
      files.add(file!.build().copyWith(patchLineEnding: transport));
    }
    file = null;
  }

  var i = 0;
  while (i < lines.length) {
    final line = lines[i];
    if (line.startsWith('diff --git ')) {
      finish();
      file = _FileBuilder()..readGitPaths(line.substring(11));
      i++;
    } else if (line.startsWith('--- ') &&
        i + 1 < lines.length &&
        lines[i + 1].startsWith('+++ ')) {
      // Plain unified diffs start each file here.
      if (file == null || file!.hunks.isNotEmpty) {
        finish();
        file = _FileBuilder();
      }
      file!
        ..oldPath = _pathField(line.substring(4))
        ..newPath = _pathField(lines[i + 1].substring(4))
        ..sawPathLines = true;
      i += 2;
    } else if (line.startsWith('@@') && file != null) {
      i = _readHunk(lines, i, file!);
    } else if (file != null && file!.hunks.isEmpty) {
      file!.readHeaderLine(line);
      i++;
    } else {
      i++;
    }
  }
  finish();
  return files;
}