Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * Support for interacting with a git repository. | |
| 7 */ | |
| 8 library analysis_server.test.stress.utilities.git; | |
| 9 | |
| 10 import 'dart:convert'; | |
| 11 import 'dart:io'; | |
| 12 | |
| 13 import 'package:path/path.dart' as path; | |
| 14 | |
| 15 /** | |
| 16 * A representation of the differences between two blobs. | |
| 17 */ | |
| 18 class BlobDiff { | |
| 19 /** | |
| 20 * The regular expression used to identify the beginning of a hunk. | |
| 21 */ | |
| 22 static final RegExp hunkHeaderRegExp = | |
| 23 new RegExp(r'@@ -([0-9]+)(?:,[0-9]+)? \+([0-9]+)(?:,[0-9]+)? @@'); | |
| 24 | |
| 25 /** | |
| 26 * A list of the hunks in the diff. | |
| 27 */ | |
| 28 List<DiffHunk> hunks = <DiffHunk>[]; | |
| 29 | |
| 30 /** | |
| 31 * Initialize a newly created blob diff by parsing the result of the git diff | |
| 32 * command (the [input]). | |
|
Paul Berry
2015/11/17 19:49:31
Git diff has a lot of options controlling its outp
Brian Wilkerson
2015/11/18 17:36:46
I don't really want to deeply document it everywhe
Paul Berry
2015/11/18 18:03:19
We ignore them, but that won't give correct result
| |
| 33 */ | |
| 34 BlobDiff(List<String> input) { | |
| 35 _parseInput(input); | |
| 36 } | |
| 37 | |
| 38 /** | |
| 39 * Parse the result of the git diff command (the [input]). | |
| 40 */ | |
| 41 void _parseInput(List<String> input) { | |
| 42 for (String line in input) { | |
| 43 _parseLine(line); | |
| 44 } | |
| 45 } | |
| 46 | |
| 47 /** | |
| 48 * Parse a single [line] from the result of the git diff command. | |
| 49 */ | |
| 50 void _parseLine(String line) { | |
| 51 DiffHunk currentHunk = hunks.isEmpty ? null : hunks.last; | |
| 52 if (line.startsWith('@@')) { | |
| 53 Match match = hunkHeaderRegExp.matchAsPrefix(line); | |
| 54 int srcLine = int.parse(match.group(1)); | |
| 55 int dstLine = int.parse(match.group(2)); | |
| 56 hunks.add(new DiffHunk(srcLine, dstLine)); | |
| 57 } else if (currentHunk != null && line.startsWith('+')) { | |
| 58 currentHunk.addLines.add(line.substring(1)); | |
| 59 } else if (currentHunk != null && line.startsWith('-')) { | |
| 60 currentHunk.removeLines.add(line.substring(1)); | |
| 61 } | |
|
Paul Berry
2015/11/17 19:49:31
Diff uses special formatting when the end of the f
Brian Wilkerson
2015/11/18 17:36:46
I'll need a pointer to understand what needs to ha
Paul Berry
2015/11/18 18:03:19
Unfortunately I don't have a pointer to give you (
| |
| 62 } | |
| 63 } | |
| 64 | |
| 65 /** | |
| 66 * A representation of the differences between two commits. | |
| 67 */ | |
| 68 class CommitDelta { | |
| 69 /** | |
| 70 * The length (in characters) of a SHA. | |
| 71 */ | |
| 72 static final int SHA_LENGTH = 40; | |
| 73 | |
| 74 /** | |
| 75 * The code-point for a colon (':'). | |
| 76 */ | |
| 77 static final int COLON = ':'.codeUnitAt(0); | |
| 78 | |
| 79 /** | |
| 80 * The code-point for a nul character. | |
| 81 */ | |
| 82 static final int NUL = 0; | |
| 83 | |
| 84 /** | |
| 85 * The code-point for a tab. | |
| 86 */ | |
| 87 static final int TAB = '\t'.codeUnitAt(0); | |
| 88 | |
| 89 /** | |
| 90 * The repository from which the commits were taken. | |
| 91 */ | |
| 92 final GitRepository repository; | |
| 93 | |
| 94 /** | |
| 95 * The records of the files that were changed. | |
| 96 */ | |
| 97 final List<DiffRecord> diffRecords = <DiffRecord>[]; | |
| 98 | |
| 99 /** | |
| 100 * Initialize a newly created representation of the differences between two | |
| 101 * commits. The differences are computed by parsing the result of a git diff | |
| 102 * command (the [diffResults]). | |
|
Paul Berry
2015/11/17 19:49:31
As with BlobDiff, it would be nice to clarify what
Brian Wilkerson
2015/11/18 17:36:46
Ditto
| |
| 103 */ | |
| 104 CommitDelta(this.repository, String diffResults) { | |
| 105 _parseInput(diffResults); | |
| 106 } | |
| 107 | |
| 108 /** | |
| 109 * Return `true` if there are differences. | |
| 110 */ | |
| 111 bool get hasDiffs => diffRecords.isNotEmpty; | |
| 112 | |
| 113 /** | |
| 114 * Return the absolute paths of all of the files in this commit whose name | |
| 115 * matches the given [fileName]. | |
| 116 */ | |
| 117 Iterable<String> filesMatching(String fileName) { | |
| 118 return diffRecords | |
| 119 .where((DiffRecord record) => record.isFor(fileName)) | |
| 120 .map((DiffRecord record) => record.srcPath); | |
| 121 } | |
| 122 | |
| 123 /** | |
| 124 * Remove any diffs for files that are either (a) outside the given | |
| 125 * [analysisRoots], or (b) are files that are not being analyzed by the server . | |
| 126 */ | |
| 127 void filterDiffs(List<String> analysisRoots) { | |
|
Paul Berry
2015/11/17 19:49:30
This method seems out of place, since it is the on
Brian Wilkerson
2015/11/18 17:36:46
I can rename the parameter; it's just a list of di
| |
| 128 diffRecords.retainWhere((DiffRecord record) { | |
| 129 String filePath = record.srcPath ?? record.dstPath; | |
| 130 for (String analysisRoot in analysisRoots) { | |
| 131 if (path.isWithin(analysisRoot, filePath)) { | |
| 132 // TODO(brianwilkerson) Generalize this by asking the server for the | |
| 133 // list of glob patterns of files to be analyzed (once there's an API | |
| 134 // for doing so). | |
| 135 if (filePath.endsWith('.dart') || | |
| 136 filePath.endsWith('.html') || | |
| 137 filePath.endsWith('.htm') || | |
| 138 filePath.endsWith('.analysisOptions')) { | |
| 139 return true; | |
| 140 } | |
| 141 } | |
| 142 } | |
| 143 return false; | |
| 144 }); | |
| 145 } | |
| 146 | |
| 147 /** | |
| 148 * Return the index of the first nul character in the given [string] that is | |
| 149 * at or after the given [start] index. | |
| 150 */ | |
| 151 int _findEnd(String string, int start) { | |
| 152 int length = string.length; | |
| 153 int end = start; | |
| 154 while (end < length && string.codeUnitAt(end) != NUL) { | |
| 155 end++; | |
| 156 } | |
| 157 return end; | |
| 158 } | |
| 159 | |
| 160 /** | |
| 161 * Return the result of converting the given [relativePath] to an absolute | |
| 162 * path. The path is assumed to be relative to the root of the repository. | |
| 163 */ | |
| 164 String _makeAbsolute(String relativePath) { | |
| 165 return path.join(repository.path, relativePath); | |
| 166 } | |
| 167 | |
| 168 /** | |
| 169 * Parse all of the diff records in the given [input]. | |
| 170 */ | |
| 171 void _parseInput(String input) { | |
| 172 int length = input.length; | |
| 173 int start = 0; | |
| 174 while (start < length) { | |
| 175 start = _parseRecord(input, start); | |
| 176 } | |
| 177 } | |
| 178 | |
| 179 /** | |
| 180 * Parse a single record from the given [input], assuming that the record | |
| 181 * starts at the given [startIndex]. | |
| 182 * | |
| 183 * Each record is formatted as a sequence of fields. The fields are, from the | |
| 184 * left to the right: | |
| 185 * | |
| 186 * 1. a colon. | |
| 187 * 2. mode for "src"; 000000 if creation or unmerged. | |
| 188 * 3. a space. | |
| 189 * 4. mode for "dst"; 000000 if deletion or unmerged. | |
| 190 * 5. a space. | |
| 191 * 6. sha1 for "src"; 0{40} if creation or unmerged. | |
| 192 * 7. a space. | |
| 193 * 8. sha1 for "dst"; 0{40} if creation, unmerged or "look at work tree". | |
| 194 * 9. a space. | |
| 195 * 10. status, followed by optional "score" number. | |
| 196 * 11. a tab or a NUL when -z option is used. | |
| 197 * 12. path for "src" | |
| 198 * 13. a tab or a NUL when -z option is used; only exists for C or R. | |
| 199 * 14. path for "dst"; only exists for C or R. | |
| 200 * 15. an LF or a NUL when -z option is used, to terminate the record. | |
| 201 */ | |
| 202 int _parseRecord(String input, int startIndex) { | |
| 203 // Skip the first five fields. | |
| 204 startIndex += 15; | |
| 205 // 6 | |
|
Paul Berry
2015/11/17 19:49:31
What do these numbers mean?
Brian Wilkerson
2015/11/18 17:36:46
They're field numbers. Added to the comments to ma
| |
| 206 String srcSha = input.substring(startIndex, startIndex + SHA_LENGTH); | |
| 207 startIndex += SHA_LENGTH + 1; | |
| 208 // 8 | |
| 209 String dstSha = input.substring(startIndex, startIndex + SHA_LENGTH); | |
| 210 startIndex += SHA_LENGTH + 1; | |
| 211 // 10 | |
| 212 int endIndex = _findEnd(input, startIndex); | |
| 213 String status = input.substring(startIndex, endIndex); | |
| 214 startIndex = endIndex + 1; | |
| 215 // 12 | |
| 216 endIndex = _findEnd(input, startIndex); | |
| 217 String srcPath = _makeAbsolute(input.substring(startIndex, endIndex)); | |
| 218 startIndex = endIndex + 1; | |
| 219 // 14 | |
| 220 String dstPath = null; | |
| 221 if (status.startsWith('C') || status.startsWith('R')) { | |
| 222 endIndex = _findEnd(input, startIndex); | |
| 223 dstPath = _makeAbsolute(input.substring(startIndex, endIndex)); | |
| 224 } | |
| 225 // Create the record. | |
| 226 diffRecords.add( | |
| 227 new DiffRecord(repository, srcSha, dstSha, status, srcPath, dstPath)); | |
| 228 return endIndex + 1; | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 /** | |
| 233 * A representation of the history of a Git repository. | |
| 234 */ | |
| 235 class CommitHistory { | |
|
Paul Berry
2015/11/17 19:49:31
Since git histories frequently have branches, we s
Brian Wilkerson
2015/11/18 17:36:46
Done
| |
| 236 /** | |
| 237 * The repository whose history is being represented. | |
| 238 */ | |
| 239 final GitRepository repository; | |
| 240 | |
| 241 /** | |
| 242 * The id's (SHA's) of the commits in the repository, with the most recent | |
| 243 * commit being first and the oldest commit being last. | |
| 244 */ | |
| 245 final List<String> commitIds; | |
| 246 | |
| 247 /** | |
| 248 * Initialize a commit history for the given [repository] to have the given | |
| 249 * [commitIds]. | |
| 250 */ | |
| 251 CommitHistory(this.repository, this.commitIds); | |
| 252 | |
| 253 /** | |
| 254 * Return an iterator that can be used to iterate over this commit history. | |
| 255 */ | |
| 256 CommitHistoryIterator iterator() { | |
| 257 return new CommitHistoryIterator(this); | |
| 258 } | |
| 259 } | |
| 260 | |
| 261 /** | |
| 262 * An iterator over the history of a Git repository. | |
| 263 */ | |
| 264 class CommitHistoryIterator { | |
|
Paul Berry
2015/11/17 19:49:31
Similar concern here.
Brian Wilkerson
2015/11/18 17:36:46
Done
| |
| 265 /** | |
| 266 * The commit history being iterated over. | |
| 267 */ | |
| 268 final CommitHistory history; | |
| 269 | |
| 270 /** | |
| 271 * The index of the current commit in the list of [commitIds]. | |
| 272 */ | |
| 273 int currentCommit; | |
| 274 | |
| 275 /** | |
| 276 * Initialize a newly created iterator to iterate over the commits with the | |
| 277 * given [commitIds]; | |
| 278 */ | |
| 279 CommitHistoryIterator(this.history) { | |
| 280 currentCommit = history.commitIds.length; | |
| 281 } | |
| 282 | |
| 283 /** | |
| 284 * Return the SHA1 of the commit after the current commit (the 'dst' of the | |
| 285 * [next] diff). | |
| 286 */ | |
| 287 String get dstCommit => history.commitIds[currentCommit - 1]; | |
| 288 | |
| 289 /** | |
| 290 * Return the SHA1 of the current commit (the 'src' of the [next] diff). | |
| 291 */ | |
| 292 String get srcCommit => history.commitIds[currentCommit]; | |
| 293 | |
| 294 /** | |
| 295 * Advance to the next commit in the history. Return `true` if it is safe to | |
| 296 * ask for the [next] diff. | |
| 297 */ | |
| 298 bool moveNext() { | |
| 299 if (currentCommit <= 1) { | |
| 300 return false; | |
| 301 } | |
| 302 currentCommit--; | |
| 303 return true; | |
| 304 } | |
| 305 | |
| 306 /** | |
| 307 * Return the difference between the current commit and the commit that | |
| 308 * followed it. | |
| 309 */ | |
| 310 CommitDelta next() => history.repository.getCommitDiff(srcCommit, dstCommit); | |
| 311 } | |
| 312 | |
| 313 /** | |
| 314 * Representation of a single diff hunk. | |
| 315 */ | |
| 316 class DiffHunk { | |
| 317 /** | |
| 318 * The index of the first line that was changed in the src. | |
|
Paul Berry
2015/11/17 19:49:31
Since diffSrcLine and diffDstLine are public, thei
Brian Wilkerson
2015/11/18 17:36:46
Done
| |
| 319 */ | |
| 320 int diffSrcLine; | |
| 321 | |
| 322 /** | |
| 323 * The index of the first line that was changed in the dst. | |
| 324 */ | |
| 325 int diffDstLine; | |
| 326 | |
| 327 /** | |
| 328 * A list of the individual lines that were removed from the src. | |
| 329 */ | |
| 330 List<String> removeLines = <String>[]; | |
| 331 | |
| 332 /** | |
| 333 * A list of the individual lines that were added to the dst. | |
| 334 */ | |
| 335 List<String> addLines = <String>[]; | |
| 336 | |
| 337 /** | |
| 338 * Initialize a newly created hunk. The lines will be | |
|
Paul Berry
2015/11/17 19:49:31
Looks like you forgot to finish this comment.
Brian Wilkerson
2015/11/18 17:36:46
Yep. Thanks.
| |
| 339 */ | |
| 340 DiffHunk(this.diffSrcLine, this.diffDstLine); | |
| 341 | |
| 342 /** | |
| 343 * Return the index of the first line that was changed in the dst. | |
|
Paul Berry
2015/11/17 19:49:31
Similarly, the doc comment here should explicitly
Brian Wilkerson
2015/11/18 17:36:46
Done
| |
| 344 */ | |
| 345 int get dstLine { | |
| 346 // The diff command numbers lines starting at 1, but it subtracts 1 from | |
| 347 // dstLine if there are no lines on the destination side of the hunk. We | |
| 348 // convert it into something reasonable. | |
| 349 return addLines.isEmpty ? diffDstLine : diffDstLine - 1; | |
| 350 } | |
| 351 | |
| 352 /** | |
| 353 * Return the index of the first line that was changed in the src. | |
| 354 */ | |
| 355 int get srcLine { | |
| 356 // The diff command numbers lines starting at 1, but it subtracts 1 from | |
| 357 // srcLine if there are no lines on the source side of the hunk. We convert | |
| 358 // it into something reasonable. | |
| 359 return removeLines.isEmpty ? diffSrcLine : diffSrcLine - 1; | |
| 360 } | |
| 361 } | |
| 362 | |
| 363 /** | |
| 364 * A representation of a single line (record) from a raw diff. | |
| 365 */ | |
| 366 class DiffRecord { | |
| 367 /** | |
| 368 * The repository containing the file(s) that were modified. | |
| 369 */ | |
| 370 final GitRepository repository; | |
| 371 | |
| 372 /** | |
| 373 * The SHA1 of the blob in the src. | |
| 374 */ | |
| 375 final String srcBlob; | |
| 376 | |
| 377 /** | |
| 378 * The SHA1 of the blob in the dst. | |
| 379 */ | |
| 380 final String dstBlob; | |
| 381 | |
| 382 /** | |
| 383 * The status of the change. Valid values are: | |
| 384 * * A: addition of a file | |
| 385 * * C: copy of a file into a new one | |
| 386 * * D: deletion of a file | |
| 387 * * M: modification of the contents or mode of a file | |
| 388 * * R: renaming of a file | |
| 389 * * T: change in the type of the file | |
| 390 * * U: file is unmerged (you must complete the merge before it can be committ ed) | |
| 391 * * X: "unknown" change type (most probably a bug, please report it) | |
| 392 * | |
| 393 * Status letters C and R are always followed by a score (denoting the | |
| 394 * percentage of similarity between the source and target of the move or | |
| 395 * copy), and are the only ones to be so. | |
| 396 */ | |
| 397 final String status; | |
| 398 | |
| 399 /** | |
| 400 * The path of the src. | |
| 401 */ | |
| 402 final String srcPath; | |
| 403 | |
| 404 /** | |
| 405 * The path of the dst if this was either a copy or a rename operation. | |
| 406 */ | |
| 407 final String dstPath; | |
| 408 | |
| 409 /** | |
| 410 * Initialize a newly created diff record. | |
| 411 */ | |
| 412 DiffRecord(this.repository, this.srcBlob, this.dstBlob, this.status, | |
| 413 this.srcPath, this.dstPath); | |
| 414 | |
| 415 /** | |
| 416 * Return `true` if this record represents a file that was added. | |
| 417 */ | |
| 418 bool get isAddition => status == 'A'; | |
|
Paul Berry
2015/11/17 19:49:31
Rather than having a bunch of "is" getters that ar
Brian Wilkerson
2015/11/18 17:36:46
I think you can do that today:
switch (record
| |
| 419 | |
| 420 /** | |
| 421 * Return `true` if this record represents a file that was copied. | |
| 422 */ | |
| 423 bool get isCopy => status.startsWith('C'); | |
| 424 | |
| 425 /** | |
| 426 * Return `true` if this record represents a file that was deleted. | |
| 427 */ | |
| 428 bool get isDeletion => status == 'D'; | |
| 429 | |
| 430 /** | |
| 431 * Return `true` if this record represents a file that was modified. | |
| 432 */ | |
| 433 bool get isModification => status == 'M'; | |
| 434 | |
| 435 /** | |
| 436 * Return `true` if this record represents a file that was renamed. | |
| 437 */ | |
| 438 bool get isRename => status.startsWith('R'); | |
| 439 | |
| 440 /** | |
| 441 * Return `true` if this record represents an entity whose type was changed | |
| 442 * (for example, from a file to a directory). | |
| 443 */ | |
| 444 bool get isTypeChange => status == 'T'; | |
| 445 | |
| 446 /** | |
| 447 * Return a representation of the individual blobs within this diff. | |
| 448 */ | |
| 449 BlobDiff getBlobDiff() => repository.getBlobDiff(srcBlob, dstBlob); | |
| 450 | |
| 451 /** | |
| 452 * Return `true` if this diff applies to a file with the given name. | |
| 453 */ | |
| 454 bool isFor(String fileName) => | |
| 455 (srcPath != null && fileName == path.basename(srcPath)) || | |
| 456 (dstPath != null && fileName == path.basename(dstPath)); | |
| 457 | |
| 458 @override | |
| 459 String toString() => srcPath ?? dstPath; | |
| 460 } | |
| 461 | |
| 462 /** | |
| 463 * A representation of a git repository. | |
| 464 */ | |
| 465 class GitRepository { | |
| 466 /** | |
| 467 * The absolute path of the directory containing the repository. | |
| 468 */ | |
| 469 final String path; | |
| 470 | |
| 471 /** | |
| 472 * Initialize a newly created repository to represent the git repository at | |
| 473 * the given [path]. | |
| 474 */ | |
| 475 GitRepository(this.path); | |
| 476 | |
| 477 /** | |
| 478 * Checkout the given [commit] from the repository. This is done by running | |
| 479 * the command `git checkout <sha>`. | |
| 480 */ | |
| 481 void checkout(String commit) { | |
| 482 _run('checkout', commit); | |
| 483 } | |
| 484 | |
| 485 /** | |
| 486 * Return details about the differences between the two blobs identified by | |
| 487 * the SHA1 of the [srcBlob] and the SHA1 of the [dstBlob]. This is done by | |
| 488 * running the command `git diff <blob> <blob>`. | |
| 489 */ | |
| 490 BlobDiff getBlobDiff(String srcBlob, String dstBlob) { | |
| 491 ProcessResult result = _run('diff', srcBlob, dstBlob); | |
| 492 List<String> diffResults = LineSplitter.split(result.stdout).toList(); | |
| 493 return new BlobDiff(diffResults); | |
| 494 } | |
| 495 | |
| 496 /** | |
| 497 * Return details about the differences between the two commits identified by | |
| 498 * the [srcCommit] and [dstCommit]. This is done by running the command | |
| 499 * `git diff --raw --no-abbrev --no-renames -z <sha> <sha>`. | |
| 500 */ | |
| 501 CommitDelta getCommitDiff(String srcCommit, String dstCommit) { | |
| 502 // Consider --find-renames instead of --no-renames if rename information is | |
| 503 // desired. | |
| 504 ProcessResult result = _run('diff', '--raw', '--no-abbrev', '--no-renames', | |
| 505 '-z', srcCommit, dstCommit); | |
| 506 return new CommitDelta(this, result.stdout); | |
| 507 } | |
| 508 | |
| 509 /** | |
| 510 * Return a representation of the history of this repository. This is done by | |
| 511 * running the command `git rev-list --first-parent HEAD`. | |
| 512 */ | |
| 513 CommitHistory getCommitHistory() { | |
| 514 ProcessResult result = _run('rev-list', '--first-parent', 'HEAD'); | |
| 515 List<String> commitIds = LineSplitter.split(result.stdout).toList(); | |
| 516 return new CommitHistory(this, commitIds); | |
| 517 } | |
| 518 | |
| 519 /** | |
| 520 * Synchronously run the given [executable] with the given [arguments]. Return | |
| 521 * the result of running the process. | |
| 522 */ | |
| 523 ProcessResult _run(String arg1, | |
| 524 [String arg2, | |
|
Paul Berry
2015/11/17 19:49:31
These optional args, and the if tree below, seem l
Brian Wilkerson
2015/11/18 17:36:46
Just following a pattern I saw somewhere else. I'v
| |
| 525 String arg3, | |
| 526 String arg4, | |
| 527 String arg5, | |
| 528 String arg6, | |
| 529 String arg7]) { | |
| 530 List<String> arguments = <String>[]; | |
| 531 arguments.add(arg1); | |
| 532 if (arg2 != null) { | |
| 533 arguments.add(arg2); | |
| 534 if (arg3 != null) { | |
| 535 arguments.add(arg3); | |
| 536 if (arg4 != null) { | |
| 537 arguments.add(arg4); | |
| 538 if (arg5 != null) { | |
| 539 arguments.add(arg5); | |
| 540 if (arg6 != null) { | |
| 541 arguments.add(arg6); | |
| 542 if (arg7 != null) { | |
| 543 arguments.add(arg7); | |
| 544 } | |
| 545 } | |
| 546 } | |
| 547 } | |
| 548 } | |
| 549 } | |
| 550 return Process.runSync('git', arguments, | |
| 551 stderrEncoding: UTF8, stdoutEncoding: UTF8, workingDirectory: path); | |
| 552 } | |
| 553 } | |
| OLD | NEW |