Add a shortcut for PATCH.

Closes #29

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1243783002 .
diff --git a/.gitignore b/.gitignore
index 388eff0..7dbf035 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 .pub/
 build/
 packages
+.packages
 
 # Or the files created by dart2js.
 *.dart.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32912e3..2438fd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.11.3
+
+* Add a `Client.patch` shortcut method and a matching top-level `patch` method.
+
 ## 0.11.2
 
 * Add a `BrowserClient.withCredentials` property.
diff --git a/lib/http.dart b/lib/http.dart
index 5b9b1f9..0fbd11b 100644
--- a/lib/http.dart
+++ b/lib/http.dart
@@ -96,6 +96,30 @@
   _withClient((client) => client.put(url,
       headers: headers, body: body, encoding: encoding));
 
+/// Sends an HTTP PATCH request with the given headers and body to the given
+/// URL, which can be a [Uri] or a [String].
+///
+/// [body] sets the body of the request. It can be a [String], a [List<int>] or
+/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
+/// used as the body of the request. The content-type of the request will
+/// default to "text/plain".
+///
+/// If [body] is a List, it's used as a list of bytes for the body of the
+/// request.
+///
+/// If [body] is a Map, it's encoded as form fields using [encoding]. The
+/// content-type of the request will be set to
+/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
+///
+/// [encoding] defaults to [UTF8].
+///
+/// For more fine-grained control over the request, use [Request] or
+/// [StreamedRequest] instead.
+Future<Response> patch(url, {Map<String, String> headers, body,
+    Encoding encoding}) =>
+  _withClient((client) => client.patch(url,
+      headers: headers, body: body, encoding: encoding));
+
 /// Sends an HTTP DELETE request with the given headers to the given URL, which
 /// can be a [Uri] or a [String].
 ///
diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart
index da38e70..ad85a67 100644
--- a/lib/src/base_client.dart
+++ b/lib/src/base_client.dart
@@ -78,6 +78,28 @@
       Encoding encoding}) =>
     _sendUnstreamed("PUT", url, headers, body, encoding);
 
+  /// Sends an HTTP PATCH request with the given headers and body to the given
+  /// URL, which can be a [Uri] or a [String].
+  ///
+  /// [body] sets the body of the request. It can be a [String], a [List<int>]
+  /// or a [Map<String, String>]. If it's a String, it's encoded using
+  /// [encoding] and used as the body of the request. The content-type of the
+  /// request will default to "text/plain".
+  ///
+  /// If [body] is a List, it's used as a list of bytes for the body of the
+  /// request.
+  ///
+  /// If [body] is a Map, it's encoded as form fields using [encoding]. The
+  /// content-type of the request will be set to
+  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
+  ///
+  /// [encoding] defaults to UTF-8.
+  ///
+  /// For more fine-grained control over the request, use [send] instead.
+  Future<Response> patch(url, {Map<String, String> headers, body,
+      Encoding encoding}) =>
+    _sendUnstreamed("PATCH", url, headers, body, encoding);
+
   /// Sends an HTTP DELETE request with the given headers to the given URL,
   /// which can be a [Uri] or a [String].
   ///
diff --git a/lib/src/client.dart b/lib/src/client.dart
index e29a9e0..eaeccfe 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -18,7 +18,7 @@
 /// The interface for HTTP clients that take care of maintaining persistent
 /// connections across multiple requests to the same server. If you only need to
 /// send a single request, it's usually easier to use [head], [get], [post],
-/// [put], or [delete] instead.
+/// [put], [patch], or [delete] instead.
 ///
 /// When creating an HTTP client class with additional functionality, you must
 /// extend [BaseClient] rather than [Client]. In most cases, you can wrap
@@ -89,6 +89,27 @@
   Future<Response> put(url, {Map<String, String> headers, body,
       Encoding encoding});
 
+  /// Sends an HTTP PATCH request with the given headers and body to the given
+  /// URL, which can be a [Uri] or a [String].
+  ///
+  /// [body] sets the body of the request. It can be a [String], a [List<int>]
+  /// or a [Map<String, String>]. If it's a String, it's encoded using
+  /// [encoding] and used as the body of the request. The content-type of the
+  /// request will default to "text/plain".
+  ///
+  /// If [body] is a List, it's used as a list of bytes for the body of the
+  /// request.
+  ///
+  /// If [body] is a Map, it's encoded as form fields using [encoding]. The
+  /// content-type of the request will be set to
+  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
+  ///
+  /// [encoding] defaults to [UTF8].
+  ///
+  /// For more fine-grained control over the request, use [send] instead.
+  Future<Response> patch(url, {Map<String, String> headers, body,
+      Encoding encoding});
+
   /// Sends an HTTP DELETE request with the given headers to the given URL,
   /// which can be a [Uri] or a [String].
   ///
diff --git a/pubspec.yaml b/pubspec.yaml
index 6c8cd4a..db04d4f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: http
-version: 0.11.2
+version: 0.11.3
 author: "Dart Team <misc@dartlang.org>"
 homepage: https://github.com/dart-lang/http
 description: A composable, Future-based API for making HTTP requests.
diff --git a/test/io/http_test.dart b/test/io/http_test.dart
index 9ba5924..0725c61 100644
--- a/test/io/http_test.dart
+++ b/test/io/http_test.dart
@@ -253,6 +253,110 @@
       }), completes);
     });
 
+    test('patch', () {
+      expect(startServer().then((_) {
+        expect(http.patch(serverUrl, headers: {
+          'X-Random-Header': 'Value',
+          'X-Other-Header': 'Other Value',
+          'Content-Type': 'text/plain',
+          'User-Agent': 'Dart'
+        }).then((response) {
+          expect(response.statusCode, equals(200));
+          expect(response.body, parse(equals({
+            'method': 'PATCH',
+            'path': '/',
+            'headers': {
+              'accept-encoding': ['gzip'],
+              'content-length': ['0'],
+              'content-type': ['text/plain'],
+              'user-agent': ['Dart'],
+              'x-random-header': ['Value'],
+              'x-other-header': ['Other Value']
+            }
+          })));
+        }), completes);
+      }), completes);
+    });
+
+    test('patch with string', () {
+      expect(startServer().then((_) {
+        expect(http.patch(serverUrl, headers: {
+          'X-Random-Header': 'Value',
+          'X-Other-Header': 'Other Value',
+          'User-Agent': 'Dart'
+        }, body: 'request body').then((response) {
+          expect(response.statusCode, equals(200));
+          expect(response.body, parse(equals({
+            'method': 'PATCH',
+            'path': '/',
+            'headers': {
+              'content-type': ['text/plain; charset=utf-8'],
+              'content-length': ['12'],
+              'accept-encoding': ['gzip'],
+              'user-agent': ['Dart'],
+              'x-random-header': ['Value'],
+              'x-other-header': ['Other Value']
+            },
+            'body': 'request body'
+          })));
+        }), completes);
+      }), completes);
+    });
+
+    test('patch with bytes', () {
+      expect(startServer().then((_) {
+        expect(http.patch(serverUrl, headers: {
+          'X-Random-Header': 'Value',
+          'X-Other-Header': 'Other Value',
+          'User-Agent': 'Dart'
+        }, body: [104, 101, 108, 108, 111]).then((response) {
+          expect(response.statusCode, equals(200));
+          expect(response.body, parse(equals({
+            'method': 'PATCH',
+            'path': '/',
+            'headers': {
+              'content-length': ['5'],
+              'accept-encoding': ['gzip'],
+              'user-agent': ['Dart'],
+              'x-random-header': ['Value'],
+              'x-other-header': ['Other Value']
+            },
+            'body': [104, 101, 108, 108, 111]
+          })));
+        }), completes);
+      }), completes);
+    });
+
+    test('patch with fields', () {
+      expect(startServer().then((_) {
+        expect(http.patch(serverUrl, headers: {
+          'X-Random-Header': 'Value',
+          'X-Other-Header': 'Other Value',
+          'User-Agent': 'Dart'
+        }, body: {
+          'some-field': 'value',
+          'other-field': 'other value'
+        }).then((response) {
+          expect(response.statusCode, equals(200));
+          expect(response.body, parse(equals({
+            'method': 'PATCH',
+            'path': '/',
+            'headers': {
+              'content-type': [
+                'application/x-www-form-urlencoded; charset=utf-8'
+              ],
+              'content-length': ['40'],
+              'accept-encoding': ['gzip'],
+              'user-agent': ['Dart'],
+              'x-random-header': ['Value'],
+              'x-other-header': ['Other Value']
+            },
+            'body': 'some-field=value&other-field=other+value'
+          })));
+        }), completes);
+      }), completes);
+    });
+
     test('delete', () {
       expect(startServer().then((_) {
         expect(http.delete(serverUrl, headers: {