Chromium Code Reviews| Index: pkg/analysis_server/test/stress/utilities/git.dart |
| diff --git a/pkg/analysis_server/test/stress/utilities/git.dart b/pkg/analysis_server/test/stress/utilities/git.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..cafad11b4b1b44018c685ee6c0effabc569445b7 |
| --- /dev/null |
| +++ b/pkg/analysis_server/test/stress/utilities/git.dart |
| @@ -0,0 +1,553 @@ |
| +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +/** |
| + * Support for interacting with a git repository. |
| + */ |
| +library analysis_server.test.stress.utilities.git; |
| + |
| +import 'dart:convert'; |
| +import 'dart:io'; |
| + |
| +import 'package:path/path.dart' as path; |
| + |
| +/** |
| + * A representation of the differences between two blobs. |
| + */ |
| +class BlobDiff { |
| + /** |
| + * The regular expression used to identify the beginning of a hunk. |
| + */ |
| + static final RegExp hunkHeaderRegExp = |
| + new RegExp(r'@@ -([0-9]+)(?:,[0-9]+)? \+([0-9]+)(?:,[0-9]+)? @@'); |
| + |
| + /** |
| + * A list of the hunks in the diff. |
| + */ |
| + List<DiffHunk> hunks = <DiffHunk>[]; |
| + |
| + /** |
| + * Initialize a newly created blob diff by parsing the result of the git diff |
| + * 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
|
| + */ |
| + BlobDiff(List<String> input) { |
| + _parseInput(input); |
| + } |
| + |
| + /** |
| + * Parse the result of the git diff command (the [input]). |
| + */ |
| + void _parseInput(List<String> input) { |
| + for (String line in input) { |
| + _parseLine(line); |
| + } |
| + } |
| + |
| + /** |
| + * Parse a single [line] from the result of the git diff command. |
| + */ |
| + void _parseLine(String line) { |
| + DiffHunk currentHunk = hunks.isEmpty ? null : hunks.last; |
| + if (line.startsWith('@@')) { |
| + Match match = hunkHeaderRegExp.matchAsPrefix(line); |
| + int srcLine = int.parse(match.group(1)); |
| + int dstLine = int.parse(match.group(2)); |
| + hunks.add(new DiffHunk(srcLine, dstLine)); |
| + } else if (currentHunk != null && line.startsWith('+')) { |
| + currentHunk.addLines.add(line.substring(1)); |
| + } else if (currentHunk != null && line.startsWith('-')) { |
| + currentHunk.removeLines.add(line.substring(1)); |
| + } |
|
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 (
|
| + } |
| +} |
| + |
| +/** |
| + * A representation of the differences between two commits. |
| + */ |
| +class CommitDelta { |
| + /** |
| + * The length (in characters) of a SHA. |
| + */ |
| + static final int SHA_LENGTH = 40; |
| + |
| + /** |
| + * The code-point for a colon (':'). |
| + */ |
| + static final int COLON = ':'.codeUnitAt(0); |
| + |
| + /** |
| + * The code-point for a nul character. |
| + */ |
| + static final int NUL = 0; |
| + |
| + /** |
| + * The code-point for a tab. |
| + */ |
| + static final int TAB = '\t'.codeUnitAt(0); |
| + |
| + /** |
| + * The repository from which the commits were taken. |
| + */ |
| + final GitRepository repository; |
| + |
| + /** |
| + * The records of the files that were changed. |
| + */ |
| + final List<DiffRecord> diffRecords = <DiffRecord>[]; |
| + |
| + /** |
| + * Initialize a newly created representation of the differences between two |
| + * commits. The differences are computed by parsing the result of a git diff |
| + * 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
|
| + */ |
| + CommitDelta(this.repository, String diffResults) { |
| + _parseInput(diffResults); |
| + } |
| + |
| + /** |
| + * Return `true` if there are differences. |
| + */ |
| + bool get hasDiffs => diffRecords.isNotEmpty; |
| + |
| + /** |
| + * Return the absolute paths of all of the files in this commit whose name |
| + * matches the given [fileName]. |
| + */ |
| + Iterable<String> filesMatching(String fileName) { |
| + return diffRecords |
| + .where((DiffRecord record) => record.isFor(fileName)) |
| + .map((DiffRecord record) => record.srcPath); |
| + } |
| + |
| + /** |
| + * Remove any diffs for files that are either (a) outside the given |
| + * [analysisRoots], or (b) are files that are not being analyzed by the server. |
| + */ |
| + 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
|
| + diffRecords.retainWhere((DiffRecord record) { |
| + String filePath = record.srcPath ?? record.dstPath; |
| + for (String analysisRoot in analysisRoots) { |
| + if (path.isWithin(analysisRoot, filePath)) { |
| + // TODO(brianwilkerson) Generalize this by asking the server for the |
| + // list of glob patterns of files to be analyzed (once there's an API |
| + // for doing so). |
| + if (filePath.endsWith('.dart') || |
| + filePath.endsWith('.html') || |
| + filePath.endsWith('.htm') || |
| + filePath.endsWith('.analysisOptions')) { |
| + return true; |
| + } |
| + } |
| + } |
| + return false; |
| + }); |
| + } |
| + |
| + /** |
| + * Return the index of the first nul character in the given [string] that is |
| + * at or after the given [start] index. |
| + */ |
| + int _findEnd(String string, int start) { |
| + int length = string.length; |
| + int end = start; |
| + while (end < length && string.codeUnitAt(end) != NUL) { |
| + end++; |
| + } |
| + return end; |
| + } |
| + |
| + /** |
| + * Return the result of converting the given [relativePath] to an absolute |
| + * path. The path is assumed to be relative to the root of the repository. |
| + */ |
| + String _makeAbsolute(String relativePath) { |
| + return path.join(repository.path, relativePath); |
| + } |
| + |
| + /** |
| + * Parse all of the diff records in the given [input]. |
| + */ |
| + void _parseInput(String input) { |
| + int length = input.length; |
| + int start = 0; |
| + while (start < length) { |
| + start = _parseRecord(input, start); |
| + } |
| + } |
| + |
| + /** |
| + * Parse a single record from the given [input], assuming that the record |
| + * starts at the given [startIndex]. |
| + * |
| + * Each record is formatted as a sequence of fields. The fields are, from the |
| + * left to the right: |
| + * |
| + * 1. a colon. |
| + * 2. mode for "src"; 000000 if creation or unmerged. |
| + * 3. a space. |
| + * 4. mode for "dst"; 000000 if deletion or unmerged. |
| + * 5. a space. |
| + * 6. sha1 for "src"; 0{40} if creation or unmerged. |
| + * 7. a space. |
| + * 8. sha1 for "dst"; 0{40} if creation, unmerged or "look at work tree". |
| + * 9. a space. |
| + * 10. status, followed by optional "score" number. |
| + * 11. a tab or a NUL when -z option is used. |
| + * 12. path for "src" |
| + * 13. a tab or a NUL when -z option is used; only exists for C or R. |
| + * 14. path for "dst"; only exists for C or R. |
| + * 15. an LF or a NUL when -z option is used, to terminate the record. |
| + */ |
| + int _parseRecord(String input, int startIndex) { |
| + // Skip the first five fields. |
| + startIndex += 15; |
| + // 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
|
| + String srcSha = input.substring(startIndex, startIndex + SHA_LENGTH); |
| + startIndex += SHA_LENGTH + 1; |
| + // 8 |
| + String dstSha = input.substring(startIndex, startIndex + SHA_LENGTH); |
| + startIndex += SHA_LENGTH + 1; |
| + // 10 |
| + int endIndex = _findEnd(input, startIndex); |
| + String status = input.substring(startIndex, endIndex); |
| + startIndex = endIndex + 1; |
| + // 12 |
| + endIndex = _findEnd(input, startIndex); |
| + String srcPath = _makeAbsolute(input.substring(startIndex, endIndex)); |
| + startIndex = endIndex + 1; |
| + // 14 |
| + String dstPath = null; |
| + if (status.startsWith('C') || status.startsWith('R')) { |
| + endIndex = _findEnd(input, startIndex); |
| + dstPath = _makeAbsolute(input.substring(startIndex, endIndex)); |
| + } |
| + // Create the record. |
| + diffRecords.add( |
| + new DiffRecord(repository, srcSha, dstSha, status, srcPath, dstPath)); |
| + return endIndex + 1; |
| + } |
| +} |
| + |
| +/** |
| + * A representation of the history of a Git repository. |
| + */ |
| +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
|
| + /** |
| + * The repository whose history is being represented. |
| + */ |
| + final GitRepository repository; |
| + |
| + /** |
| + * The id's (SHA's) of the commits in the repository, with the most recent |
| + * commit being first and the oldest commit being last. |
| + */ |
| + final List<String> commitIds; |
| + |
| + /** |
| + * Initialize a commit history for the given [repository] to have the given |
| + * [commitIds]. |
| + */ |
| + CommitHistory(this.repository, this.commitIds); |
| + |
| + /** |
| + * Return an iterator that can be used to iterate over this commit history. |
| + */ |
| + CommitHistoryIterator iterator() { |
| + return new CommitHistoryIterator(this); |
| + } |
| +} |
| + |
| +/** |
| + * An iterator over the history of a Git repository. |
| + */ |
| +class CommitHistoryIterator { |
|
Paul Berry
2015/11/17 19:49:31
Similar concern here.
Brian Wilkerson
2015/11/18 17:36:46
Done
|
| + /** |
| + * The commit history being iterated over. |
| + */ |
| + final CommitHistory history; |
| + |
| + /** |
| + * The index of the current commit in the list of [commitIds]. |
| + */ |
| + int currentCommit; |
| + |
| + /** |
| + * Initialize a newly created iterator to iterate over the commits with the |
| + * given [commitIds]; |
| + */ |
| + CommitHistoryIterator(this.history) { |
| + currentCommit = history.commitIds.length; |
| + } |
| + |
| + /** |
| + * Return the SHA1 of the commit after the current commit (the 'dst' of the |
| + * [next] diff). |
| + */ |
| + String get dstCommit => history.commitIds[currentCommit - 1]; |
| + |
| + /** |
| + * Return the SHA1 of the current commit (the 'src' of the [next] diff). |
| + */ |
| + String get srcCommit => history.commitIds[currentCommit]; |
| + |
| + /** |
| + * Advance to the next commit in the history. Return `true` if it is safe to |
| + * ask for the [next] diff. |
| + */ |
| + bool moveNext() { |
| + if (currentCommit <= 1) { |
| + return false; |
| + } |
| + currentCommit--; |
| + return true; |
| + } |
| + |
| + /** |
| + * Return the difference between the current commit and the commit that |
| + * followed it. |
| + */ |
| + CommitDelta next() => history.repository.getCommitDiff(srcCommit, dstCommit); |
| +} |
| + |
| +/** |
| + * Representation of a single diff hunk. |
| + */ |
| +class DiffHunk { |
| + /** |
| + * 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
|
| + */ |
| + int diffSrcLine; |
| + |
| + /** |
| + * The index of the first line that was changed in the dst. |
| + */ |
| + int diffDstLine; |
| + |
| + /** |
| + * A list of the individual lines that were removed from the src. |
| + */ |
| + List<String> removeLines = <String>[]; |
| + |
| + /** |
| + * A list of the individual lines that were added to the dst. |
| + */ |
| + List<String> addLines = <String>[]; |
| + |
| + /** |
| + * 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.
|
| + */ |
| + DiffHunk(this.diffSrcLine, this.diffDstLine); |
| + |
| + /** |
| + * 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
|
| + */ |
| + int get dstLine { |
| + // The diff command numbers lines starting at 1, but it subtracts 1 from |
| + // dstLine if there are no lines on the destination side of the hunk. We |
| + // convert it into something reasonable. |
| + return addLines.isEmpty ? diffDstLine : diffDstLine - 1; |
| + } |
| + |
| + /** |
| + * Return the index of the first line that was changed in the src. |
| + */ |
| + int get srcLine { |
| + // The diff command numbers lines starting at 1, but it subtracts 1 from |
| + // srcLine if there are no lines on the source side of the hunk. We convert |
| + // it into something reasonable. |
| + return removeLines.isEmpty ? diffSrcLine : diffSrcLine - 1; |
| + } |
| +} |
| + |
| +/** |
| + * A representation of a single line (record) from a raw diff. |
| + */ |
| +class DiffRecord { |
| + /** |
| + * The repository containing the file(s) that were modified. |
| + */ |
| + final GitRepository repository; |
| + |
| + /** |
| + * The SHA1 of the blob in the src. |
| + */ |
| + final String srcBlob; |
| + |
| + /** |
| + * The SHA1 of the blob in the dst. |
| + */ |
| + final String dstBlob; |
| + |
| + /** |
| + * The status of the change. Valid values are: |
| + * * A: addition of a file |
| + * * C: copy of a file into a new one |
| + * * D: deletion of a file |
| + * * M: modification of the contents or mode of a file |
| + * * R: renaming of a file |
| + * * T: change in the type of the file |
| + * * U: file is unmerged (you must complete the merge before it can be committed) |
| + * * X: "unknown" change type (most probably a bug, please report it) |
| + * |
| + * Status letters C and R are always followed by a score (denoting the |
| + * percentage of similarity between the source and target of the move or |
| + * copy), and are the only ones to be so. |
| + */ |
| + final String status; |
| + |
| + /** |
| + * The path of the src. |
| + */ |
| + final String srcPath; |
| + |
| + /** |
| + * The path of the dst if this was either a copy or a rename operation. |
| + */ |
| + final String dstPath; |
| + |
| + /** |
| + * Initialize a newly created diff record. |
| + */ |
| + DiffRecord(this.repository, this.srcBlob, this.dstBlob, this.status, |
| + this.srcPath, this.dstPath); |
| + |
| + /** |
| + * Return `true` if this record represents a file that was added. |
| + */ |
| + 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
|
| + |
| + /** |
| + * Return `true` if this record represents a file that was copied. |
| + */ |
| + bool get isCopy => status.startsWith('C'); |
| + |
| + /** |
| + * Return `true` if this record represents a file that was deleted. |
| + */ |
| + bool get isDeletion => status == 'D'; |
| + |
| + /** |
| + * Return `true` if this record represents a file that was modified. |
| + */ |
| + bool get isModification => status == 'M'; |
| + |
| + /** |
| + * Return `true` if this record represents a file that was renamed. |
| + */ |
| + bool get isRename => status.startsWith('R'); |
| + |
| + /** |
| + * Return `true` if this record represents an entity whose type was changed |
| + * (for example, from a file to a directory). |
| + */ |
| + bool get isTypeChange => status == 'T'; |
| + |
| + /** |
| + * Return a representation of the individual blobs within this diff. |
| + */ |
| + BlobDiff getBlobDiff() => repository.getBlobDiff(srcBlob, dstBlob); |
| + |
| + /** |
| + * Return `true` if this diff applies to a file with the given name. |
| + */ |
| + bool isFor(String fileName) => |
| + (srcPath != null && fileName == path.basename(srcPath)) || |
| + (dstPath != null && fileName == path.basename(dstPath)); |
| + |
| + @override |
| + String toString() => srcPath ?? dstPath; |
| +} |
| + |
| +/** |
| + * A representation of a git repository. |
| + */ |
| +class GitRepository { |
| + /** |
| + * The absolute path of the directory containing the repository. |
| + */ |
| + final String path; |
| + |
| + /** |
| + * Initialize a newly created repository to represent the git repository at |
| + * the given [path]. |
| + */ |
| + GitRepository(this.path); |
| + |
| + /** |
| + * Checkout the given [commit] from the repository. This is done by running |
| + * the command `git checkout <sha>`. |
| + */ |
| + void checkout(String commit) { |
| + _run('checkout', commit); |
| + } |
| + |
| + /** |
| + * Return details about the differences between the two blobs identified by |
| + * the SHA1 of the [srcBlob] and the SHA1 of the [dstBlob]. This is done by |
| + * running the command `git diff <blob> <blob>`. |
| + */ |
| + BlobDiff getBlobDiff(String srcBlob, String dstBlob) { |
| + ProcessResult result = _run('diff', srcBlob, dstBlob); |
| + List<String> diffResults = LineSplitter.split(result.stdout).toList(); |
| + return new BlobDiff(diffResults); |
| + } |
| + |
| + /** |
| + * Return details about the differences between the two commits identified by |
| + * the [srcCommit] and [dstCommit]. This is done by running the command |
| + * `git diff --raw --no-abbrev --no-renames -z <sha> <sha>`. |
| + */ |
| + CommitDelta getCommitDiff(String srcCommit, String dstCommit) { |
| + // Consider --find-renames instead of --no-renames if rename information is |
| + // desired. |
| + ProcessResult result = _run('diff', '--raw', '--no-abbrev', '--no-renames', |
| + '-z', srcCommit, dstCommit); |
| + return new CommitDelta(this, result.stdout); |
| + } |
| + |
| + /** |
| + * Return a representation of the history of this repository. This is done by |
| + * running the command `git rev-list --first-parent HEAD`. |
| + */ |
| + CommitHistory getCommitHistory() { |
| + ProcessResult result = _run('rev-list', '--first-parent', 'HEAD'); |
| + List<String> commitIds = LineSplitter.split(result.stdout).toList(); |
| + return new CommitHistory(this, commitIds); |
| + } |
| + |
| + /** |
| + * Synchronously run the given [executable] with the given [arguments]. Return |
| + * the result of running the process. |
| + */ |
| + ProcessResult _run(String arg1, |
| + [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
|
| + String arg3, |
| + String arg4, |
| + String arg5, |
| + String arg6, |
| + String arg7]) { |
| + List<String> arguments = <String>[]; |
| + arguments.add(arg1); |
| + if (arg2 != null) { |
| + arguments.add(arg2); |
| + if (arg3 != null) { |
| + arguments.add(arg3); |
| + if (arg4 != null) { |
| + arguments.add(arg4); |
| + if (arg5 != null) { |
| + arguments.add(arg5); |
| + if (arg6 != null) { |
| + arguments.add(arg6); |
| + if (arg7 != null) { |
| + arguments.add(arg7); |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + return Process.runSync('git', arguments, |
| + stderrEncoding: UTF8, stdoutEncoding: UTF8, workingDirectory: path); |
| + } |
| +} |