From 45a68affb075da95b8f75ea667412c304f909e92 Mon Sep 17 00:00:00 2001
From: "rnystrom@google.com" <rnystrom@google.com>
Date: Fri, 15 Aug 2014 00:05:55 +0000
Subject: [PATCH] Support Git in pub global activate.

BUG=https://code.google.com/p/dart/issues/detail?id=19902
R=nweiz@google.com

Review URL: https://codereview.chromium.org//448933002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@39279 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/command/global_activate.dart          | 15 +++++--
 lib/src/global_packages.dart                  | 33 ++++++++++++--
 lib/src/source/git.dart                       | 40 +++++++++++++----
 .../activate_git_after_hosted_test.dart       | 41 ++++++++++++++++++
 .../activate_hosted_after_git_test.dart       | 43 +++++++++++++++++++
 test/global/activate/bad_version_test.dart    |  2 +-
 .../activate/constraint_with_path_test.dart   |  2 +-
 test/global/activate/git_package_test.dart    | 25 +++++++++++
 .../installs_dependencies_for_git_test.dart   | 33 ++++++++++++++
 .../activate/installs_dependencies_test.dart  |  2 +-
 .../activate/missing_git_repo_test.dart       | 18 ++++++++
 .../activate/missing_package_arg_test.dart    |  2 +-
 .../reactivating_git_upgrades_test.dart       | 34 +++++++++++++++
 .../activate/unexpected_arguments_test.dart   |  2 +-
 test/global/deactivate/git_package_test.dart  | 25 +++++++++++
 test/global/run/runs_git_script_test.dart     | 26 +++++++++++
 16 files changed, 322 insertions(+), 21 deletions(-)
 create mode 100644 test/global/activate/activate_git_after_hosted_test.dart
 create mode 100644 test/global/activate/activate_hosted_after_git_test.dart
 create mode 100644 test/global/activate/git_package_test.dart
 create mode 100644 test/global/activate/installs_dependencies_for_git_test.dart
 create mode 100644 test/global/activate/missing_git_repo_test.dart
 create mode 100644 test/global/activate/reactivating_git_upgrades_test.dart
 create mode 100644 test/global/deactivate/git_package_test.dart
 create mode 100644 test/global/run/runs_git_script_test.dart

diff --git a/lib/src/command/global_activate.dart b/lib/src/command/global_activate.dart
index 98ba40e5..8448af58 100644
--- a/lib/src/command/global_activate.dart
+++ b/lib/src/command/global_activate.dart
@@ -20,7 +20,7 @@ class GlobalActivateCommand extends PubCommand {
     commandParser.addOption("source",
         abbr: "s",
         help: "The source used to find the package.",
-        allowed: ["hosted", "path"],
+        allowed: ["git", "hosted", "path"],
         defaultsTo: "hosted");
   }
 
@@ -41,10 +41,16 @@ class GlobalActivateCommand extends PubCommand {
       usageError("Unexpected $arguments ${toSentence(unexpected)}.");
     }
 
-    var package = readArg("No package to activate given.");
-
     switch (commandOptions["source"]) {
+      case "git":
+        var repo = readArg("No Git repository given.");
+        // TODO(rnystrom): Allow passing in a Git ref too.
+        validateNoExtraArgs();
+        return globals.activateGit(repo);
+
       case "hosted":
+        var package = readArg("No package to activate given.");
+
         // Parse the version constraint, if there is one.
         var constraint = VersionConstraint.any;
         if (args.isNotEmpty) {
@@ -59,8 +65,9 @@ class GlobalActivateCommand extends PubCommand {
         return globals.activateHosted(package, constraint);
 
       case "path":
+        var path = readArg("No package to activate given.");
         validateNoExtraArgs();
-        return globals.activatePath(package);
+        return globals.activatePath(path);
     }
 
     throw "unreachable";
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 0baa6502..f2828ecf 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -16,8 +16,8 @@ import 'log.dart' as log;
 import 'package.dart';
 import 'system_cache.dart';
 import 'solver/version_solver.dart';
-import 'source.dart';
 import 'source/cached.dart';
+import 'source/git.dart';
 import 'source/path.dart';
 import 'utils.dart';
 import 'version.dart';
@@ -60,6 +60,19 @@ class GlobalPackages {
   /// when needed.
   GlobalPackages(this.cache);
 
+  /// Caches the package located in the Git repository [repo] and makes it the
+  /// active global version.
+  Future activateGit(String repo) {
+    var source = cache.sources["git"] as GitSource;
+    return source.getPackageNameFromRepo(repo).then((name) {
+      // Call this just to log what the current active package is, if any.
+      _describeActive(name);
+
+      var id = new PackageId(name, "git", Version.none, repo);
+      return _installInCache(id);
+    });
+  }
+
   /// Finds the latest version of the hosted package with [name] that matches
   /// [constraint] and makes it the active global version.
   Future activateHosted(String name, VersionConstraint constraint) {
@@ -155,7 +168,11 @@ class GlobalPackages {
     writeTextFile(_getLockFilePath(id.name),
         lockFile.serialize(cache.rootDir, cache.sources));
 
-    if (id.source == "path") {
+    if (id.source == "git") {
+      var url = GitSource.urlFromDescription(id.description);
+      log.message('Activated ${log.bold(id.name)} ${id.version} from Git '
+          'repository "$url".');
+    } else if (id.source == "path") {
       var path = PathSource.pathFromDescription(id.description);
       log.message('Activated ${log.bold(id.name)} ${id.version} at path '
           '"$path".');
@@ -174,7 +191,11 @@ class GlobalPackages {
           cache.sources);
       var id = lockFile.packages[package];
 
-      if (id.source == "path") {
+      if (id.source == "git") {
+        var url = GitSource.urlFromDescription(id.description);
+        log.message('Package ${log.bold(id.name)} is currently active from '
+            'Git repository "${url}".');
+      } else if (id.source == "path") {
         var path = PathSource.pathFromDescription(id.description);
         log.message('Package ${log.bold(package)} is currently active at '
             'path "$path".');
@@ -204,7 +225,11 @@ class GlobalPackages {
     deleteEntry(lockFilePath);
 
     if (logDeactivate) {
-      if (id.source == "path") {
+      if (id.source == "git") {
+        var url = GitSource.urlFromDescription(id.description);
+        log.message('Deactivated package ${log.bold(name)} from Git repository '
+            '"$url".');
+      } else if (id.source == "path") {
         var path = PathSource.pathFromDescription(id.description);
         log.message('Deactivated package ${log.bold(name)} at path "$path".');
       } else {
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 9f4c38c3..9696f45b 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -18,12 +18,28 @@ import 'cached.dart';
 
 /// A package source that gets packages from Git repos.
 class GitSource extends CachedSource {
+  /// Given a valid git package description, returns the URL of the repository
+  /// it pulls from.
+  static String urlFromDescription(description) => description["url"];
+
   final name = "git";
 
   /// The paths to the canonical clones of repositories for which "git fetch"
   /// has already been run during this run of pub.
   final _updatedRepos = new Set<String>();
 
+  /// Given a Git repo that contains a pub package, gets the name of the pub
+  /// package.
+  Future<String> getPackageNameFromRepo(String repo) {
+    // Clone the repo to a temp directory.
+    return withTempDir((tempDir) {
+      return _clone(repo, tempDir, shallow: true).then((_) {
+        var pubspec = new Pubspec.load(tempDir, systemCache.sources);
+        return pubspec.name;
+      });
+    });
+  }
+
   /// Since we don't have an easy way to read from a remote Git repo, this
   /// just installs [id] into the system cache, then describes it from there.
   Future<Pubspec> describeUncached(PackageId id) {
@@ -134,10 +150,10 @@ class GitSource extends CachedSource {
     var failures = 0;
 
     var packages = listDir(systemCacheRoot)
-      .where((entry) => dirExists(path.join(entry, ".git")))
-      .map((packageDir) => new Package.load(null, packageDir,
-          systemCache.sources))
-      .toList();
+        .where((entry) => dirExists(path.join(entry, ".git")))
+        .map((packageDir) => new Package.load(null, packageDir,
+            systemCache.sources))
+        .toList();
 
     // Note that there may be multiple packages with the same name and version
     // (pinned to different commits). The sort order of those is unspecified.
@@ -219,16 +235,24 @@ class GitSource extends CachedSource {
   /// Clones the repo at the URI [from] to the path [to] on the local
   /// filesystem.
   ///
-  /// If [mirror] is true, create a bare, mirrored clone. This doesn't check out
-  /// the working tree, but instead makes the repository a local mirror of the
-  /// remote repository. See the manpage for `git clone` for more information.
-  Future _clone(String from, String to, {bool mirror: false}) {
+  /// If [mirror] is true, creates a bare, mirrored clone. This doesn't check
+  /// out the working tree, but instead makes the repository a local mirror of
+  /// the remote repository. See the manpage for `git clone` for more
+  /// information.
+  ///
+  /// If [shallow] is true, creates a shallow clone that contains no history
+  /// for the repository.
+  Future _clone(String from, String to, {bool mirror: false,
+      bool shallow: false}) {
     return syncFuture(() {
       // Git on Windows does not seem to automatically create the destination
       // directory.
       ensureDir(to);
       var args = ["clone", from, to];
+
       if (mirror) args.insert(1, "--mirror");
+      if (shallow) args.insertAll(1, ["--depth", "1"]);
+
       return git.run(args);
     }).then((result) => null);
   }
diff --git a/test/global/activate/activate_git_after_hosted_test.dart b/test/global/activate/activate_git_after_hosted_test.dart
new file mode 100644
index 00000000..a553f5f4
--- /dev/null
+++ b/test/global/activate/activate_git_after_hosted_test.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2014, 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 '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('activating a Git package deactivates the hosted one', () {
+    ensureGit();
+
+    servePackages([
+      packageMap("foo", "1.0.0")
+    ], contents: [
+      d.dir("bin", [
+        d.file("foo.dart", "main(args) => print('hosted');")
+      ])
+    ]);
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('git');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "foo"]);
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: """
+            Package foo is currently active at version 1.0.0.
+            Resolving dependencies...
+            Activated foo 1.0.0 from Git repository "../foo.git".""");
+
+    // Should now run the git one.
+    var pub = pubRun(global: true, args: ["foo"]);
+    pub.stdout.expect("git");
+    pub.shouldExit();
+  });
+}
diff --git a/test/global/activate/activate_hosted_after_git_test.dart b/test/global/activate/activate_hosted_after_git_test.dart
new file mode 100644
index 00000000..9afb49fd
--- /dev/null
+++ b/test/global/activate/activate_hosted_after_git_test.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+
+import '../../../lib/src/io.dart';
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('activating a hosted package deactivates the Git one', () {
+    servePackages([
+      packageMap("foo", "2.0.0")
+    ], contents: [
+      d.dir("bin", [
+        d.file("foo.dart", "main(args) => print('hosted');")
+      ])
+    ]);
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('git');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"]);
+
+    var path = canonicalize(p.join(sandboxDir, "foo"));
+    schedulePub(args: ["global", "activate", "foo"], output: """
+        Package foo is currently active from Git repository "../foo.git".
+        Downloading foo 2.0.0...
+        Resolving dependencies...
+        Activated foo 2.0.0.""");
+
+    // Should now run the hosted one.
+    var pub = pubRun(global: true, args: ["foo"]);
+    pub.stdout.expect("hosted");
+    pub.shouldExit();
+  });
+}
diff --git a/test/global/activate/bad_version_test.dart b/test/global/activate/bad_version_test.dart
index 245fe8b7..73cb2ae3 100644
--- a/test/global/activate/bad_version_test.dart
+++ b/test/global/activate/bad_version_test.dart
@@ -15,7 +15,7 @@ main() {
             Usage: pub global activate <package...>
             -h, --help      Print usage information for this command.
             -s, --source    The source used to find the package.
-                            [hosted (default), path]
+                            [git, hosted (default), path]
 
             Run "pub help" to see global options.
             """,
diff --git a/test/global/activate/constraint_with_path_test.dart b/test/global/activate/constraint_with_path_test.dart
index 68f2ac38..2795ca41 100644
--- a/test/global/activate/constraint_with_path_test.dart
+++ b/test/global/activate/constraint_with_path_test.dart
@@ -15,7 +15,7 @@ main() {
             Usage: pub global activate <package...>
             -h, --help      Print usage information for this command.
             -s, --source    The source used to find the package.
-                            [hosted (default), path]
+                            [git, hosted (default), path]
 
             Run "pub help" to see global options.
             """,
diff --git a/test/global/activate/git_package_test.dart b/test/global/activate/git_package_test.dart
new file mode 100644
index 00000000..4426c74c
--- /dev/null
+++ b/test/global/activate/git_package_test.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2014, 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 '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('activates a package from a Git repo', () {
+    ensureGit();
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: '''
+Resolving dependencies...
+Activated foo 1.0.0 from Git repository "../foo.git".''');
+  });
+}
diff --git a/test/global/activate/installs_dependencies_for_git_test.dart b/test/global/activate/installs_dependencies_for_git_test.dart
new file mode 100644
index 00000000..b50c1f1f
--- /dev/null
+++ b/test/global/activate/installs_dependencies_for_git_test.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('activating a Git package installs its dependencies', () {
+    servePackages([
+      packageMap("bar", "1.0.0", {"baz": "any"}),
+      packageMap("baz", "1.0.0")
+    ]);
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0", deps: {
+        "bar": "any"
+      }),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: allOf([
+      contains("Downloading bar 1.0.0..."),
+      contains("Downloading baz 1.0.0...")
+    ]));
+  });
+}
diff --git a/test/global/activate/installs_dependencies_test.dart b/test/global/activate/installs_dependencies_test.dart
index d7726f65..7918bfdf 100644
--- a/test/global/activate/installs_dependencies_test.dart
+++ b/test/global/activate/installs_dependencies_test.dart
@@ -8,7 +8,7 @@ import '../../test_pub.dart';
 
 main() {
   initConfig();
-  integration('activating a package installs its dependencies too', () {
+  integration('activating a package installs its dependencies', () {
     servePackages([
       packageMap("foo", "1.0.0", {"bar": "any"}),
       packageMap("bar", "1.0.0", {"baz": "any"}),
diff --git a/test/global/activate/missing_git_repo_test.dart b/test/global/activate/missing_git_repo_test.dart
new file mode 100644
index 00000000..6b840b8f
--- /dev/null
+++ b/test/global/activate/missing_git_repo_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('fails if the Git repo does not exist', () {
+    ensureGit();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../nope.git"],
+        error: contains("repository '../nope.git' does not exist"),
+        exitCode: 1);
+  });
+}
diff --git a/test/global/activate/missing_package_arg_test.dart b/test/global/activate/missing_package_arg_test.dart
index 54384b4c..9469de2d 100644
--- a/test/global/activate/missing_package_arg_test.dart
+++ b/test/global/activate/missing_package_arg_test.dart
@@ -15,7 +15,7 @@ main() {
             Usage: pub global activate <package...>
             -h, --help      Print usage information for this command.
             -s, --source    The source used to find the package.
-                            [hosted (default), path]
+                            [git, hosted (default), path]
 
             Run "pub help" to see global options.""",
         exitCode: exit_codes.USAGE);
diff --git a/test/global/activate/reactivating_git_upgrades_test.dart b/test/global/activate/reactivating_git_upgrades_test.dart
new file mode 100644
index 00000000..6a25d23f
--- /dev/null
+++ b/test/global/activate/reactivating_git_upgrades_test.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2014, 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 '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('ignores previously activated git commit',
+        () {
+    ensureGit();
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0")
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: '''
+Resolving dependencies...
+Activated foo 1.0.0 from Git repository "../foo.git".''');
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.1")
+    ]).commit();
+
+    // Activating it again pulls down the latest commit.
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"],
+        output: '''
+Package foo is currently active from Git repository "../foo.git".
+Resolving dependencies...
+Activated foo 1.0.1 from Git repository "../foo.git".''');
+  });
+}
diff --git a/test/global/activate/unexpected_arguments_test.dart b/test/global/activate/unexpected_arguments_test.dart
index 82a94de0..5d519177 100644
--- a/test/global/activate/unexpected_arguments_test.dart
+++ b/test/global/activate/unexpected_arguments_test.dart
@@ -15,7 +15,7 @@ main() {
             Usage: pub global activate <package...>
             -h, --help      Print usage information for this command.
             -s, --source    The source used to find the package.
-                            [hosted (default), path]
+                            [git, hosted (default), path]
 
             Run "pub help" to see global options.""",
         exitCode: exit_codes.USAGE);
diff --git a/test/global/deactivate/git_package_test.dart b/test/global/deactivate/git_package_test.dart
new file mode 100644
index 00000000..cc191913
--- /dev/null
+++ b/test/global/deactivate/git_package_test.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2014, 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 '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('deactivates an active Git package', () {
+    ensureGit();
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"]);
+
+    schedulePub(args: ["global", "deactivate", "foo"],
+        output: 'Deactivated package foo from Git repository "../foo.git".');
+  });
+}
diff --git a/test/global/run/runs_git_script_test.dart b/test/global/run/runs_git_script_test.dart
new file mode 100644
index 00000000..f9c1c5c5
--- /dev/null
+++ b/test/global/run/runs_git_script_test.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2014, 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 '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration('runs a script in a git package', () {
+    ensureGit();
+
+    d.git('foo.git', [
+      d.libPubspec("foo", "1.0.0"),
+      d.dir("bin", [
+        d.file("foo.dart", "main() => print('ok');")
+      ])
+    ]).create();
+
+    schedulePub(args: ["global", "activate", "-sgit", "../foo.git"]);
+
+    var pub = pubRun(global: true, args: ["foo"]);
+    pub.stdout.expect("ok");
+    pub.shouldExit();
+  });
+}
-- 
GitLab