Login command (#2479)

diff --git a/lib/src/command/login.dart b/lib/src/command/login.dart
new file mode 100644
index 0000000..5af099a
--- /dev/null
+++ b/lib/src/command/login.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import '../command.dart';
+import '../http.dart';
+import '../log.dart' as log;
+import '../oauth2.dart' as oauth2;
+
+/// Handles the `login` pub command.
+class LoginCommand extends PubCommand {
+  @override
+  String get name => 'login';
+  @override
+  String get description => 'Log into pub.dev.';
+  @override
+  String get invocation => 'pub login';
+
+  LoginCommand();
+
+  @override
+  Future<void> runProtected() async {
+    final credentials = oauth2.loadCredentials(cache);
+    if (credentials == null) {
+      final userInfo = await retrieveUserInfo();
+      log.message('You are now logged in as $userInfo');
+    } else {
+      final userInfo = await retrieveUserInfo();
+      if (userInfo == null) {
+        log.warning('Your credentials seems broken.\n'
+            'Run `pub logout` to delete your credentials  and try again.');
+      }
+      log.warning('You are already logged in as $userInfo\n'
+          'Run `pub logout` to log out and try again.');
+    }
+  }
+
+  Future<_UserInfo> retrieveUserInfo() async {
+    return await oauth2.withClient(cache, (client) async {
+      final discovery = await httpClient
+          .get('https://accounts.google.com/.well-known/openid-configuration');
+      final userInfoEndpoint = json.decode(discovery.body)['userinfo_endpoint'];
+      final userInfoRequest = await client.get(userInfoEndpoint);
+      if (userInfoRequest.statusCode != 200) return null;
+      try {
+        final userInfo = json.decode(userInfoRequest.body);
+        return _UserInfo(userInfo['name'], userInfo['email']);
+      } on FormatException {
+        return null;
+      }
+    });
+  }
+}
+
+class _UserInfo {
+  final String name;
+  final String email;
+  _UserInfo(this.name, this.email);
+  @override
+  String toString() => '<$email> "$name"';
+}
diff --git a/lib/src/command/logout.dart b/lib/src/command/logout.dart
index ea5b340..04ffe12 100644
--- a/lib/src/command/logout.dart
+++ b/lib/src/command/logout.dart
@@ -12,7 +12,7 @@
   @override
   String get name => 'logout';
   @override
-  String get description => 'Log out of pub.dartlang.org.';
+  String get description => 'Log out of pub.dev.';
   @override
   bool get takesArguments => false;
 
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 532cb0e..f36d5b4 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -19,6 +19,7 @@
 import 'command/global.dart';
 import 'command/lish.dart';
 import 'command/list_package_dirs.dart';
+import 'command/login.dart';
 import 'command/logout.dart';
 import 'command/outdated.dart';
 import 'command/remove.dart';
@@ -119,6 +120,7 @@
     addCommand(ServeCommand());
     addCommand(UpgradeCommand());
     addCommand(UploaderCommand());
+    addCommand(LoginCommand());
     addCommand(LogoutCommand());
     addCommand(VersionCommand());
   }
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index 6095640..fe0cef1 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -121,7 +121,7 @@
 /// If saved credentials are available, those are used; otherwise, the user is
 /// prompted to authorize the pub client.
 Future<Client> _getClient(SystemCache cache) async {
-  var credentials = _loadCredentials(cache);
+  var credentials = loadCredentials(cache);
   if (credentials == null) return await _authorize();
 
   var client = Client(credentials,
@@ -139,7 +139,7 @@
 ///
 /// If the credentials can't be loaded for any reason, the returned [Future]
 /// completes to `null`.
-Credentials _loadCredentials(SystemCache cache) {
+Credentials loadCredentials(SystemCache cache) {
   log.fine('Loading OAuth2 credentials.');
 
   try {
@@ -200,7 +200,6 @@
 
     log.message('Authorization received, processing...');
     var queryString = request.url.query ?? '';
-
     // Closing the server here is safe, since it will wait until the response
     // is sent to actually shut down.
     server.close();
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index b774e4a..2bc6133 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -12,6 +12,7 @@
 import 'command/get.dart';
 import 'command/global.dart';
 import 'command/lish.dart';
+import 'command/login.dart';
 import 'command/logout.dart';
 import 'command/outdated.dart';
 import 'command/remove.dart';
@@ -56,6 +57,7 @@
     addSubcommand(RunCommand(deprecated: true));
     addSubcommand(UpgradeCommand());
     addSubcommand(UploaderCommand());
+    addSubcommand(LoginCommand());
     addSubcommand(LogoutCommand());
   }
 
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 1713f9c..347f68e 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -127,7 +127,7 @@
       if (!completer.isCompleted) completer.completeError(error, stackTrace);
     });
   } else {
-    runZoned(wrappedCallback, onError: (e, stackTrace) {
+    runZonedGuarded(wrappedCallback, (e, stackTrace) {
       if (stackTrace == null) {
         stackTrace = Chain.current();
       } else {
diff --git a/test/embedding/goldens/helptext.txt b/test/embedding/goldens/helptext.txt
index 227d857..864e30f 100644
--- a/test/embedding/goldens/helptext.txt
+++ b/test/embedding/goldens/helptext.txt
@@ -39,7 +39,8 @@
 [E]   downgrade   Downgrade the current package's dependencies to oldest versions.
 [E]   get         Get the current package's dependencies.
 [E]   global      Work with global packages.
-[E]   logout      Log out of pub.dartlang.org.
+[E]   login       Log into pub.dev.
+[E]   logout      Log out of pub.dev.
 [E]   outdated    Analyze your dependencies to find which ones can be upgraded.
 [E]   publish     Publish the current package to pub.dartlang.org.
 [E]   remove      Removes a dependency from the current package.
@@ -64,7 +65,8 @@
   downgrade   Downgrade the current package's dependencies to oldest versions.
   get         Get the current package's dependencies.
   global      Work with global packages.
-  logout      Log out of pub.dartlang.org.
+  login       Log into pub.dev.
+  logout      Log out of pub.dev.
   outdated    Analyze your dependencies to find which ones can be upgraded.
   publish     Publish the current package to pub.dartlang.org.
   remove      Removes a dependency from the current package.
diff --git a/test/pub_test.dart b/test/pub_test.dart
index 3e52f94..167a260 100644
--- a/test/pub_test.dart
+++ b/test/pub_test.dart
@@ -35,7 +35,8 @@
           downgrade   Downgrade the current package's dependencies to oldest versions.
           get         Get the current package's dependencies.
           global      Work with global packages.
-          logout      Log out of pub.dartlang.org.
+          login       Log into pub.dev.
+          logout      Log out of pub.dev.
           outdated    Analyze your dependencies to find which ones can be upgraded.
           publish     Publish the current package to pub.dartlang.org.
           remove      Removes a dependency from the current package.