dependency_services: use revisions as versions for git deps. (#3383)

diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 46c1b94..19cc867 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -23,6 +23,7 @@
 import '../pubspec.dart';
 import '../pubspec_utils.dart';
 import '../solver.dart';
+import '../source/git.dart';
 import '../system_cache.dart';
 import '../utils.dart';
 
@@ -74,11 +75,11 @@
     Future<List<Object>> _computeUpgradeSet(
       Pubspec rootPubspec,
       PackageId? package, {
-      required UpgradeType upgradeType,
+      required _UpgradeType upgradeType,
     }) async {
       if (package == null) return [];
       final lockFile = entrypoint.lockFile;
-      final pubspec = upgradeType == UpgradeType.multiBreaking
+      final pubspec = upgradeType == _UpgradeType.multiBreaking
           ? stripVersionUpperBounds(rootPubspec)
           : Pubspec(
               rootPubspec.name,
@@ -87,7 +88,7 @@
               sdkConstraints: rootPubspec.sdkConstraints,
             );
 
-      final dependencySet = dependencySetOfPackage(pubspec, package);
+      final dependencySet = _dependencySetOfPackage(pubspec, package);
       if (dependencySet != null) {
         // Force the version to be the new version.
         dependencySet[package.name] =
@@ -110,38 +111,43 @@
         ...resolution.packages.where((r) {
           if (r.name == rootPubspec.name) return false;
           final originalVersion = currentPackages[r.name];
-          return originalVersion == null ||
-              r.version != originalVersion.version;
+          return originalVersion == null || r != originalVersion;
         }).map((p) {
-          final depset = dependencySetOfPackage(rootPubspec, p);
+          final depset = _dependencySetOfPackage(rootPubspec, p);
           final originalConstraint = depset?[p.name]?.constraint;
+          final currentPackage = currentPackages[p.name];
           return {
             'name': p.name,
-            'version': p.version.toString(),
+            'version': p.versionOrHash(),
             'kind': _kindString(pubspec, p.name),
+            'source': _source(p, containingDir: directory),
             'constraintBumped': originalConstraint == null
                 ? null
-                : upgradeType == UpgradeType.compatible
+                : upgradeType == _UpgradeType.compatible
                     ? originalConstraint.toString()
                     : VersionConstraint.compatibleWith(p.version).toString(),
             'constraintWidened': originalConstraint == null
                 ? null
-                : upgradeType == UpgradeType.compatible
+                : upgradeType == _UpgradeType.compatible
                     ? originalConstraint.toString()
                     : _widenConstraint(originalConstraint, p.version)
                         .toString(),
             'constraintBumpedIfNeeded': originalConstraint == null
                 ? null
-                : upgradeType == UpgradeType.compatible
+                : upgradeType == _UpgradeType.compatible
                     ? originalConstraint.toString()
                     : originalConstraint.allows(p.version)
                         ? originalConstraint.toString()
                         : VersionConstraint.compatibleWith(p.version)
                             .toString(),
-            'previousVersion': currentPackages[p.name]?.version.toString(),
+            'previousVersion': currentPackage?.versionOrHash(),
             'previousConstraint': originalConstraint?.toString(),
+            'previousSource': currentPackage == null
+                ? null
+                : _source(currentPackage, containingDir: directory),
           };
         }),
+        // Find packages that were removed by the resolution
         for (final oldPackageName in lockFile.packages.keys)
           if (!resolution.packages
               .any((newPackage) => newPackage.name == oldPackageName))
@@ -154,8 +160,10 @@
               'constraintWidened': null,
               'constraintBumpedIfNeeded': null,
               'previousVersion':
-                  currentPackages[oldPackageName]?.version.toString(),
+                  currentPackages[oldPackageName]?.versionOrHash(),
               'previousConstraint': null,
+              'previous': _source(currentPackages[oldPackageName]!,
+                  containingDir: directory)
             },
       ];
     }
@@ -173,7 +181,7 @@
         devDependencies: compatiblePubspec.devDependencies.values,
       );
       final dependencySet =
-          dependencySetOfPackage(singleBreakingPubspec, package);
+          _dependencySetOfPackage(singleBreakingPubspec, package);
       final kind = _kindString(compatiblePubspec, package.name);
       PackageId? singleBreakingVersion;
       if (dependencySet != null) {
@@ -187,25 +195,24 @@
       }
       dependencies.add({
         'name': package.name,
-        'version': package.version.toString(),
+        'version': package.versionOrHash(),
         'kind': kind,
+        'source': _source(package, containingDir: directory),
         'latest':
             (await cache.getLatest(package.toRef(), version: package.version))
-                ?.version
-                .toString(),
+                ?.versionOrHash(),
         'constraint':
             _constraintOf(compatiblePubspec, package.name)?.toString(),
-        if (compatibleVersion != null)
-          'compatible': await _computeUpgradeSet(
-              compatiblePubspec, compatibleVersion,
-              upgradeType: UpgradeType.compatible),
+        'compatible': await _computeUpgradeSet(
+            compatiblePubspec, compatibleVersion,
+            upgradeType: _UpgradeType.compatible),
         'singleBreaking': kind != 'transitive' && singleBreakingVersion == null
             ? []
             : await _computeUpgradeSet(compatiblePubspec, singleBreakingVersion,
-                upgradeType: UpgradeType.singleBreaking),
+                upgradeType: _UpgradeType.singleBreaking),
         'multiBreaking': kind != 'transitive' && multiBreakingVersion != null
             ? await _computeUpgradeSet(compatiblePubspec, multiBreakingVersion,
-                upgradeType: UpgradeType.multiBreaking)
+                upgradeType: _UpgradeType.multiBreaking)
             : [],
       });
     }
@@ -227,6 +234,14 @@
           : 'transitive';
 }
 
+Map<String, Object?> _source(PackageId id, {required String containingDir}) {
+  return {
+    'type': id.source.name,
+    'description':
+        id.description.serializeForLockfile(containingDir: containingDir),
+  };
+}
+
 /// Try to solve [pubspec] return [PackageId]s in the resolution or `null` if no
 /// resolution was found.
 Future<List<PackageId>?> _tryResolve(Pubspec pubspec, SystemCache cache) async {
@@ -269,16 +284,28 @@
     for (final package in currentPackages) {
       dependencies.add({
         'name': package.name,
-        'version': package.version.toString(),
+        'version': package.versionOrHash(),
         'kind': _kindString(pubspec, package.name),
         'constraint': _constraintOf(pubspec, package.name).toString(),
+        'source': _source(package, containingDir: directory),
       });
     }
     log.message(JsonEncoder.withIndent('  ').convert(result));
   }
 }
 
-enum UpgradeType {
+extension on PackageId {
+  String versionOrHash() {
+    final description = this.description;
+    if (description is GitResolvedDescription) {
+      return description.resolvedRef;
+    } else {
+      return version.toString();
+    }
+  }
+}
+
+enum _UpgradeType {
   /// Only upgrade pubspec.lock.
   compatible,
 
@@ -315,7 +342,7 @@
       toApply.add(
         _PackageVersion(
           change['name'],
-          change['version'] != null ? Version.parse(change['version']) : null,
+          change['version'],
           change['constraint'] != null
               ? VersionConstraint.parse(change['constraint'])
               : null,
@@ -334,13 +361,24 @@
       final targetPackage = p.name;
       final targetVersion = p.version;
       final targetConstraint = p.constraint;
+      final targetRevision = p.gitRevision;
 
       if (targetConstraint != null) {
         final section = pubspec.dependencies[targetPackage] != null
             ? 'dependencies'
             : 'dev_dependencies';
-        pubspecEditor
-            .update([section, targetPackage], targetConstraint.toString());
+        final packageConfig =
+            pubspecEditor.parseAt([section, targetPackage]).value;
+        if (packageConfig == null || packageConfig is String) {
+          pubspecEditor
+              .update([section, targetPackage], targetConstraint.toString());
+        } else if (packageConfig is Map) {
+          pubspecEditor.update(
+              [section, targetPackage, 'version'], targetConstraint.toString());
+        } else {
+          fail(
+              'The dependency $targetPackage does not have a map or string as a description');
+        }
       } else if (targetVersion != null) {
         final constraint = _constraintOf(pubspec, targetPackage);
         if (constraint != null && !constraint.allows(targetVersion)) {
@@ -351,18 +389,43 @@
               VersionConstraint.compatibleWith(targetVersion).toString());
         }
       }
-      if (targetVersion != null &&
-          lockFileEditor != null &&
-          lockFileYaml['packages'].containsKey(targetPackage)) {
-        lockFileEditor.update(
-            ['packages', targetPackage, 'version'], targetVersion.toString());
-      }
-      if (targetVersion == null &&
-          lockFileEditor != null &&
-          !lockFileYaml['packages'].containsKey(targetPackage)) {
-        dataError(
-          'Trying to remove non-existing transitive dependency $targetPackage.',
-        );
+      if (lockFileEditor != null) {
+        if (targetVersion != null &&
+            lockFileYaml['packages'].containsKey(targetPackage)) {
+          lockFileEditor.update(
+              ['packages', targetPackage, 'version'], targetVersion.toString());
+        } else if (targetRevision != null &&
+            lockFileYaml['packages'].containsKey(targetPackage)) {
+          final ref = entrypoint.lockFile.packages[targetPackage]!.toRef();
+          final currentDescription = ref.description as GitDescription;
+          final updatedRef = PackageRef(
+              targetPackage,
+              GitDescription(
+                  url: currentDescription.url,
+                  path: currentDescription.path,
+                  ref: targetRevision,
+                  containingDir: directory));
+          final versions = await cache.getVersions(updatedRef);
+          if (versions.isEmpty) {
+            dataError(
+                'Found no versions of $targetPackage with git revision `$targetRevision`.');
+          }
+          // GitSource can only return a single version.
+          assert(versions.length == 1);
+
+          lockFileEditor.update(['packages', targetPackage, 'version'],
+              versions.single.version.toString());
+          lockFileEditor.update(
+            ['packages', targetPackage, 'description', 'resolved-ref'],
+            targetRevision,
+          );
+        } else if (targetVersion == null &&
+            targetRevision == null &&
+            !lockFileYaml['packages'].containsKey(targetPackage)) {
+          dataError(
+            'Trying to remove non-existing transitive dependency $targetPackage.',
+          );
+        }
       }
     }
 
@@ -407,11 +470,31 @@
 class _PackageVersion {
   String name;
   Version? version;
+  String? gitRevision;
   VersionConstraint? constraint;
-  _PackageVersion(this.name, this.version, this.constraint);
+  _PackageVersion(this.name, String? versionOrHash, this.constraint)
+      : version =
+            versionOrHash == null ? null : _tryParseVersion(versionOrHash),
+        gitRevision =
+            versionOrHash == null ? null : _tryParseHash(versionOrHash);
 }
 
-Map<String, PackageRange>? dependencySetOfPackage(
+Version? _tryParseVersion(String v) {
+  try {
+    return Version.parse(v);
+  } on FormatException {
+    return null;
+  }
+}
+
+String? _tryParseHash(String v) {
+  if (RegExp(r'^[a-fA-F0-9]+$').hasMatch(v)) {
+    return v;
+  }
+  return null;
+}
+
+Map<String, PackageRange>? _dependencySetOfPackage(
     Pubspec pubspec, PackageId package) {
   return pubspec.dependencies.containsKey(package.name)
       ? pubspec.dependencies
@@ -427,7 +510,7 @@
     final min = original.min;
     final max = original.max;
     if (max != null && newVersion >= max) {
-      return compatibleWithIfPossible(
+      return _compatibleWithIfPossible(
         VersionRange(
           min: min,
           includeMin: original.includeMin,
@@ -436,7 +519,7 @@
       );
     }
     if (min != null && newVersion <= min) {
-      return compatibleWithIfPossible(
+      return _compatibleWithIfPossible(
         VersionRange(
             min: newVersion,
             includeMin: true,
@@ -451,7 +534,7 @@
       original, 'original', 'Must be a Version range or empty');
 }
 
-VersionConstraint compatibleWithIfPossible(VersionRange versionRange) {
+VersionConstraint _compatibleWithIfPossible(VersionRange versionRange) {
   final min = versionRange.min;
   if (min != null && min.nextBreaking.firstPreRelease == versionRange.max) {
     return VersionConstraint.compatibleWith(min);
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index 014c8a7..cd28e21 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -117,9 +117,7 @@
       final packageRange = constrained[name]!;
       var unconstrainedRange = packageRange;
 
-      /// We only need to remove the upper bound if it is a hosted package.
-      if (packageRange.description is HostedDescription &&
-          (stripOnly!.isEmpty || stripOnly.contains(packageRange.name))) {
+      if (stripOnly!.isEmpty || stripOnly.contains(packageRange.name)) {
         unconstrainedRange = PackageRange(
           packageRange.toRef(),
           stripUpperBound(packageRange.constraint),
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 3cfd32d..cb3d3f8 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -718,6 +718,13 @@
         other.path == path;
   }
 
+  GitDescription withRef(String newRef) => GitDescription._(
+        url: url,
+        relative: relative,
+        ref: newRef,
+        path: path,
+      );
+
   @override
   int get hashCode => Object.hash(url, ref, path);
 
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
index 3830e27..b007a6b 100644
--- a/test/dependency_services/dependency_services_test.dart
+++ b/test/dependency_services/dependency_services_test.dart
@@ -127,7 +127,7 @@
     await pubGet();
     server.dontAllowDownloads();
     await listReportApply(context, [
-      _PackageVersion('foo', Version.parse('2.2.3')),
+      _PackageVersion('foo', '2.2.3'),
       _PackageVersion('transitive', null)
     ], reportAssertions: (report) {
       expect(
@@ -166,7 +166,7 @@
     server.dontAllowDownloads();
 
     await listReportApply(context, [
-      _PackageVersion('foo', Version.parse('1.2.4')),
+      _PackageVersion('foo', '1.2.4'),
     ], reportAssertions: (report) {
       expect(
         findChangeVersion(report, 'compatible', 'foo'),
@@ -193,8 +193,8 @@
     server.dontAllowDownloads();
 
     await listReportApply(context, [
-      _PackageVersion('foo', Version.parse('2.2.3')),
-      _PackageVersion('transitive', Version.parse('1.0.0'))
+      _PackageVersion('foo', '2.2.3'),
+      _PackageVersion('transitive', '1.0.0')
     ], reportAssertions: (report) {
       expect(
         findChangeVersion(report, 'singleBreaking', 'foo'),
@@ -236,9 +236,9 @@
     server.dontAllowDownloads();
 
     await listReportApply(context, [
-      _PackageVersion('foo', Version.parse('3.0.1'),
+      _PackageVersion('foo', '3.0.1',
           constraint: VersionConstraint.parse('^3.0.0')),
-      _PackageVersion('bar', Version.parse('2.0.0'))
+      _PackageVersion('bar', '2.0.0')
     ], reportAssertions: (report) {
       expect(
         findChangeVersion(report, 'multiBreaking', 'foo'),
@@ -263,7 +263,7 @@
     await pubGet();
     server.serve('foo', '2.0.0');
     await listReportApply(context, [
-      _PackageVersion('foo', Version.parse('2.0.0'),
+      _PackageVersion('foo', '2.0.0',
           constraint: VersionConstraint.parse('^2.0.0')),
     ], reportAssertions: (report) {
       expect(
@@ -272,22 +272,55 @@
       );
     });
   });
+
+  testWithGolden('Can update a git package', (context) async {
+    await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
+    await d.git('bar.git', [d.libPubspec('bar', '1.0.0')]).create();
+
+    await d.appDir({
+      'foo': {
+        'git': {'url': '../foo.git'}
+      },
+      'bar': {
+        // A git dependency with a version constraint.
+        'git': {'url': '../bar.git'},
+        'version': '^1.0.0',
+      }
+    }).create();
+    await pubGet();
+    final secondVersion = d.git('foo.git', [d.libPubspec('foo', '2.0.0')]);
+    await secondVersion.commit();
+    final newRef = await secondVersion.revParse('HEAD');
+
+    final barSecondVersion = d.git('bar.git', [d.libPubspec('bar', '2.0.0')]);
+    await barSecondVersion.commit();
+
+    await listReportApply(context, [
+      _PackageVersion('foo', newRef),
+    ], reportAssertions: (report) {
+      expect(
+        findChangeVersion(report, 'multiBreaking', 'foo'),
+        newRef,
+      );
+    });
+  });
 }
 
 dynamic findChangeVersion(dynamic json, String updateType, String name) {
   final dep = json['dependencies'].firstWhere((p) => p['name'] == 'foo');
+  if (dep == null) return null;
   return dep[updateType].firstWhere((p) => p['name'] == name)['version'];
 }
 
 class _PackageVersion {
   String name;
-  Version? version;
+  String? version;
   VersionConstraint? constraint;
   _PackageVersion(this.name, this.version, {this.constraint});
 
   Map<String, Object?> toJson() => {
         'name': name,
-        'version': version?.toString(),
+        'version': version,
         if (constraint != null) 'constraint': constraint.toString()
       };
 }
diff --git a/test/descriptor/git.dart b/test/descriptor/git.dart
index edfc7bd..9209815 100644
--- a/test/descriptor/git.dart
+++ b/test/descriptor/git.dart
@@ -62,7 +62,10 @@
       'GIT_AUTHOR_NAME': 'Pub Test',
       'GIT_AUTHOR_EMAIL': 'pub@dartlang.org',
       'GIT_COMMITTER_NAME': 'Pub Test',
-      'GIT_COMMITTER_EMAIL': 'pub@dartlang.org'
+      'GIT_COMMITTER_EMAIL': 'pub@dartlang.org',
+      // To make stable commits ids we fix the date.
+      'GIT_COMMITTER_DATE': DateTime(1970).toIso8601String(),
+      'GIT_AUTHOR_DATE': DateTime(1970).toIso8601String(),
     };
 
     return git.run(args,
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
index 7b8290e..7b01ac0 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
@@ -25,7 +25,14 @@
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     }
   ]
 }
@@ -40,6 +47,13 @@
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.2.3",
       "constraint": "^1.0.0",
       "compatible": [],
@@ -48,21 +62,43 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "transitive",
           "version": "1.0.0",
           "kind": "transitive",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": null,
           "constraintWidened": null,
           "constraintBumpedIfNeeded": null,
           "previousVersion": null,
-          "previousConstraint": null
+          "previousConstraint": null,
+          "previousSource": null
         }
       ],
       "multiBreaking": [
@@ -70,21 +106,43 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "transitive",
           "version": "1.0.0",
           "kind": "transitive",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": null,
           "constraintWidened": null,
           "constraintBumpedIfNeeded": null,
           "previousVersion": null,
-          "previousConstraint": null
+          "previousConstraint": null,
+          "previousSource": null
         }
       ]
     }
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt
new file mode 100644
index 0000000..1f90e4c
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt
@@ -0,0 +1,235 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"myapp","environment":{"sdk":">=0.1.2 <1.0.0"},"dependencies":{"foo":{"git":{"url":"../foo.git"}},"bar":{"git":{"url":"../bar.git"},"version":"^1.0.0"}}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: HEAD
+      resolved-ref: "92cdd4ac724a6da0db2a69ac149820aa220e8518"
+      url: "../bar.git"
+    source: git
+    version: "1.0.0"
+  foo:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: HEAD
+      resolved-ref: "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba"
+      url: "../foo.git"
+    source: git
+    version: "1.0.0"
+sdks:
+  dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "git",
+        "description": {
+          "url": "../bar.git",
+          "ref": "HEAD",
+          "resolved-ref": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+          "path": "."
+        }
+      }
+    },
+    {
+      "name": "foo",
+      "version": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+      "kind": "direct",
+      "constraint": "any",
+      "source": {
+        "type": "git",
+        "description": {
+          "url": "../foo.git",
+          "ref": "HEAD",
+          "resolved-ref": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+          "path": "."
+        }
+      }
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+      "kind": "direct",
+      "source": {
+        "type": "git",
+        "description": {
+          "url": "../bar.git",
+          "ref": "HEAD",
+          "resolved-ref": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+          "path": "."
+        }
+      },
+      "latest": "cf473c33e39eeec4615bc2cde8bf68cbb10ad02c",
+      "constraint": "^1.0.0",
+      "compatible": [],
+      "singleBreaking": [
+        {
+          "name": "bar",
+          "version": "cf473c33e39eeec4615bc2cde8bf68cbb10ad02c",
+          "kind": "direct",
+          "source": {
+            "type": "git",
+            "description": {
+              "url": "../bar.git",
+              "ref": "HEAD",
+              "resolved-ref": "cf473c33e39eeec4615bc2cde8bf68cbb10ad02c",
+              "path": "."
+            }
+          },
+          "constraintBumped": "^2.0.0",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.0.0",
+          "previousVersion": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "git",
+            "description": {
+              "url": "../bar.git",
+              "ref": "HEAD",
+              "resolved-ref": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+              "path": "."
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "bar",
+          "version": "cf473c33e39eeec4615bc2cde8bf68cbb10ad02c",
+          "kind": "direct",
+          "source": {
+            "type": "git",
+            "description": {
+              "url": "../bar.git",
+              "ref": "HEAD",
+              "resolved-ref": "cf473c33e39eeec4615bc2cde8bf68cbb10ad02c",
+              "path": "."
+            }
+          },
+          "constraintBumped": "^2.0.0",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.0.0",
+          "previousVersion": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "git",
+            "description": {
+              "url": "../bar.git",
+              "ref": "HEAD",
+              "resolved-ref": "92cdd4ac724a6da0db2a69ac149820aa220e8518",
+              "path": "."
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+      "kind": "direct",
+      "source": {
+        "type": "git",
+        "description": {
+          "url": "../foo.git",
+          "ref": "HEAD",
+          "resolved-ref": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+          "path": "."
+        }
+      },
+      "latest": "ad2d659389d4475f6c3a34282e2204160b753fd9",
+      "constraint": "any",
+      "compatible": [],
+      "singleBreaking": [],
+      "multiBreaking": [
+        {
+          "name": "foo",
+          "version": "ad2d659389d4475f6c3a34282e2204160b753fd9",
+          "kind": "direct",
+          "source": {
+            "type": "git",
+            "description": {
+              "url": "../foo.git",
+              "ref": "HEAD",
+              "resolved-ref": "ad2d659389d4475f6c3a34282e2204160b753fd9",
+              "path": "."
+            }
+          },
+          "constraintBumped": "^2.0.0",
+          "constraintWidened": "any",
+          "constraintBumpedIfNeeded": "any",
+          "previousVersion": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+          "previousConstraint": "any",
+          "previousSource": {
+            "type": "git",
+            "description": {
+              "url": "../foo.git",
+              "ref": "HEAD",
+              "resolved-ref": "1ae220cc484311a7a1e2e31d1ccc6ea995acb6ba",
+              "path": "."
+            }
+          }
+        }
+      ]
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"ad2d659389d4475f6c3a34282e2204160b753fd9"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"myapp","environment":{"sdk":">=0.1.2 <1.0.0"},"dependencies":{"foo":{"git":{"url":"../foo.git"}},"bar":{"git":{"url":"../bar.git"},"version":"^1.0.0"}}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: HEAD
+      resolved-ref: "92cdd4ac724a6da0db2a69ac149820aa220e8518"
+      url: "../bar.git"
+    source: git
+    version: "1.0.0"
+  foo:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: HEAD
+      resolved-ref: ad2d659389d4475f6c3a34282e2204160b753fd9
+      url: "../foo.git"
+    source: git
+    version: "2.0.0"
+sdks:
+  dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
index c133458..b4726c5 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
@@ -39,19 +39,40 @@
       "name": "bar",
       "version": "1.2.3",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      }
     },
     {
       "name": "boo",
       "version": "1.2.3",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "boo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     },
     {
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     }
   ]
 }
@@ -66,6 +87,13 @@
       "name": "bar",
       "version": "1.2.3",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.2.3",
       "constraint": "^1.0.0",
       "compatible": [],
@@ -74,11 +102,25 @@
           "name": "bar",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -86,11 +128,25 @@
           "name": "bar",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     },
@@ -98,6 +154,13 @@
       "name": "boo",
       "version": "1.2.3",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "boo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "1.2.4",
       "constraint": "^1.0.0",
       "compatible": [
@@ -105,11 +168,25 @@
           "name": "boo",
           "version": "1.2.4",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.0.0",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.0.0",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "singleBreaking": [
@@ -117,11 +194,25 @@
           "name": "boo",
           "version": "1.2.4",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.2.4",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.0.0",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -129,11 +220,25 @@
           "name": "boo",
           "version": "1.2.4",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.2.4",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.0.0",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "boo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     },
@@ -141,6 +246,13 @@
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.2.3",
       "constraint": "^1.0.0",
       "compatible": [
@@ -148,11 +260,25 @@
           "name": "foo",
           "version": "1.2.4",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.0.0",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.0.0",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "singleBreaking": [
@@ -160,11 +286,25 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -172,11 +312,25 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     }
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
index bbe5052..69a06d4 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
@@ -32,13 +32,27 @@
       "name": "bar",
       "version": "1.0.0",
       "kind": "direct",
-      "constraint": "any"
+      "constraint": "any",
+      "source": {
+        "type": "path",
+        "description": {
+          "path": "../bar",
+          "relative": true
+        }
+      }
     },
     {
       "name": "foo",
       "version": "1.0.0",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     }
   ]
 }
@@ -53,6 +67,13 @@
       "name": "bar",
       "version": "1.0.0",
       "kind": "direct",
+      "source": {
+        "type": "path",
+        "description": {
+          "path": "../bar",
+          "relative": true
+        }
+      },
       "latest": "1.0.0",
       "constraint": "any",
       "compatible": [],
@@ -63,6 +84,13 @@
       "name": "foo",
       "version": "1.0.0",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.0.0",
       "constraint": "^1.0.0",
       "compatible": [],
@@ -71,11 +99,25 @@
           "name": "foo",
           "version": "2.0.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.0.0",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -83,11 +125,25 @@
           "name": "foo",
           "version": "2.0.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.0.0",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     }
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
index bfc5a78..50ee46f 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
@@ -32,13 +32,27 @@
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     },
     {
       "name": "transitive",
       "version": "1.0.0",
       "kind": "transitive",
-      "constraint": "null"
+      "constraint": "null",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "transitive",
+          "url": "http://localhost:$PORT"
+        }
+      }
     }
   ]
 }
@@ -53,6 +67,13 @@
       "name": "foo",
       "version": "1.2.3",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.2.3",
       "constraint": "^1.0.0",
       "compatible": [],
@@ -61,11 +82,25 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "transitive",
@@ -75,7 +110,14 @@
           "constraintWidened": null,
           "constraintBumpedIfNeeded": null,
           "previousVersion": "1.0.0",
-          "previousConstraint": null
+          "previousConstraint": null,
+          "previous": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -83,11 +125,25 @@
           "name": "foo",
           "version": "2.2.3",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.2.3",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.2.3",
           "previousVersion": "1.2.3",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "transitive",
@@ -97,7 +153,14 @@
           "constraintWidened": null,
           "constraintBumpedIfNeeded": null,
           "previousVersion": "1.0.0",
-          "previousConstraint": null
+          "previousConstraint": null,
+          "previous": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     },
@@ -105,6 +168,13 @@
       "name": "transitive",
       "version": "1.0.0",
       "kind": "transitive",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "transitive",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "1.0.0",
       "constraint": null,
       "compatible": [],
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
index 32bc04e..da9222d 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
@@ -39,19 +39,40 @@
       "name": "bar",
       "version": "1.0.0",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      }
     },
     {
       "name": "baz",
       "version": "1.0.0",
       "kind": "direct",
-      "constraint": "1.0.0"
+      "constraint": "1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "baz",
+          "url": "http://localhost:$PORT"
+        }
+      }
     },
     {
       "name": "foo",
       "version": "1.0.0",
       "kind": "direct",
-      "constraint": "^1.0.0"
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      }
     }
   ]
 }
@@ -66,6 +87,13 @@
       "name": "bar",
       "version": "1.0.0",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "2.0.0",
       "constraint": "^1.0.0",
       "compatible": [],
@@ -75,21 +103,49 @@
           "name": "bar",
           "version": "2.0.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.0.0",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "foo",
           "version": "3.0.1",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^3.0.1",
           "constraintWidened": ">=1.0.0 <4.0.0",
           "constraintBumpedIfNeeded": "^3.0.1",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     },
@@ -97,6 +153,13 @@
       "name": "baz",
       "version": "1.0.0",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "baz",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "1.1.0",
       "constraint": "1.0.0",
       "compatible": [],
@@ -105,11 +168,25 @@
           "name": "baz",
           "version": "1.1.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "baz",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.1.0",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.1.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "1.0.0"
+          "previousConstraint": "1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "baz",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -117,11 +194,25 @@
           "name": "baz",
           "version": "1.1.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "baz",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.1.0",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.1.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "1.0.0"
+          "previousConstraint": "1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "baz",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     },
@@ -129,6 +220,13 @@
       "name": "foo",
       "version": "1.0.0",
       "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT"
+        }
+      },
       "latest": "3.0.1",
       "constraint": "^1.0.0",
       "compatible": [
@@ -136,11 +234,25 @@
           "name": "foo",
           "version": "1.5.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^1.0.0",
           "constraintWidened": "^1.0.0",
           "constraintBumpedIfNeeded": "^1.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "singleBreaking": [
@@ -148,11 +260,25 @@
           "name": "foo",
           "version": "2.0.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.0.0",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ],
       "multiBreaking": [
@@ -160,21 +286,49 @@
           "name": "foo",
           "version": "3.0.1",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^3.0.1",
           "constraintWidened": ">=1.0.0 <4.0.0",
           "constraintBumpedIfNeeded": "^3.0.1",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT"
+            }
+          }
         },
         {
           "name": "bar",
           "version": "2.0.0",
           "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          },
           "constraintBumped": "^2.0.0",
           "constraintWidened": ">=1.0.0 <3.0.0",
           "constraintBumpedIfNeeded": "^2.0.0",
           "previousVersion": "1.0.0",
-          "previousConstraint": "^1.0.0"
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT"
+            }
+          }
         }
       ]
     }