diff --git a/CHANGELOG.md b/CHANGELOG.md index 725aaad..06afa2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Introduce `neil dep upgrade` API for upgrading existing dependencies + ## 0.1.46 - Add `neil version` subcommands diff --git a/neil b/neil index bd1dd76..442c539 100755 --- a/neil +++ b/neil @@ -26,9 +26,14 @@ (defn url-encode [s] (URLEncoder/encode s "UTF-8")) +(def dev-github-user (System/getenv "BABASHKA_NEIL_DEV_GITHUB_USER")) +(def dev-github-token (System/getenv "BABASHKA_NEIL_DEV_GITHUB_TOKEN")) + (def curl-opts - {:throw false - :compressed (not (fs/windows?))}) + (merge {:throw false + :compressed (not (fs/windows?))} + (when (and dev-github-user dev-github-token) + {:basic-auth [dev-github-user dev-github-token]}))) (defn curl-get-json [url] (-> (curl/get url curl-opts) @@ -790,7 +795,8 @@ using the [major|minor|patch] subcommands. :deps-file {:ref "" :desc "Add to instead of deps.edn." :default "deps.edn"} - :limit {:coerce :long}}) + :limit {:coerce :long} + :dry-run {:coerce :boolean}}) (def windows? (fs/windows?)) @@ -1181,6 +1187,81 @@ will return libraries with 'test framework' in their description."))) :version (:version search-result) :description (pr-str (:description search-result))))))) + +(defn lib+current->latest + "Expects a lib symbol like `'babashka/fs` and a dep description + like `{:mvn/version \"some-version\"}` or `{:git/sha \"sha-blah\"}`. + + Returns a dep description of the same form (a map with `:mvn/version` or `:git/sha`) + with the latest version or latest sha." + [lib current-dep] + (cond (:git/sha current-dep) + (when-let [sha (git/latest-github-sha lib)] + {:git/sha sha}) + + (:mvn/version current-dep) + (when-let [version (or (latest-clojars-version lib) + (latest-mvn-version lib))] + {:mvn/version version}))) + +(defn do-dep-upgrade + "Updates the deps version in deps.edn for a single lib with the version passed in `latest`. + First compares `current` and `latest`. + Supports `:dry-run` in the passed `opts`." + [opts lib current {:keys [git/sha mvn/version] :as _latest}] + (when (or (and sha (not= (:git/sha current) sha)) + (and version (not= (:mvn/version current) version))) + (if (:dry-run opts) + (if sha + (println "Would upgrade" :lib lib :version (:git/sha current) :latest-sha sha) + (println "Would upgrade" :lib lib :version (:mvn/version current) :latest-version version)) + (do + (if sha + (println "Upgrading" :lib lib :version (:git/sha current) :latest-sha sha) + (println "Upgrading" :lib lib :version (:mvn/version current) :latest-version version)) + (dep-add {:opts (cond-> opts + lib (assoc :lib lib) + version (assoc :version version) + sha (assoc :sha sha))}))))) + +(defn dep-upgrade [{:keys [opts] :as _all}] + (if (:lib opts) + ;; upgrade a single dependency + (let [lib (:lib opts) + lib (symbol lib) + current (-> (edn-string opts) + edn/read-string + :deps + (get lib)) + latest (when current (lib+current->latest lib current))] + (cond (not current) + (binding [*out* *err*] + (println "Local dependency not found:" lib) + (println "Use `neil dep add` to add dependencies.") + (System/exit 1)) + + (and (not (:git/sha latest)) (not (:mvn/version latest))) + (binding [*out* *err*] + (println "No remote version found for" lib) + (System/exit 1)) + + :else (do-dep-upgrade opts lib current latest))) + + ;; upgrade multiple dependencies + (let [current-deps (-> (edn-string opts) + edn/read-string + :deps) + latest-deps (->> current-deps + (pmap (fn [[lib current]] + [lib (lib+current->latest lib current)])) + (filter (fn [[_lib new-version]] + (some? new-version))) + (into {}))] + (doseq [[lib current] current-deps] + (let [latest (get latest-deps lib)] + (do-dep-upgrade opts lib current latest)))))) + + (defn print-help [_] (println (str/trim " Usage: neil @@ -1205,6 +1286,14 @@ dep search: Search Clojars for a string in any attribute of an artifact Run `neil dep search --help` to see all options. + upgrade: Upgrade all libs in the deps.edn file. + Supports --lib or :lib for upgrading a single, specified lib. + Supports --dry-run for printing updates without updating the deps.edn file. + Ex: `neil dep upgrade` - upgrade all deps. + Ex: `neil dep upgrade --dry-run` - print deps that would be upgraded. + Ex: `neil dep upgrade :lib clj-kondo/clj-kondo` - update a single dep. + update: Alias for `upgrade`. + license list Lists commonly-used licenses available to be added to project. Takes an optional search string to filter results. search Alias for `list` @@ -1282,6 +1371,8 @@ test {:cmds ["dep" "versions"] :fn dep-versions :args->opts [:lib]} {:cmds ["dep" "add"] :fn dep-add :args->opts [:lib]} {:cmds ["dep" "search"] :fn dep-search :args->opts [:search-term]} + {:cmds ["dep" "upgrade"] :fn dep-upgrade} + {:cmds ["dep" "update"] :fn dep-upgrade} ;; supported as an alias {:cmds ["license" "list"] :fn license-search :args->opts [:search-term]} {:cmds ["license" "search"] :fn license-search :args->opts [:search-term]} {:cmds ["license" "add"] :fn add-license :args->opts [:license]} diff --git a/src/babashka/neil.clj b/src/babashka/neil.clj index d624631..650a8db 100644 --- a/src/babashka/neil.clj +++ b/src/babashka/neil.clj @@ -27,7 +27,8 @@ :deps-file {:ref "" :desc "Add to instead of deps.edn." :default "deps.edn"} - :limit {:coerce :long}}) + :limit {:coerce :long} + :dry-run {:coerce :boolean}}) (def windows? (fs/windows?)) @@ -418,6 +419,81 @@ will return libraries with 'test framework' in their description."))) :version (:version search-result) :description (pr-str (:description search-result))))))) + +(defn lib+current->latest + "Expects a lib symbol like `'babashka/fs` and a dep description + like `{:mvn/version \"some-version\"}` or `{:git/sha \"sha-blah\"}`. + + Returns a dep description of the same form (a map with `:mvn/version` or `:git/sha`) + with the latest version or latest sha." + [lib current-dep] + (cond (:git/sha current-dep) + (when-let [sha (git/latest-github-sha lib)] + {:git/sha sha}) + + (:mvn/version current-dep) + (when-let [version (or (latest-clojars-version lib) + (latest-mvn-version lib))] + {:mvn/version version}))) + +(defn do-dep-upgrade + "Updates the deps version in deps.edn for a single lib with the version passed in `latest`. + First compares `current` and `latest`. + Supports `:dry-run` in the passed `opts`." + [opts lib current {:keys [git/sha mvn/version] :as _latest}] + (when (or (and sha (not= (:git/sha current) sha)) + (and version (not= (:mvn/version current) version))) + (if (:dry-run opts) + (if sha + (println "Would upgrade" :lib lib :version (:git/sha current) :latest-sha sha) + (println "Would upgrade" :lib lib :version (:mvn/version current) :latest-version version)) + (do + (if sha + (println "Upgrading" :lib lib :version (:git/sha current) :latest-sha sha) + (println "Upgrading" :lib lib :version (:mvn/version current) :latest-version version)) + (dep-add {:opts (cond-> opts + lib (assoc :lib lib) + version (assoc :version version) + sha (assoc :sha sha))}))))) + +(defn dep-upgrade [{:keys [opts] :as _all}] + (if (:lib opts) + ;; upgrade a single dependency + (let [lib (:lib opts) + lib (symbol lib) + current (-> (edn-string opts) + edn/read-string + :deps + (get lib)) + latest (when current (lib+current->latest lib current))] + (cond (not current) + (binding [*out* *err*] + (println "Local dependency not found:" lib) + (println "Use `neil dep add` to add dependencies.") + (System/exit 1)) + + (and (not (:git/sha latest)) (not (:mvn/version latest))) + (binding [*out* *err*] + (println "No remote version found for" lib) + (System/exit 1)) + + :else (do-dep-upgrade opts lib current latest))) + + ;; upgrade multiple dependencies + (let [current-deps (-> (edn-string opts) + edn/read-string + :deps) + latest-deps (->> current-deps + (pmap (fn [[lib current]] + [lib (lib+current->latest lib current)])) + (filter (fn [[_lib new-version]] + (some? new-version))) + (into {}))] + (doseq [[lib current] current-deps] + (let [latest (get latest-deps lib)] + (do-dep-upgrade opts lib current latest)))))) + + (defn print-help [_] (println (str/trim " Usage: neil @@ -442,6 +518,14 @@ dep search: Search Clojars for a string in any attribute of an artifact Run `neil dep search --help` to see all options. + upgrade: Upgrade all libs in the deps.edn file. + Supports --lib or :lib for upgrading a single, specified lib. + Supports --dry-run for printing updates without updating the deps.edn file. + Ex: `neil dep upgrade` - upgrade all deps. + Ex: `neil dep upgrade --dry-run` - print deps that would be upgraded. + Ex: `neil dep upgrade :lib clj-kondo/clj-kondo` - update a single dep. + update: Alias for `upgrade`. + license list Lists commonly-used licenses available to be added to project. Takes an optional search string to filter results. search Alias for `list` @@ -519,6 +603,8 @@ test {:cmds ["dep" "versions"] :fn dep-versions :args->opts [:lib]} {:cmds ["dep" "add"] :fn dep-add :args->opts [:lib]} {:cmds ["dep" "search"] :fn dep-search :args->opts [:search-term]} + {:cmds ["dep" "upgrade"] :fn dep-upgrade} + {:cmds ["dep" "update"] :fn dep-upgrade} ;; supported as an alias {:cmds ["license" "list"] :fn license-search :args->opts [:search-term]} {:cmds ["license" "search"] :fn license-search :args->opts [:search-term]} {:cmds ["license" "add"] :fn add-license :args->opts [:license]} diff --git a/src/babashka/neil/curl.clj b/src/babashka/neil/curl.clj index 60084ff..d7e9c71 100644 --- a/src/babashka/neil/curl.clj +++ b/src/babashka/neil/curl.clj @@ -9,9 +9,14 @@ (defn url-encode [s] (URLEncoder/encode s "UTF-8")) +(def dev-github-user (System/getenv "BABASHKA_NEIL_DEV_GITHUB_USER")) +(def dev-github-token (System/getenv "BABASHKA_NEIL_DEV_GITHUB_TOKEN")) + (def curl-opts - {:throw false - :compressed (not (fs/windows?))}) + (merge {:throw false + :compressed (not (fs/windows?))} + (when (and dev-github-user dev-github-token) + {:basic-auth [dev-github-user dev-github-token]}))) (defn curl-get-json [url] (-> (curl/get url curl-opts) diff --git a/test/babashka/neil/dep_upgrade_test.clj b/test/babashka/neil/dep_upgrade_test.clj new file mode 100644 index 0000000..70fa7a3 --- /dev/null +++ b/test/babashka/neil/dep_upgrade_test.clj @@ -0,0 +1,89 @@ +(ns babashka.neil.dep-upgrade-test + (:require + [babashka.neil.test-util :as test-util] + [clojure.test :as t :refer [deftest is testing]] + [clojure.edn :as edn])) + +(def test-file-path (str (test-util/test-file "deps.edn"))) +(defn get-dep-version [dep-name] + (-> test-file-path slurp edn/read-string :deps (get dep-name))) + +(deftest dep-upgrade-test + (testing "a fresh project is up-to-date" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo" :deps-file test-file-path) + (let [clj-kondo-version-original (get-dep-version 'clj-kondo/clj-kondo)] + (test-util/neil "dep upgrade" :deps-file test-file-path) + (is (= clj-kondo-version-original (get-dep-version 'clj-kondo/clj-kondo))))) + + (testing "an old dependency can be upgraded, and --dry-run is respected" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo :version 2022.01.01" :deps-file test-file-path) + (let [clj-kondo-version-original (get-dep-version 'clj-kondo/clj-kondo)] + + ;; should be the same version after --dry-run + (test-util/neil "dep upgrade" :deps-file test-file-path :dry-run true) + (is (= clj-kondo-version-original (get-dep-version 'clj-kondo/clj-kondo))) + + ;; after a non-dry-run, the version should be changed + (test-util/neil "dep upgrade" :deps-file test-file-path) + (is (not (= clj-kondo-version-original (get-dep-version 'clj-kondo/clj-kondo))))))) + +(deftest dep-upgrade-test-one-lib + (testing "specifying :lib only updates one dep" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo :version 2022.01.01" :deps-file test-file-path) + (test-util/neil "dep add :lib babashka/fs :version 0.0.1" :deps-file test-file-path) + (let [clj-kondo-original (get-dep-version 'clj-kondo/clj-kondo) + fs-original (get-dep-version 'babashka/fs)] + (test-util/neil "dep upgrade :lib clj-kondo/clj-kondo" :deps-file test-file-path) + (let [clj-kondo-upgraded (get-dep-version 'clj-kondo/clj-kondo) + fs-upgraded (get-dep-version 'babashka/fs)] + (is (not (= clj-kondo-original clj-kondo-upgraded))) + (is (= fs-original fs-upgraded)))))) + +(deftest dep-upgrade-test-maintain-dep-source + (testing "upgrading a :git/sha dep should maintain :git/sha" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo :latest-sha true" :deps-file test-file-path) + (let [clj-kondo-original (get-dep-version 'clj-kondo/clj-kondo)] + ;; this upgrade should return the same latest sha, NOT a :mvn/version + (test-util/neil "dep upgrade" :deps-file test-file-path) + (let [{:keys [git/url git/sha]} (get-dep-version 'clj-kondo/clj-kondo)] + (is url) + (is sha) + (is (= clj-kondo-original (get-dep-version 'clj-kondo/clj-kondo)))))) + + (testing "upgrading an older :git/sha dep should set the latest :git/sha" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo :sha 6ffc3934cb83d2c4fff16d84198c73b40cd8a078" + :deps-file test-file-path) + (let [clj-kondo-original (get-dep-version 'clj-kondo/clj-kondo)] + ;; here we upgrade and then assert that the sha is different, but still :git/sha based + (test-util/neil "dep upgrade" :deps-file test-file-path) + (let [{:keys [git/url git/sha]} (get-dep-version 'clj-kondo/clj-kondo)] + (is url) + (is sha) + ;; should be a different sha + (is (not (= sha (:git/sha clj-kondo-original))))))) + + (testing "upgrading a single lib should also maintain :git/url and sha" + (spit test-file-path "{}") + (test-util/neil "dep add :lib clj-kondo/clj-kondo :sha 6ffc3934cb83d2c4fff16d84198c73b40cd8a078" + :deps-file test-file-path) + (test-util/neil "dep add :lib babashka/fs :sha 791009052fe8916b4e10e55732622a69250c7598" + :deps-file test-file-path) + + (let [clj-kondo-original (get-dep-version 'clj-kondo/clj-kondo) + fs-original (get-dep-version 'babashka/fs)] + (test-util/neil "dep upgrade :lib babashka/fs" :deps-file test-file-path) + (let [clj-kondo-upgraded (get-dep-version 'clj-kondo/clj-kondo) + fs-upgraded (get-dep-version 'babashka/fs)] + (is (:git/sha clj-kondo-upgraded)) + (is (:git/url clj-kondo-upgraded)) + (is (:git/sha fs-upgraded)) + (is (:git/url fs-upgraded)) + ;; should be unchanged + (is (= clj-kondo-original clj-kondo-upgraded)) + ;; should be a different sha + (is (not (= fs-original fs-upgraded))))))) diff --git a/test/babashka/neil/test_runner.clj b/test/babashka/neil/test_runner.clj index f22539b..5b69a68 100644 --- a/test/babashka/neil/test_runner.clj +++ b/test/babashka/neil/test_runner.clj @@ -8,7 +8,8 @@ (def test-namespaces '[tests - babashka.neil.version-test]) + babashka.neil.version-test + babashka.neil.dep-upgrade-test]) (doseq [ns test-namespaces] (require ns)) diff --git a/test/babashka/neil/test_util.clj b/test/babashka/neil/test_util.clj index 02d570c..ccd9146 100644 --- a/test/babashka/neil/test_util.clj +++ b/test/babashka/neil/test_util.clj @@ -22,12 +22,13 @@ (defn set-deps-edn! [x] (spit (test-file "deps.edn") (pr-str x))) -(defn neil [cli-args & {:keys [out] :or {out :string}}] - (let [deps-file (str (test-file "deps.edn")) - cli-args' (concat (if (string? cli-args) - (process/tokenize cli-args) - cli-args) - [:deps-file deps-file])] +(defn neil [cli-args & {:keys [deps-file dry-run out] :or {out :string}}] + (let [backup-deps-file (str (test-file "deps.edn")) + cli-args' (concat (if (string? cli-args) + (process/tokenize cli-args) + cli-args) + [:deps-file (or deps-file backup-deps-file)] + (when dry-run [:dry-run "true"]))] (binding [*command-line-args* cli-args'] (let [s (with-out-str (tasks/exec `neil-main/-main))] {:out (if (#{:edn} out) (edn/read-string s) (str/trim s))}))))