* Parses a unified diff into a list of "diff hunks" (each hunk starts with a * line starting with @@ and represents a collection of localized changes). * * @param diff a diff in git's unified diff format * @returns a list of hunk structures * * The diff is assumed to be a collection of hunks, where each hunk has the * following structure: * * METADATA
| 222 | * generates them anyway for files with unresolved conflicts. |
| 223 | */ |
| 224 | std::vector<DiffHunk> parseHunks(VcsDiff& diff) |
| 225 | { |
| 226 | std::vector<DiffHunk> ret; |
| 227 | int lineNo = -1; |
| 228 | QString curSrcFileName, curTgtFileName; |
| 229 | QStringListIterator lines(diff.diff().split(QLatin1Char('\n'))); |
| 230 | while (lines.hasNext()) { |
| 231 | lineNo++; |
| 232 | auto curln = lines.next(); |
| 233 | auto m = DIFF_FILENAME_RE->match(curln); |
| 234 | if (m.hasMatch()) { |
| 235 | if (curln.startsWith(QLatin1Char('-'))) |
| 236 | curSrcFileName = m.captured(1); |
| 237 | else if (curln.startsWith(QLatin1Char('+'))) |
| 238 | curTgtFileName = m.captured(1); |
| 239 | continue; |
| 240 | } |
| 241 | m = HUNK_HEADER_RE->match(curln); |
| 242 | if (!m.hasMatch()) |
| 243 | continue; |
| 244 | const auto [oldStart, oldCount] = parseRange(m.capturedView(1)); |
| 245 | const auto [newStart, newCount] = parseRange(m.capturedView(2)); |
| 246 | auto heading = m.captured(3); |
| 247 | uint firstLineIdx = lineNo; |
| 248 | QStringList hunkLines; |
| 249 | while (lines.hasNext() |
| 250 | && (CONFLICT_START_RE->match(lines.peekNext()).hasMatch() |
| 251 | || !META_LINE_RE->match(lines.peekNext()).hasMatch())) { |
| 252 | // Consume the conflict |
| 253 | if (CONFLICT_START_RE->match(lines.peekNext()).hasMatch()) { |
| 254 | lineNo++; |
| 255 | hunkLines << lines.next(); |
| 256 | while (lines.hasNext() && !CONFLICT_END_RE->match(lines.peekNext()).hasMatch()) { |
| 257 | lineNo++; |
| 258 | hunkLines << lines.next(); |
| 259 | } |
| 260 | if (!CONFLICT_END_RE->match(lines.peekNext()).hasMatch()) { |
| 261 | qCWarning(VCS) << "Invalid diff format, end of file reached before conflict finished"; |
| 262 | qCDebug(VCS) << diff.diff(); |
| 263 | break; |
| 264 | } |
| 265 | } |
| 266 | lineNo++; |
| 267 | hunkLines << lines.next(); |
| 268 | } |
| 269 | |
| 270 | // The number of filenames present in the diff should match the number |
| 271 | // of hunks |
| 272 | ret.push_back({ oldStart, oldCount, newStart, newCount, firstLineIdx, curSrcFileName, curTgtFileName, |
| 273 | heading, hunkLines }); |
| 274 | } |
| 275 | |
| 276 | // If the diff ends with a newline, for the last hunk, when splitting into lines above |
| 277 | // we will always get an empty string at the end, which we now remove |
| 278 | if (diff.diff().endsWith(QLatin1Char('\n'))) { |
| 279 | if (ret.size() > 0 && ret.back().lines.size() > 0) { |
| 280 | ret.back().lines.pop_back(); |
| 281 | } else { |
no test coverage detected