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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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);
+ }
+}
« 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