Add Version.nextBreaking and "^" operator
diff --git a/README.md b/README.md
index 1cb1527..475c1a3 100644
--- a/README.md
+++ b/README.md
@@ -73,5 +73,23 @@
specifically selects that unstable version -- they've deliberately opted
into it.
+ * **There is a notion of compatibility between pre-1.0.0 versions.** Semver
+ deems all pre-1.0.0 versions to be incompatible. This means that the only
+ way to ensure compatibility when depending on a pre-1.0.0 package is to
+ pin the dependency to an exact version. Pinned version constraints prevent
+ automatic patch and pre-release updates. To avoid this situation, pub
+ defines the "next breaking" version to be the version with the left-most
+ non-zero digit in [major, minor, patch] incremented, and the subsequent
+ digits reset to zero. For example, here are some versions along with their
+ next breaking ones:
+
+ `0.0.3` -> `0.0.4`
+ `0.7.2-alpha` -> `0.8.0`
+ `1.2.3` -> `2.0.0`
+
+ To make use of this, pub defines a "^" operator which yields a version
+ constraint greater than or equal to a given version, but less than its next
+ breaking one.
+
[pub]: http://pub.dartlang.org/
[semver]: http://semver.org/
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
index 4a22618..59b0c09 100644
--- a/lib/src/patterns.dart
+++ b/lib/src/patterns.dart
@@ -17,3 +17,6 @@
/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
/// a string.
final START_COMPARISON = new RegExp(r"^[<>]=?");
+
+/// Parses the "compatible with" operator ("^") at the beginning of a string.
+final START_COMPATIBLE_WITH = new RegExp(r"^\^");
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 866d89c..496178f 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -189,7 +189,7 @@
return new Version(major, minor, patch);
}
- return new Version(major + 1, 0, 0);
+ return _incrementMajor();
}
/// Gets the next minor version number that follows this one.
@@ -202,7 +202,7 @@
return new Version(major, minor, patch);
}
- return new Version(major, minor + 1, 0);
+ return _incrementMinor();
}
/// Gets the next patch version number that follows this one.
@@ -214,9 +214,31 @@
return new Version(major, minor, patch);
}
- return new Version(major, minor, patch + 1);
+ return _incrementPatch();
}
+ /// Gets the next breaking version number that follows this one.
+ ///
+ /// Increments the left-most non-zero digit in ([major], [minor], [patch]).
+ /// In other words, if [major] and [minor] are zero (e.g. 0.0.3), then it
+ /// increments [patch]. If [major] is zero and [minor] is not (e.g. 0.7.2),
+ /// then it increments [minor]. Otherwise, it increments [major].
+ Version get nextBreaking {
+ if (major == 0) {
+ if (minor == 0) {
+ return _incrementPatch();
+ }
+
+ return _incrementMinor();
+ }
+
+ return _incrementMajor();
+ }
+
+ Version _incrementMajor() => new Version(major + 1, 0, 0);
+ Version _incrementMinor() => new Version(major, minor + 1, 0);
+ Version _incrementPatch() => new Version(major, minor, patch + 1);
+
/// Tests if [other] matches this version exactly.
bool allows(Version other) => this == other;
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index 499892e..29dd574 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -23,19 +23,26 @@
/// Parses a version constraint.
///
- /// This string is either "any" or a series of version parts. Each part can
- /// be one of:
- ///
- /// * A version string like `1.2.3`. In other words, anything that can be
- /// parsed by [Version.parse()].
- /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version
- /// string.
+ /// This string is one of:
+ ///
+ /// * "any". See [any].
+ /// * "^" followed by a version string. Versions compatible with the
+ /// version number. This allows versions greater than or equal to the
+ /// version, and less then the next breaking version (see
+ /// [Version.nextBreaking]).
+ /// * a series of version parts. Each part can be one of:
+ /// * A version string like `1.2.3`. In other words, anything that can be
+ /// parsed by [Version.parse()].
+ /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a
+ /// version string.
///
/// Whitespace is ignored.
///
/// Examples:
///
/// any
+ /// ^0.7.2
+ /// ^1.0.0-alpha
/// 1.2.3-alpha
/// <=5.1.4
/// >2.0.4 <= 2.4.6
@@ -82,9 +89,38 @@
}
throw "Unreachable.";
}
+
+ // Try to parse the "^" operator followed by a version.
+ matchCompatibleWith() {
+ var compatibleWith = START_COMPATIBLE_WITH.firstMatch(text);
+ if (compatibleWith == null) return null;
+ var op = compatibleWith[0];
+ text = text.substring(compatibleWith.end);
+ skipWhitespace();
+
+ var version = matchVersion();
+ if (version == null) {
+ throw new FormatException('Expected version number after "$op" in '
+ '"$originalText", got "$text".');
+ }
+
+ getCurrentTextIndex() => originalText.length - text.length;
+ var startTextIndex = getCurrentTextIndex();
+ if(constraints.isNotEmpty || text.isNotEmpty) {
+ var constraint = op + originalText.substring(startTextIndex,
+ getCurrentTextIndex());
+ throw new FormatException('Cannot include other constraints with '
+ '"^" constraint "$constraint" in "$originalText".');
+ }
+
+ return new VersionRange(min: version, includeMin: true,
+ max: version.nextBreaking);
+ }
+
while (true) {
skipWhitespace();
+
if (text.isEmpty) break;
var version = matchVersion();
@@ -98,6 +134,11 @@
constraints.add(comparison);
continue;
}
+
+ var compatibleWith = matchCompatibleWith();
+ if (compatibleWith != null) {
+ return compatibleWith;
+ }
// If we got here, we couldn't parse the remaining string.
throw new FormatException('Could not parse version "$originalText". '
diff --git a/test/utils.dart b/test/utils.dart
index da89683..0cc0f65 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -9,6 +9,10 @@
import 'package:pub_semver/pub_semver.dart';
/// Some stock example versions to use in tests.
+final v003 = new Version.parse('0.0.3');
+final v004 = new Version.parse('0.0.4');
+final v072 = new Version.parse('0.7.2');
+final v080 = new Version.parse('0.8.0');
final v114 = new Version.parse('1.1.4');
final v123 = new Version.parse('1.2.3');
final v124 = new Version.parse('1.2.4');
diff --git a/test/version_constraint_test.dart b/test/version_constraint_test.dart
index df8c099..16a3fdc 100644
--- a/test/version_constraint_test.dart
+++ b/test/version_constraint_test.dart
@@ -123,11 +123,48 @@
throwsFormatException);
});
+ test('parses a "^" post-1.0.0 version', () {
+ var constraint = new VersionConstraint.parse('^1.2.3');
+
+ expect(constraint, equals(new VersionRange(min: v123, includeMin: true,
+ max: v123.nextBreaking)));
+ });
+
+ test('parses a "^" pre-1.0.0, post-0.1.0 version', () {
+ var constraint = new VersionConstraint.parse('^0.7.2');
+
+ expect(constraint, equals(new VersionRange(min: v072, includeMin: true,
+ max: v072.nextBreaking)));
+ });
+
+ test('parses a "^" pre-0.1.0 version', () {
+ var constraint = new VersionConstraint.parse('^0.0.3');
+
+ expect(constraint, equals(new VersionRange(min: v003, includeMin: true,
+ max: v003.nextBreaking)));
+ });
+
+ test('parses a "^" pre-release version', () {
+ var constraint = new VersionConstraint.parse('^0.7.2-pre+1');
+
+ var min = new Version.parse('0.7.2-pre+1');
+ expect(constraint, equals(new VersionRange(min: min, includeMin: true,
+ max: min.nextBreaking)));
+ });
+
+ test('does not allow "^" to be mixed with other constraints', () {
+ expect(() => new VersionConstraint.parse('>=1.2.3 ^1.0.0'),
+ throwsFormatException);
+ expect(() => new VersionConstraint.parse('^1.0.0 <1.2.3'),
+ throwsFormatException);
+ });
+
test('throws FormatException on a bad string', () {
var bad = [
"", " ", // Empty string.
"foo", // Bad text.
">foo", // Bad text after operator.
+ "^foo", // Bad text after "^".
"1.0.0 foo", "1.0.0foo", // Bad text after version.
"anything", // Bad text after "any".
"<>1.0.0", // Multiple operators.
diff --git a/test/version_test.dart b/test/version_test.dart
index b271ecc..527db29 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -198,6 +198,18 @@
expect(new Version.parse('1.2.3+patch').nextPatch, equals(v124));
});
+ test('nextBreaking', () {
+ expect(v123.nextBreaking, equals(v200));
+ expect(v072.nextBreaking, equals(v080));
+ expect(v003.nextBreaking, equals(v004));
+
+ // Removes pre-release version if present.
+ expect(new Version.parse('1.2.3-dev').nextBreaking, equals(v200));
+
+ // Strips build suffix.
+ expect(new Version.parse('1.2.3+patch').nextBreaking, equals(v200));
+ });
+
test('parse()', () {
expect(new Version.parse('0.0.0'), equals(new Version(0, 0, 0)));
expect(new Version.parse('12.34.56'), equals(new Version(12, 34, 56)));