Add a PathSet class
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3dd398c..61e1788 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 * Add a `PathMap` class that uses path equality for its keys.
 
+* Add a `PathSet` class that uses path equality for its contents.
+
 ## 1.5.1
 
 * Fix a number of bugs that occurred when the current working directory was `/`
diff --git a/lib/path.dart b/lib/path.dart
index 7348a34..ed70f9d 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -50,6 +50,7 @@
 export 'src/context.dart' hide createInternal;
 export 'src/path_exception.dart';
 export 'src/path_map.dart';
+export 'src/path_set.dart';
 export 'src/style.dart';
 
 /// A default context for manipulating POSIX paths.
diff --git a/lib/src/path_set.dart b/lib/src/path_set.dart
new file mode 100644
index 0000000..98f08a9
--- /dev/null
+++ b/lib/src/path_set.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2018, 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.
+
+import 'dart:collection';
+
+import '../path.dart' as p;
+
+/// A set containing paths, compared using [equals] and [hash].
+class PathSet extends IterableBase<String> implements Set<String> {
+  /// The set to which we forward implementation methods.
+  final Set<String> _inner;
+
+  /// Creates an empty [PathSet] whose contents are compared using
+  /// `context.equals` and `context.hash`.
+  ///
+  /// The [context] defaults to the current path context.
+  PathSet({p.Context context}) : _inner = _create(context);
+
+  /// Creates a [PathSet] with the same contents as [other] whose elements are
+  /// compared using `context.equals` and `context.hash`.
+  ///
+  /// The [context] defaults to the current path context. If multiple elements
+  /// in [other] represent the same logical path, the first value will be
+  /// used.
+  PathSet.of(Iterable<String> other, {p.Context context})
+      : _inner = _create(context)..addAll(other);
+
+  /// Creates a set that uses [context] for equality and hashing.
+  static Set<String> _create(p.Context context) {
+    context ??= p.context;
+    return new LinkedHashSet(
+        equals: (path1, path2) {
+          if (path1 == null) return path2 == null;
+          if (path2 == null) return false;
+          return context.equals(path1, path2);
+        },
+        hashCode: (path) => path == null ? 0 : context.hash(path),
+        isValidKey: (path) => path is String || path == null);
+  }
+
+  // Normally we'd use DelegatingSetView from the collection package to
+  // implement these, but we want to avoid adding dependencies from path because
+  // it's so widely used that even brief version skew can be very painful.
+
+  Iterator<String> get iterator => _inner.iterator;
+
+  int get length => _inner.length;
+
+  bool add(String value) => _inner.add(value);
+
+  void addAll(Iterable<String> elements) => _inner.addAll(elements);
+
+  Set<T> cast<T>() => _inner.cast<T>();
+
+  void clear() => _inner.clear();
+
+  bool contains(Object other) => _inner.contains(other);
+
+  bool containsAll(Iterable<Object> other) => _inner.containsAll(other);
+
+  Set<String> difference(Set<Object> other) => _inner.difference(other);
+
+  Set<String> intersection(Set<Object> other) => _inner.intersection(other);
+
+  String lookup(Object element) => _inner.lookup(element);
+
+  bool remove(Object value) => _inner.remove(value);
+
+  void removeAll(Iterable<Object> elements) => _inner.removeAll(elements);
+
+  void removeWhere(bool test(String element)) => _inner.removeWhere(test);
+
+  void retainAll(Iterable<Object> elements) => _inner.retainAll(elements);
+
+  Set<T> retype<T>() => _inner.retype<T>();
+
+  void retainWhere(bool test(String element)) => _inner.retainWhere(test);
+
+  Set<String> union(Set<String> other) => _inner.union(other);
+
+  Set<String> toSet() => _inner.toSet();
+}
diff --git a/test/path_set_test.dart b/test/path_set_test.dart
new file mode 100644
index 0000000..884c184
--- /dev/null
+++ b/test/path_set_test.dart
@@ -0,0 +1,81 @@
+// Copyright (c) 2018, 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.
+
+import 'package:test/test.dart';
+
+import 'package:path/path.dart';
+
+void main() {
+  group("considers equal", () {
+    test("two identical paths", () {
+      var set = new PathSet();
+      expect(set.add(join("foo", "bar")), isTrue);
+      expect(set.add(join("foo", "bar")), isFalse);
+      expect(set, hasLength(1));
+      expect(set, contains(join("foo", "bar")));
+    });
+
+    test("two logically equivalent paths", () {
+      var set = new PathSet();
+      expect(set.add("foo"), isTrue);
+      expect(set.add(absolute("foo")), isFalse);
+      expect(set, hasLength(1));
+      expect(set, contains("foo"));
+      expect(set, contains(absolute("foo")));
+    });
+
+    test("two nulls", () {
+      var set = new PathSet();
+      expect(set.add(null), isTrue);
+      expect(set.add(null), isFalse);
+      expect(set, hasLength(1));
+      expect(set, contains(null));
+    });
+  });
+
+  group("considers unequal", () {
+    test("two distinct paths", () {
+      var set = new PathSet();
+      expect(set.add("foo"), isTrue);
+      expect(set.add("bar"), isTrue);
+      expect(set, hasLength(2));
+      expect(set, contains("foo"));
+      expect(set, contains("bar"));
+    });
+
+    test("a path and null", () {
+      var set = new PathSet();
+      expect(set.add("foo"), isTrue);
+      expect(set.add(null), isTrue);
+      expect(set, hasLength(2));
+      expect(set, contains("foo"));
+      expect(set, contains(null));
+    });
+  });
+
+  test("uses the custom context", () {
+    var set = new PathSet(context: windows);
+    expect(set.add("FOO"), isTrue);
+    expect(set.add("foo"), isFalse);
+    expect(set, hasLength(1));
+    expect(set, contains("fOo"));
+  });
+
+  group(".of()", () {
+    test("copies the existing set's keys", () {
+      var set = new PathSet.of(["foo", "bar"]);
+      expect(set, hasLength(2));
+      expect(set, contains("foo"));
+      expect(set, contains("bar"));
+    });
+
+    test("uses the first value in the case of duplicates", () {
+      var set = new PathSet.of(["foo", absolute("foo")]);
+      expect(set, hasLength(1));
+      expect(set, contains("foo"));
+      expect(set, contains(absolute("foo")));
+      expect(set.first, "foo");
+    });
+  });
+}