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 |