Resolve symlinks when publishing from Git repos.

"git ls-files" doesn't natively resolve symlinks, so this adds some
logic to do so manually. This brings the behavior of uploading from a
Git repo in line with non-Git uploading.

We still don't have a multipart parser in Dart, so we unfortunately have
to way of writing automated tests that the tarballs are created as
expected. I did verify this behavior locally, though.

Closes #1400

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1862833002 .
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 2ba4c3e..e82f4da 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -252,9 +252,14 @@
       files = files.map((file) {
         if (Platform.operatingSystem != 'windows') return "$dir/$file";
         return "$dir\\${file.replaceAll("/", "\\")}";
-      }).where((file) {
-        // Filter out broken symlinks, since git doesn't do so automatically.
-        return fileExists(file);
+      }).expand((file) {
+        if (fileExists(file)) return [file];
+        if (!dirExists(file)) return [];
+
+        // `git ls-files` only returns files, except in the case of a symlink to
+        // a directory. So if we're here, [file] refers to a valid symlink to a
+        // directory.
+        return recursive ? _listSymlinkedDir(file) : [file];
       });
     } else {
       files = listDir(beneath, recursive: recursive, includeDirs: false,
@@ -279,6 +284,38 @@
     }).toList();
   }
 
+  /// List all files recursively beneath [link], which should be a symlink to a
+  /// directory.
+  ///
+  /// This is used by [list] when listing a Git repository, since `git ls-files`
+  /// can't natively follow symlinks.
+  Iterable<String> _listSymlinkedDir(String link) {
+    assert(linkExists(link));
+    assert(dirExists(link));
+    assert(p.isWithin(dir, link));
+
+    var target = new Directory(link).resolveSymbolicLinksSync();
+
+    List<String> targetFiles;
+    if (p.isWithin(dir, target)) {
+      // If the link points within this repo, use git to list the target
+      // location so we respect .gitignore.
+      targetFiles = listFiles(
+          beneath: p.relative(target, from: dir),
+          recursive: true,
+          useGitIgnore: true);
+    } else {
+      // If the link points outside this repo, just use the default listing
+      // logic.
+      targetFiles = listDir(target, recursive: true, includeDirs: false,
+          whitelist: _WHITELISTED_FILES);
+    }
+
+    // Re-write the paths so they're underneath the symlink.
+    return targetFiles.map((targetFile) =>
+        p.join(link, p.relative(targetFile, from: target)));
+  }
+
   /// Returns a debug string for the package.
   String toString() => '$name $version ($dir)';
 }