Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(200)

Side by Side Diff: pkg/analysis_server/test/stress/utilities/git.dart

Issue 1453213002: Add support for running git in tests (Closed) Base URL: https://github.com/dart-lang/sdk.git@master
Patch Set: Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698