Version 0.2.9.1

Cherrypick the following changes from bleeding_edge into trunk:
16056 16059 16060 16063 16065 16066 16067 16068 16073

git-svn-id: http://dart.googlecode.com/svn/trunk@16076 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index fb99370..d1dee9b 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -7351,17 +7351,56 @@
   }
 }
 
-/// @domName Element
+/**
+ * An abstract class, which all HTML elements extend.
+ */
 abstract class Element extends Node implements ElementTraversal native "*Element" {
 
+  /**
+   * Creates an HTML element from a valid fragment of HTML.
+   *
+   * The [html] fragment must represent valid HTML with a single element root,
+   * which will be parsed and returned.
+   *
+   * Important: the contents of [html] should not contain any user-supplied
+   * data. Without strict data validation it is impossible to prevent script
+   * injection exploits.
+   *
+   * It is instead recommended that elements be constructed via [Element.tag]
+   * and text be added via [text].
+   *
+   *     var element = new Element.html('<div class="foo">content</div>');
+   */
   factory Element.html(String html) =>
       _ElementFactoryProvider.createElement_html(html);
+
+  /**
+   * Creates the HTML element specified by the tag name.
+   *
+   * This is similar to [Document.createElement].
+   * [tag] should be a valid HTML tag name. If [tag] is an unknown tag then
+   * this will create an [UnknownElement].
+   *
+   *     var divElement = new Element.tag('div');
+   *     print(divElement is DivElement); // 'true'
+   *     var myElement = new Element.tag('unknownTag');
+   *     print(myElement is UnknownElement); // 'true'
+   *
+   * For standard elements it is more preferable to use the type constructors:
+   *     var element = new DivElement();
+   */
   factory Element.tag(String tag) =>
       _ElementFactoryProvider.createElement_tag(tag);
 
   /**
-   * @domName Element.hasAttribute, Element.getAttribute, Element.setAttribute,
-   *   Element.removeAttribute
+   * All attributes on this element.
+   *
+   * Any modifications to the attribute map will automatically be applied to
+   * this element.
+   *
+   * This only includes attributes which are not in a namespace
+   * (such as 'xlink:href'), additional attributes can be accessed via
+   * [getNamespacedAttributes].
    */
   Map<String, String> get attributes => new _ElementAttributeMap(this);
 
@@ -7395,8 +7434,16 @@
   List<Element> get elements => this.children;
 
   /**
-   * @domName childElementCount, firstElementChild, lastElementChild,
-   *   children, Node.nodes.add
+   * List of the direct children of this element.
+   *
+   * This collection can be used to add and remove elements from the document.
+   *
+   *     var item = new DivElement();
+   *     item.text = 'Something';
+   *     document.body.children.add(item) // Item is now displayed on the page.
+   *     for (var element in document.body.children) {
+   *       element.style.background = 'red'; // Turns every child of body red.
+   *     }
    */
   List<Element> get children => new _ChildrenElementList._wrap(this);
 
@@ -7408,12 +7455,46 @@
     children.addAll(copy);
   }
 
+  /**
+   * Finds the first descendant element of this element that matches the
+   * specified group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     // Gets the first descendant with the class 'classname'
+   *     var element = element.query('.className');
+   *     // Gets the element with id 'id'
+   *     var element = element.query('#id');
+   *     // Gets the first descendant [ImageElement]
+   *     var img = element.query('img');
+   *
+   * See also:
+   *
+   * * [CSS Selectors](http://docs.webplatform.org/wiki/css/selectors)
+   */
   Element query(String selectors) => $dom_querySelector(selectors);
 
+  /**
+   * Finds all descendent elements of this element that match the specified
+   * group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     var items = element.query('.itemClassName');
+   */
   List<Element> queryAll(String selectors) =>
     new _FrozenElementList._wrap($dom_querySelectorAll(selectors));
 
-  /** @domName className, classList */
+  /**
+   * The set of CSS classes applied to this element.
+   *
+   * This set makes it easy to add, remove or toggle the classes applied to
+   * this element.
+   *
+   *     element.classes.add('selected');
+   *     element.classes.toggle('isOnline');
+   *     element.classes.remove('selected');
+   */
   CssClassSet get classes => new _ElementCssClassSet(this);
 
   void set classes(Collection<String> value) {
@@ -7422,6 +7503,29 @@
     classSet.addAll(value);
   }
 
+  /**
+   * Allows access to all custom data attributes (data-*) set on this element.
+   *
+   * The keys for the map must follow these rules:
+   *
+   * * The name must not begin with 'xml'.
+   * * The name cannot contain a semi-colon (';').
+   * * The name cannot contain any capital letters.
+   *
+   * Any keys from markup will be converted to camel-cased keys in the map.
+   *
+   * For example, HTML specified as:
+   *
+   *     <div data-my-random-value='value'></div>
+   *
+   * Would be accessed in Dart as:
+   *
+   *     var value = element.dataAttributes['myRandomValue'];
+   *
+   * See also:
+   *
+   * * [Custom data attributes](http://www.w3.org/TR/html5/global-attributes.html#custom-data-attribute)
+   */
   Map<String, String> get dataAttributes =>
     new _DataAttributeMap(attributes);
 
@@ -7435,19 +7539,39 @@
 
   /**
    * Gets a map for manipulating the attributes of a particular namespace.
+   *
    * This is primarily useful for SVG attributes such as xref:link.
    */
   Map<String, String> getNamespacedAttributes(String namespace) {
     return new _NamespacedAttributeMap(this, namespace);
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * The set of all CSS values applied to this element, including inherited
+   * and default values.
+   *
+   * The computedStyle contains values that are inherited from other
+   * sources, such as parent elements or stylesheets. This differs from the
+   * [style] property, which contains only the values specified directly on this
+   * element.
+   *
+   * See also:
+   *
+   * * [CSS Inheritance and Cascade](http://docs.webplatform.org/wiki/tutorials/inheritance_and_cascade)
+   */
   Future<CssStyleDeclaration> get computedStyle {
      // TODO(jacobr): last param should be null, see b/5045788
      return getComputedStyle('');
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * Returns the computed styles for pseudo-elements such as `::after`,
+   * `::before`, `::marker`, `::line-marker`.
+   *
+   * See also:
+   *
+   * * [Pseudo-elements](http://docs.webplatform.org/wiki/css/selectors/pseudo-elements)
+   */
   Future<CssStyleDeclaration> getComputedStyle(String pseudoElement) {
     return _createMeasurementFuture(
         () => window.$dom_getComputedStyle(this, pseudoElement),
@@ -7455,14 +7579,15 @@
   }
 
   /**
-   * Adds the specified element to after the last child of this.
+   * Adds the specified element to after the last child of this element.
    */
   void append(Element e) {
     this.children.add(e);
   }
 
   /**
-   * Adds the specified text as a text node after the last child of this.
+   * Adds the specified text as a text node after the last child of this
+   * element.
    */
   void appendText(String text) {
     this.insertAdjacentText('beforeend', text);
@@ -7470,7 +7595,7 @@
 
   /**
    * Parses the specified text as HTML and adds the resulting node after the
-   * last child of this.
+   * last child of this element.
    */
   void appendHtml(String text) {
     this.insertAdjacentHtml('beforeend', text);
@@ -7492,7 +7617,16 @@
 
   // TODO(vsm): Implement noSuchMethod or similar for dart2js.
 
-  /** @domName Element.insertAdjacentText */
+  /**
+   * Creates a text node and inserts it into the DOM at the specified location.
+   *
+   * To see the possible values for [where], read the doc for
+   * [insertAdjacentHtml].
+   *
+   * See also:
+   *
+   * * [insertAdjacentHtml]
+   */
   void insertAdjacentText(String where, String text) {
     if (JS('bool', '!!#.insertAdjacentText', this)) {
       _insertAdjacentText(where, text);
@@ -7504,7 +7638,28 @@
   @JSName('insertAdjacentText')
   void _insertAdjacentText(String where, String text) native;
 
-  /** @domName Element.insertAdjacentHTML */
+  /**
+   * Parses text as an HTML fragment and inserts it into the DOM at the
+   * specified location.
+   *
+   * The [where] parameter indicates where to insert the HTML fragment:
+   *
+   * * 'beforeBegin': Immediately before this element.
+   * * 'afterBegin': As the first child of this element.
+   * * 'beforeEnd': As the last child of this element.
+   * * 'afterEnd': Immediately after this element.
+   *
+   *     var html = '<div class="something">content</div>';
+   *     // Inserts as the first child
+   *     document.body.insertAdjacentHtml('afterBegin', html);
+   *     var createdElement = document.body.children[0];
+   *     print(createdElement.classes[0]); // Prints 'something'
+   *
+   * See also:
+   *
+   * * [insertAdjacentText]
+   * * [insertAdjacentElement]
+   */
   void insertAdjacentHtml(String where, String text) {
     if (JS('bool', '!!#.insertAdjacentHtml', this)) {
       _insertAdjacentHtml(where, text);
@@ -7516,7 +7671,16 @@
   @JSName('insertAdjacentHTML')
   void _insertAdjacentHTML(String where, String text) native;
 
-  /** @domName Element.insertAdjacentHTML */
+  /**
+   * Inserts [element] into the DOM at the specified location.
+   *
+   * To see the possible values for [where], read the doc for
+   * [insertAdjacentHtml].
+   *
+   * See also:
+   *
+   * * [insertAdjacentHtml]
+   */
   Element insertAdjacentElement(String where, Element element) {
     if (JS('bool', '!!#.insertAdjacentElement', this)) {
       _insertAdjacentElement(where, element);
diff --git a/sdk/lib/html/dartium/html_dartium.dart b/sdk/lib/html/dartium/html_dartium.dart
index 9f69c01..9396f1e 100644
--- a/sdk/lib/html/dartium/html_dartium.dart
+++ b/sdk/lib/html/dartium/html_dartium.dart
@@ -8826,17 +8826,56 @@
   }
 }
 
-/// @domName Element
+/**
+ * An abstract class, which all HTML elements extend.
+ */
 abstract class Element extends Node implements ElementTraversal {
 
+  /**
+   * Creates an HTML element from a valid fragment of HTML.
+   *
+   * The [html] fragment must represent valid HTML with a single element root,
+   * which will be parsed and returned.
+   *
+   * Important: the contents of [html] should not contain any user-supplied
+   * data. Without strict data validation it is impossible to prevent script
+   * injection exploits.
+   *
+   * It is instead recommended that elements be constructed via [Element.tag]
+   * and text be added via [text].
+   *
+   *     var element = new Element.html('<div class="foo">content</div>');
+   */
   factory Element.html(String html) =>
       _ElementFactoryProvider.createElement_html(html);
+
+  /**
+   * Creates the HTML element specified by the tag name.
+   *
+   * This is similar to [Document.createElement].
+   * [tag] should be a valid HTML tag name. If [tag] is an unknown tag then
+   * this will create an [UnknownElement].
+   *
+   *     var divElement = new Element.tag('div');
+   *     print(divElement is DivElement); // 'true'
+   *     var myElement = new Element.tag('unknownTag');
+   *     print(myElement is UnknownElement); // 'true'
+   *
+   * For standard elements it is more preferable to use the type constructors:
+   *     var element = new DivElement();
+   */
   factory Element.tag(String tag) =>
       _ElementFactoryProvider.createElement_tag(tag);
 
   /**
-   * @domName Element.hasAttribute, Element.getAttribute, Element.setAttribute,
-   *   Element.removeAttribute
+   * All attributes on this element.
+   *
+   * Any modifications to the attribute map will automatically be applied to
+   * this element.
+   *
+   * This only includes attributes which are not in a namespace
+   * (such as 'xlink:href'), additional attributes can be accessed via
+   * [getNamespacedAttributes].
    */
   Map<String, String> get attributes => new _ElementAttributeMap(this);
 
@@ -8870,8 +8909,16 @@
   List<Element> get elements => this.children;
 
   /**
-   * @domName childElementCount, firstElementChild, lastElementChild,
-   *   children, Node.nodes.add
+   * List of the direct children of this element.
+   *
+   * This collection can be used to add and remove elements from the document.
+   *
+   *     var item = new DivElement();
+   *     item.text = 'Something';
+   *     document.body.children.add(item) // Item is now displayed on the page.
+   *     for (var element in document.body.children) {
+   *       element.style.background = 'red'; // Turns every child of body red.
+   *     }
    */
   List<Element> get children => new _ChildrenElementList._wrap(this);
 
@@ -8883,12 +8930,46 @@
     children.addAll(copy);
   }
 
+  /**
+   * Finds the first descendant element of this element that matches the
+   * specified group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     // Gets the first descendant with the class 'classname'
+   *     var element = element.query('.className');
+   *     // Gets the element with id 'id'
+   *     var element = element.query('#id');
+   *     // Gets the first descendant [ImageElement]
+   *     var img = element.query('img');
+   *
+   * See also:
+   *
+   * * [CSS Selectors](http://docs.webplatform.org/wiki/css/selectors)
+   */
   Element query(String selectors) => $dom_querySelector(selectors);
 
+  /**
+   * Finds all descendent elements of this element that match the specified
+   * group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     var items = element.query('.itemClassName');
+   */
   List<Element> queryAll(String selectors) =>
     new _FrozenElementList._wrap($dom_querySelectorAll(selectors));
 
-  /** @domName className, classList */
+  /**
+   * The set of CSS classes applied to this element.
+   *
+   * This set makes it easy to add, remove or toggle the classes applied to
+   * this element.
+   *
+   *     element.classes.add('selected');
+   *     element.classes.toggle('isOnline');
+   *     element.classes.remove('selected');
+   */
   CssClassSet get classes => new _ElementCssClassSet(this);
 
   void set classes(Collection<String> value) {
@@ -8897,6 +8978,29 @@
     classSet.addAll(value);
   }
 
+  /**
+   * Allows access to all custom data attributes (data-*) set on this element.
+   *
+   * The keys for the map must follow these rules:
+   *
+   * * The name must not begin with 'xml'.
+   * * The name cannot contain a semi-colon (';').
+   * * The name cannot contain any capital letters.
+   *
+   * Any keys from markup will be converted to camel-cased keys in the map.
+   *
+   * For example, HTML specified as:
+   *
+   *     <div data-my-random-value='value'></div>
+   *
+   * Would be accessed in Dart as:
+   *
+   *     var value = element.dataAttributes['myRandomValue'];
+   *
+   * See also:
+   *
+   * * [Custom data attributes](http://www.w3.org/TR/html5/global-attributes.html#custom-data-attribute)
+   */
   Map<String, String> get dataAttributes =>
     new _DataAttributeMap(attributes);
 
@@ -8910,19 +9014,39 @@
 
   /**
    * Gets a map for manipulating the attributes of a particular namespace.
+   *
    * This is primarily useful for SVG attributes such as xref:link.
    */
   Map<String, String> getNamespacedAttributes(String namespace) {
     return new _NamespacedAttributeMap(this, namespace);
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * The set of all CSS values applied to this element, including inherited
+   * and default values.
+   *
+   * The computedStyle contains values that are inherited from other
+   * sources, such as parent elements or stylesheets. This differs from the
+   * [style] property, which contains only the values specified directly on this
+   * element.
+   *
+   * See also:
+   *
+   * * [CSS Inheritance and Cascade](http://docs.webplatform.org/wiki/tutorials/inheritance_and_cascade)
+   */
   Future<CssStyleDeclaration> get computedStyle {
      // TODO(jacobr): last param should be null, see b/5045788
      return getComputedStyle('');
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * Returns the computed styles for pseudo-elements such as `::after`,
+   * `::before`, `::marker`, `::line-marker`.
+   *
+   * See also:
+   *
+   * * [Pseudo-elements](http://docs.webplatform.org/wiki/css/selectors/pseudo-elements)
+   */
   Future<CssStyleDeclaration> getComputedStyle(String pseudoElement) {
     return _createMeasurementFuture(
         () => window.$dom_getComputedStyle(this, pseudoElement),
@@ -8930,14 +9054,15 @@
   }
 
   /**
-   * Adds the specified element to after the last child of this.
+   * Adds the specified element to after the last child of this element.
    */
   void append(Element e) {
     this.children.add(e);
   }
 
   /**
-   * Adds the specified text as a text node after the last child of this.
+   * Adds the specified text as a text node after the last child of this
+   * element.
    */
   void appendText(String text) {
     this.insertAdjacentText('beforeend', text);
@@ -8945,7 +9070,7 @@
 
   /**
    * Parses the specified text as HTML and adds the resulting node after the
-   * last child of this.
+   * last child of this element.
    */
   void appendHtml(String text) {
     this.insertAdjacentHtml('beforeend', text);
diff --git a/sdk/lib/html/templates/html/impl/impl_Element.darttemplate b/sdk/lib/html/templates/html/impl/impl_Element.darttemplate
index b253d2d..fc2bcfb 100644
--- a/sdk/lib/html/templates/html/impl/impl_Element.darttemplate
+++ b/sdk/lib/html/templates/html/impl/impl_Element.darttemplate
@@ -348,17 +348,56 @@
   }
 }
 
-/// @domName $DOMNAME
+/**
+ * An abstract class, which all HTML elements extend.
+ */
 abstract class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC {
 
+  /**
+   * Creates an HTML element from a valid fragment of HTML.
+   *
+   * The [html] fragment must represent valid HTML with a single element root,
+   * which will be parsed and returned.
+   *
+   * Important: the contents of [html] should not contain any user-supplied
+   * data. Without strict data validation it is impossible to prevent script
+   * injection exploits.
+   *
+   * It is instead recommended that elements be constructed via [Element.tag]
+   * and text be added via [text].
+   *
+   *     var element = new Element.html('<div class="foo">content</div>');
+   */
   factory $CLASSNAME.html(String html) =>
       _$(CLASSNAME)FactoryProvider.createElement_html(html);
+
+  /**
+   * Creates the HTML element specified by the tag name.
+   *
+   * This is similar to [Document.createElement].
+   * [tag] should be a valid HTML tag name. If [tag] is an unknown tag then
+   * this will create an [UnknownElement].
+   *
+   *     var divElement = new Element.tag('div');
+   *     print(divElement is DivElement); // 'true'
+   *     var myElement = new Element.tag('unknownTag');
+   *     print(myElement is UnknownElement); // 'true'
+   *
+   * For standard elements it is more preferable to use the type constructors:
+   *     var element = new DivElement();
+   */
   factory $CLASSNAME.tag(String tag) =>
       _$(CLASSNAME)FactoryProvider.createElement_tag(tag);
 
   /**
-   * @domName Element.hasAttribute, Element.getAttribute, Element.setAttribute,
-   *   Element.removeAttribute
+   * All attributes on this element.
+   *
+   * Any modifications to the attribute map will automatically be applied to
+   * this element.
+   *
+   * This only includes attributes which are not in a namespace
+   * (such as 'xlink:href'), additional attributes can be accessed via
+   * [getNamespacedAttributes].
    */
   Map<String, String> get attributes => new _ElementAttributeMap(this);
 
@@ -392,8 +431,16 @@
   List<Element> get elements => this.children;
 
   /**
-   * @domName childElementCount, firstElementChild, lastElementChild,
-   *   children, Node.nodes.add
+   * List of the direct children of this element.
+   *
+   * This collection can be used to add and remove elements from the document.
+   *
+   *     var item = new DivElement();
+   *     item.text = 'Something';
+   *     document.body.children.add(item) // Item is now displayed on the page.
+   *     for (var element in document.body.children) {
+   *       element.style.background = 'red'; // Turns every child of body red.
+   *     }
    */
   List<Element> get children => new _ChildrenElementList._wrap(this);
 
@@ -405,12 +452,46 @@
     children.addAll(copy);
   }
 
+  /**
+   * Finds the first descendant element of this element that matches the
+   * specified group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     // Gets the first descendant with the class 'classname'
+   *     var element = element.query('.className');
+   *     // Gets the element with id 'id'
+   *     var element = element.query('#id');
+   *     // Gets the first descendant [ImageElement]
+   *     var img = element.query('img');
+   *
+   * See also:
+   *
+   * * [CSS Selectors](http://docs.webplatform.org/wiki/css/selectors)
+   */
   Element query(String selectors) => $dom_querySelector(selectors);
 
+  /**
+   * Finds all descendent elements of this element that match the specified
+   * group of selectors.
+   *
+   * [selectors] should be a string using CSS selector syntax.
+   *
+   *     var items = element.query('.itemClassName');
+   */
   List<Element> queryAll(String selectors) =>
     new _FrozenElementList._wrap($dom_querySelectorAll(selectors));
 
-  /** @domName className, classList */
+  /**
+   * The set of CSS classes applied to this element.
+   *
+   * This set makes it easy to add, remove or toggle the classes applied to
+   * this element.
+   *
+   *     element.classes.add('selected');
+   *     element.classes.toggle('isOnline');
+   *     element.classes.remove('selected');
+   */
   CssClassSet get classes => new _ElementCssClassSet(this);
 
   void set classes(Collection<String> value) {
@@ -419,6 +500,29 @@
     classSet.addAll(value);
   }
 
+  /**
+   * Allows access to all custom data attributes (data-*) set on this element.
+   *
+   * The keys for the map must follow these rules:
+   *
+   * * The name must not begin with 'xml'.
+   * * The name cannot contain a semi-colon (';').
+   * * The name cannot contain any capital letters.
+   *
+   * Any keys from markup will be converted to camel-cased keys in the map.
+   *
+   * For example, HTML specified as:
+   *
+   *     <div data-my-random-value='value'></div>
+   *
+   * Would be accessed in Dart as:
+   *
+   *     var value = element.dataAttributes['myRandomValue'];
+   *
+   * See also:
+   *
+   * * [Custom data attributes](http://www.w3.org/TR/html5/global-attributes.html#custom-data-attribute)
+   */
   Map<String, String> get dataAttributes =>
     new _DataAttributeMap(attributes);
 
@@ -432,19 +536,39 @@
 
   /**
    * Gets a map for manipulating the attributes of a particular namespace.
+   *
    * This is primarily useful for SVG attributes such as xref:link.
    */
   Map<String, String> getNamespacedAttributes(String namespace) {
     return new _NamespacedAttributeMap(this, namespace);
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * The set of all CSS values applied to this element, including inherited
+   * and default values.
+   *
+   * The computedStyle contains values that are inherited from other
+   * sources, such as parent elements or stylesheets. This differs from the
+   * [style] property, which contains only the values specified directly on this
+   * element.
+   *
+   * See also:
+   *
+   * * [CSS Inheritance and Cascade](http://docs.webplatform.org/wiki/tutorials/inheritance_and_cascade)
+   */
   Future<CssStyleDeclaration> get computedStyle {
      // TODO(jacobr): last param should be null, see b/5045788
      return getComputedStyle('');
   }
 
-  /** @domName Window.getComputedStyle */
+  /**
+   * Returns the computed styles for pseudo-elements such as `::after`,
+   * `::before`, `::marker`, `::line-marker`.
+   *
+   * See also:
+   *
+   * * [Pseudo-elements](http://docs.webplatform.org/wiki/css/selectors/pseudo-elements)
+   */
   Future<CssStyleDeclaration> getComputedStyle(String pseudoElement) {
     return _createMeasurementFuture(
         () => window.$dom_getComputedStyle(this, pseudoElement),
@@ -452,14 +576,15 @@
   }
 
   /**
-   * Adds the specified element to after the last child of this.
+   * Adds the specified element to after the last child of this element.
    */
   void append(Element e) {
     this.children.add(e);
   }
 
   /**
-   * Adds the specified text as a text node after the last child of this.
+   * Adds the specified text as a text node after the last child of this
+   * element.
    */
   void appendText(String text) {
     this.insertAdjacentText('beforeend', text);
@@ -467,7 +592,7 @@
 
   /**
    * Parses the specified text as HTML and adds the resulting node after the
-   * last child of this.
+   * last child of this element.
    */
   void appendHtml(String text) {
     this.insertAdjacentHtml('beforeend', text);
@@ -509,7 +634,16 @@
 $endif
 
 $if DART2JS
-  /** @domName Element.insertAdjacentText */
+  /**
+   * Creates a text node and inserts it into the DOM at the specified location.
+   *
+   * To see the possible values for [where], read the doc for
+   * [insertAdjacentHtml].
+   *
+   * See also:
+   *
+   * * [insertAdjacentHtml]
+   */
   void insertAdjacentText(String where, String text) {
     if (JS('bool', '!!#.insertAdjacentText', this)) {
       _insertAdjacentText(where, text);
@@ -521,7 +655,28 @@
   @JSName('insertAdjacentText')
   void _insertAdjacentText(String where, String text) native;
 
-  /** @domName Element.insertAdjacentHTML */
+  /**
+   * Parses text as an HTML fragment and inserts it into the DOM at the
+   * specified location.
+   *
+   * The [where] parameter indicates where to insert the HTML fragment:
+   *
+   * * 'beforeBegin': Immediately before this element.
+   * * 'afterBegin': As the first child of this element.
+   * * 'beforeEnd': As the last child of this element.
+   * * 'afterEnd': Immediately after this element.
+   *
+   *     var html = '<div class="something">content</div>';
+   *     // Inserts as the first child
+   *     document.body.insertAdjacentHtml('afterBegin', html);
+   *     var createdElement = document.body.children[0];
+   *     print(createdElement.classes[0]); // Prints 'something'
+   *
+   * See also:
+   *
+   * * [insertAdjacentText]
+   * * [insertAdjacentElement]
+   */
   void insertAdjacentHtml(String where, String text) {
     if (JS('bool', '!!#.insertAdjacentHtml', this)) {
       _insertAdjacentHtml(where, text);
@@ -533,7 +688,16 @@
   @JSName('insertAdjacentHTML')
   void _insertAdjacentHTML(String where, String text) native;
 
-  /** @domName Element.insertAdjacentHTML */
+  /**
+   * Inserts [element] into the DOM at the specified location.
+   *
+   * To see the possible values for [where], read the doc for
+   * [insertAdjacentHtml].
+   *
+   * See also:
+   *
+   * * [insertAdjacentHtml]
+   */
   Element insertAdjacentElement(String where, Element element) {
     if (JS('bool', '!!#.insertAdjacentElement', this)) {
       _insertAdjacentElement(where, element);
diff --git a/tools/VERSION b/tools/VERSION
index 0ade99d..f2f2502 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -1,4 +1,4 @@
 MAJOR 0
 MINOR 2
 BUILD 9
-PATCH 0
+PATCH 1
diff --git a/utils/pub/command_lish.dart b/utils/pub/command_lish.dart
index f775b46..420952c 100644
--- a/utils/pub/command_lish.dart
+++ b/utils/pub/command_lish.dart
@@ -10,6 +10,7 @@
 
 import '../../pkg/args/lib/args.dart';
 import '../../pkg/http/lib/http.dart' as http;
+import 'directory_tree.dart';
 import 'git.dart' as git;
 import 'io.dart';
 import 'log.dart' as log;
@@ -33,21 +34,13 @@
   /// The URL of the server to which to upload the package.
   Uri get server => new Uri.fromString(commandOptions['server']);
 
-  Future onRun() {
+  Future _publish(packageBytes) {
     var cloudStorageUrl;
     return oauth2.withClient(cache, (client) {
       // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
       // should report that error and exit.
-      return Futures.wait([
-        client.get(server.resolve("/packages/versions/new.json")),
-        _filesToPublish.transform((files) {
-          log.fine('Archiving and publishing ${entrypoint.root}.');
-          return createTarGz(files, baseDir: entrypoint.root.dir);
-        }).chain(consumeInputStream),
-        _validate()
-      ]).chain((results) {
-        var response = results[0];
-        var packageBytes = results[1];
+      var newUri = server.resolve("/packages/versions/new.json");
+      return client.get(newUri).chain((response) {
         var parameters = _parseJson(response);
 
         var url = _expectField(parameters, 'url', response);
@@ -98,19 +91,38 @@
         }
       } else if (e is oauth2.ExpirationException) {
         log.error("Pub's authorization to upload packages has expired and "
-            "can't be automatically refreshed.");
-        return onRun();
+        "can't be automatically refreshed.");
+        return _publish(packageBytes);
       } else if (e is oauth2.AuthorizationException) {
         var message = "OAuth2 authorization failed";
         if (e.description != null) message = "$message (${e.description})";
         log.error("$message.");
-        return oauth2.clearCredentials(cache).chain((_) => onRun());
+        return oauth2.clearCredentials(cache).chain((_) =>
+            _publish(packageBytes));
       } else {
         throw e;
       }
     });
   }
 
+  Future onRun() {
+    var files;
+    return _filesToPublish.transform((f) {
+      files = f;
+      log.fine('Archiving and publishing ${entrypoint.root}.');
+      return createTarGz(files, baseDir: entrypoint.root.dir);
+    }).chain(consumeInputStream).chain((packageBytes) {
+      // Show the package contents so the user can verify they look OK.
+      var package = entrypoint.root;
+      log.message(
+          'Publishing "${package.name}" ${package.version}:\n'
+          '${generateTree(files)}');
+
+      // Validate the package.
+      return _validate().chain((_) => _publish(packageBytes));
+    });
+  }
+
   /// The basenames of files that are automatically excluded from archives.
   final _BLACKLISTED_FILES = const ['pubspec.lock'];
 
@@ -123,6 +135,14 @@
   /// will return all non-hidden files.
   Future<List<String>> get _filesToPublish {
     var rootDir = entrypoint.root.dir;
+
+    // TODO(rnystrom): listDir() returns real file paths after symlinks are
+    // resolved. This means if libDir contains a symlink, the resulting paths
+    // won't appear to be within it, which confuses relativeTo(). Work around
+    // that here by making sure we have the real path to libDir. Remove this
+    // when #7346 is fixed.
+    rootDir = new File(rootDir).fullPathSync();
+
     return Futures.wait([
       dirExists(join(rootDir, '.git')),
       git.isInstalled
@@ -135,15 +155,25 @@
 
       return listDir(rootDir, recursive: true).chain((entries) {
         return Futures.wait(entries.map((entry) {
-          return fileExists(entry).transform((isFile) => isFile ? entry : null);
+          return fileExists(entry).transform((isFile) {
+            // Skip directories.
+            if (!isFile) return null;
+
+            // TODO(rnystrom): Making these relative will break archive
+            // creation if the cwd is ever *not* the package root directory.
+            // Should instead only make these relative right before generating
+            // the tree display (which is what really needs them to be).
+            // Make it relative to the package root.
+            return relativeTo(entry, rootDir);
+          });
         }));
       });
     }).transform((files) => files.filter((file) {
       if (file == null || _BLACKLISTED_FILES.contains(basename(file))) {
         return false;
       }
-      return !splitPath(relativeTo(file, rootDir))
-          .some(_BLACKLISTED_DIRECTORIES.contains);
+
+      return !splitPath(file).some(_BLACKLISTED_DIRECTORIES.contains);
     }));
   }
 
@@ -180,15 +210,22 @@
       var errors = pair.first;
       var warnings = pair.last;
 
-      if (errors.isEmpty && warnings.isEmpty) return new Future.immediate(null);
-      if (!errors.isEmpty) throw "Package validation failed.";
+      if (!errors.isEmpty) {
+        throw "Sorry, your package is missing "
+            "${(errors.length > 1) ? 'some requirements' : 'a requirement'} "
+            "and can't be published yet.\nFor more information, see: "
+            "http://pub.dartlang.org/doc/pub-lish.html.\n";
+      }
 
-      var s = warnings.length == 1 ? '' : 's';
-      stdout.writeString("Package has ${warnings.length} warning$s. Upload "
-          "anyway (y/n)? ");
-      return readLine().transform((line) {
-        if (new RegExp(r"^[yY]").hasMatch(line)) return;
-        throw "Package upload canceled.";
+      var message = 'Looks great! Are you ready to upload your package';
+
+      if (!warnings.isEmpty) {
+        var s = warnings.length == 1 ? '' : 's';
+        message = "Package has ${warnings.length} warning$s. Upload anyway";
+      }
+
+      return confirm(message).transform((confirmed) {
+        if (!confirmed) throw "Package upload canceled.";
       });
     });
   }
diff --git a/utils/pub/directory_tree.dart b/utils/pub/directory_tree.dart
new file mode 100644
index 0000000..f5baa62
--- /dev/null
+++ b/utils/pub/directory_tree.dart
@@ -0,0 +1,133 @@
+// Copyright (c) 2012, 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.
+
+/// A simple library for rendering a list of files as a directory tree.
+library directory_tree;
+
+import 'log.dart' as log;
+import 'path.dart' as path;
+
+/// Draws a directory tree for the given list of files. Given a list of files
+/// like:
+///
+///     TODO
+///     example/console_example.dart
+///     example/main.dart
+///     example/web copy/web_example.dart
+///     test/absolute_test.dart
+///     test/basename_test.dart
+///     test/dirname_test.dart
+///     test/extension_test.dart
+///     test/is_absolute_test.dart
+///     test/is_relative_test.dart
+///     test/join_test.dart
+///     test/normalize_test.dart
+///     test/relative_test.dart
+///     test/split_test.dart
+///     .gitignore
+///     README.md
+///     lib/path.dart
+///     pubspec.yaml
+///     test/all_test.dart
+///     test/path_posix_test.dart
+///     test/path_windows_test.dart
+///
+/// this will render:
+///
+///     |-- .gitignore
+///     |-- README.md
+///     |-- TODO
+///     |-- example
+///     |   |-- console_example.dart
+///     |   |-- main.dart
+///     |   '-- web copy
+///     |       '-- web_example.dart
+///     |-- lib
+///     |   '-- path.dart
+///     |-- pubspec.yaml
+///     '-- test
+///         |-- absolute_test.dart
+///         |-- all_test.dart
+///         |-- basename_test.dart
+///         |   (7 more...)
+///         |-- path_windows_test.dart
+///         |-- relative_test.dart
+///         '-- split_test.dart
+///
+String generateTree(List<String> files) {
+  // Parse out the files into a tree of nested maps.
+  var root = {};
+  for (var file in files) {
+    var parts = path.split(file);
+    var directory = root;
+    for (var part in path.split(file)) {
+      directory = directory.putIfAbsent(part, () => {});
+    }
+  }
+
+  // Walk the map recursively and render to a string.
+  var buffer = new StringBuffer();
+  _draw(buffer, '', false, null, root);
+  return buffer.toString();
+}
+
+void _drawLine(StringBuffer buffer, String prefix, bool isLastChild,
+               String name) {
+  // Print lines.
+  buffer.add(prefix);
+  if (name != null) {
+    if (isLastChild) {
+      buffer.add("'-- ");
+    } else {
+      buffer.add("|-- ");
+    }
+  }
+
+  // Print name.
+  buffer.add(name);
+  buffer.add('\n');
+}
+
+String _getPrefix(bool isRoot, bool isLast) {
+  if (isRoot) return "";
+  if (isLast) return "    ";
+  return "|   ";
+}
+
+void _draw(StringBuffer buffer, String prefix, bool isLast,
+                 String name, Map children) {
+  // Don't draw a line for the root node.
+  if (name != null) _drawLine(buffer, prefix, isLast, name);
+
+  // Recurse to the children.
+  var childNames = new List.from(children.keys);
+  childNames.sort();
+
+  _drawChild(bool isLastChild, String child) {
+    var childPrefix = _getPrefix(name == null, isLast);
+    _draw(buffer, '$prefix$childPrefix', isLastChild, child, children[child]);
+  }
+
+  if (childNames.length <= 10) {
+    // Not too many, so show all the children.
+    for (var i = 0; i < childNames.length; i++) {
+      _drawChild(i == childNames.length - 1, childNames[i]);
+    }
+  } else {
+    // Show the first few.
+    _drawChild(false, childNames[0]);
+    _drawChild(false, childNames[1]);
+    _drawChild(false, childNames[2]);
+
+    // Elide the middle ones.
+    buffer.add(prefix);
+    buffer.add(_getPrefix(name == null, isLast));
+    buffer.add('|   (${childNames.length - 6} more...)\n');
+
+    // Show the last few.
+    _drawChild(false, childNames[childNames.length - 3]);
+    _drawChild(false, childNames[childNames.length - 2]);
+    _drawChild(true, childNames[childNames.length - 1]);
+  }
+}
diff --git a/utils/pub/io.dart b/utils/pub/io.dart
index 0ecc7b2..39a854d 100644
--- a/utils/pub/io.dart
+++ b/utils/pub/io.dart
@@ -30,45 +30,24 @@
  * platform-specific path separators. Parts can be [String], [Directory], or
  * [File] objects.
  */
-String join(part1, [part2, part3, part4]) {
-  part1 = _getPath(part1);
-  if (part2 != null) part2 = _getPath(part2);
-  if (part3 != null) part3 = _getPath(part3);
-  if (part4 != null) part4 = _getPath(part4);
+String join(part1, [part2, part3, part4, part5, part6, part7, part8]) {
+  var parts = [part1, part2, part3, part4, part5, part6, part7, part8]
+      .map((part) => part == null ? null : _getPath(part));
 
-  // TODO(nweiz): Don't use "?part" in path.dart.
-  if (part4 != null) {
-    return path.join(part1, part2, part3, part4);
-  } else if (part3 != null) {
-    return path.join(part1, part2, part3);
-  } else if (part2 != null) {
-    return path.join(part1, part2);
-  } else {
-    return path.join(part1);
-  }
+  return path.join(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5],
+      parts[6], parts[7]);
 }
 
 /// Gets the basename, the file name without any leading directory path, for
 /// [file], which can either be a [String], [File], or [Directory].
 String basename(file) => path.basename(_getPath(file));
 
-// TODO(nweiz): move this into path.dart.
 /// Gets the the leading directory path for [file], which can either be a
 /// [String], [File], or [Directory].
-String dirname(file) {
-  file = _sanitizePath(file);
+String dirname(file) => path.dirname(_getPath(file));
 
-  int lastSlash = file.lastIndexOf('/', file.length);
-  if (lastSlash == -1) {
-    return '.';
-  } else {
-    return file.substring(0, lastSlash);
-  }
-}
-
-// TODO(nweiz): move this into path.dart.
-/// Splits [path] into its individual components.
-List<String> splitPath(path) => _sanitizePath(path).split('/');
+/// Splits [entry] into its individual components.
+List<String> splitPath(entry) => path.split(_getPath(entry));
 
 /// Returns whether or not [entry] is nested somewhere within [dir]. This just
 /// performs a path comparison; it doesn't look at the actual filesystem.
@@ -77,10 +56,8 @@
   return !path.isAbsolute(relative) && splitPath(relative)[0] != '..';
 }
 
-// TODO(nweiz): move this into path.dart.
 /// Returns the path to [target] from [base].
-String relativeTo(target, base) =>
-  new path.Builder(root: base).relative(target);
+String relativeTo(target, base) => path.relative(target, from: base);
 
 /**
  * Asynchronously determines if [path], which can be a [String] file path, a
@@ -469,6 +446,18 @@
 /// A StringInputStream reading from stdin.
 final _stringStdin = new StringInputStream(stdin);
 
+/// Displays a message and reads a yes/no confirmation from the user. Returns
+/// a [Future] that completes to `true` if the user confirms or `false` if they
+/// do not.
+///
+/// This will automatically append " (y/n)?" to the message, so [message]
+/// should just be a fragment like, "Are you sure you want to proceed".
+Future<bool> confirm(String message) {
+  log.fine('Showing confirm message: $message');
+  stdout.writeString("$message (y/n)? ");
+  return readLine().transform((line) => new RegExp(r"^[yY]").hasMatch(line));
+}
+
 /// Returns a single line read from a [StringInputStream]. By default, reads
 /// from stdin.
 ///
@@ -498,7 +487,9 @@
 
   stream.onLine = () {
     removeCallbacks();
-    completer.complete(stream.readLine());
+    var line = stream.readLine();
+    log.io('Read line: $line');
+    completer.complete(line);
   };
 
   stream.onError = (e) {
@@ -952,7 +943,7 @@
 InputStream createTarGz(List contents, {baseDir}) {
   var buffer = new StringBuffer();
   buffer.add('Creating .tag.gz stream containing:\n');
-  contents.forEach(buffer.add);
+  contents.forEach((file) => buffer.add('$file\n'));
   log.fine(buffer.toString());
 
   // TODO(nweiz): Propagate errors to the returned stream (including non-zero
@@ -1067,40 +1058,6 @@
   throw 'Entry $entry is not a supported type.';
 }
 
-/// Gets the path string for [entry], normalizing backslashes to forward slashes
-/// on Windows.
-String _sanitizePath(entry) {
-  entry = _getPath(entry);
-  if (Platform.operatingSystem != 'windows') return entry;
-
-  var split = _splitAbsolute(entry);
-  if (split.first == null) return split.last.replaceAll('\\', '/');
-
-  // For absolute Windows paths, we don't want the prefix (either "\\" or e.g.
-  // "C:\") to look like a normal path component, so we ensure that it only
-  // contains backslashes.
-  return '${split.first.replaceAll('/', '\\')}'
-         '${split.last.replaceAll('\\', '/')}';
-}
-
-// TODO(nweiz): Add something like this to path.dart.
-/// Splits [entry] into two components: the absolute path prefix and the
-/// remaining path. Takes into account Windows' quirky absolute paths syntaxes.
-Pair<String, String> _splitAbsolute(entry) {
-  var path = _getPath(entry);
-
-  if (Platform.operatingSystem != 'windows') {
-    return !path.startsWith('/') ? new Pair(null, path)
-        : new Pair('/', path.substring(1));
-  }
-
-  // An absolute path on Windows is either UNC (two leading backslashes),
-  // or a drive letter followed by a colon and a slash.
-  var match = new RegExp(r'^(\\\\|[a-zA-Z]:[/\\])').firstMatch(path);
-  return match == null ? new Pair(null, path)
-      : new Pair(match.group(0), path.substring(match.end));
-}
-
 /**
  * Gets a [Directory] for [entry], which can either already be one, or be a
  * [String].
diff --git a/utils/pub/path.dart b/utils/pub/path.dart
index d803fd1..593396d0 100644
--- a/utils/pub/path.dart
+++ b/utils/pub/path.dart
@@ -37,6 +37,12 @@
 String basenameWithoutExtension(String path) =>
     _builder.basenameWithoutExtension(path);
 
+/// Gets the part of [path] before the last separator.
+///
+///     path.dirname('path/to/foo.dart'); // -> 'path/to'
+///     path.dirname('path/to');          // -> 'to'
+String dirname(String path) => _builder.dirname(path);
+
 /// Gets the file extension of [path]: the portion of [basename] from the last
 /// `.` to the end (including the `.` itself).
 ///
@@ -52,6 +58,19 @@
 ///     path.extension('~/.notes.txt'); // -> '.txt'
 String extension(String path) => _builder.extension(path);
 
+// TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
+/// Returns the root of [path], if it's absolute, or the empty string if it's
+/// relative.
+///
+///     // Unix
+///     path.rootPrefix('path/to/foo'); // -> ''
+///     path.rootPrefix('/path/to/foo'); // -> '/'
+///
+///     // Windows
+///     path.rootPrefix(r'path\to\foo'); // -> ''
+///     path.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
+String rootPrefix(String path) => _builder.rootPrefix(path);
+
 /// Returns `true` if [path] is an absolute path and `false` if it is a
 /// relative path. On POSIX systems, absolute paths start with a `/` (forward
 /// slash). On Windows, an absolute path starts with `\\`, or a drive letter
@@ -78,17 +97,27 @@
 ///
 ///     path.join('path', '/to', 'foo'); // -> '/to/foo'
 String join(String part1, [String part2, String part3, String part4,
-            String part5, String part6, String part7, String part8]) {
-  if (!?part2) return _builder.join(part1);
-  if (!?part3) return _builder.join(part1, part2);
-  if (!?part4) return _builder.join(part1, part2, part3);
-  if (!?part5) return _builder.join(part1, part2, part3, part4);
-  if (!?part6) return _builder.join(part1, part2, part3, part4, part5);
-  if (!?part7) return _builder.join(part1, part2, part3, part4, part5, part6);
-  if (!?part8) return _builder.join(part1, part2, part3, part4, part5, part6,
-                                    part7);
-  return _builder.join(part1, part2, part3, part4, part5, part6, part7, part8);
-}
+            String part5, String part6, String part7, String part8]) =>
+  _builder.join(part1, part2, part3, part4, part5, part6, part7, part8);
+
+// TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
+/// Splits [path] into its components using the current platform's [separator].
+///
+///     path.split('path/to/foo'); // -> ['path', 'to', 'foo']
+///
+/// The path will *not* be normalized before splitting.
+///
+///     path.split('path/../foo'); // -> ['path', '..', 'foo']
+///
+/// If [path] is absolute, the root directory will be the first element in the
+/// array. Example:
+///
+///     // Unix
+///     path.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo']
+///
+///     // Windows
+///     path.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo']
+List<String> split(String path) => _builder.split(path);
 
 /// Normalizes [path], simplifying it by handling `..`, and `.`, and
 /// removing redundant path separators whenever possible.
@@ -103,12 +132,19 @@
 ///     path.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
 ///     path.relative('/root/other.dart'); // -> '../other.dart'
 ///
+/// If the [from] argument is passed, [path] is made relative to that instead.
+///
+///     path.relative('/root/path/a/b.dart',
+///         from: '/root/path'); // -> 'a/b.dart'
+///     path.relative('/root/other.dart',
+///         from: '/root/path'); // -> '../other.dart'
+///
 /// Since there is no relative path from one drive letter to another on Windows,
 /// this will return an absolute path in that case.
 ///
-///     // Given current directory is C:\home:
-///     path.relative(r'D:\other'); // -> 'D:\other'
-String relative(String path) => _builder.relative(path);
+///     path.relative(r'D:\other', from: r'C:\home'); // -> 'D:\other'
+String relative(String path, {String from}) =>
+    _builder.relative(path, from: from);
 
 /// Removes a trailing extension from the last part of [path].
 ///
@@ -163,6 +199,24 @@
   String basenameWithoutExtension(String path) =>
       _parse(path).basenameWithoutExtension;
 
+  /// Gets the part of [path] before the last separator.
+  ///
+  ///     builder.dirname('path/to/foo.dart'); // -> 'path/to'
+  ///     builder.dirname('path/to');          // -> 'to'
+  String dirname(String path) {
+    var parsed = _parse(path);
+    if (parsed.parts.isEmpty) return parsed.root == null ? '.' : parsed.root;
+    if (!parsed.hasTrailingSeparator) {
+      if (parsed.parts.length == 1) {
+        return parsed.root == null ? '.' : parsed.root;
+      }
+      parsed.parts.removeLast();
+      parsed.separators.removeLast();
+    }
+    parsed.separators[parsed.separators.length - 1] = '';
+    return parsed.toString();
+  }
+
   /// Gets the file extension of [path]: the portion of [basename] from the last
   /// `.` to the end (including the `.` itself).
   ///
@@ -178,6 +232,22 @@
   ///     builder.extension('~/.notes.txt'); // -> '.txt'
   String extension(String path) => _parse(path).extension;
 
+  // TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
+  /// Returns the root of [path], if it's absolute, or an empty string if it's
+  /// relative.
+  ///
+  ///     // Unix
+  ///     builder.rootPrefix('path/to/foo'); // -> ''
+  ///     builder.rootPrefix('/path/to/foo'); // -> '/'
+  ///
+  ///     // Windows
+  ///     builder.rootPrefix(r'path\to\foo'); // -> ''
+  ///     builder.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
+  String rootPrefix(String path) {
+    var root = _parse(path).root;
+    return root == null ? '' : root;
+  }
+
   /// Returns `true` if [path] is an absolute path and `false` if it is a
   /// relative path. On POSIX systems, absolute paths start with a `/` (forward
   /// slash). On Windows, an absolute path starts with `\\`, or a drive letter
@@ -208,8 +278,16 @@
     var buffer = new StringBuffer();
     var needsSeparator = false;
 
-    addPart(condition, part) {
-      if (!condition) return;
+    var parts = [part1, part2, part3, part4, part5, part6, part7, part8];
+    for (var i = 1; i < parts.length; i++) {
+      if (parts[i] != null && parts[i - 1] == null) {
+        throw new ArgumentError("join(): part ${i - 1} was null, but part $i "
+            "was not.");
+      }
+    }
+
+    for (var part in parts) {
+      if (part == null) continue;
 
       if (this.isAbsolute(part)) {
         // An absolute path discards everything before it.
@@ -231,18 +309,35 @@
           !style.separatorPattern.hasMatch(part[part.length - 1]);
     }
 
-    addPart(true, part1);
-    addPart(?part2, part2);
-    addPart(?part3, part3);
-    addPart(?part4, part4);
-    addPart(?part5, part5);
-    addPart(?part6, part6);
-    addPart(?part7, part7);
-    addPart(?part8, part8);
-
     return buffer.toString();
   }
 
+  // TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
+  /// Splits [path] into its components using the current platform's
+  /// [separator]. Example:
+  ///
+  ///     builder.split('path/to/foo'); // -> ['path', 'to', 'foo']
+  ///
+  /// The path will *not* be normalized before splitting.
+  ///
+  ///     builder.split('path/../foo'); // -> ['path', '..', 'foo']
+  ///
+  /// If [path] is absolute, the root directory will be the first element in the
+  /// array. Example:
+  ///
+  ///     // Unix
+  ///     builder.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo']
+  ///
+  ///     // Windows
+  ///     builder.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo']
+  List<String> split(String path) {
+    var parsed = _parse(path);
+    // Filter out empty parts that exist due to multiple separators in a row.
+    parsed.parts = parsed.parts.filter((part) => part != '');
+    if (parsed.root != null) parsed.parts.insertRange(0, 1, parsed.root);
+    return parsed.parts;
+  }
+
   /// Normalizes [path], simplifying it by handling `..`, and `.`, and
   /// removing redundant path separators whenever possible.
   ///
@@ -278,23 +373,44 @@
   ///     builder.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
   ///     builder.relative('/root/other.dart'); // -> '../other.dart'
   ///
+  /// If the [from] argument is passed, [path] is made relative to that instead.
+  ///
+  ///     builder.relative('/root/path/a/b.dart',
+  ///         from: '/root/path'); // -> 'a/b.dart'
+  ///     builder.relative('/root/other.dart',
+  ///         from: '/root/path'); // -> '../other.dart'
+  ///
   /// Since there is no relative path from one drive letter to another on
   /// Windows, this will return an absolute path in that case.
   ///
-  ///     var builder = new Builder(root: r'C:\home');
-  ///     builder.relative(r'D:\other'); // -> 'D:\other'
-  String relative(String path) {
+  ///     builder.relative(r'D:\other', from: r'C:\other'); // -> 'D:\other'
+  ///
+  /// This will also return an absolute path if an absolute [path] is passed to
+  /// a builder with a relative [root].
+  ///
+  ///     var builder = new Builder(r'some/relative/path');
+  ///     builder.relative(r'/absolute/path'); // -> '/absolute/path'
+  String relative(String path, {String from}) {
     if (path == '') return '.';
 
-    // If the base path is relative, resolve it relative to the current
-    // directory.
-    var base = root;
-    if (this.isRelative(base)) base = absolute(base);
+    from = from == null ? root : this.join(root, from);
 
-    // If the given path is relative, resolve it relative to the base.
-    if (this.isRelative(path)) return this.normalize(path);
+    // We can't determine the path from a relative path to an absolute path.
+    if (this.isRelative(from) && this.isAbsolute(path)) {
+      return this.normalize(path);
+    }
 
-    var baseParsed = _parse(base)..normalize();
+    // If the given path is relative, resolve it relative to the root of the
+    // builder.
+    if (this.isRelative(path)) path = this.resolve(path);
+
+    // If the path is still relative and `from` is absolute, we're unable to
+    // find a path from `from` to `path`.
+    if (this.isRelative(path) && this.isAbsolute(from)) {
+      throw new ArgumentError('Unable to find a path to "$path" from "$from".');
+    }
+
+    var fromParsed = _parse(from)..normalize();
     var pathParsed = _parse(path)..normalize();
 
     // If the root prefixes don't match (for example, different drive letters
@@ -302,21 +418,21 @@
     // one.
     // TODO(rnystrom): Drive letters are case-insentive on Windows. Should
     // handle "C:\" and "c:\" being the same root.
-    if (baseParsed.root != pathParsed.root) return pathParsed.toString();
+    if (fromParsed.root != pathParsed.root) return pathParsed.toString();
 
     // Strip off their common prefix.
-    while (baseParsed.parts.length > 0 && pathParsed.parts.length > 0 &&
-           baseParsed.parts[0] == pathParsed.parts[0]) {
-      baseParsed.parts.removeAt(0);
-      baseParsed.separators.removeAt(0);
+    while (fromParsed.parts.length > 0 && pathParsed.parts.length > 0 &&
+           fromParsed.parts[0] == pathParsed.parts[0]) {
+      fromParsed.parts.removeAt(0);
+      fromParsed.separators.removeAt(0);
       pathParsed.parts.removeAt(0);
       pathParsed.separators.removeAt(0);
     }
 
     // If there are any directories left in the root path, we need to walk up
     // out of them.
-    pathParsed.parts.insertRange(0, baseParsed.parts.length, '..');
-    pathParsed.separators.insertRange(0, baseParsed.parts.length,
+    pathParsed.parts.insertRange(0, fromParsed.parts.length, '..');
+    pathParsed.separators.insertRange(0, fromParsed.parts.length,
         style.separator);
 
     // Corner case: the paths completely collapsed.
diff --git a/utils/pub/validator.dart b/utils/pub/validator.dart
index 3e491a9..5c0c850 100644
--- a/utils/pub/validator.dart
+++ b/utils/pub/validator.dart
@@ -57,17 +57,17 @@
       var warnings = flatten(validators.map((validator) => validator.warnings));
 
       if (!errors.isEmpty) {
-        log.error("== Errors:");
+        log.error("Missing requirements:");
         for (var error in errors) {
-          log.error("* $error");
+          log.error("* ${Strings.join(error.split('\n'), '\n  ')}");
         }
         log.error("");
       }
 
       if (!warnings.isEmpty) {
-        log.warning("== Warnings:");
+        log.warning("Suggestions:");
         for (var warning in warnings) {
-          log.warning("* $warning");
+          log.warning("* ${Strings.join(warning.split('\n'), '\n  ')}");
         }
         log.warning("");
       }
diff --git a/utils/pub/validator/lib.dart b/utils/pub/validator/lib.dart
index 64f44c6..39b71af 100644
--- a/utils/pub/validator/lib.dart
+++ b/utils/pub/validator/lib.dart
@@ -21,21 +21,30 @@
 
   Future validate() {
     var libDir = join(entrypoint.root.dir, "lib");
+
     return dirExists(libDir).chain((libDirExists) {
       if (!libDirExists) {
-        errors.add('Your package must have a "lib/" directory so users have '
-            'something to import.');
+        errors.add('You must have a "lib" directory.\n'
+            "Without that, users cannot import any code from your package.");
         return new Future.immediate(null);
       }
 
+      // TODO(rnystrom): listDir() returns real file paths after symlinks are
+      // resolved. This means if libDir contains a symlink, the resulting paths
+      // won't appear to be within it, which confuses relativeTo(). Work around
+      // that here by making sure we have the real path to libDir. Remove this
+      // when #7346 is fixed.
+      libDir = new File(libDir).fullPathSync();
+
       return listDir(libDir).transform((files) {
         files = files.map((file) => relativeTo(file, libDir));
         if (files.isEmpty) {
-          errors.add('The "lib/" directory may not be empty so users have '
-              'something to import');
+          errors.add('You must have a non-empty "lib" directory.\n'
+              "Without that, users cannot import any code from your package.");
         } else if (files.length == 1 && files.first == "src") {
-          errors.add('The "lib/" directory must contain something other than '
-              '"src/" so users have something to import');
+          errors.add('The "lib" directory must contain something other than '
+              '"src".\n'
+              "Otherwise, users cannot import any code from your package.");
         }
       });
     });
diff --git a/utils/pub/validator/license.dart b/utils/pub/validator/license.dart
index 46a1c40..fcc6a49 100644
--- a/utils/pub/validator/license.dart
+++ b/utils/pub/validator/license.dart
@@ -20,9 +20,10 @@
           r"^([a-zA-Z0-9]+[-_])?(LICENSE|COPYING)(\..*)?$");
       if (files.map(basename).some(licenseLike.hasMatch)) return;
 
-      errors.add("Your package must have a COPYING or LICENSE file containing "
-          "an open-source license. For more details, see "
-          "http://pub.dartlang.org/doc/pub-lish.html.");
+      errors.add(
+          "You must have a COPYING or LICENSE file in the root directory.\n"
+          "An open-source license helps ensure people can legally use your "
+          "code.");
     });
   }
 }
diff --git a/utils/pub/validator/name.dart b/utils/pub/validator/name.dart
index ecc34e4..687cfc7 100644
--- a/utils/pub/validator/name.dart
+++ b/utils/pub/validator/name.dart
@@ -45,14 +45,15 @@
     if (name == "") {
       errors.add("$description may not be empty.");
     } else if (!new RegExp(r"^[a-zA-Z0-9_]*$").hasMatch(name)) {
-      errors.add("$description must be a valid Dart identifier: it may only "
-          "contain letters, numbers, and underscores.");
+      errors.add("$description may only contain letters, numbers, and "
+          "underscores.\n"
+          "Using a valid Dart identifier makes the name usable in Dart code.");
     } else if (!new RegExp(r"^[a-zA-Z]").hasMatch(name)) {
-      errors.add("$description must be a valid Dart identifier: it must begin "
-          "with a letter.");
+      errors.add("$description must begin with letter.\n"
+          "Using a valid Dart identifier makes the name usable in Dart code.");
     } else if (_RESERVED_WORDS.contains(name.toLowerCase())) {
-      errors.add("$description must be a valid Dart identifier: it may not be "
-          "a reserved word in Dart.");
+      errors.add("$description may not be a reserved word in Dart.\n"
+          "Using a valid Dart identifier makes the name usable in Dart code.");
     } else if (new RegExp(r"[A-Z]").hasMatch(name)) {
       warnings.add('$description should be lower-case. Maybe use '
           '"${_unCamelCase(name)}"?');
diff --git a/utils/pub/validator/pubspec_field.dart b/utils/pub/validator/pubspec_field.dart
index e7e8ca7..17ddc27 100644
--- a/utils/pub/validator/pubspec_field.dart
+++ b/utils/pub/validator/pubspec_field.dart
@@ -7,6 +7,7 @@
 import '../entrypoint.dart';
 import '../system_cache.dart';
 import '../validator.dart';
+import '../version.dart';
 
 /// A validator that checks that the pubspec has valid "author" and "homepage"
 /// fields.
@@ -20,7 +21,7 @@
     var author = pubspec.fields['author'];
     var authors = pubspec.fields['authors'];
     if (author == null && authors == null) {
-      errors.add('pubspec.yaml is missing an "author" or "authors" field.');
+      errors.add('Your pubspec.yaml must have an "author" or "authors" field.');
     } else {
       if (authors == null) authors = [author];
 
@@ -28,24 +29,29 @@
       var hasEmail = new RegExp(r"<[^>]+> *$");
       for (var authorName in authors) {
         if (!hasName.hasMatch(authorName)) {
-          warnings.add('Author "$authorName" in pubspec.yaml is missing a '
+          warnings.add('Author "$authorName" in pubspec.yaml should have a '
               'name.');
         }
         if (!hasEmail.hasMatch(authorName)) {
-          warnings.add('Author "$authorName" in pubspec.yaml is missing an '
-              'email address (e.g. "name <email>").');
+          warnings.add('Author "$authorName" in pubspec.yaml should have an '
+              'email address\n(e.g. "name <email>").');
         }
       }
     }
 
     var homepage = pubspec.fields['homepage'];
     if (homepage == null) {
-      errors.add('pubspec.yaml is missing a "homepage" field.');
+      errors.add('Your pubspec.yaml is missing a "homepage" field.');
     }
 
     var description = pubspec.fields['description'];
     if (description == null) {
-      errors.add('pubspec.yaml is missing a "description" field.');
+      errors.add('Your pubspec.yaml is missing a "description" field.');
+    }
+
+    var version = pubspec.fields['version'];
+    if (version == null) {
+      errors.add('Your pubspec.yaml is missing a "version" field.');
     }
 
     return new Future.immediate(null);
diff --git a/utils/tests/pub/directory_tree_test.dart b/utils/tests/pub/directory_tree_test.dart
new file mode 100644
index 0000000..e9f892b
--- /dev/null
+++ b/utils/tests/pub/directory_tree_test.dart
@@ -0,0 +1,114 @@
+// Copyright (c) 2012, 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.
+
+library lock_file_test;
+
+import '../../../pkg/unittest/lib/unittest.dart';
+import '../../pub/directory_tree.dart';
+
+main() {
+  test('no files', () {
+    expect(generateTree([]), equals(""));
+  });
+
+  test('up to ten files in one directory are shown', () {
+    var files = [
+      "a.dart",
+      "b.dart",
+      "c.dart",
+      "d.dart",
+      "e.dart",
+      "f.dart",
+      "g.dart",
+      "h.dart",
+      "i.dart",
+      "j.dart"
+    ];
+    expect(generateTree(files), equals("""
+|-- a.dart
+|-- b.dart
+|-- c.dart
+|-- d.dart
+|-- e.dart
+|-- f.dart
+|-- g.dart
+|-- h.dart
+|-- i.dart
+'-- j.dart
+"""));
+  });
+
+  test('files are elided if there are more than ten', () {
+    var files = [
+      "a.dart",
+      "b.dart",
+      "c.dart",
+      "d.dart",
+      "e.dart",
+      "f.dart",
+      "g.dart",
+      "h.dart",
+      "i.dart",
+      "j.dart",
+      "k.dart"
+    ];
+    expect(generateTree(files), equals("""
+|-- a.dart
+|-- b.dart
+|-- c.dart
+|   (5 more...)
+|-- i.dart
+|-- j.dart
+'-- k.dart
+"""));
+  });
+
+  test('a complex example', () {
+    var files = [
+      "TODO",
+      "example/console_example.dart",
+      "example/main.dart",
+      "example/web copy/web_example.dart",
+      "test/absolute_test.dart",
+      "test/basename_test.dart",
+      "test/dirname_test.dart",
+      "test/extension_test.dart",
+      "test/is_absolute_test.dart",
+      "test/is_relative_test.dart",
+      "test/join_test.dart",
+      "test/normalize_test.dart",
+      "test/relative_test.dart",
+      "test/split_test.dart",
+      ".gitignore",
+      "README.md",
+      "lib/path.dart",
+      "pubspec.yaml",
+      "test/all_test.dart",
+      "test/path_posix_test.dart",
+      "test/path_windows_test.dart"
+    ];
+
+    expect(generateTree(files), equals("""
+|-- .gitignore
+|-- README.md
+|-- TODO
+|-- example
+|   |-- console_example.dart
+|   |-- main.dart
+|   '-- web copy
+|       '-- web_example.dart
+|-- lib
+|   '-- path.dart
+|-- pubspec.yaml
+'-- test
+    |-- absolute_test.dart
+    |-- all_test.dart
+    |-- basename_test.dart
+    |   (7 more...)
+    |-- path_windows_test.dart
+    |-- relative_test.dart
+    '-- split_test.dart
+"""));
+  });
+}
diff --git a/utils/tests/pub/oauth2_test.dart b/utils/tests/pub/oauth2_test.dart
index 32e4a75..f640e11 100644
--- a/utils/tests/pub/oauth2_test.dart
+++ b/utils/tests/pub/oauth2_test.dart
@@ -21,6 +21,7 @@
       () {
     var server = new ScheduledServer();
     var pub = startPubLish(server);
+    confirmPublish(pub);
     authorizePub(pub, server);
 
     server.handle('GET', '/packages/versions/new.json', (request, response) {
@@ -41,6 +42,7 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+    confirmPublish(pub);
 
     server.handle('GET', '/packages/versions/new.json', (request, response) {
       expect(request.headers.value('authorization'),
@@ -63,6 +65,7 @@
         .scheduleCreate();
 
     var pub = startPubLish(server);
+    confirmPublish(pub);
 
     server.handle('POST', '/token', (request, response) {
       return consumeInputStream(request.inputStream).transform((bytes) {
@@ -102,6 +105,7 @@
         .scheduleCreate();
 
     var pub = startPubLish(server);
+    confirmPublish(pub);
 
     expectLater(pub.nextErrLine(), equals("Pub's authorization to upload "
           "packages has expired and can't be automatically refreshed."));
@@ -129,6 +133,7 @@
     ]).scheduleCreate();
 
     var pub = startPubLish(server);
+    confirmPublish(pub);
     authorizePub(pub, server, "new access token");
 
     server.handle('GET', '/packages/versions/new.json', (request, response) {
@@ -148,8 +153,12 @@
 
 void authorizePub(ScheduledProcess pub, ScheduledServer server,
     [String accessToken="access token"]) {
-  expectLater(pub.nextLine(), equals('Pub needs your '
-     'authorization to upload packages on your behalf.'));
+  // TODO(rnystrom): The confirm line is run together with this one because
+  // in normal usage, the user will have entered a newline on stdin which
+  // gets echoed to the terminal. Do something better here?
+  expectLater(pub.nextLine(), equals(
+      'Looks great! Are you ready to upload your package (y/n)? '
+      'Pub needs your authorization to upload packages on your behalf.'));
 
   expectLater(pub.nextLine().chain((line) {
     var match = new RegExp(r'[?&]redirect_uri=([0-9a-zA-Z%+-]+)[$&]')
diff --git a/utils/tests/pub/path/path_posix_test.dart b/utils/tests/pub/path/path_posix_test.dart
index a34c32d..60ed65a 100644
--- a/utils/tests/pub/path/path_posix_test.dart
+++ b/utils/tests/pub/path/path_posix_test.dart
@@ -33,6 +33,30 @@
     expect(builder.extension(r'a.b\c'), r'.b\c');
   });
 
+  test('rootPrefix', () {
+    expect(builder.rootPrefix(''), '');
+    expect(builder.rootPrefix('a'), '');
+    expect(builder.rootPrefix('a/b'), '');
+    expect(builder.rootPrefix('/a/c'), '/');
+    expect(builder.rootPrefix('/'), '/');
+  });
+
+  test('dirname', () {
+    expect(builder.dirname(''), '.');
+    expect(builder.dirname('a'), '.');
+    expect(builder.dirname('a/b'), 'a');
+    expect(builder.dirname('a/b/c'), 'a/b');
+    expect(builder.dirname('a/b.c'), 'a');
+    expect(builder.dirname('a/'), 'a');
+    expect(builder.dirname('a/.'), 'a');
+    expect(builder.dirname(r'a\b/c'), r'a\b');
+    expect(builder.dirname('/a'), '/');
+    expect(builder.dirname('/'), '/');
+    expect(builder.dirname('a/b/'), 'a/b');
+    expect(builder.dirname(r'a/b\c'), 'a');
+    expect(builder.dirname('a//'), 'a/');
+  });
+
   test('basename', () {
     expect(builder.basename(''), '');
     expect(builder.basename('a'), 'a');
@@ -41,7 +65,13 @@
     expect(builder.basename('a/b.c'), 'b.c');
     expect(builder.basename('a/'), '');
     expect(builder.basename('a/.'), '.');
+    expect(builder.basename(r'a\b/c'), 'c');
+    expect(builder.basename('/a'), 'a');
+    // TODO(nweiz): this should actually return '/'
+    expect(builder.basename('/'), '');
+    expect(builder.basename('a/b/'), '');
     expect(builder.basename(r'a/b\c'), r'b\c');
+    expect(builder.basename('a//'), '');
   });
 
   test('basenameWithoutExtension', () {
@@ -104,10 +134,45 @@
     });
 
     test('ignores parts before an absolute path', () {
+      expect(builder.join('a', '/', 'b', 'c'), '/b/c');
       expect(builder.join('a', '/b', '/c', 'd'), '/c/d');
       expect(builder.join('a', r'c:\b', 'c', 'd'), r'a/c:\b/c/d');
       expect(builder.join('a', r'\\b', 'c', 'd'), r'a/\\b/c/d');
     });
+
+    test('ignores trailing nulls', () {
+      expect(builder.join('a', null), equals('a'));
+      expect(builder.join('a', 'b', 'c', null, null), equals('a/b/c'));
+    });
+
+    test('disallows intermediate nulls', () {
+      expect(() => builder.join('a', null, 'b'), throwsArgumentError);
+      expect(() => builder.join(null, 'a'), throwsArgumentError);
+    });
+  });
+
+  group('split', () {
+    test('simple cases', () {
+      expect(builder.split(''), []);
+      expect(builder.split('.'), ['.']);
+      expect(builder.split('..'), ['..']);
+      expect(builder.split('foo'), equals(['foo']));
+      expect(builder.split('foo/bar.txt'), equals(['foo', 'bar.txt']));
+      expect(builder.split('foo/bar/baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split('foo/../bar/./baz'),
+          equals(['foo', '..', 'bar', '.', 'baz']));
+      expect(builder.split('foo//bar///baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split('foo/\\/baz'), equals(['foo', '\\', 'baz']));
+      expect(builder.split('.'), equals(['.']));
+      expect(builder.split(''), equals([]));
+      expect(builder.split('foo/'), equals(['foo']));
+      expect(builder.split('//'), equals(['/']));
+    });
+
+    test('includes the root for absolute paths', () {
+      expect(builder.split('/foo/bar/baz'), equals(['/', 'foo', 'bar', 'baz']));
+      expect(builder.split('/'), equals(['/']));
+    });
   });
 
   group('normalize', () {
@@ -211,15 +276,10 @@
     group('from relative root', () {
       var r = new path.Builder(style: path.Style.posix, root: 'foo/bar');
 
-      // These tests rely on the current working directory, so don't do the
-      // right thing if you run them on the wrong platform.
-      if (io.Platform.operatingSystem != 'windows') {
-        test('given absolute path', () {
-          var b = new path.Builder(style: path.Style.posix);
-          expect(r.relative('/'), b.join(b.relative('/'), '../..'));
-          expect(r.relative('/a/b'), b.join(b.relative('/'), '../../a/b'));
-        });
-      }
+      test('given absolute path', () {
+        expect(r.relative('/'), equals('/'));
+        expect(r.relative('/a/b'), equals('/a/b'));
+      });
 
       test('given relative path', () {
         // The path is considered relative to the root, so it basically just
@@ -238,6 +298,23 @@
       var r = new path.Builder(style: path.Style.posix, root: '/dir.ext');
       expect(r.relative('/dir.ext/file'), 'file');
     });
+
+    test('with a root parameter', () {
+      expect(builder.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz'));
+      expect(builder.relative('..', from: '/foo/bar'), equals('../../root'));
+      expect(builder.relative('/foo/bar/baz', from: 'foo/bar'),
+          equals('../../../../foo/bar/baz'));
+      expect(builder.relative('..', from: 'foo/bar'), equals('../../..'));
+    });
+
+    test('with a root parameter and a relative root', () {
+      var r = new path.Builder(style: path.Style.posix, root: 'relative/root');
+      expect(r.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz'));
+      expect(() => r.relative('..', from: '/foo/bar'), throwsArgumentError);
+      expect(r.relative('/foo/bar/baz', from: 'foo/bar'),
+          equals('/foo/bar/baz'));
+      expect(r.relative('..', from: 'foo/bar'), equals('../../..'));
+    });
   });
 
   group('resolve', () {
diff --git a/utils/tests/pub/path/path_windows_test.dart b/utils/tests/pub/path/path_windows_test.dart
index 1861a67..fdad9b0 100644
--- a/utils/tests/pub/path/path_windows_test.dart
+++ b/utils/tests/pub/path/path_windows_test.dart
@@ -35,6 +35,35 @@
     expect(builder.extension(r'a.b/c'), r'');
   });
 
+  test('rootPrefix', () {
+    expect(builder.rootPrefix(''), '');
+    expect(builder.rootPrefix('a'), '');
+    expect(builder.rootPrefix(r'a\b'), '');
+    expect(builder.rootPrefix(r'C:\a\c'), r'C:\');
+    expect(builder.rootPrefix('C:\\'), r'C:\');
+    expect(builder.rootPrefix('C:/'), 'C:/');
+
+    // TODO(nweiz): enable this once issue 7323 is fixed.
+    // expect(builder.rootPrefix(r'\\server\a\b'), r'\\server\');
+  });
+
+  test('dirname', () {
+    expect(builder.dirname(r''), '.');
+    expect(builder.dirname(r'a'), '.');
+    expect(builder.dirname(r'a\b'), 'a');
+    expect(builder.dirname(r'a\b\c'), r'a\b');
+    expect(builder.dirname(r'a\b.c'), 'a');
+    expect(builder.dirname(r'a\'), 'a');
+    expect(builder.dirname('a/'), 'a');
+    expect(builder.dirname(r'a\.'), 'a');
+    expect(builder.dirname(r'a\b/c'), r'a\b');
+    expect(builder.dirname(r'C:\a'), r'C:\');
+    expect(builder.dirname('C:\\'), r'C:\');
+    expect(builder.dirname(r'a\b\'), r'a\b');
+    expect(builder.dirname(r'a/b\c'), 'a/b');
+    expect(builder.dirname(r'a\\'), r'a\');
+  });
+
   test('basename', () {
     expect(builder.basename(r''), '');
     expect(builder.basename(r'a'), 'a');
@@ -45,6 +74,12 @@
     expect(builder.basename(r'a/'), '');
     expect(builder.basename(r'a\.'), '.');
     expect(builder.basename(r'a\b/c'), r'c');
+    expect(builder.basename(r'C:\a'), 'a');
+    // TODO(nweiz): this should actually return 'C:\'
+    expect(builder.basename(r'C:\'), '');
+    expect(builder.basename(r'a\b\'), '');
+    expect(builder.basename(r'a/b\c'), 'c');
+    expect(builder.basename(r'a\\'), '');
   });
 
   test('basenameWithoutExtension', () {
@@ -123,6 +158,46 @@
       expect(builder.join('a', r'c:\b', 'c', 'd'), r'c:\b\c\d');
       expect(builder.join('a', r'\\b', r'\\c', 'd'), r'\\c\d');
     });
+
+    test('ignores trailing nulls', () {
+      expect(builder.join('a', null), equals('a'));
+      expect(builder.join('a', 'b', 'c', null, null), equals(r'a\b\c'));
+    });
+
+    test('disallows intermediate nulls', () {
+      expect(() => builder.join('a', null, 'b'), throwsArgumentError);
+      expect(() => builder.join(null, 'a'), throwsArgumentError);
+    });
+  });
+
+  group('split', () {
+    test('simple cases', () {
+      expect(builder.split(''), []);
+      expect(builder.split('.'), ['.']);
+      expect(builder.split('..'), ['..']);
+      expect(builder.split('foo'), equals(['foo']));
+      expect(builder.split(r'foo\bar.txt'), equals(['foo', 'bar.txt']));
+      expect(builder.split(r'foo\bar/baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split(r'foo\..\bar\.\baz'),
+          equals(['foo', '..', 'bar', '.', 'baz']));
+      expect(builder.split(r'foo\\bar\\\baz'), equals(['foo', 'bar', 'baz']));
+      expect(builder.split(r'foo\/\baz'), equals(['foo', 'baz']));
+      expect(builder.split('.'), equals(['.']));
+      expect(builder.split(''), equals([]));
+      expect(builder.split('foo/'), equals(['foo']));
+      expect(builder.split(r'C:\'), equals([r'C:\']));
+    });
+
+    test('includes the root for absolute paths', () {
+      expect(builder.split(r'C:\foo\bar\baz'),
+          equals([r'C:\', 'foo', 'bar', 'baz']));
+      expect(builder.split(r'C:\\'), equals([r'C:\']));
+
+      // TODO(nweiz): enable these once issue 7323 is fixed.
+      // expect(builder.split(r'\\server\foo\bar\baz'),
+      //     equals([r'\\server\', 'foo', 'bar', 'baz']));
+      // expect(builder.split(r'\\server\'), equals([r'\\server\']));
+    });
   });
 
   group('normalize', () {
@@ -225,21 +300,10 @@
     group('from relative root', () {
       var r = new path.Builder(style: path.Style.windows, root: r'foo\bar');
 
-      // These tests rely on the current working directory, so don't do the
-      // right thing if you run them on the wrong platform.
-      if (io.Platform.operatingSystem == 'windows') {
-        test('given absolute path', () {
-          var b = new path.Builder(style: path.Style.windows);
-          // TODO(rnystrom): Use a path method here to get the root prefix
-          // when one exists.
-          var drive = path.current.substring(0, 3);
-          expect(r.relative(drive), b.join(b.relative(drive), r'..\..'));
-          expect(r.relative(b.join(drive, r'a\b')),
-              b.join(b.relative(drive), r'..\..\a\b'));
-
-          // TODO(rnystrom): Test behavior when drive letters differ.
-        });
-      }
+      test('given absolute path', () {
+        expect(r.relative(r'C:\'), equals(r'C:\'));
+        expect(r.relative(r'C:\a\b'), equals(r'C:\a\b'));
+      });
 
       test('given relative path', () {
         // The path is considered relative to the root, so it basically just
@@ -259,6 +323,26 @@
       expect(r.relative(r'C:\dir.ext\file'), 'file');
     });
 
+    test('with a root parameter', () {
+      expect(builder.relative(r'C:\foo\bar\baz', from: r'C:\foo\bar'),
+          equals('baz'));
+      expect(builder.relative('..', from: r'C:\foo\bar'),
+          equals(r'..\..\root'));
+      expect(builder.relative('..', from: r'D:\foo\bar'), equals(r'C:\root'));
+      expect(builder.relative(r'C:\foo\bar\baz', from: r'foo\bar'),
+          equals(r'..\..\..\..\foo\bar\baz'));
+      expect(builder.relative('..', from: r'foo\bar'), equals(r'..\..\..'));
+    });
+
+    test('with a root parameter and a relative root', () {
+      var r = new path.Builder(style: path.Style.windows, root: r'relative\root');
+      expect(r.relative(r'C:\foo\bar\baz', from: r'C:\foo\bar'), equals('baz'));
+      expect(() => r.relative('..', from: r'C:\foo\bar'), throwsArgumentError);
+      expect(r.relative(r'C:\foo\bar\baz', from: r'foo\bar'),
+          equals(r'C:\foo\bar\baz'));
+      expect(r.relative('..', from: r'foo\bar'), equals(r'..\..\..'));
+    });
+
     test('given absolute with different root prefix', () {
       expect(builder.relative(r'D:\a\b'), r'D:\a\b');
       expect(builder.relative(r'\\a\b'), r'\\a\b');
diff --git a/utils/tests/pub/pub.status b/utils/tests/pub/pub.status
index de0fba4..4e6f9e3 100644
--- a/utils/tests/pub/pub.status
+++ b/utils/tests/pub/pub.status
@@ -9,7 +9,3 @@
 # Pub only runs on the standalone VM, not the browser.
 [ $runtime == drt || $runtime == dartium || $runtime == opera ]
 *: Skip
-
-[ $system == macos ]
-validator_test: Fail # Issue 7330
-
diff --git a/utils/tests/pub/pub_lish_test.dart b/utils/tests/pub/pub_lish_test.dart
index a947601..b787ec9 100644
--- a/utils/tests/pub/pub_lish_test.dart
+++ b/utils/tests/pub/pub_lish_test.dart
@@ -53,6 +53,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
     handleUpload(server);
 
@@ -63,7 +65,12 @@
       response.outputStream.close();
     });
 
-    expectLater(pub.nextLine(), equals('Package test_pkg 1.0.0 uploaded!'));
+    // TODO(rnystrom): The confirm line is run together with this one because
+    // in normal usage, the user will have entered a newline on stdin which
+    // gets echoed to the terminal. Do something better here?
+    expectLater(pub.nextLine(), equals(
+        'Looks great! Are you ready to upload your package (y/n)?'
+        ' Package test_pkg 1.0.0 uploaded!'));
     pub.shouldExit(0);
 
     run();
@@ -77,6 +84,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     server.handle('GET', '/packages/versions/new.json', (request, response) {
       response.statusCode = 401;
       response.headers.set('www-authenticate', 'Bearer error="invalid_token",'
@@ -89,10 +98,13 @@
 
     expectLater(pub.nextErrLine(), equals('OAuth2 authorization failed (your '
         'token sucks).'));
-    expectLater(pub.nextLine(), equals('Pub needs your authorization to upload '
-        'packages on your behalf.'));
+    // TODO(rnystrom): The confirm line is run together with this one because
+    // in normal usage, the user will have entered a newline on stdin which
+    // gets echoed to the terminal. Do something better here?
+    expectLater(pub.nextLine(), equals(
+        'Looks great! Are you ready to upload your package (y/n)? '
+        'Pub needs your authorization to upload packages on your behalf.'));
     pub.kill();
-
     run();
   });
 
@@ -102,12 +114,12 @@
     dir(appPath, [pubspec(package)]).scheduleCreate();
 
     var server = new ScheduledServer();
-    credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
-    server.ignore('GET', '/packages/versions/new.json');
 
     pub.shouldExit(1);
-    expectLater(pub.remainingStderr(), contains("Package validation failed."));
+    expectLater(pub.remainingStderr(),
+        contains("Sorry, your package is missing a requirement and can't be "
+            "published yet."));
 
     run();
   });
@@ -118,9 +130,7 @@
     dir(appPath, [pubspec(package)]).scheduleCreate();
 
     var server = new ScheduledServer();
-    credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
-    server.ignore('GET', '/packages/versions/new.json');
 
     pub.writeLine("n");
     pub.shouldExit(1);
@@ -160,6 +170,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     server.handle('GET', '/packages/versions/new.json', (request, response) {
       response.statusCode = 400;
       response.outputStream.writeString(JSON.stringify({
@@ -179,6 +191,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     server.handle('GET', '/packages/versions/new.json', (request, response) {
       response.outputStream.writeString('{not json');
       response.outputStream.close();
@@ -196,6 +210,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     var body = {
       'fields': {
         'field1': 'value1',
@@ -216,6 +232,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     var body = {
       'url': 12,
       'fields': {
@@ -237,6 +255,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     var body = {'url': 'http://example.com/upload'};
     handleUploadForm(server, body);
     expectLater(pub.nextErrLine(), equals('Invalid server response:'));
@@ -251,6 +271,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     var body = {'url': 'http://example.com/upload', 'fields': 12};
     handleUploadForm(server, body);
     expectLater(pub.nextErrLine(), equals('Invalid server response:'));
@@ -265,6 +287,8 @@
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
 
+    confirmPublish(pub);
+
     var body = {
       'url': 'http://example.com/upload',
       'fields': {'field': 12}
@@ -281,6 +305,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
 
     server.handle('POST', '/upload', (request, response) {
@@ -303,6 +329,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
 
     server.handle('POST', '/upload', (request, response) {
@@ -320,6 +348,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
     handleUpload(server);
 
@@ -341,6 +371,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
     handleUpload(server);
 
@@ -360,6 +392,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
     handleUpload(server);
 
@@ -381,6 +415,8 @@
     var server = new ScheduledServer();
     credentialsFile(server, 'access token').scheduleCreate();
     var pub = startPubLish(server);
+
+    confirmPublish(pub);
     handleUploadForm(server);
     handleUpload(server);
 
diff --git a/utils/tests/pub/test_pub.dart b/utils/tests/pub/test_pub.dart
index aa8e879..80137a0 100644
--- a/utils/tests/pub/test_pub.dart
+++ b/utils/tests/pub/test_pub.dart
@@ -629,6 +629,23 @@
   return new ScheduledProcess("pub lish", process);
 }
 
+/// Handles the beginning confirmation process for uploading a packages.
+/// Ensures that the right output is shown and then enters "y" to confirm the
+/// upload.
+void confirmPublish(ScheduledProcess pub) {
+  // TODO(rnystrom): This is overly specific and inflexible regarding different
+  // test packages. Should validate this a little more loosely.
+  expectLater(pub.nextLine(), equals('Publishing "test_pkg" 1.0.0:'));
+  expectLater(pub.nextLine(), equals("|-- LICENSE"));
+  expectLater(pub.nextLine(), equals("|-- lib"));
+  expectLater(pub.nextLine(), equals("|   '-- test_pkg.dart"));
+  expectLater(pub.nextLine(), equals("'-- pubspec.yaml"));
+  expectLater(pub.nextLine(), equals(""));
+
+  pub.writeLine("y");
+}
+
+
 /// Calls [fn] with appropriately modified arguments to run a pub process. [fn]
 /// should have the same signature as [startProcess], except that the returned
 /// [Future] may have a type other than [Process].