parsePatch function
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;
}