diff --git a/lib/src/git.dart b/lib/src/git.dart
index 0633169..5fa7536 100644
--- a/lib/src/git.dart
+++ b/lib/src/git.dart
@@ -5,6 +5,8 @@
 /// Helper functionality for invoking Git.
 import 'dart:async';
 
+import 'package:path/path.dart' as p;
+
 import 'exceptions.dart';
 import 'io.dart';
 import 'log.dart' as log;
@@ -103,6 +105,22 @@
 
 String? _commandCache;
 
+/// Returns the root of the git repo [dir] belongs to. Returns `null` if not
+/// in a git repo or git is not installed.
+String? repoRoot(String dir) {
+  if (isInstalled) {
+    try {
+      return p.normalize(
+        runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first,
+      );
+    } on GitException {
+      // Not in a git folder.
+      return null;
+    }
+  }
+  return null;
+}
+
 /// Checks whether [command] is the Git command for this computer.
 bool _tryGitCommand(String command) {
   // If "git --version" prints something familiar, git is working.
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 6a3b0ff..1aeb9dc 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -224,16 +224,7 @@
     // An in-memory package has no files.
     if (dir == null) return [];
 
-    var root = dir;
-    if (git.isInstalled) {
-      try {
-        root = p.normalize(
-          git.runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first,
-        );
-      } on git.GitException {
-        // Not in a git folder.
-      }
-    }
+    var root = git.repoRoot(dir) ?? dir;
     beneath = p
         .toUri(p.normalize(p.relative(p.join(dir, beneath ?? '.'), from: root)))
         .path;
@@ -279,7 +270,7 @@
             : (fileExists(gitIgnore) ? gitIgnore : null);
 
         final rules = [
-          if (dir == '.') ..._basicIgnoreRules,
+          if (dir == beneath) ..._basicIgnoreRules,
           if (ignoreFile != null) readTextFile(ignoreFile),
         ];
         return rules.isEmpty
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
index fad20f7..0493dcc 100644
--- a/lib/src/package_config.dart
+++ b/lib/src/package_config.dart
@@ -2,10 +2,6 @@
 // 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.
 
-// @dart=2.10
-
-import 'package:meta/meta.dart';
-
 import 'package:pub_semver/pub_semver.dart';
 
 import 'language_version.dart';
@@ -22,38 +18,36 @@
 
   /// Date-time the `.dart_tool/package_config.json` file was generated.
   ///
-  /// This property is **optional** and may be `null` if not given.
-  DateTime generated;
+  /// `null` if not given.
+  DateTime? generated;
 
   /// Tool that generated the `.dart_tool/package_config.json` file.
   ///
   /// For `pub` this is always `'pub'`.
   ///
-  /// This property is **optional** and may be `null` if not given.
-  String generator;
+  /// `null` if not given.
+  String? generator;
 
   /// Version of the tool that generated the `.dart_tool/package_config.json`
   /// file.
   ///
   /// For `pub` this is the Dart SDK version from which `pub get` was called.
   ///
-  /// This property is **optional** and may be `null` if not given.
-  Version generatorVersion;
+  /// `null` if not given.
+  Version? generatorVersion;
 
   /// Additional properties not in the specification for the
   /// `.dart_tool/package_config.json` file.
   Map<String, dynamic> additionalProperties;
 
   PackageConfig({
-    @required this.configVersion,
-    @required this.packages,
+    required this.configVersion,
+    required this.packages,
     this.generated,
     this.generator,
     this.generatorVersion,
-    this.additionalProperties,
-  }) {
-    additionalProperties ??= {};
-  }
+    Map<String, dynamic>? additionalProperties,
+  }) : additionalProperties = additionalProperties ?? {};
 
   /// Create [PackageConfig] from JSON [data].
   ///
@@ -63,7 +57,7 @@
     if (data is! Map<String, dynamic>) {
       throw FormatException('package_config.json must be a JSON object');
     }
-    final root = data as Map<String, dynamic>;
+    final root = data;
 
     void _throw(String property, String mustBe) => throw FormatException(
         '"$property" in .dart_tool/package_config.json $mustBe');
@@ -87,7 +81,7 @@
     }
 
     // Read the 'generated' property
-    DateTime generated;
+    DateTime? generated;
     final generatedRaw = root['generated'];
     if (generatedRaw != null) {
       if (generatedRaw is! String) {
@@ -104,7 +98,7 @@
     }
 
     // Read the 'generatorVersion' property
-    Version generatorVersion;
+    Version? generatorVersion;
     final generatorVersionRaw = root['generatorVersion'];
     if (generatorVersionRaw != null) {
       if (generatorVersionRaw is! String) {
@@ -134,13 +128,13 @@
   }
 
   /// Convert to JSON structure.
-  Map<String, Object> toJson() => {
+  Map<String, Object?> toJson() => {
         'configVersion': configVersion,
         'packages': packages.map((p) => p.toJson()).toList(),
-        'generated': generated?.toUtc()?.toIso8601String(),
+        'generated': generated?.toUtc().toIso8601String(),
         'generator': generator,
         'generatorVersion': generatorVersion?.toString(),
-      }..addAll(additionalProperties ?? {});
+      }..addAll(additionalProperties);
 }
 
 class PackageConfigEntry {
@@ -158,8 +152,8 @@
   /// Import statements in Dart programs are resolved relative to this folder.
   /// This must be in the sub-tree under [rootUri].
   ///
-  /// This property is **optional** and may be `null` if not given.
-  Uri packageUri;
+  /// `null` if not given.
+  Uri? packageUri;
 
   /// Language version used by package.
   ///
@@ -167,16 +161,16 @@
   /// comment. This is derived from the lower-bound on the Dart SDK requirement
   /// in the `pubspec.yaml` for the given package.
   ///
-  /// This property is **optional** and may be `null` if not given.
-  LanguageVersion languageVersion;
+  /// `null` if not given.
+  LanguageVersion? languageVersion;
 
   /// Additional properties not in the specification for the
   /// `.dart_tool/package_config.json` file.
-  Map<String, dynamic> additionalProperties;
+  Map<String, dynamic>? additionalProperties;
 
   PackageConfigEntry({
-    @required this.name,
-    @required this.rootUri,
+    required this.name,
+    required this.rootUri,
     this.packageUri,
     this.languageVersion,
     this.additionalProperties,
@@ -193,9 +187,9 @@
       throw FormatException(
           'packages[] entries in package_config.json must be JSON objects');
     }
-    final root = data as Map<String, dynamic>;
+    final root = data;
 
-    void _throw(String property, String mustBe) => throw FormatException(
+    Never _throw(String property, String mustBe) => throw FormatException(
         '"packages[].$property" in .dart_tool/package_config.json $mustBe');
 
     final name = root['name'];
@@ -203,7 +197,7 @@
       _throw('name', 'must be a string');
     }
 
-    Uri rootUri;
+    final Uri rootUri;
     final rootUriRaw = root['rootUri'];
     if (rootUriRaw is! String) {
       _throw('rootUri', 'must be a string');
@@ -214,7 +208,7 @@
       _throw('rootUri', 'must be a URI');
     }
 
-    Uri packageUri;
+    Uri? packageUri;
     var packageUriRaw = root['packageUri'];
     if (packageUriRaw != null) {
       if (packageUriRaw is! String) {
@@ -230,7 +224,7 @@
       }
     }
 
-    LanguageVersion languageVersion;
+    LanguageVersion? languageVersion;
     final languageVersionRaw = root['languageVersion'];
     if (languageVersionRaw != null) {
       if (languageVersionRaw is! String) {
@@ -252,7 +246,7 @@
   }
 
   /// Convert to JSON structure.
-  Map<String, Object> toJson() => {
+  Map<String, Object?> toJson() => {
         'name': name,
         'rootUri': rootUri.toString(),
         if (packageUri != null) 'packageUri': packageUri?.toString(),
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 5a3d3c0..b825cb0 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -19,17 +19,12 @@
 import 'language_version.dart';
 import 'log.dart';
 import 'package_name.dart';
+import 'pubspec_parse.dart';
 import 'sdk.dart';
 import 'source_registry.dart';
 import 'utils.dart';
 
-/// A regular expression matching allowed package names.
-///
-/// This allows dot-separated valid Dart identifiers. The dots are there for
-/// compatibility with Google's internal Dart packages, but they may not be used
-/// when publishing a package to pub.dartlang.org.
-final _packageName =
-    RegExp('^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$');
+export 'pubspec_parse.dart' hide PubspecBase;
 
 /// The default SDK upper bound constraint for packages that don't declare one.
 ///
@@ -72,7 +67,7 @@
 /// accessed. This allows a partially-invalid pubspec to be used if only the
 /// valid portions are relevant. To get a list of all errors in the pubspec, use
 /// [allErrors].
-class Pubspec {
+class Pubspec extends PubspecBase {
   // If a new lazily-initialized field is added to this class and the
   // initialization can throw a [PubspecException], that error should also be
   // exposed through [allErrors].
@@ -90,76 +85,6 @@
   /// is unknown.
   Uri get _location => fields.span.sourceUrl;
 
-  /// All pubspec fields.
-  ///
-  /// This includes the fields from which other properties are derived.
-  final YamlMap fields;
-
-  /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this
-  /// pubspec.
-  final bool _includeDefaultSdkConstraint;
-
-  /// Whether or not the SDK version was overridden from <2.0.0 to
-  /// <2.0.0-dev.infinity.
-  bool get dartSdkWasOverridden => _dartSdkWasOverridden;
-  bool _dartSdkWasOverridden = false;
-
-  /// The package's name.
-  String get name {
-    if (_name != null) return _name;
-
-    var name = fields['name'];
-    if (name == null) {
-      throw PubspecException('Missing the required "name" field.', fields.span);
-    } else if (name is! String) {
-      throw PubspecException(
-          '"name" field must be a string.', fields.nodes['name'].span);
-    } else if (!_packageName.hasMatch(name)) {
-      throw PubspecException('"name" field must be a valid Dart identifier.',
-          fields.nodes['name'].span);
-    } else if (reservedWords.contains(name)) {
-      throw PubspecException('"name" field may not be a Dart reserved word.',
-          fields.nodes['name'].span);
-    }
-
-    _name = name;
-    return _name;
-  }
-
-  String _name;
-
-  /// The package's version.
-  Version get version {
-    if (_version != null) return _version;
-
-    var version = fields['version'];
-    if (version == null) {
-      _version = Version.none;
-      return _version;
-    }
-
-    var span = fields.nodes['version'].span;
-    if (version is num) {
-      var fixed = '$version.0';
-      if (version is int) {
-        fixed = '$fixed.0';
-      }
-      _error(
-          '"version" field must have three numeric components: major, '
-          'minor, and patch. Instead of "$version", consider "$fixed".',
-          span);
-    }
-    if (version is! String) {
-      _error('"version" field must be a string.', span);
-    }
-
-    _version = _wrapFormatException(
-        'version number', span, () => Version.parse(version));
-    return _version;
-  }
-
-  Version _version;
-
   /// The additional packages this package depends on.
   Map<String, PackageRange> get dependencies {
     if (_dependencies != null) return _dependencies;
@@ -251,6 +176,15 @@
 
   Map<String, VersionConstraint> _sdkConstraints;
 
+  /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this
+  /// pubspec.
+  final bool _includeDefaultSdkConstraint;
+
+  /// Whether or not the SDK version was overridden from <2.0.0 to
+  /// <2.0.0-dev.infinity.
+  bool get dartSdkWasOverridden => _dartSdkWasOverridden;
+  bool _dartSdkWasOverridden = false;
+
   /// The original Dart SDK constraint as written in the pubspec.
   ///
   /// If [dartSdkWasOverridden] is `false`, this will be identical to
@@ -353,137 +287,6 @@
     return constraints;
   }
 
-  /// The URL of the server that the package should default to being published
-  /// to, "none" if the package should not be published, or `null` if it should
-  /// be published to the default server.
-  ///
-  /// If this does return a URL string, it will be a valid parseable URL.
-  String get publishTo {
-    if (_parsedPublishTo) return _publishTo;
-
-    var publishTo = fields['publish_to'];
-    if (publishTo != null) {
-      var span = fields.nodes['publish_to'].span;
-
-      if (publishTo is! String) {
-        _error('"publish_to" field must be a string.', span);
-      }
-
-      // It must be "none" or a valid URL.
-      if (publishTo != 'none') {
-        _wrapFormatException('"publish_to" field', span, () {
-          var url = Uri.parse(publishTo);
-          if (url.scheme.isEmpty) {
-            throw FormatException('must be an absolute URL.');
-          }
-        });
-      }
-    }
-
-    _parsedPublishTo = true;
-    _publishTo = publishTo;
-    return _publishTo;
-  }
-
-  bool _parsedPublishTo = false;
-  String _publishTo;
-
-  /// The list of patterns covering _false-positive secrets_ in the package.
-  ///
-  /// This is a list of git-ignore style patterns for files that should be
-  /// ignored when trying to detect possible leaks of secrets during
-  /// package publication.
-  List<String> get falseSecrets {
-    if (_falseSecrets == null) {
-      final falseSecrets = <String>[];
-
-      // Throws a [PubspecException]
-      void _falseSecretsError(SourceSpan span) => _error(
-            '"false_secrets" field must be a list of git-ignore style patterns',
-            span,
-          );
-
-      final falseSecretsNode = fields.nodes['false_secrets'];
-      if (falseSecretsNode != null) {
-        if (falseSecretsNode is YamlList) {
-          for (final node in falseSecretsNode.nodes) {
-            final value = node.value;
-            if (value is! String) {
-              _falseSecretsError(node.span);
-            }
-            falseSecrets.add(value);
-          }
-        } else {
-          _falseSecretsError(falseSecretsNode.span);
-        }
-      }
-
-      _falseSecrets = List.unmodifiable(falseSecrets);
-    }
-    return _falseSecrets;
-  }
-
-  List<String> _falseSecrets;
-
-  /// The executables that should be placed on the user's PATH when this
-  /// package is globally activated.
-  ///
-  /// It is a map of strings to string. Each key is the name of the command
-  /// that will be placed on the user's PATH. The value is the name of the
-  /// .dart script (without extension) in the package's `bin` directory that
-  /// should be run for that command. Both key and value must be "simple"
-  /// strings: alphanumerics, underscores and hypens only. If a value is
-  /// omitted, it is inferred to use the same name as the key.
-  Map<String, String> get executables {
-    if (_executables != null) return _executables;
-
-    _executables = {};
-    var yaml = fields['executables'];
-    if (yaml == null) return _executables;
-
-    if (yaml is! Map) {
-      _error('"executables" field must be a map.',
-          fields.nodes['executables'].span);
-    }
-
-    yaml.nodes.forEach((key, value) {
-      if (key.value is! String) {
-        _error('"executables" keys must be strings.', key.span);
-      }
-
-      final keyPattern = RegExp(r'^[a-zA-Z0-9_-]+$');
-      if (!keyPattern.hasMatch(key.value)) {
-        _error(
-            '"executables" keys may only contain letters, '
-            'numbers, hyphens and underscores.',
-            key.span);
-      }
-
-      if (value.value == null) {
-        value = key;
-      } else if (value.value is! String) {
-        _error('"executables" values must be strings or null.', value.span);
-      }
-
-      final valuePattern = RegExp(r'[/\\]');
-      if (valuePattern.hasMatch(value.value)) {
-        _error('"executables" values may not contain path separators.',
-            value.span);
-      }
-
-      _executables[key.value] = value.value;
-    });
-
-    return _executables;
-  }
-
-  Map<String, String> _executables;
-
-  /// Whether the package is private and cannot be published.
-  ///
-  /// This is specified in the pubspec by setting "publish_to" to "none".
-  bool get isPrivate => publishTo == 'none';
-
   /// Whether or not the pubspec has no contents.
   bool get isEmpty =>
       name == null && version == Version.none && dependencies.isEmpty;
@@ -513,7 +316,7 @@
         expectedName: expectedName, location: pubspecUri);
   }
 
-  Pubspec(this._name,
+  Pubspec(String name,
       {Version version,
       Iterable<PackageRange> dependencies,
       Iterable<PackageRange> devDependencies,
@@ -521,8 +324,7 @@
       Map fields,
       SourceRegistry sources,
       Map<String, VersionConstraint> sdkConstraints})
-      : _version = version,
-        _dependencies = dependencies == null
+      : _dependencies = dependencies == null
             ? null
             : Map.fromIterable(dependencies, key: (range) => range.name),
         _devDependencies = devDependencies == null
@@ -534,18 +336,23 @@
         _sdkConstraints = sdkConstraints ??
             UnmodifiableMapView({'dart': VersionConstraint.any}),
         _includeDefaultSdkConstraint = false,
-        fields = fields == null ? YamlMap() : YamlMap.wrap(fields),
-        _sources = sources;
+        _sources = sources,
+        super(
+          fields == null ? YamlMap() : YamlMap.wrap(fields),
+          name: name,
+          version: version,
+        );
 
   Pubspec.empty()
       : _sources = null,
-        _name = null,
-        _version = Version.none,
         _dependencies = {},
         _devDependencies = {},
         _sdkConstraints = {'dart': VersionConstraint.any},
         _includeDefaultSdkConstraint = false,
-        fields = YamlMap();
+        super(
+          YamlMap(),
+          version: Version.none,
+        );
 
   /// Returns a Pubspec object for an already-parsed map representing its
   /// contents.
@@ -556,10 +363,10 @@
   /// [location] is the location from which this pubspec was loaded.
   Pubspec.fromMap(Map fields, this._sources,
       {String expectedName, Uri location})
-      : fields = fields is YamlMap
+      : _includeDefaultSdkConstraint = true,
+        super(fields is YamlMap
             ? fields
-            : YamlMap.wrap(fields, sourceUrl: location),
-        _includeDefaultSdkConstraint = true {
+            : YamlMap.wrap(fields, sourceUrl: location)) {
     // If [expectedName] is passed, ensure that the actual 'name' field exists
     // and matches the expectation.
     if (expectedName == null) return;
@@ -778,7 +585,7 @@
     var name = node.value;
     if (name is! String) {
       _error('A feature name must be a string.', node.span);
-    } else if (!_packageName.hasMatch(name)) {
+    } else if (!packageNameRegExp.hasMatch(name)) {
       _error('A feature name must be a valid Dart identifier.', node.span);
     }
 
@@ -840,14 +647,6 @@
   }
 }
 
-/// An exception thrown when parsing a pubspec.
-///
-/// These exceptions are often thrown lazily while accessing pubspec properties.
-class PubspecException extends SourceSpanFormatException
-    implements ApplicationException {
-  PubspecException(String message, SourceSpan span) : super(message, span);
-}
-
 /// Returns whether [uri] is a file URI.
 ///
 /// This is slightly more complicated than just checking if the scheme is
diff --git a/lib/src/pubspec_parse.dart b/lib/src/pubspec_parse.dart
new file mode 100644
index 0000000..dccc438
--- /dev/null
+++ b/lib/src/pubspec_parse.dart
@@ -0,0 +1,263 @@
+// Copyright (c) 2021, 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:meta/meta.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'exceptions.dart' show ApplicationException;
+import 'utils.dart' show identifierRegExp, reservedWords;
+
+/// A regular expression matching allowed package names.
+///
+/// This allows dot-separated valid Dart identifiers. The dots are there for
+/// compatibility with Google's internal Dart packages, but they may not be used
+/// when publishing a package to pub.dartlang.org.
+final packageNameRegExp =
+    RegExp('^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$');
+
+/// Helper class for pubspec parsing to:
+/// - extract the fields and methods that are reusable outside of `pub` client, and
+/// - help null-safety migration a bit.
+///
+/// This class should be eventually extracted to a separate library, or re-merged with `Pubspec`.
+abstract class PubspecBase {
+  /// All pubspec fields.
+  ///
+  /// This includes the fields from which other properties are derived.
+  final YamlMap fields;
+
+  PubspecBase(
+    this.fields, {
+    String? name,
+    Version? version,
+  })  : _name = name,
+        _version = version;
+
+  /// The package's name.
+  String get name {
+    if (_name != null) return _name!;
+
+    final name = fields['name'];
+    if (name == null) {
+      throw PubspecException('Missing the required "name" field.', fields.span);
+    } else if (name is! String) {
+      throw PubspecException(
+          '"name" field must be a string.', fields.nodes['name']?.span);
+    } else if (!packageNameRegExp.hasMatch(name)) {
+      throw PubspecException('"name" field must be a valid Dart identifier.',
+          fields.nodes['name']?.span);
+    } else if (reservedWords.contains(name)) {
+      throw PubspecException('"name" field may not be a Dart reserved word.',
+          fields.nodes['name']?.span);
+    }
+
+    _name = name;
+    return _name!;
+  }
+
+  String? _name;
+
+  /// The package's version.
+  Version get version {
+    if (_version != null) return _version!;
+
+    final version = fields['version'];
+    if (version == null) {
+      _version = Version.none;
+      return _version!;
+    }
+
+    final span = fields.nodes['version']?.span;
+    if (version is num) {
+      var fixed = '$version.0';
+      if (version is int) {
+        fixed = '$fixed.0';
+      }
+      _error(
+          '"version" field must have three numeric components: major, '
+          'minor, and patch. Instead of "$version", consider "$fixed".',
+          span);
+    }
+    if (version is! String) {
+      _error('"version" field must be a string.', span);
+    }
+
+    _version = _wrapFormatException(
+        'version number', span, () => Version.parse(version));
+    return _version!;
+  }
+
+  Version? _version;
+
+  /// The URL of the server that the package should default to being published
+  /// to, "none" if the package should not be published, or `null` if it should
+  /// be published to the default server.
+  ///
+  /// If this does return a URL string, it will be a valid parseable URL.
+  String? get publishTo {
+    if (_parsedPublishTo) return _publishTo;
+
+    final publishTo = fields['publish_to'];
+    if (publishTo != null) {
+      final span = fields.nodes['publish_to']?.span;
+
+      if (publishTo is! String) {
+        _error('"publish_to" field must be a string.', span);
+      }
+
+      // It must be "none" or a valid URL.
+      if (publishTo != 'none') {
+        _wrapFormatException('"publish_to" field', span, () {
+          final url = Uri.parse(publishTo);
+          if (url.scheme.isEmpty) {
+            throw FormatException('must be an absolute URL.');
+          }
+        });
+      }
+    }
+
+    _parsedPublishTo = true;
+    _publishTo = publishTo;
+    return _publishTo;
+  }
+
+  bool _parsedPublishTo = false;
+  String? _publishTo;
+
+  /// The list of patterns covering _false-positive secrets_ in the package.
+  ///
+  /// This is a list of git-ignore style patterns for files that should be
+  /// ignored when trying to detect possible leaks of secrets during
+  /// package publication.
+  List<String> get falseSecrets {
+    if (_falseSecrets == null) {
+      final falseSecrets = <String>[];
+
+      // Throws a [PubspecException]
+      void _falseSecretsError(SourceSpan span) => _error(
+            '"false_secrets" field must be a list of git-ignore style patterns',
+            span,
+          );
+
+      final falseSecretsNode = fields.nodes['false_secrets'];
+      if (falseSecretsNode != null) {
+        if (falseSecretsNode is YamlList) {
+          for (final node in falseSecretsNode.nodes) {
+            final value = node.value;
+            if (value is! String) {
+              _falseSecretsError(node.span);
+            }
+            falseSecrets.add(value);
+          }
+        } else {
+          _falseSecretsError(falseSecretsNode.span);
+        }
+      }
+
+      _falseSecrets = List.unmodifiable(falseSecrets);
+    }
+    return _falseSecrets!;
+  }
+
+  List<String>? _falseSecrets;
+
+  /// The executables that should be placed on the user's PATH when this
+  /// package is globally activated.
+  ///
+  /// It is a map of strings to string. Each key is the name of the command
+  /// that will be placed on the user's PATH. The value is the name of the
+  /// .dart script (without extension) in the package's `bin` directory that
+  /// should be run for that command. Both key and value must be "simple"
+  /// strings: alphanumerics, underscores and hypens only. If a value is
+  /// omitted, it is inferred to use the same name as the key.
+  Map<String, String> get executables {
+    if (_executables != null) return _executables!;
+
+    _executables = {};
+    var yaml = fields['executables'];
+    if (yaml == null) return _executables!;
+
+    if (yaml is! Map) {
+      _error('"executables" field must be a map.',
+          fields.nodes['executables']?.span);
+    }
+
+    yaml.nodes.forEach((key, value) {
+      if (key.value is! String) {
+        _error('"executables" keys must be strings.', key.span);
+      }
+
+      final keyPattern = RegExp(r'^[a-zA-Z0-9_-]+$');
+      if (!keyPattern.hasMatch(key.value)) {
+        _error(
+            '"executables" keys may only contain letters, '
+            'numbers, hyphens and underscores.',
+            key.span);
+      }
+
+      if (value.value == null) {
+        value = key;
+      } else if (value.value is! String) {
+        _error('"executables" values must be strings or null.', value.span);
+      }
+
+      final valuePattern = RegExp(r'[/\\]');
+      if (valuePattern.hasMatch(value.value)) {
+        _error('"executables" values may not contain path separators.',
+            value.span);
+      }
+
+      _executables![key.value] = value.value;
+    });
+
+    return _executables!;
+  }
+
+  Map<String, String>? _executables;
+
+  /// Whether the package is private and cannot be published.
+  ///
+  /// This is specified in the pubspec by setting "publish_to" to "none".
+  bool get isPrivate => publishTo == 'none';
+
+  /// Runs [fn] and wraps any [FormatException] it throws in a
+  /// [PubspecException].
+  ///
+  /// [description] should be a noun phrase that describes whatever's being
+  /// parsed or processed by [fn]. [span] should be the location of whatever's
+  /// being processed within the pubspec.
+  ///
+  /// If [targetPackage] is provided, the value is used to describe the
+  /// dependency that caused the problem.
+  T _wrapFormatException<T>(
+      String description, SourceSpan? span, T Function() fn,
+      {String? targetPackage}) {
+    try {
+      return fn();
+    } on FormatException catch (e) {
+      var msg = 'Invalid $description';
+      if (targetPackage != null) {
+        msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency';
+      }
+      msg = '$msg: ${e.message}';
+      throw PubspecException(msg, span);
+    }
+  }
+
+  /// Throws a [PubspecException] with the given message.
+  @alwaysThrows
+  void _error(String message, SourceSpan? span) {
+    throw PubspecException(message, span);
+  }
+}
+
+/// An exception thrown when parsing a pubspec.
+///
+/// These exceptions are often thrown lazily while accessing pubspec properties.
+class PubspecException extends SourceSpanFormatException
+    implements ApplicationException {
+  PubspecException(String message, SourceSpan? span) : super(message, span);
+}
diff --git a/lib/src/rate_limited_scheduler.dart b/lib/src/rate_limited_scheduler.dart
index 6e62310..daeb76f 100644
--- a/lib/src/rate_limited_scheduler.dart
+++ b/lib/src/rate_limited_scheduler.dart
@@ -2,12 +2,9 @@
 // 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.
 
-// @dart=2.10
-
 import 'dart:async';
 import 'dart:collection';
 
-import 'package:meta/meta.dart';
 import 'package:pedantic/pedantic.dart';
 import 'package:pool/pool.dart';
 
@@ -65,7 +62,7 @@
   final Set<J> _started = {};
 
   RateLimitedScheduler(Future<V> Function(J) runJob,
-      {@required int maxConcurrentOperations})
+      {required int maxConcurrentOperations})
       : _runJob = runJob,
         _pool = Pool(maxConcurrentOperations);
 
@@ -77,7 +74,7 @@
       return;
     }
     final task = _queue.removeFirst();
-    final completer = _cache[task.jobId];
+    final completer = _cache[task.jobId]!;
 
     if (!_started.add(task.jobId)) {
       return;
@@ -140,7 +137,7 @@
 
   /// Returns the result of running [jobId] if that is already done.
   /// Otherwise returns `null`.
-  V peek(J jobId) => _results[jobId];
+  V? peek(J jobId) => _results[jobId];
 }
 
 class _Task<J> {
diff --git a/lib/src/solver/reformat_ranges.dart b/lib/src/solver/reformat_ranges.dart
index bdb64b8..d40bd6f 100644
--- a/lib/src/solver/reformat_ranges.dart
+++ b/lib/src/solver/reformat_ranges.dart
@@ -4,6 +4,7 @@
 
 // @dart=2.10
 
+import 'package:meta/meta.dart';
 import 'package:pub_semver/pub_semver.dart';
 
 import '../package_name.dart';
@@ -48,7 +49,7 @@
   var range = term.package.constraint as VersionRange;
 
   var min = _reformatMin(versions, range);
-  var tuple = _reformatMax(versions, range);
+  var tuple = reformatMax(versions, range);
   var max = tuple?.first;
   var includeMax = tuple?.last;
 
@@ -84,10 +85,16 @@
 
 /// Returns the new maximum version to use for [range] and whether that maximum
 /// is inclusive, or `null` if it doesn't need to be reformatted.
-Pair<Version, bool> _reformatMax(List<PackageId> versions, VersionRange range) {
+@visibleForTesting
+Pair<Version, bool> reformatMax(List<PackageId> versions, VersionRange range) {
+  // This corresponds to the logic in the constructor of [VersionRange] with
+  // `alwaysIncludeMaxPreRelease = false` for discovering when a max-bound
+  // should not include prereleases.
+
   if (range.max == null) return null;
   if (range.includeMax) return null;
   if (range.max.isPreRelease) return null;
+  if (range.max.build.isNotEmpty) return null;
   if (range.min != null &&
       range.min.isPreRelease &&
       equalsIgnoringPreRelease(range.min, range.max)) {
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index df01214..0986fed 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -31,18 +31,25 @@
         '--exclude-standard',
         '--recurse-submodules'
       ], workingDir: entrypoint.root.dir);
+      final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir;
+      var beneath = p.posix.joinAll(
+          p.split(p.normalize(p.relative(entrypoint.root.dir, from: root))));
+      if (beneath == './') {
+        beneath = '';
+      }
       String resolve(String path) {
         if (Platform.isWindows) {
-          return p.joinAll([entrypoint.root.dir, ...p.posix.split(path)]);
+          return p.joinAll([root, ...p.posix.split(path)]);
         }
-        return p.join(entrypoint.root.dir, path);
+        return p.join(root, path);
       }
 
       final unignoredByGitignore = Ignore.listFiles(
+        beneath: beneath,
         listDir: (dir) {
           var contents = Directory(resolve(dir)).listSync();
-          return contents.map((entity) => p.posix.joinAll(
-              p.split(p.relative(entity.path, from: entrypoint.root.dir))));
+          return contents.map((entity) =>
+              p.posix.joinAll(p.split(p.relative(entity.path, from: root))));
         },
         ignoreForDir: (dir) {
           final gitIgnore = resolve('$dir/.gitignore');
@@ -52,8 +59,12 @@
           return rules.isEmpty ? null : Ignore(rules);
         },
         isDir: (dir) => dirExists(resolve(dir)),
-      ).toSet();
-
+      ).map((file) {
+        final relative = p.relative(resolve(file), from: entrypoint.root.dir);
+        return Platform.isWindows
+            ? p.posix.joinAll(p.split(relative))
+            : relative;
+      }).toSet();
       final ignoredFilesCheckedIn = checkedIntoGit
           .where((file) => !unignoredByGitignore.contains(file))
           .toList();
diff --git a/test/package_list_files_test.dart b/test/package_list_files_test.dart
index 7891270..921175d 100644
--- a/test/package_list_files_test.dart
+++ b/test/package_list_files_test.dart
@@ -254,6 +254,23 @@
       });
     });
 
+    test("Don't ignore packages/ before the package root", () async {
+      await d.dir(appPath, [
+        d.dir('packages', [
+          d.dir('app', [
+            d.appPubspec(),
+            d.dir('packages', [d.file('a.txt')]),
+          ]),
+        ]),
+      ]).create();
+
+      createEntrypoint(p.join(appPath, 'packages', 'app'));
+
+      expect(entrypoint.root.listFiles(), {
+        p.join(root, 'pubspec.yaml'),
+      });
+    });
+
     group('with a submodule', () {
       setUp(() async {
         await d.git('submodule', [
diff --git a/test/reformat_ranges_test.dart b/test/reformat_ranges_test.dart
new file mode 100644
index 0000000..4af9cf7
--- /dev/null
+++ b/test/reformat_ranges_test.dart
@@ -0,0 +1,57 @@
+// Copyright (c) 2021, 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.
+
+// @dart=2.10
+
+import 'package:pub/src/package_name.dart';
+import 'package:pub/src/solver/reformat_ranges.dart';
+import 'package:pub/src/utils.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('reformatMax when max has a build identifier', () {
+    expect(
+      reformatMax(
+        [PackageId('abc', null, Version.parse('1.2.3'), null)],
+        VersionRange(
+          min: Version.parse('0.2.4'),
+          max: Version.parse('1.2.4'),
+          alwaysIncludeMaxPreRelease: true,
+        ),
+      ),
+      equals(
+        Pair(
+          Version.parse('1.2.4-0'),
+          false,
+        ),
+      ),
+    );
+    expect(
+      reformatMax(
+        [PackageId('abc', null, Version.parse('1.2.4-3'), null)],
+        VersionRange(
+          min: Version.parse('0.2.4'),
+          max: Version.parse('1.2.4'),
+          alwaysIncludeMaxPreRelease: true,
+        ),
+      ),
+      equals(
+        Pair(
+          Version.parse('1.2.4-3'),
+          true,
+        ),
+      ),
+    );
+    expect(
+        reformatMax(
+          [],
+          VersionRange(
+            max: Version.parse('1.2.4+1'),
+            alwaysIncludeMaxPreRelease: true,
+          ),
+        ),
+        equals(null));
+  });
+}
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart
index d6140f5..06d3be4 100644
--- a/test/validator/gitignore_test.dart
+++ b/test/validator/gitignore_test.dart
@@ -4,18 +4,23 @@
 
 // @dart=2.10
 
+import 'package:path/path.dart' as p;
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
-Future<void> expectValidation(error, int exitCode) async {
+Future<void> expectValidation(
+  error,
+  int exitCode, {
+  String workingDirectory,
+}) async {
   await runPub(
     error: error,
     args: ['publish', '--dry-run'],
     environment: {'_PUB_TEST_SDK_VERSION': '2.12.0'},
-    workingDirectory: d.path(appPath),
+    workingDirectory: workingDirectory ?? d.path(appPath),
     exitCode: exitCode,
   );
 }
@@ -46,4 +51,37 @@
         ]),
         exit_codes.DATA);
   });
+
+  test('Should also consider gitignores from above the package root', () async {
+    await d.git('reporoot', [
+      d.dir(
+        'myapp',
+        [
+          d.file('foo.txt'),
+          ...d.validPackage.contents,
+        ],
+      ),
+    ]).create();
+    final packageRoot = p.join(d.sandbox, 'reporoot', 'myapp');
+    await pubGet(
+        environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'},
+        workingDirectory: packageRoot);
+
+    await expectValidation(contains('Package has 0 warnings.'), 0,
+        workingDirectory: packageRoot);
+
+    await d.dir('reporoot', [
+      d.file('.gitignore', '*.txt'),
+    ]).create();
+
+    await expectValidation(
+        allOf([
+          contains('Package has 1 warning.'),
+          contains('foo.txt'),
+          contains(
+              'Consider adjusting your `.gitignore` files to not ignore those files'),
+        ]),
+        exit_codes.DATA,
+        workingDirectory: packageRoot);
+  });
 }
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index e2136c5..caca301 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -31,6 +31,8 @@
   group('override', override);
   group('downgrade', downgrade);
   group('features', features, skip: true);
+
+  group('regressions', regressions);
 }
 
 void basicGraph() {
@@ -3014,3 +3016,22 @@
 
   expect(ids, isEmpty, reason: 'Expected no additional packages.');
 }
+
+void regressions() {
+  test('reformatRanges with a build', () async {
+    await servePackages((b) {
+      b.serve('integration_test', '1.0.1',
+          deps: {'vm_service': '>= 4.2.0 <6.0.0'});
+      b.serve('integration_test', '1.0.2+2',
+          deps: {'vm_service': '>= 4.2.0 <7.0.0'});
+
+      b.serve('vm_service', '7.3.0');
+    });
+    await d.appDir({'integration_test': '^1.0.2'}).create();
+    await expectResolves(
+      error: contains(
+        'Because no versions of integration_test match >=1.0.2 <1.0.2+2',
+      ),
+    );
+  });
+}
diff --git a/tool/test.dart b/tool/test.dart
index 2f4769d..e7877a2 100755
--- a/tool/test.dart
+++ b/tool/test.dart
@@ -3,8 +3,6 @@
 // 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.
 
-// @dart=2.10
-
 /// Test wrapper script.
 /// Many of the integration tests runs the `pub` command, this is slow if every
 /// invocation requires the dart compiler to load all the sources. This script
@@ -19,7 +17,7 @@
 import 'package:pub/src/exceptions.dart';
 
 Future<void> main(List<String> args) async {
-  Process testProcess;
+  Process? testProcess;
   final sub = ProcessSignal.sigint.watch().listen((signal) {
     testProcess?.kill(signal);
   });