From dfaa8c2f14ca4e6ee3dfe2749b64376f88c8e9be Mon Sep 17 00:00:00 2001
From: Liam Chen <liamchzh@gmail.com>
Date: Fri, 8 Oct 2021 16:51:32 -0400
Subject: [PATCH] Initial release

---
 .circleci/config.yml                        |  71 +++
 .github/pull_request_template.md            |   5 +
 .gitignore                                  |  13 +
 CHANGELOG.adoc                              |  16 +
 CODEOWNERS                                  |   1 +
 LICENSE                                     | 214 ++++++++
 README.adoc                                 | 264 ++++++++++
 codecov.yml                                 |  12 +
 project.clj                                 |  27 +
 src/circleci/deps_plus/core.clj             | 514 ++++++++++++++++++++
 src/circleci/deps_plus/graph.clj            |  29 ++
 src/leiningen/deps_plus.clj                 | 275 +++++++++++
 test-projects/classpath-conflict.clj        |   3 +
 test-projects/dependency-diff.clj           |  10 +
 test-projects/dependency-diff.list          |   7 +
 test-projects/dependency-list.clj           |   3 +
 test-projects/downgrade.clj                 |   4 +
 test-projects/family-mismatch.clj           |   3 +
 test-projects/management-conflict.clj       |   5 +
 test-projects/pedantic-range.clj            |   3 +
 test-projects/pedantic-version-conflict.clj |   3 +
 test-projects/shaded-dependencies.clj       |   7 +
 test/circleci/deps_plus/core_test.clj       | 274 +++++++++++
 test/circleci/deps_plus/graph_test.clj      |  58 +++
 24 files changed, 1821 insertions(+)
 create mode 100644 .circleci/config.yml
 create mode 100644 .github/pull_request_template.md
 create mode 100644 .gitignore
 create mode 100644 CHANGELOG.adoc
 create mode 100644 CODEOWNERS
 create mode 100644 LICENSE
 create mode 100644 README.adoc
 create mode 100644 codecov.yml
 create mode 100644 project.clj
 create mode 100644 src/circleci/deps_plus/core.clj
 create mode 100644 src/circleci/deps_plus/graph.clj
 create mode 100644 src/leiningen/deps_plus.clj
 create mode 100644 test-projects/classpath-conflict.clj
 create mode 100644 test-projects/dependency-diff.clj
 create mode 100644 test-projects/dependency-diff.list
 create mode 100644 test-projects/dependency-list.clj
 create mode 100644 test-projects/downgrade.clj
 create mode 100644 test-projects/family-mismatch.clj
 create mode 100644 test-projects/management-conflict.clj
 create mode 100644 test-projects/pedantic-range.clj
 create mode 100644 test-projects/pedantic-version-conflict.clj
 create mode 100644 test-projects/shaded-dependencies.clj
 create mode 100644 test/circleci/deps_plus/core_test.clj
 create mode 100644 test/circleci/deps_plus/graph_test.clj

diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..6638760
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,71 @@
+version: 2.1
+
+orbs:
+  codecov: codecov/codecov@1.2.2
+
+executors:
+  clojure:
+    docker:
+      - image: circleci/clojure:lein
+
+jobs:
+  clj-kondo:
+    docker:
+      - image: cljkondo/clj-kondo:2021.02.13
+    steps:
+      - checkout
+      - run: clj-kondo --lint .
+
+  test-deps-plus:
+    executor: clojure
+    steps:
+      - checkout
+      - restore_cache:
+          keys:
+            - v1-jars-{{ checksum "project.clj" }}
+            - v1-jars
+      - run: lein test-ci
+      - run:
+          command: lein eastwood
+          when: always
+      - codecov/upload
+      - store_test_results:
+          path: target/test-results
+      - save_cache:
+          key: v1-jars-{{ checksum "project.clj" }}
+          paths:
+            - ~/.m2
+
+  publish:
+    docker:
+      - image: circleci/clojure:lein
+    steps:
+      - checkout
+      - restore_cache:
+          keys:
+            - v1-jars-{{ checksum "project.clj" }}
+            - v1-jars
+      - run:
+          name: Download dependencies
+          command: lein deps
+      - run:
+          name: Publish to clojars
+          command: lein deploy
+
+workflows:
+  build-test:
+    jobs:
+      - clj-kondo
+      - test-deps-plus:
+          filters:
+            tags:
+              only: /.*/
+      - publish:
+          context: clojars-publish
+          requires:
+            - test-deps-plus
+          filters:
+            tags:
+              only: /.*/
+            branches:
+              ignore: /.*/
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..42c1f23
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,5 @@
+# Checklist
+
+- [ ] Updated the version, if [applicable](../blob/main/README.adoc#releasing)
+- [ ] Updated the [CHANGELOG.adoc](../blob/main/CHANGELOG.adoc), if applicable
+- [ ] Updated any documentation (READMEs and docstrings), if applicable
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a17fabe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+target
+classes
+checkouts
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+.lein-*
+.nrepl-port
+.eastwood
+.clj-kondo/.cache
+.idea/
+
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
new file mode 100644
index 0000000..a9653f7
--- /dev/null
+++ b/CHANGELOG.adoc
@@ -0,0 +1,16 @@
+:toc:
+:toclevels: 1
+
+= Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog].
+
+= 2021
+
+== 0.1.X - 2021-10-08
+
+=== Added
+
+* Initial release. All commits are squashed for safety reasons. The original repository lives in GitHub's CircleCI-Archived org.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..ee9bc81
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @circleci/backplane
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d921d3d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,214 @@
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from
+a Contributor if it was added to the Program by such Contributor itself or
+anyone acting on such Contributor's behalf. Contributions do not include
+additions to the Program which: (i) are separate modules of software
+distributed in conjunction with the Program under their own license
+agreement, and (ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and
+such derivative works, in source code and object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form.  This patent license shall apply to the combination of the
+Contribution and the Program if, at the time the Contribution is added by the
+Contributor, such addition of the Contribution causes such combination to be
+covered by the Licensed Patents. The patent license shall not apply to any
+other combinations which include the Contribution. No hardware per se is
+licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the licenses
+to its Contributions set forth herein, no assurances are provided by any
+Contributor that the Program does not infringe the patent or other
+intellectual property rights of any other entity. Each Contributor disclaims
+any liability to Recipient for claims brought by any other entity based on
+infringement of intellectual property rights or otherwise. As a condition to
+exercising the rights and licenses granted hereunder, each Recipient hereby
+assumes sole responsibility to secure any other intellectual property rights
+needed, if any. For example, if a third party patent license is required to
+allow Recipient to distribute the Program, it is Recipient's responsibility
+to acquire that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are offered
+by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable manner on
+or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within
+the Program.
+
+Each Contributor must identify itself as the originator of its Contribution,
+if any, in a manner that reasonably allows subsequent Recipients to identify
+the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a
+manner which does not create potential liability for other Contributors.
+Therefore, if a Contributor includes the Program in a commercial product
+offering, such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor") against any
+losses, damages and costs (collectively "Losses") arising from claims,
+lawsuits and other legal actions brought by a third party against the
+Indemnified Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the Program in
+a commercial product offering.  The obligations in this section do not apply
+to any claims or Losses relating to any actual or alleged intellectual
+property infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such claim, and
+b) allow the Commercial Contributor to control, and cooperate with the
+Commercial Contributor in, the defense and any related settlement
+negotiations. The Indemnified Contributor may participate in any such claim
+at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If
+that Commercial Contributor then makes performance claims, or offers
+warranties related to Product X, those performance claims and warranties are
+such Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a
+court requires any other Contributor to pay any damages as a result, the
+Commercial Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
+AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
+EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
+CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
+PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
+appropriateness of using and distributing the Program and assumes all risks
+associated with its exercise of rights under this Agreement , including but
+not limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment, and
+unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
+LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of the
+remainder of the terms of this Agreement, and without further action by the
+parties hereto, such provision shall be reformed to the minimum extent
+necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program itself
+(excluding combinations of the Program with other software or hardware)
+infringes such Recipient's patent(s), then such Recipient's rights granted
+under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and
+does not cure such failure in a reasonable period of time after becoming
+aware of such noncompliance. If all Recipient's rights under this Agreement
+terminate, Recipient agrees to cease use and distribution of the Program as
+soon as reasonably practicable. However, Recipient's obligations under this
+Agreement and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to
+time. No one other than the Agreement Steward has the right to modify this
+Agreement. The Eclipse Foundation is the initial Agreement Steward. The
+Eclipse Foundation may assign the responsibility to serve as the Agreement
+Steward to a suitable separate entity. Each new version of the Agreement will
+be given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the
+Agreement under which it was received. In addition, after a new version of
+the Agreement is published, Contributor may elect to distribute the Program
+(including its Contributions) under the new version. Except as expressly
+stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under this
+Agreement, whether expressly, by implication, estoppel or otherwise. All
+rights in the Program not expressly granted under this Agreement are
+reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial
+in any resulting litigation.
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..5de319d
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,264 @@
+= deps-plus plugin
+
+deps-plus is a Leiningen plugin designed to help with analyzing project dependencies and reviewing dependency changes.
+
+== Installation
+
+Add `+[com.circleci/deps-plus "0.1.0"]+` to your `+:user+` profile plugins in `+~/.lein/profiles.clj+`. For example,
+
+[source,clj]
+....
+{:user {:plugins [[com.circleci/deps-plus "0.1.0"]]}}
+....
+
+== Usage
+
+To run a deps-plus task simply run `+lein deps-plus <task name>+` from a Leiningen project.
+
+=== Tasks
+
+* *list* - Lists all dependencies of the current project.
+* *list-save* - Save the project dependency list to "deps.list".
+* *list-diff* - Compare project dependencies to those previously saved with `+list-save+`.
+* *why* - Shows all paths to given dependency as a tree. This is similar to `+lein deps :why+`, but instead of only showing the single resolved path to dependency chosen by Maven, all copies and paths to the dependency (including those excluded by conflict resolution) are shown.
+
+=== Checks
+
+* link:#check-classpath-conflicts[*check-classpath-conflicts*] - Checks for classpath conflicts.
+* link:#check-downgraded[*check-downgraded*] - Checks for dependencies which have been downgraded.
+* link:#check-families[*check-families*] - Checks for inconsistent dependency versions within families.
+* link:#check-management-conflicts[*check-management-conflicts*] - Checks for dependency management conflicts.
+* link:#check-pedantic[*check-pedantic*] - Checks for version conflicts and version ranges.
+
+==== check-classpath-conflicts
+
+Checks for classpath conflicts. Classpath conflicts occur when resources which should be unique
+(e.g. Java class files, Clojure namespaces, etc) are found in multiple dependencies. Classpath
+conflicts can lead to unpredictable behavior at runtime as the version of the resource which is loaded
+can depend on subtle factors like the ordering of JARs on the classpath, or the order in which JARs
+are merged into an uberjar. Classpath conflicts are often caused by dependencies being duplicated
+(e.g. two versions of a dependency with different Maven coordinates) or dependencies which bundle
+other dependencies (e.g. shaded JARs). In these cases the conflict can be resolved by replacing the
+offending dependency with a more appropriate one (e.g. a non-shaded version).
+
+_Example: Apache logging classes_
+
+....
+Found 6 classpath conflicts between commons-logging:commons-logging:jar:1.2:compile and org.slf4j:jcl-over-slf4j:jar:1.7.30:compile
+  org/apache/commons/logging/Log.class
+  org/apache/commons/logging/LogConfigurationException.class
+  org/apache/commons/logging/LogFactory.class
+  org/apache/commons/logging/impl/NoOpLog.class
+  org/apache/commons/logging/impl/SimpleLog$1.class
+  org/apache/commons/logging/impl/SimpleLog.class
+....
+
+These packages both implement the same classes, but the latter implements the former with SLF4J.
+
+_Solution:_ Pick which package should be chosen (there's no practical way to tell which version of
+the class has been chosen in the past).  In this case, we'll pick `org.slf4j/jcl-over-slf4j`.  So we
+add a top level exclusion for `commons-logging/commons-logging`, preventing _any_ dependency from
+pulling it in:
+
+[source,clj]
+....
+  :exclusions [... commons-logging/commons-logging ...]
+....
+
+_Example: com.google.javascript:closure-compiler_
+
+The jar for the Closure Compiler includes a shaded version of https://github.com/google/gson[Gson]
+without renaming the classes:
+
+....
+$ jar tf ~/.m2/repository/com/google/javascript/closure-compiler/v20160315/closure-compiler-v20160315.jar | grep 'com/google/gson'
+com/google/gson/
+com/google/gson/Gson$5.class
+com/google/gson/JsonNull.class
+...
+....
+
+This always causes classpath conflicts when we also declare a direct dependency on Gson, even if the
+version of Gson is the same in both places.  You'll have to deal with this type of conflict on a case
+by case basis.  Here, there is an
+https://mvnrepository.com/artifact/com.google.javascript/closure-compiler-unshaded[_unshaded_ version]
+of the Closure Compiler available that does not include the Gson classes.
+
+==== check-downgraded
+
+A dependency is considered "downgraded" if the resolved version is older than the version specified by
+some other included dependency. This can occur if `:managed-dependencies` is used to pin a dependency
+version or if Maven conflict resolution happens to not select the latest version.
+
+==== check-families
+
+Checks for inconsistent dependency versions within families. Dependencies families are sets of
+dependencies which share a common group ID and version. Inconsistent family versions can occur if the
+`:managed-dependencies` for a family are incomplete or inconsistent. This situation can also occur
+when different family members are brought in through different transitive dependency paths. To fix
+this issue consider adding a complete set of `:managed-dependencies` entries for the family.
+
+_Example: io.netty family_
+....
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-buffer:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-codec:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-common:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-codec-socks:jar:4.1.34.Final:compile and io.netty:netty-transport:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-buffer:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-codec-http:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-codec:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-common:jar:4.1.50.Final:compile
+Suspicious combination, io.netty:netty-handler-proxy:jar:4.1.34.Final:compile and io.netty:netty-transport:jar:4.1.50.Final:compile
+....
+
+These netty dependencies are all in the same family, but they have different versions (`4.1.34.Final`
+vs. `4.1.50.Final`).  In this case, the `project.clj` declares a managed dependency on
+`io.netty/netty-codec-http2` but not the other members of the family:
+
+[source,clj]
+....
+  :managed-dependencies [... [io.netty/netty-codec-http2 "4.1.50.Final"] ...]
+....
+The related projects are coming in through transitive dependencies, and have different versions:
+....
+$ lein deps-plus why io.netty:netty-codec-socks:jar:4.1.34.Final
+...
+   ;; +- [circleci/my-project "0.1.0"]
+...
+   ;; |  \- [io.grpc/grpc-netty "1.20.0"]
+   ;; |     \- [io.netty/netty-handler-proxy "4.1.34.Final"]
+   ;; |        \- [io.netty/netty-codec-socks "4.1.34.Final"] (chosen)
+...
+....
+
+_Solution:_ add a managed dependency for the family.  Pick the desired version for the family (in
+this case, we'll pick `4.1.50.Final`).  At a minimum, for every package mentioned in the list of
+suspicious combinations that has the _undesired_ version, add an entry in the `:managed-dependencies`:
+
+[source,clj]
+....
+  :managed-dependencies [[io.netty/netty-codec-socks "4.1.50.Final"]
+                         [io.netty/netty-handler-proxy "4.1.50.Final"]
+                         ...]
+....
+
+For completeness, you can add _every_ member of the family the project uses to managed dependencies.
+
+==== check-management-conflicts
+
+Checks for dependency management conflicts. A dependency management conflict occurs when a dependency
+has versions specified in both `:dependencies` and `:managed-dependencies`. To resolve this issue you
+can remove the version number from `:dependencies`. If you wish to override a managed dependency
+version inherited from a parent project you should do so in your own `:managed-dependencies` section.
+
+_Example: org.clojure/core.async_
+
+....
+org.clojure:core.async:jar:1.2.603 conflicts with managed dependency org.clojure:core.async:jar:1.3.610
+....
+
+_Solution 1:_ if the exact version of `core.async` does not matter, remove the version number from
+the `org.clojure/core.async` version in your dependencies to automatically get the version provided by
+clj-parent:
+
+[source,clj]
+....
+  :dependencies [... [org.clojure/core.async] ...]
+....
+
+This solution also applies when the versions are identical:
+....
+org.clojure:core.async:jar:1.3.610 conflicts with managed dependency org.clojure:core.async:jar:1.3.610
+....
+
+_Solution 2:_ if it is necessary to pin the version `1.2.603`, move the dependency to the managed
+dependencies:
+
+[source,clj]
+....
+  :managed-dependencies [... [org.clojure/core.async "1.2.603"] ...]
+....
+
+==== check-pedantic
+
+Checks for version conflicts and version ranges. This check is similar to Leiningen’s `:pedantic?
+:abort` mode, but suggests `+:managed-dependencies+` instead of `:exclusions`.  In general, expect to
+see warnings when:
+
+1. A top-level dependency is overridden by another version
+2. A transitive dependency is overridden by an _older_ version
+
+Unlike Leiningen, this task ignores plugin dependencies since these are unaffected by managed
+dependencies. By default, each suggested managed dependency is shown alongside a dependency tree
+for the conflict. Pass the `:quiet` flag to suppress the output of these trees.
+
+_Example: cheshire_
+
+....
+Found 7 dependency conflicts.
+Considering adding the following :managed-dependencies,
+
+...
+   ;; +- [cheshire/cheshire "5.9.0"] (chosen)
+...
+   ;; \- [circleci/my-project "0.1.0"]
+   ;;    +- [circleci/the-other-project "0.1.0"]
+   ;;    |  +- [circleci/rollcage "1.0.203"]
+   ;;    |  |  \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;;    |  +- [cheshire/cheshire "5.10.0"] (omitted)
+   ;;    |  \- [amperity/vault-clj "0.7.0"]
+   ;;    |     \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;;    \- [cheshire/cheshire "5.10.0"] (omitted)
+   [cheshire/cheshire "5.9.0"]
+...
+....
+
+This shows all of the different versions of `cheshire/cheshire`, including which versions were chosen
+(would actually be used when the program runs) vs. which were excluded.  check-pedantic complains
+because multiple dependencies ask for different versions of `cheshire/cheshire`, and the newest version
+(transitively `"5.10.0"`), is omitted.
+
+_Solution 1:_ if the version of `cheshire/cheshire` from the `:dependencies` is not required for
+correctness, remove it as an explicit dependency and retry.  If the warning disappears, you can see
+that the newest version wins with `why`:
+
+....
+$ lein deps-plus why cheshire
+   ;; +- [circleci/the-other-project "0.1.0"]
+   ;; |  +- [circleci/rollcage "1.0.203"]
+   ;; |  |  \- [cheshire/cheshire "5.8.1"] (omitted)
+   ;; |  +- [cheshire/cheshire "5.10.0"] (chosen)
+   ;; |  \- [amperity/vault-clj "0.7.0"]
+   ;; |     \- [cheshire/cheshire "5.8.1"] (omitted)
+...
+....
+
+_Solution 2:_ add a managed dependency for the preferred version.  Pick the version that should be
+included (in this case, we'll pick `"5.9.0"`).  This is the version `check-pedantic` suggests at the
+bottom of the dependency knot.  It's also the version that the project explicitly requires as a
+`:dependency`.  Move it to a managed dependency:
+
+[source,clj]
+....
+  :managed-dependencies [... [cheshire/cheshire "5.9.0"] ...]
+....
+
+Releasing
+---------
+
+New git tags are automatically published to Clojars. To find the most recent version of lein-deps-plus, look at the most recent tag in Clojars.
+
+The following should be updated on the `main` branch before tagging:
+
+- `project.clj` - version
+- `CHANGELOG.adoc` - summary of changes
+
+Using semantic versioning is recommended, increment the:
+1. MAJOR version when you make backward incompatible changes.
+2. MINOR version when you add user-facing features in a backward compatible manner.
+3. PATCH version when you make backward compatible bug fixes.
+
+License
+-------
+
+Distributed under the http://www.eclipse.org/legal/epl-v10.html[Eclipse Public License].
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..5dbecc5
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,12 @@
+coverage:
+  status:
+    project:
+      default:
+        informational: true
+    patch:
+      default:
+        informational: true
+
+comment:
+  layout: "files"
+  require_changes: true
diff --git a/project.clj b/project.clj
new file mode 100644
index 0000000..02382cc
--- /dev/null
+++ b/project.clj
@@ -0,0 +1,27 @@
+(defproject com.circleci/deps-plus "0.1.0"
+  :plugins [[jonase/eastwood "0.3.6" :exclusions [org.clojure/clojure]]]
+
+  :pedantic? :abort
+
+  :managed-dependencies [[org.clojure/clojure "1.10.1"]]
+
+  :profiles {:dev {:dependencies [[org.clojure/clojure]
+                                  [lambdaisland/kaocha "0.0-601"]
+                                  [lambdaisland/kaocha-cloverage "0.0-41"]
+                                  [lambdaisland/kaocha-junit-xml "0.0-70"]]}
+             :provided {:dependencies [[org.clojure/clojure]
+                                       [com.google.guava/guava "24.1.1-jre"]
+                                       [org.jsoup/jsoup "1.9.2"]
+                                       [leiningen/leiningen "2.9.1"]]}}
+
+  :aliases {"test"    ["run" "-m" "kaocha.runner"]
+            "test-ci" ["test"
+                       "--plugin" "cloverage"
+                       "--codecov" "--no-cov-html" "--no-cov-summary"
+                       "--plugin" "kaocha.plugin/profiling"
+                       "--plugin" "kaocha.plugin/junit-xml"
+                       "--junit-xml-file" "target/test-results/results.xml"]}
+  :repositories [["releases" {:url "https://clojars.org/repo"
+                              :username :env/clojars_username
+                              :password :env/clojars_token
+                              :sign-releases false}]])
diff --git a/src/circleci/deps_plus/core.clj b/src/circleci/deps_plus/core.clj
new file mode 100644
index 0000000..a018bf5
--- /dev/null
+++ b/src/circleci/deps_plus/core.clj
@@ -0,0 +1,514 @@
+(ns circleci.deps-plus.core
+  (:require [leiningen.core.classpath :as lein-classpath]
+            [cemerick.pomegranate.aether :as aether]
+            [circleci.deps-plus.graph :as graph]
+            [clojure.set :as set]
+            [clojure.string :as string]
+            [leiningen.core.main :as lein])
+  (:import (org.eclipse.aether DefaultRepositorySystemSession)
+           (org.eclipse.aether.graph DependencyNode)
+           (org.eclipse.aether.artifact Artifact)
+           (org.apache.maven.artifact.versioning ComparableVersion)
+           (org.eclipse.aether.collection CollectResult)
+           (java.io Writer File)
+           (java.util.jar JarFile)
+           (java.util Properties)
+           (org.eclipse.aether.util.graph.manager DependencyManagerUtils)
+           (org.eclipse.aether.resolution DependencyResult)))
+
+(def dependencies-key :dependencies)
+(def managed-dependencies-key :managed-dependencies)
+
+(defrecord Coordinates [group-id artifact-id classifier version extension]
+  Object
+  (toString [this]
+    (->> [group-id artifact-id extension classifier (str version) (:scope (meta this))]
+         (remove empty?)
+         (string/join ":"))))
+
+(defn parse-coordinates [s]
+  (let [[group-id artifact-id ext x y z] (string/split s #":")
+        ext (or ext "jar")]
+    (cond
+      (some? z)
+      (let [classifier x
+            version (ComparableVersion. y)
+            scope z]
+        (with-meta
+          (->Coordinates group-id artifact-id classifier version ext)
+          {:scope scope}))
+
+      (#{"compile" "provided" "runtime" "test"} y)
+      (let [version (ComparableVersion. x)
+            scope y]
+        (with-meta
+          (->Coordinates group-id artifact-id "" version ext)
+          {:scope scope}))
+
+      (some? y)
+      (let [classifier x
+            version (ComparableVersion. y)]
+        (->Coordinates group-id artifact-id classifier version ext))
+
+      (some? x)
+      (let [version (ComparableVersion. x)]
+        (->Coordinates group-id artifact-id "" version ext))
+
+      :else
+      (->Coordinates group-id artifact-id "" nil ext))))
+
+(defn- artifact-coordinates [^Artifact a]
+  (when a
+    (->Coordinates
+      (.getGroupId a)
+      (.getArtifactId a)
+      (.getClassifier a)
+      (some-> (.getVersion a) (ComparableVersion.))
+      (.getExtension a))))
+
+(defn- node-coordinates [^DependencyNode n]
+  (when-let [coord (artifact-coordinates (.getArtifact n))]
+    (with-meta coord
+      {:scope              (.getScope (.getDependency n))
+       :version-range      (some-> n .getVersionConstraint .getRange)
+       :premanaged-version (DependencyManagerUtils/getPremanagedVersion n)
+       :premanaged-scope   (DependencyManagerUtils/getPremanagedScope n)
+       :file               (some-> n .getArtifact .getFile)})))
+
+(defn coordinates->aether [{:keys [group-id artifact-id classifier version extension] :as dep}]
+  (let [scope (:scope (meta dep))]
+    (concat [(symbol group-id artifact-id) (str version)]
+      (when-not (string/blank? classifier) [:classifier classifier])
+      (when-not (#{"jar" nil} extension) [:extension extension])
+      (when-not (#{"compile" nil} scope) [:scope scope]))))
+
+(defn aether->coordinates [[group-artifact version & {:keys [scope classifier extension]}]]
+  (with-meta
+    (->Coordinates
+      (or (namespace group-artifact) (name group-artifact))
+      (name group-artifact)
+      (or classifier "")
+      (some-> version ComparableVersion.)
+      (or extension "jar"))
+    {:scope scope}))
+
+(defn unversioned [coordinates]
+  (assoc coordinates :version nil))
+
+(defn- basic-repository-session
+  "Returns a basic repository session that enables verbose dependency
+  management property tracking (pre-managed version, scope, etc)."
+  ^DefaultRepositorySystemSession [& args]
+  (doto ^DefaultRepositorySystemSession (apply aether/repository-session args)
+    (.setConfigProperty DependencyManagerUtils/CONFIG_PROP_VERBOSE true)))
+
+(defn- full-dependency-graph-repository-session
+  "Returns a repository session without a dependency graph transformer.
+  Without a dependency graph transformer the full dependency graph will
+  be returned by the dependency collector."
+  [& args]
+  (doto (apply basic-repository-session args)
+    (.setDependencyGraphTransformer nil)))
+
+(defn- dependency-graph [^DependencyNode root]
+  (loop [edges {} to-visit [root] visited #{root}]
+    (if (seq to-visit)
+      (let [[node & rest] to-visit
+            coordinates (node-coordinates node)
+            child-nodes (seq (.getChildren node))
+            child-coordinates (into #{} (map node-coordinates child-nodes))]
+        (recur
+          (update edges coordinates set/union child-coordinates)
+          (into rest (filter #(not (visited %)) child-nodes))
+          (into visited child-nodes)))
+      edges)))
+
+(defn- transfer-listener [e]
+  (let [{:keys [type] {:keys [name repository]} :resource} e]
+    (when (= :started type)
+      (locking *err*
+        (lein/warn "Retrieving" name "from" repository)))))
+
+(defn resolve-dependencies [project & flags]
+  (let [{:keys [repositories local-repo mirrors offline?]} project
+        flags (into #{} flags)
+        managed-coordinates (get project managed-dependencies-key)
+        coordinates (-> (get project dependencies-key)
+                        (aether/merge-versions-from-managed-coords managed-coordinates))
+        args (concat
+               [:local-repo local-repo
+                :repositories (into {} (map lein-classpath/add-repo-auth repositories))
+                :coordinates coordinates
+                :mirrors (map lein-classpath/add-repo-auth mirrors)
+                :proxy (lein-classpath/get-proxy-settings)
+                :retrieve (boolean (:retrieve flags))
+                :transfer-listener transfer-listener
+                :repository-session-fn (if (:full-graph flags)
+                                         full-dependency-graph-repository-session
+                                         basic-repository-session)
+                :offline? offline?]
+
+               (when-not (:ignore-managed-dependencies flags)
+                 [:managed-coordinates managed-coordinates]))
+        result (apply aether/resolve-dependencies* args)
+        root (if (:retrieve flags)
+               (.getRoot ^DependencyResult result)
+               (.getRoot ^CollectResult result))]
+    (dependency-graph root)))
+
+(defn- resolve-natural-dependencies
+  "Returns a graph obtained by merging the full \"natural\" dependency graphs
+  for each dependency specified by the given coordinates. Since this is a full
+  dependency graph, cyclic dependencies, version conflicts, and duplicate nodes
+  may be present. This graph can be useful for comparing the dependencies declared
+  by a given dependency to those resolved for use by the project."
+  [project coordinates]
+  (let [aether-coordinates (->> coordinates
+                                (map coordinates->aether)
+                                (into []))
+        alt-project (merge project
+                      {:dependencies         aether-coordinates
+                       :managed-dependencies nil})]
+    (resolve-dependencies alt-project :full-graph)))
+
+(defn- max*
+  "Returns the greatest value as ordered by `compare`."
+  [values]
+  (reduce
+    (fn [acc x]
+      (if (> (compare x acc) 0) x acc))
+    values))
+
+(defn check-downgraded*
+  "Returns a sequence of downgraded dependencies as described by `check-downgraded`.
+  Each downgraded dependency is returned as a pair, `[dependency max-version]`."
+  [project]
+  (let [resolved-deps (->> (resolve-dependencies project)
+                           (keys)
+                           (remove nil?)
+                           (into #{}))
+
+        ;; Get the full dependency graph obtained by depending directly on all of our
+        ;; resolved dependencies without managed dependencies in place. This allows us
+        ;; to compare the transitive dependencies that each of our dependencies would
+        ;; bring by default, to those we get with dependency management in place.
+        natural-dep-graph (resolve-natural-dependencies project resolved-deps)
+        natural-dep-versions (->> (keys natural-dep-graph)
+                                  (remove nil?)
+                                  (group-by unversioned)
+                                  (reduce-kv (fn [m k v]
+                                               (assoc m k (map :version v)))
+                                    {}))
+
+        max-versions (reduce-kv
+                       (fn [m k v] (assoc m k (max* v)))
+                       {}
+                       natural-dep-versions)]
+
+    (for [dep (sort-by str resolved-deps)
+          :let [version (:version dep)
+                max-version (get max-versions (unversioned dep))]
+          :when (< (compare version max-version) 0)]
+      [dep max-version])))
+
+(defn check-families*
+  "Returns a sequence of questionable `[child parent]` dependencies as
+  described by `check-families`."
+  [project]
+  (let [deps (->> (resolve-dependencies project)
+                  (keys)
+                  (remove nil?)
+                  (into #{}))
+
+        versions (->> deps
+                      (map (fn [dep] [(unversioned dep) dep]))
+                      (into {}))
+
+        natural-deps (resolve-natural-dependencies project deps)
+
+        same-family? (fn [a b]
+                       (let [keys [:group-id :version]]
+                         (= (select-keys a keys) (select-keys b keys))))]
+
+    (for [dep (sort-by str deps)
+          ;; Find direct dependencies of dep which, in the natural dependency graph,
+          ;; are in part of the same family as dep (same group-id and version). For
+          ;; each of these family members, lookup the actual version resolved.
+          :let [family-members (->> (get natural-deps dep)
+                                    (filter #(same-family? dep %))
+                                    (map unversioned)
+                                    (map #(get versions % (format "%s (excluded)" %))))]
+          member (sort-by str family-members)
+          :when (not= (:version member) (:version dep))]
+      [dep member])))
+
+(defn write-dependency-list
+  [project ^Writer writer]
+  (doseq [dep (->> (resolve-dependencies project)
+                   (keys)
+                   (remove nil?)
+                   (map str)
+                   (sort))]
+    (.write writer (str dep "\n")))
+  (.flush writer))
+
+(defn- parse-dependency-list [s]
+  (when (seq s)
+    (->> (string/split-lines s)
+         (map parse-coordinates))))
+
+(defn- diff-categorise
+  [^Coordinates old-dep ^Coordinates new-dep]
+  (let [old-scope (:scope (meta old-dep))
+        old-version (:version old-dep)
+        new-scope (:scope (meta new-dep))
+        new-version (:version new-dep)]
+    (cond
+      (not old-dep)
+      [:added (format "%s" new-dep)]
+
+      (not new-dep)
+      [:removed (format "%s" old-dep)]
+
+      (> (compare old-version new-version) 0)
+      [:downgraded (format "%s (%s -> %s)" new-dep old-version new-version)]
+
+      (< (compare old-version new-version) 0)
+      [:upgraded (format "%s (%s -> %s)" new-dep old-version new-version)]
+
+      (not= old-scope new-scope)
+      [:rescoped (format "%s (%s -> %s)" new-dep old-scope new-scope)])))
+
+(defn diff-dependency-list
+  [project old-list ^Writer writer]
+  (let [index #(->> (group-by unversioned %)
+                    (reduce-kv
+                     (fn [m k v]
+                       (assoc m k (first v)))
+                     {}))
+        old (->> (parse-dependency-list old-list)
+                 (index))
+        new (->> (resolve-dependencies project)
+                 (keys)
+                 (remove nil?)
+                 (index))
+        all (into #{} (concat (keys old) (keys new)))
+        groups (->> (sort-by str all)
+                    (keep #(diff-categorise (get old %) (get new %)))
+                    (group-by first))]
+    (doseq [category [:added :removed :upgraded :downgraded :rescoped]
+            :when (contains? groups category)
+            :let [changes (category groups)]]
+      (.write writer (format ";; %s\n" (string/capitalize (name category))))
+      (doseq [change changes]
+        (.write writer (format "%s\n" (second change))))
+      (.write writer "\n")))
+  (.flush writer))
+
+(defn same-dependency?
+  "Returns true if the dependencies `a` and `b` are equal ignoring version."
+  [a b]
+  (= (unversioned a) (unversioned b)))
+
+(defn- apply-managed-dependency
+  "Applies a managed dependency to a dependency graph.
+  Matching dependencies are updated to point to the managed version of
+  the dependency. The dependency graph is then pruned of no longer
+  reachable dependencies."
+  [deps managed-dep]
+  (let [managed-dep (vary-meta managed-dep dissoc :version-range)
+        replace #(if (same-dependency? managed-dep %) managed-dep %)
+        deps (reduce-kv
+               (fn [acc dep directs]
+                 (assoc acc
+                   (if (same-dependency? managed-dep dep)
+                     (vary-meta dep dissoc :version-range)
+                     dep)
+                   (into #{} (map replace directs))))
+               {}
+               deps)
+        reachable (graph/graph-seq #(get deps %) nil)]
+    (select-keys deps reachable)))
+
+(defn- pedantic-conflict-check [resolved-dep all-versions]
+  (let [v (:version resolved-dep)
+        max-v (max* (map :version all-versions))]
+    (or (< (compare v max-v) 0)
+        (some #(:version-range (meta %)) all-versions))))
+
+(defn- extract-targeted-subtree
+  "Extracts the minimal subtree from `full-graph` that captures all paths from
+  the nil root node to `target`."
+  [full-graph target]
+  (->> (graph/graph-path-seq #(get full-graph %) nil)
+       (filter #(same-dependency? (peek %) target))
+       (mapcat #(partition 2 1 %))
+       (group-by first)
+       (reduce-kv
+         (fn [acc node children]
+           (assoc acc node (into #{} (map second children))))
+         {})))
+
+(defn- find-first-conflict
+  "Find the first conflicting dependency in the given dependency graph.
+  The dependency returned is the first dependency found via a breadth
+  first search having conflicts."
+  [full-graph resolved conflict-check]
+  (let [;; For "all-versions" we look at the map values (i.e. the right-hand
+        ;; side of each graph edge) instead of the map keys, because the
+        ;; values preserve metadata, including :version-range information.
+        all-versions (->> (vals full-graph)
+                          (mapcat identity)
+                          (remove nil?)
+                          (group-by unversioned))
+        resolved-versions (reduce
+                            (fn [acc dep]
+                              (assoc acc (unversioned dep) dep))
+                            {}
+                            resolved)
+        conflict (->> (graph/graph-seq #(get full-graph %) nil)
+                      (remove nil?)
+                      (map unversioned)
+                      (filter #(conflict-check
+                                 (resolved-versions %)
+                                 (all-versions %)))
+                      (map resolved-versions)
+                      (first))]
+    (when conflict
+      [conflict (extract-targeted-subtree full-graph conflict)])))
+
+(defn find-pedantic-conflicts
+  "Finds dependency version conflicts.
+  For each conflict, returns a vector containing the chosen version of the dependency,
+  and a minimal dependency tree containing all the paths to the conflicting dependency."
+  [project]
+  (let [resolved (->> (resolve-dependencies project)
+                      (keys)
+                      (remove nil?))]
+    (loop [conflicts [] full-graph (resolve-dependencies project :full-graph)]
+      (if-let [conflict (find-first-conflict full-graph resolved pedantic-conflict-check)]
+        (recur (conj conflicts conflict)
+               (apply-managed-dependency full-graph (first conflict)))
+        conflicts))))
+
+(defn check-management-conflicts*
+  "Checks for conflicts between dependencies and managed dependencies.
+  A conflict occurs whenever a version is specified for a :dependency which
+  is also specified in :managed-dependencies. For each conflict, the
+  dependency and managed dependencies are returned as a tuple."
+  [project]
+  (let [managed-by-key (->> (:managed-dependencies project)
+                            (map aether->coordinates)
+                            (reduce
+                              (fn [acc dep]
+                                (assoc acc (unversioned dep) dep))
+                              {}))]
+    (for [dep (map aether->coordinates (:dependencies project))
+          :let [managed (managed-by-key (unversioned dep))]
+          :when (and managed (:version dep))]
+      [dep managed])))
+
+(defn resolve-targeted-dependency-subtree
+  "Returns the minimal dependency subtree that captures all paths to `target`.
+  The returned tree has a single nil root node. All leaf nodes of the returned
+  tree match `target`."
+  [project target]
+  (-> (resolve-dependencies project :full-graph)
+      (extract-targeted-subtree target)))
+
+(defn- jar-classpath-resources
+  "Returns a list of \"classpath resources\" found in the given JAR file.
+  \"Classpath resources\" are files in the JAR which should probably be unique
+  within the classpath (i.e. they should only appear in one JAR file). This
+  detection is heuristic based and may change over time."
+  [^File file]
+  (with-open [jar (JarFile. file)]
+    (->> (enumeration-seq (.entries jar))
+         (remove #(.isDirectory %))
+         (map #(.getName %))
+         ;; Limit to well known extensions. There are otherwise a lot of
+         ;; false positives which are difficult to filter out.
+         (filter #(let [ext (peek (string/split % #"\."))]
+                    (#{"class" "clj" "cljs" "cljc" "edn" "xml" "properties"} ext)))
+         ;; Maven metadata files under META-INF frequently conflict, but
+         ;; these conflicts are low risk. Some other things under META-INF
+         ;; (like Java service loader files) are merged by Leiningen.
+         (remove #(string/starts-with? % "META-INF/"))
+         ;; module-info.class -> Java 9+ module info (not an actual conflict)
+         ;; project.clj -> Leiningen project files (safe to ignore)
+         ;; data_readers.clj -> Merged by Leiningen when building an uberjar
+         (remove #{"module-info.class"
+                   "project.clj"
+                   "data_readers.clj"})
+         (doall))))
+
+(defn check-classpath-conflicts*
+  "Checks for classpath conflicts.
+  A classpath conflict occurs when multiple dependencies provide the same
+  resource. This detection applies heuristics to limit the number of false
+  positives (e.g. LICENSE files appear in multiple JARs). Conflicts are
+  returned as a map. Map keys are resource names. Map values are sets of
+  dependencies where the resource was found. Only conflicts are included so
+  these dependency sets always contain a least 2 entries."
+  [project]
+  (let [deps (resolve-dependencies project :retrieve)
+        dep-files (->> (keys deps)
+                       (remove nil?)
+                       (reduce
+                         (fn [acc dep]
+                           (if-let [file (:file (meta dep))]
+                             (assoc acc dep file)
+                             acc))
+                         {}))
+        jars (filter #(.endsWith (.getName (val %)) ".jar") dep-files)]
+    (->> (mapcat
+           (fn [[dep file]]
+             (->> (jar-classpath-resources file)
+                  (map (fn [entry] [entry dep]))))
+           jars)
+         (group-by first)
+         (reduce-kv
+           (fn [acc entry deps]
+             (if (> (count deps) 1)
+               (assoc acc entry (into [] (map second deps)))
+               acc))
+           {}))))
+
+(defn- shaded-jar-dependencies
+  "Return the coordinates for each dependency shaded within the given JAR."
+  [filename]
+  (with-open [jar (JarFile. filename)]
+    (doall (for [e (enumeration-seq (.entries jar))
+                 :when (.endsWith (.getName e) "/pom.properties")]
+             (with-open [is (.getInputStream jar e)]
+               (let [props (doto (Properties.)
+                             (.load is))
+                     group-id    (.get props "groupId")
+                     artifact-id (.get props "artifactId")
+                     version     (.get props "version")]
+                 (aether->coordinates
+                  [(symbol group-id artifact-id) version])))))))
+
+(defn who-shades*
+  "Return a map of dependencies which shade the given target dependency."
+  [project target]
+  (let [deps (resolve-dependencies project :retrieve)
+        dep-files (->> (keys deps)
+                       (remove nil?)
+                       (remove #(same-dependency? target %))
+                       (reduce
+                         (fn [acc dep]
+                           (let [file (:file (meta dep))]
+                             (if (some-> file .getName (.endsWith ".jar"))
+                               (assoc acc dep file)
+                               acc)))
+                         {}))]
+    (reduce-kv
+     (fn [acc dep jar-path]
+       (if-let [version (->> (shaded-jar-dependencies jar-path)
+                             (filter #(same-dependency? target %))
+                             (first))]
+         (assoc acc dep version)
+         acc))
+     {}
+     dep-files)))
diff --git a/src/circleci/deps_plus/graph.clj b/src/circleci/deps_plus/graph.clj
new file mode 100644
index 0000000..834d6fe
--- /dev/null
+++ b/src/circleci/deps_plus/graph.clj
@@ -0,0 +1,29 @@
+(ns circleci.deps-plus.graph)
+
+(defn graph-path-seq
+  "Returns a sequence of non-cyclic paths found via a breadth-first walk.
+  Equivalent to `graph-seq`, but each node is returned as a vector of nodes
+  represent the path from `root` (the first element) to the visited node (the last element).
+  `neighbors` must be a fn of one arg that returns the nodes adjacent to the given node.
+  `root` is the starting node."
+  [neighbors root]
+  (loop [to-visit [[root]] visited []]
+    (if (seq to-visit)
+      (let [path (first to-visit)
+            path-set (into #{} path)
+            adj (->> (neighbors (peek path))
+                     (remove path-set))
+            adj-paths (map #(conj path %) adj)]
+        (recur
+          (into (subvec to-visit 1) adj-paths)
+          (conj visited path)))
+      (seq visited))))
+
+(defn graph-seq
+  "Returns a sequence of graph nodes found via a breadth-first walk. Inspired by `tree-seq`.
+  `neighbors` must be a fn of one arg that returns the nodes adjacent to the given node.
+  `root` is the starting node."
+  [neighbors root]
+  (->> (graph-path-seq neighbors root)
+       (map peek)
+       (distinct)))
diff --git a/src/leiningen/deps_plus.clj b/src/leiningen/deps_plus.clj
new file mode 100644
index 0000000..6d50298
--- /dev/null
+++ b/src/leiningen/deps_plus.clj
@@ -0,0 +1,275 @@
+(ns leiningen.deps-plus
+  (:refer-clojure :exclude [list])
+  (:require [leiningen.core.main :as lein]
+            [circleci.deps-plus.core :as deps]
+            [clojure.java.io :as io]
+            [leiningen.core.project :as project]
+            [clojure.string :as str]))
+
+(defn- list
+  "Lists all dependencies of the current project."
+  [project]
+  (deps/write-dependency-list project *out*))
+
+(defn- list-save
+  "Save a list of all dependencies of the current project."
+  [project]
+  (with-open [out (io/writer "deps.list")]
+    (deps/write-dependency-list project out)))
+
+(defn- list-diff
+  "Compare current dependencies to those previously saved with `list-save`."
+  [project]
+  (let [in (io/file "deps.list")]
+    (when-not (.exists in)
+      (lein/warn "No deps.list file found. list-save must be run before list-diff.")
+      (lein/exit 1))
+    (deps/diff-dependency-list project (slurp in) *out*)))
+
+(defn- check-downgraded
+  "Checks for dependencies which have been downgraded.
+  A dependency is considered \"downgraded\" if the resolved version is older
+  than the version specified by some other included dependency. This can occur
+  if `:managed-dependencies` is used to pin a dependency version or if Maven
+  conflict resolution happens to not select the latest version."
+  [project]
+  (doseq [[dep max-version] (deps/check-downgraded* project)]
+    (lein/info (format "%s downgraded to %s"
+                 (assoc dep :version max-version)
+                 (:version dep)))))
+
+(defn- check-families
+  "Checks for inconsistent dependency versions within families.
+  Dependencies families are sets of dependencies which share a common group ID
+  and version. Inconsistent family versions can occur if the `:managed-dependencies`
+  for a family are incomplete or inconsistent. This situation can also occur
+  when different family members are brought in through different transitive
+  dependency paths. To fix this issue consider adding a complete set of
+  `:managed-dependencies` entries for the family."
+  [project]
+  (when-let [combos (seq (deps/check-families* project))]
+    (lein/info (format "Found %d suspicious combinations." (count combos)))
+    (doseq [[parent child] combos]
+      (lein/info "Suspicious combination," (str parent) "and" (str child)))))
+
+(defn- format-dependency-tree-node
+  "Formats the dependency in Leiningen style."
+  [dep]
+  (let [dep (deps/coordinates->aether dep)]
+    ;; A Leiningen dependency is formatted as,
+    ;;
+    ;;   [group/artifact "version" :param-key "param-value" ...]
+    ;;
+    ;; We rely on the fact that unquoted and quoted values alternate
+    ;; to convert to a string representation.
+    (str "[" (->> (partition 2 dep)
+                  (map #(apply format "%s \"%s\"" %))
+                  (str/join " ")) "]")))
+
+(defn- print-dependency-tree
+  "Prints a human readable dependency tree.
+  `deps` should be a dependency tree encoded as a map from coordinates to
+  collections of direct dependency coordinates. If provided, `resolved` should
+  be a set of resolved dependency coordinates. Matching dependencies will then
+  be annotated in the dependency graph output."
+  [deps resolved]
+  (let [;; Build a sequence of tree paths in depth first order (i.e. the order
+        ;; we need to print them in). Each path element is a `[node is-last?]`
+        ;; tuple where "is-last?" indicates if the node is the last child of
+        ;; its parent node.
+        paths (tree-seq
+                (constantly true)
+                (fn [path]
+                  (let [[node] (peek path)
+                        children (get deps node)
+                        last-idx (dec (count children))]
+                    (->> (map (fn [child idx] [child (= idx last-idx)])
+                              children (range))
+                         (map #(conj path %)))))
+                [[nil true]])
+        ;; Every path begins with the nil root node.
+        ;; Remove it to simplify output.
+        paths (->> (map #(subvec % 1) paths)
+                   (remove empty?))]
+    (doseq [path paths]
+      (let [[node last?] (peek path)
+            prefix-parts (concat
+                           (->> (pop path)
+                                (map second)
+                                (map (fn [last?] (if last? "   " "|  "))))
+                           [(if last? "\\- " "+- ")])
+            prefix (apply str prefix-parts)
+            annotated? (some #(deps/same-dependency? % node) resolved)
+            version-range (:version-range (meta node))
+            premanaged-version (or (:premanaged-version (meta node))
+                                 (:version node))
+            suffix (cond
+                     ;; Dependency should not be annotated.
+                     (not annotated?)
+                     ""
+
+                     ;; Version was omitted by conflict resolution.
+                     (not (resolved node))
+                     " (omitted)"
+
+                     ;; Version was resolved from version range.
+                     version-range
+                     (str " (chosen from range " version-range ")")
+
+                     ;; Version was chosen by :managed-dependencies.
+                     (not= (str premanaged-version) (str (:version node)))
+                     (str " (managed from " premanaged-version ")")
+
+                     ;; Version matches the version chosen.
+                     :else " (chosen)")]
+        (lein/info (str "   ;; " prefix (format-dependency-tree-node node) suffix))))))
+
+(defn- check-pedantic
+  "Checks for dependency conflicts and shows suggested :managed-dependencies.
+  Version conflicts occur when dependency resolution selects a version of a
+  dependency that is older than one appearing elsewhere in the dependency
+  graph (e.g. clj-time 0.14.0 is chosen at the exclusion of 0.15.0). Conflicts
+  are also reported any time a version range is encountered in the dependency
+  graph.
+
+  The output from this task is a set of suggested managed dependencies. For
+  each managed dependency a dependency tree is also output that shows how the
+  conflicting dependency is depended upon. To suppress the outputting of these
+  trees pass the \":quiet\" flag.
+
+  This check is similar to Leiningen's `:pedantic? :abort` mode, but shows
+  suggested :managed-dependencies instead of :exclusions. Unlike Leiningen,
+  plugin dependencies are ignored by this task."
+  [project & args]
+  (when-let [conflicts (seq (sort-by str (deps/find-pedantic-conflicts project)))]
+    (lein/info (format "Found %d dependency conflicts." (count conflicts)))
+    (lein/info "Considering adding the following :managed-dependencies,")
+    (doseq [[dep conflict-tree] conflicts]
+      (when-not (some #{":quiet"} args)
+        (lein/info "")
+        (print-dependency-tree conflict-tree #{dep}))
+      (lein/info (format "   [%s/%s \"%s\"]"
+                   (:group-id dep) (:artifact-id dep) (:version dep))))))
+
+(defn- check-management-conflicts
+  "Checks for dependency management conflicts.
+  A dependency management conflict occurs when a dependency has versions
+  specified in both :dependencies and :managed-dependencies."
+  [project]
+  (when-let [conflicts (seq (deps/check-management-conflicts* project))]
+    (lein/info (format "Found %d management conflicts." (count conflicts)))
+    (doseq [[dep managed] conflicts]
+      (lein/info (format "%s conflicts with managed dependency %s"
+                         dep managed)))))
+
+(defn- check-classpath-conflicts
+  "Checks for classpath conflicts.
+  Classpath conflicts occur when resources which should be unique (e.g. Java
+  class files, Clojure namespaces, etc) are found in multiple dependencies.
+  Classpath conflicts can lead to unpredictable behavior at runtime as the
+  version of the resource which is loaded can depend on subtle factors like
+  the ordering of JARs on the classpath, or the order in which JARs are merged
+  into an uberjar. Classpath conflicts are often caused by dependencies being
+  duplicated (e.g. two versions of a dependency with different Maven
+  coordinates) or dependencies which bundle other dependencies (e.g. shaded
+  JARs). In these cases the conflict can be resolved by replacing the offending
+  dependency with a more appropriate one (e.g. a non-shaded version)."
+  [project]
+  (let [conflicts (deps/check-classpath-conflicts* project)
+        ;; Index conflicts by the sets of dependencies they appear in
+        conflicts-by-set (->> (group-by val conflicts)
+                              (reduce-kv
+                                (fn [acc k entries]
+                                  (assoc acc k (into [] (map key entries))))
+                                {}))
+        format-dep-set (fn [dep-set]
+                         (let [deps (into [] (sort (map str dep-set)))]
+                           (str (str/join ", " (pop deps)) " and " (peek deps))))
+        ;; Pre-format the dependency set keys as strings so we can
+        ;; sort the output and provide a stable output order.
+        conflicts-by-set-str (reduce-kv
+                               (fn [acc dep-set conflicts]
+                                 (assoc acc
+                                   (format-dep-set dep-set)
+                                   conflicts))
+                               {}
+                               conflicts-by-set)]
+    (when-not (empty? conflicts)
+      (lein/info (format "Found %d classpath conflicts between %d dependencies."
+                   (count conflicts)
+                   (->> conflicts-by-set keys (reduce into) count)))
+      (doseq [[dep-set-str conflicts] (sort conflicts-by-set-str)]
+        (lein/info "")
+        (lein/info (format "Found %d classpath conflicts between %s"
+                     (count conflicts) dep-set-str))
+        (let [n 10]
+          (doseq [entry (take n (sort conflicts))]
+            (lein/info " " entry))
+          (when (< n (count conflicts))
+            (lein/info " " (- (count conflicts) n) "more...")))))))
+
+(defn- parse-target-dependency [^String target]
+  (if (.contains target ":")
+    (deps/parse-coordinates target)
+    (deps/aether->coordinates [(symbol target) nil])))
+
+(defn- why
+  "Show all paths to the given dependency as a tree.
+  This is similar to `lein deps :why`, but all copies and paths to the given
+  dependency are shown."
+  [project group-name]
+  (let [target (parse-target-dependency group-name)
+        target (->> (deps/resolve-dependencies project)
+                    (keys)
+                    (filter #(deps/same-dependency? % target))
+                    (first))]
+    (if target
+      (let [subtree (deps/resolve-targeted-dependency-subtree project target)]
+        (print-dependency-tree subtree #{target}))
+      (lein/warn "No dependency found matching" group-name))))
+
+(defn- who-shades
+  "Show project dependencies which shade the given dependency."
+  [project group-name]
+  (let [target (parse-target-dependency group-name)]
+    (doseq [[dep target-version] (deps/who-shades* project target)]
+      (println (str dep) "shades" (str target-version)))))
+
+(defn- error [project message]
+  (lein/warn "[ERROR]" message "\n")
+  (lein/resolve-and-apply project ["help" "deps-plus"])
+  (lein/exit 1 message))
+
+(defn- with-managed-deps
+  "Run a Leiningen task with the projects managed dependencies used as its dependencies."
+  [project args]
+  (let [dependencies (->> (:managed-dependencies project)
+                          (map (fn [[name _ & rest]]
+                                 (into [name nil] rest))))
+        project (project/merge-profiles project [{:dependencies dependencies}])]
+    (lein/resolve-and-apply project args)))
+
+(defn deps-plus
+  "Extra tasks for analyzing dependencies"
+  {:subtasks [#'list #'list-save #'list-diff #'check-downgraded #'check-families #'check-pedantic
+              #'check-management-conflicts #'check-classpath-conflicts #'why #'who-shades]}
+  [project & [sub-task & args]]
+  (let [project (project/unmerge-profiles project [:base :user])]
+    (case sub-task
+      "list" (list project)
+      "list-save" (list-save project)
+      "list-diff" (list-diff project)
+      "check-downgraded" (check-downgraded project)
+      "check-families" (check-families project)
+      "check-pedantic" (apply check-pedantic project args)
+      "check-management-conflicts" (check-management-conflicts project)
+      "check-classpath-conflicts" (check-classpath-conflicts project)
+      "why" (apply why project args)
+      "who-shades" (apply who-shades project args)
+      "with-managed-deps" (with-managed-deps project args)
+      ;; Leiningen is supposed to translate "lein <task> help" into
+      ;; "lein help <task>" by default. For some reason it does not
+      ;; seem to be working.
+      "help" (lein/resolve-and-apply project (concat ["help" "deps-plus"] args))
+      nil (error project "Task required.")
+      (error project "Unknown task."))))
diff --git a/test-projects/classpath-conflict.clj b/test-projects/classpath-conflict.clj
new file mode 100644
index 0000000..2466977
--- /dev/null
+++ b/test-projects/classpath-conflict.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/classpath-conflict "0-SNAPSHOT"
+  :dependencies [[com.google.guava/guava "17.0"]
+                 [com.google.guava/guava-jdk5 "17.0"]])
diff --git a/test-projects/dependency-diff.clj b/test-projects/dependency-diff.clj
new file mode 100644
index 0000000..67c95c4
--- /dev/null
+++ b/test-projects/dependency-diff.clj
@@ -0,0 +1,10 @@
+(defproject deps-plus-tests/dependency-diff "0-SNAPSHOT"
+  :offline? true
+  :dependencies [[fixture/added "1.0"]
+                 [fixture/changed-classifier "1.0"]
+                 [fixture/changed-extension "1.0"]
+                 [fixture/changed-scope "1.0"]
+                 [fixture/downgraded "0.9"]
+                 ;; [fixture/removed "1.0"]
+                 [fixture/unmodified "1.0"]
+                 [fixture/upgraded "2.0"]])
diff --git a/test-projects/dependency-diff.list b/test-projects/dependency-diff.list
new file mode 100644
index 0000000..d2fae44
--- /dev/null
+++ b/test-projects/dependency-diff.list
@@ -0,0 +1,7 @@
+fixture:changed-classifier:jar:sources:1.0:compile
+fixture:changed-extension:tar.gz:1.0:compile
+fixture:changed-scope:jar:1.0:test
+fixture:downgraded:jar:1.0:compile
+fixture:removed:jar:1.0:compile
+fixture:unmodified:jar:1.0:compile
+fixture:upgraded:jar:1.0:compile
diff --git a/test-projects/dependency-list.clj b/test-projects/dependency-list.clj
new file mode 100644
index 0000000..4b51799
--- /dev/null
+++ b/test-projects/dependency-list.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/dependency-list "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-context "1.20.0"]
+                 [com.google.code.gson/gson "2.7"]])
diff --git a/test-projects/downgrade.clj b/test-projects/downgrade.clj
new file mode 100644
index 0000000..8182aa7
--- /dev/null
+++ b/test-projects/downgrade.clj
@@ -0,0 +1,4 @@
+(defproject deps-plus-tests/family-mismatch "0-SNAPSHOT"
+  ;; grpc-core 1.20 depends on gson 2.7
+  :managed-dependencies [[com.google.code.gson/gson "2.6"]]
+  :dependencies [[io.grpc/grpc-core "1.20.0"]])
diff --git a/test-projects/family-mismatch.clj b/test-projects/family-mismatch.clj
new file mode 100644
index 0000000..08199b9
--- /dev/null
+++ b/test-projects/family-mismatch.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/family-mismatch "0-SNAPSHOT"
+  :managed-dependencies [[io.grpc/grpc-context "1.19.0"]]
+  :dependencies [[io.grpc/grpc-core "1.20.0"]])
diff --git a/test-projects/management-conflict.clj b/test-projects/management-conflict.clj
new file mode 100644
index 0000000..17e14c5
--- /dev/null
+++ b/test-projects/management-conflict.clj
@@ -0,0 +1,5 @@
+(defproject deps-plus-tests/management-conflict "0-SNAPSHOT"
+  :managed-dependencies [[io.grpc/grpc-api "1.21.0"]
+                         [io.grpc/grpc-core "1.21.0"]]
+  :dependencies [[io.grpc/grpc-api "1.22.0"]
+                 [io.grpc/grpc-core]])
diff --git a/test-projects/pedantic-range.clj b/test-projects/pedantic-range.clj
new file mode 100644
index 0000000..42d6608
--- /dev/null
+++ b/test-projects/pedantic-range.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/pedantic-range "0-SNAPSHOT"
+  ;; grpc-grpclb has a version ranged dependency on grpc-core
+  :dependencies [[io.grpc/grpc-grpclb "1.19.0"]])
diff --git a/test-projects/pedantic-version-conflict.clj b/test-projects/pedantic-version-conflict.clj
new file mode 100644
index 0000000..7d0f4d1
--- /dev/null
+++ b/test-projects/pedantic-version-conflict.clj
@@ -0,0 +1,3 @@
+(defproject deps-plus-tests/pedantic-version-conflict "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-protobuf "1.20.0"]
+                 [io.grpc/grpc-grpclb "1.19.0"]])
diff --git a/test-projects/shaded-dependencies.clj b/test-projects/shaded-dependencies.clj
new file mode 100644
index 0000000..63da9e2
--- /dev/null
+++ b/test-projects/shaded-dependencies.clj
@@ -0,0 +1,7 @@
+(defproject deps-plus-tests/classpath-conflict "0-SNAPSHOT"
+  :dependencies [[io.grpc/grpc-netty-shaded "1.20.0"]
+                 ;; Included to ensure we also have unshaded copies of Netty
+                 ;; dependencies to search. We do not want who-shades to return
+                 ;; self matches (e.g. netty-common does not shade netty-common).
+                 [io.grpc/grpc-netty "1.20.0"]
+                 [io.honeycomb.libhoney/libhoney-java "1.3.1"]])
diff --git a/test/circleci/deps_plus/core_test.clj b/test/circleci/deps_plus/core_test.clj
new file mode 100644
index 0000000..65f3af3
--- /dev/null
+++ b/test/circleci/deps_plus/core_test.clj
@@ -0,0 +1,274 @@
+(ns circleci.deps-plus.core-test
+  (:require [clojure.test :refer (deftest testing is use-fixtures)])
+  (:require [circleci.deps-plus.core :as deps]
+            [leiningen.core.project :as lein-project]
+            [clojure.string :as str])
+  (:import (org.apache.maven.artifact.versioning ComparableVersion)
+           (java.io StringWriter)))
+
+;; Needed when not running with :eval-in :leiningen (which would do this for us)
+(use-fixtures :once
+  (fn [f]
+    (lein-project/ensure-dynamic-classloader)
+    (f)))
+
+(deftest check-families-tests
+  (testing "check-families*"
+    (let [project (lein-project/read "test-projects/family-mismatch.clj")
+          problems (into [] (deps/check-families* project))
+          [parent child] (first problems)]
+      (is (= 1 (count problems)))
+      (is (= "grpc-core" (:artifact-id parent)))
+      (is (= "grpc-context" (:artifact-id child)))
+      (is (= "1.20.0" (str (:version parent))))
+      (is (= "1.19.0" (str (:version child)))))))
+
+(deftest check-downgraded-tests
+  (testing "check-downgraded*"
+    (let [project (lein-project/read "test-projects/downgrade.clj")
+          problems (into [] (deps/check-downgraded* project))
+          [dep max-version] (first problems)]
+      (is (= 1 (count problems)))
+      (is (= "com.google.code.gson:gson:jar:2.6:compile" (str dep)))
+      (is (= "2.7" (str max-version))))))
+
+(deftest parse-coordinates
+  (testing "minimal coordinates"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (nil? (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "minimal coordinates with extension"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:tar.gz")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "tar.gz" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (nil? (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "basic coordinates"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:1.20.0")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "with scope"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:1.20.0:provided")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (= "provided" (:scope (meta coord))))))
+
+  (testing "with classifier"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:sources:1.20.0")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "sources" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (nil? (:scope (meta coord))))))
+
+  (testing "with scope and classifier"
+    (let [coord (deps/parse-coordinates "io.grpc:grpc-core:jar:sources:1.20.0:provided")]
+      (is (= "io.grpc" (:group-id coord)))
+      (is (= "grpc-core" (:artifact-id coord)))
+      (is (= "jar" (:extension coord)))
+      (is (= "sources" (:classifier coord)))
+      (is (= (ComparableVersion. "1.20.0") (:version coord)))
+      (is (= "provided" (:scope (meta coord)))))))
+
+(deftest write-dependency-list
+  (let [project (lein-project/read "test-projects/dependency-list.clj" [])
+        buffer (StringWriter.)
+        _ (deps/write-dependency-list project buffer)
+        lines (str/split-lines (str buffer))]
+    (is (= "com.google.code.gson:gson:jar:2.7:compile" (first lines)))
+    (is (= "io.grpc:grpc-context:jar:1.20.0:compile" (second lines)))))
+
+(deftest diff-dependency-list
+  (let [project (lein-project/read "test-projects/dependency-diff.clj" [])
+        list (slurp "test-projects/dependency-diff.list")
+        buffer (StringWriter.)
+        _ (deps/diff-dependency-list project list buffer)
+        lines (str/split-lines (str buffer))]
+    (is (= [";; Added"
+            "fixture:added:jar:1.0:compile"
+            "fixture:changed-classifier:jar:1.0:compile"
+            "fixture:changed-extension:jar:1.0:compile"
+            ""
+            ";; Removed"
+            "fixture:changed-classifier:jar:sources:1.0:compile"
+            "fixture:changed-extension:tar.gz:1.0:compile"
+            "fixture:removed:jar:1.0:compile"
+            ""
+            ";; Upgraded"
+            "fixture:upgraded:jar:2.0:compile (1.0 -> 2.0)"
+            ""
+            ";; Downgraded"
+            "fixture:downgraded:jar:0.9:compile (1.0 -> 0.9)"
+            ""
+            ";; Rescoped"
+            "fixture:changed-scope:jar:1.0:compile (test -> compile)"]
+           lines)))
+  (testing "empty sections aren't included"
+    (let [project (lein-project/read "test-projects/dependency-diff.clj" [])
+          buffer (StringWriter.)
+          _ (deps/diff-dependency-list project "" buffer)
+          out (str buffer)]
+      (is (= [";; Added"]
+             (filter (fn [line] (str/starts-with? line ";; "))
+                     (str/split-lines out)))))))
+
+(deftest find-pedantic-conflicts-tests
+  (testing "version conflict"
+    (let [project (lein-project/read "test-projects/pedantic-version-conflict.clj" [])
+          conflicts (deps/find-pedantic-conflicts project)]
+      (is (= 1 (count conflicts)))
+      (is (= "io.grpc:grpc-core:jar:1.19.0:compile" (str (first (first conflicts)))))))
+
+  (testing "version range"
+    (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+          conflicts (deps/find-pedantic-conflicts project)]
+      (is (= 1 (count conflicts)))
+      (is (= "io.grpc:grpc-core:jar:1.19.0:compile" (str (first (first conflicts))))))))
+
+(deftest parse-dependency-list-test
+  (testing "empty list"
+    (let [deps (#'deps/parse-dependency-list "")]
+      (is (empty? deps))))
+
+  (testing "one dependency"
+    (let [deps (#'deps/parse-dependency-list "io.grpc:grpc-core:jar:1.19.0:compile")
+          {:keys [group-id artifact-id extension classifier version]} (first deps)]
+      (is (= 1 (count deps)))
+      (is (= "io.grpc" group-id))
+      (is (= "grpc-core" artifact-id))
+      (is (= "jar" extension))
+      (is (= "" classifier))
+      (is (= (ComparableVersion. "1.19.0") version))
+      (is (= "compile" (:scope (meta (first deps)))))))
+
+  (testing "multiple dependencies"
+    (let [deps (#'deps/parse-dependency-list (str "io.grpc:grpc-core:jar:1.19.0:compile\n"
+                                             "io.grpc:grpc-api:jar:1.21.0:compile\n"))]
+      (is (= 2 (count deps)))
+      (let [{:keys [group-id artifact-id extension classifier version]} (first deps)]
+        (is (= "io.grpc" group-id))
+        (is (= "grpc-core" artifact-id))
+        (is (= "jar" extension))
+        (is (= "" classifier))
+        (is (= (ComparableVersion. "1.19.0") version))
+        (is (= "compile" (:scope (meta (first deps))))))
+      (let [{:keys [group-id artifact-id extension classifier version]} (second deps)]
+        (is (= "io.grpc" group-id))
+        (is (= "grpc-api" artifact-id))
+        (is (= "jar" extension))
+        (is (= "" classifier))
+        (is (= (ComparableVersion. "1.21.0") version))
+        (is (= "compile" (:scope (meta (first deps)))))))))
+
+(deftest check-management-conflicts-tests
+  (testing "management conflict"
+    (let [project (lein-project/read "test-projects/management-conflict.clj" [])
+          conflicts (deps/check-management-conflicts* project)]
+      (is (= 1 (count conflicts)))
+      (let [[dep managed] (first conflicts)]
+        (is (= "io.grpc:grpc-api:jar:1.22.0" (str dep)))
+        (is (= "io.grpc:grpc-api:jar:1.21.0" (str managed)))))))
+
+(deftest resolve-dependencies
+  (testing "standard resolution includes ranges"
+    (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+          resolved (deps/resolve-dependencies project)
+          grpc-cores (->> (vals resolved)
+                          (mapcat identity)
+                          (filter #(= "grpc-core" (:artifact-id %))))]
+      (is (< 0 (count grpc-cores)))
+      ;; Every copy should be identical and should therefore have a version range.
+      (is (every? (comp :version-range meta) grpc-cores))
+      (is (= "[1.19.0,1.19.0]" (str (:version-range (meta (first grpc-cores))))))))
+
+  (testing "full-graph resolution includes ranges"
+      (let [project (lein-project/read "test-projects/pedantic-range.clj" [])
+            resolved (deps/resolve-dependencies project :full-graph)
+            grpc-cores (->> (vals resolved)
+                            (mapcat identity)
+                            (filter #(= "grpc-core" (:artifact-id %))))]
+        ;; We should have copies both with and without version ranges
+        (is (< 0 (count (filter (comp :version-range meta) grpc-cores))))
+        (is (< 0 (count (remove (comp :version-range meta) grpc-cores))))))
+
+  (testing "standard resolution includes premanaged version"
+    (let [project (lein-project/read "test-projects/downgrade.clj" [])
+          resolved (deps/resolve-dependencies project)
+          gson (->> (keys resolved)
+                    (filter #(= "gson" (:artifact-id %)))
+                    (first))]
+      (is (some? gson))
+      (is (= "2.6" (str (:version gson))))
+      (is (= "2.7" (str (:premanaged-version (meta gson)))))))
+
+  (testing "full-graph resolution includes premanaged version"
+    (let [project (lein-project/read "test-projects/downgrade.clj" [])
+          resolved (deps/resolve-dependencies project :full-graph)
+          gson (->> (keys resolved)
+                    (filter #(= "gson" (:artifact-id %)))
+                    (first))]
+      (is (some? gson))
+      (is (= "2.6" (str (:version gson))))
+      (is (= "2.7" (str (:premanaged-version (meta gson))))))))
+
+(deftest check-classpath-conflicts-test
+  (let [project (lein-project/read "test-projects/classpath-conflict.clj" [])
+        conflicts (deps/check-classpath-conflicts* project)
+        conflicting-deps (get conflicts "com/google/common/base/Preconditions.class")]
+    (is (= 2 (count conflicting-deps)))
+    (is (= #{"guava" "guava-jdk5"} (into #{} (map :artifact-id conflicting-deps))))))
+
+(deftest who-shades
+  (testing "dependency only appearing as shaded"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "commons-codec:commons-codec")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.honeycomb.libhoney:libhoney-java:jar:1.3.1:compile" (str shader)))
+        (is (= "commons-codec:commons-codec:jar:1.9" (str shadee))))))
+
+  ;; This case validates that who-shades does not return self
+  ;; matches (e.g. netty-common does not shade itself).
+  (testing "dependency appearing as both shaded and standalone"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "io.netty:netty-common")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.grpc:grpc-netty-shaded:jar:1.20.0:compile" (str shader)))
+        (is (= "io.netty:netty-common:jar:4.1.34.Final" (str shadee))))))
+
+  ;; This behavior could be changed, but at least at the moment the version
+  ;; number in the target is ignored.
+  (testing "dependency only appearing as shaded"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "commons-codec:commons-codec:jar:1.2.3")
+          result  (deps/who-shades* project target)]
+      (is (= 1 (count result)))
+      (let [[shader shadee] (first result)]
+        (is (= "io.honeycomb.libhoney:libhoney-java:jar:1.3.1:compile" (str shader)))
+        (is (= "commons-codec:commons-codec:jar:1.9" (str shadee))))))
+
+  (testing "no matches"
+    (let [project (lein-project/read "test-projects/shaded-dependencies.clj" [])
+          target  (deps/parse-coordinates "super-fake:so-fake:jar:what-big-versions")
+          result  (deps/who-shades* project target)]
+      (is (empty? result)))))
diff --git a/test/circleci/deps_plus/graph_test.clj b/test/circleci/deps_plus/graph_test.clj
new file mode 100644
index 0000000..0aa78b5
--- /dev/null
+++ b/test/circleci/deps_plus/graph_test.clj
@@ -0,0 +1,58 @@
+(ns circleci.deps-plus.graph-test
+  (:require [clojure.test :refer [deftest is testing]])
+  (:require [circleci.deps-plus.graph :as graph]))
+
+(defn- neighbors-fn [g]
+  (fn [n] (sort (get g n))))
+
+(deftest graph-seq
+  (testing "break cycle"
+    (let [g {:a #{:b}
+             :b #{:a}}]
+      (is (= [:a :b]
+            (into [] (graph/graph-seq (neighbors-fn g) :a))))))
+
+  (testing "breadth first"
+    (let [g {:a #{:b :c}
+             :b #{:d :e}
+             :c #{:f :g}}]
+      (is (= [:a :b :c :d :e :f :g]
+            (into [] (graph/graph-seq (neighbors-fn g) :a))))))
+
+  (testing "remove duplicates"
+    (let [g {:a #{:b :c}
+             :b #{:c}}]
+      (is (= [:a :b :c]
+            (into [] (graph/graph-seq (neighbors-fn g) :a)))))))
+
+(deftest graph-path-seq
+  (testing "break cycle"
+    (let [g {:a #{:b}
+             :b #{:c}
+             :c #{:b}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :b :c]]
+             (into [] (graph/graph-path-seq (neighbors-fn g) :a))))))
+
+  (testing "breadth first"
+    (let [g {:a #{:b :c}
+             :b #{:d :e}
+             :c #{:f :g}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :c]
+              [:a :b :d]
+              [:a :b :e]
+              [:a :c :f]
+              [:a :c :g]]
+             (into [] (graph/graph-path-seq (neighbors-fn g) :a))))))
+
+  (testing "follow all paths"
+    (let [g {:a #{:b :c}
+             :b #{:c}}]
+      (is (= [[:a]
+              [:a :b]
+              [:a :c]
+              [:a :b :c]]
+            (into [] (graph/graph-path-seq (neighbors-fn g) :a)))))))