From a702fc1cd3007ee4b2d7c5674d460d0edf8a9d89 Mon Sep 17 00:00:00 2001 From: Nick Nichols Date: Sun, 13 Oct 2024 11:15:28 -0500 Subject: [PATCH] Extend config locations (#114) * Allow sealog to be configured in project.clj * Allow sealog to be configured in the .wallbrew directory * Update docs * Add alternative config locations * [Format] Auto-formatting --------- Co-authored-by: nnichols --- .bouncer/config.edn | 2 +- .sealog/changes/1-8-0.edn | 12 +++ CHANGELOG.md | 7 ++ Makefile | 2 +- README.md | 14 ++- pom.xml | 6 +- project.clj | 2 +- src/leiningen/sealog.clj | 8 +- src/leiningen/sealog/api.clj | 55 +++++------ src/leiningen/sealog/impl.clj | 133 ++++++++++---------------- src/leiningen/sealog/impl/io.clj | 69 +++++++++++++ src/leiningen/sealog/types/config.clj | 5 + 12 files changed, 191 insertions(+), 124 deletions(-) create mode 100644 .sealog/changes/1-8-0.edn create mode 100644 src/leiningen/sealog/impl/io.clj diff --git a/.bouncer/config.edn b/.bouncer/config.edn index 11f24dd..bf26498 100644 --- a/.bouncer/config.edn +++ b/.bouncer/config.edn @@ -1,7 +1,7 @@ {:comment "The default configuration for Wall Brew projects." :project-rules {:license {:comment "https://github.com/Wall-Brew-Co/open-source?tab=readme-ov-file#licensing" :disabled false} - :plugins {:disabled false, + :plugins {:disabled false :available-plugins [{:comment "https://github.com/Wall-Brew-Co/rebroadcast?tab=readme-ov-file#sealog-configuration" :plugin-name "com.wallbrew/lein-sealog" :plugin-version "1.6.0"} diff --git a/.sealog/changes/1-8-0.edn b/.sealog/changes/1-8-0.edn new file mode 100644 index 0000000..b7180ad --- /dev/null +++ b/.sealog/changes/1-8-0.edn @@ -0,0 +1,12 @@ +{:version {:major 1 + :minor 8 + :patch 0} + :version-type :semver3 + :changes {:added ["Support for loading configuration from `project.clj`." + "Support for loading configuration from `.wallbrew/sealog/config.edn`."] + :changed [] + :deprecated [] + :removed [] + :fixed [] + :security []} + :timestamp "2024-10-13T16:09:40.636206814Z"} diff --git a/CHANGELOG.md b/CHANGELOG.md index fbddd05..6404191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Table of Contents +* [1.8.0 - 2024-10-13](#180---2024-10-13) * [1.7.0 - 2024-09-21](#170---2024-09-21) * [1.6.0 - 2024-05-03](#160---2024-05-03) * [1.5.0 - 2024-05-03](#150---2024-05-03) @@ -18,6 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [1.0.1 - 2022-10-23](#101---2022-10-23) * [1.0.0 - 2022-10-23](#100---2022-10-23) +## 1.8.0 - 2024-10-13 + +* Added + * Support for loading configuration from `project.clj`. + * Support for loading configuration from `.wallbrew/sealog/config.edn`. + ## 1.7.0 - 2024-09-21 * Fixed diff --git a/Makefile b/Makefile index 4b1a5d7..11ad828 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ - +.PHONY: version/major version/minor version/patch changelog/render MAKE = make # These are the locations of the directories we'll use diff --git a/README.md b/README.md index 1cbd497..89714d9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Sealog is available as a leiningen plugin and can be downloaded from [clojars](h To install Sealog, add the following in your `:plugins` list in your `project.clj` file: ```clj -[com.wallbrew/lein-sealog "1.4.0"] +[com.wallbrew/lein-sealog "1.7.0"] ``` The first time you invoke this plugin, Leiningen will automatically fetch the dependency for you. @@ -39,11 +39,11 @@ From the root of your project directory, you may invoke the following commands: * `check` - To validate Sealog's configuration, changelog entry files, the changelog, and the project version * `help` - To view the help text of sealog or any command -Most commands will accept several options, which can be configured by the command line arguments passed in or by a configuration file located at `.sealog/config.edn`. +Most commands will accept several options, which can be configured by the command line arguments passed in or in [project configuration](#configuration). In all cases, the options will follow this order of precedence: 1. Arguments passed by command line -2. Values stored in `.sealog/config.edn` +2. Values stored in configuration 3. Default values in Sealog's implementation ### Initialize Sealog @@ -337,7 +337,13 @@ Empty change lists may be kept for consistency or removed. Many of Sealog's commands may be modified at execution time with additional arguments. Long-term decisions, such as where the Changelog should be written to, can also be configured in a static file. -As of version `1.1.0`, Sealog will inspect the local `.sealog/config.edn` file for configuration. +As of version `1.7.0`, Sealog will inspect the following locations for configuration. +The first location found will be used, and the others will be ignored: + +- The `:sealog` key in the project's `project.clj` file +- The `.sealog/config.edn` file, relative to the project's root +- The `.wallbrew/sealog/config.edn` file, relative to the project's root +- The default configuration that would be written by `lein sealog init` Sealog will create this file while executing `lein sealog init` if no prior Sealog configuration file is detected. The file will be created with the defaults outlined in this README. diff --git a/pom.xml b/pom.xml index 5b775f5..7e17399 100644 --- a/pom.xml +++ b/pom.xml @@ -4,10 +4,10 @@ com.wallbrew lein-sealog jar - 1.7.0 + 1.8.0 lein-sealog A Leiningen plugin for managing your changelog. - https://github.com/Wall-Brew-Co/common-beer-format + https://github.com/Wall-Brew-Co/lein-sealog MIT @@ -20,7 +20,7 @@ https://github.com/Wall-Brew-Co/lein-sealog scm:git:git://github.com/Wall-Brew-Co/lein-sealog.git scm:git:ssh://git@github.com/Wall-Brew-Co/lein-sealog.git - 31ce3c975205b4a4e2f213e5b7a24cf60b9e9c56 + 604d39fea1862e1c45710b238e5ecec831567913 src diff --git a/project.clj b/project.clj index 9c22cc0..79f5d6b 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject com.wallbrew/lein-sealog "1.7.0" +(defproject com.wallbrew/lein-sealog "1.8.0" :description "A Leiningen plugin for managing your changelog." :url "https://github.com/Wall-Brew-Co/lein-sealog" :license {:name "MIT" diff --git a/src/leiningen/sealog.clj b/src/leiningen/sealog.clj index 78cb9ec..b6f6c8f 100644 --- a/src/leiningen/sealog.clj +++ b/src/leiningen/sealog.clj @@ -140,10 +140,10 @@ (let [command (first args) options (rest args)] (case command - "init" (sealog/init options) - "bump" (sealog/bump-version options) - "render" (sealog/render-changelog options) - "insert" (sealog/insert-entry options) + "init" (sealog/init project options) + "bump" (sealog/bump-version project options) + "render" (sealog/render-changelog project options) + "insert" (sealog/insert-entry project options) "version" (sealog/display-version project options) "check" (sealog/check project options) "help" (help options) diff --git a/src/leiningen/sealog/api.clj b/src/leiningen/sealog/api.clj index 719fa16..7200e37 100644 --- a/src/leiningen/sealog/api.clj +++ b/src/leiningen/sealog/api.clj @@ -4,15 +4,16 @@ This namespace contains the public functions that are called by the leiningen" (:require [leiningen.core.main :as main] [leiningen.sealog.impl :as impl] + [leiningen.sealog.impl.io :as io] [leiningen.sealog.types.changelog :as changelog] [leiningen.sealog.types.commands :as commands])) (defn init "Create a new changelog directory." - [_opts] - (let [configuration (impl/load-config!)] - (if (impl/sealog-configured?) + [project _options] + (let [configuration (impl/load-config! project)] + (if (impl/sealog-configured? project) (main/info "Sealog configuration found.") (impl/configure! configuration)) (if (impl/sealog-initialized? configuration) @@ -22,52 +23,52 @@ (defn render-changelog "Render the changelog to the target file." - [opts] - (let [configuration (impl/load-config!) - filepath (or (first opts) (:changelog-filename configuration)) + [project options] + (let [configuration (impl/load-config! project) + filepath (or (first options) (:changelog-filename configuration)) changelog (impl/load-changelog-entry-directory! configuration) sorted-changelog (changelog/sort-changelog-descending changelog) changes (impl/render-changelog sorted-changelog)] - (impl/write-file! filepath changes) + (io/write-file! filepath changes) (main/info (format "Wrote changelog to: %s" filepath)))) (defn bump-version "Create a new changelog entry for the version bump." - [opts] - (let [configuration (impl/load-config!) + [project options] + (let [configuration (impl/load-config! project) changelog-directory (:changelog-entry-directory configuration) - bump-type (keyword (or (first opts) "patch")) + bump-type (keyword (or (first options) "patch")) changelog (impl/load-changelog-entry-directory! configuration) new-entry (update (changelog/bump changelog bump-type) :timestamp str) new-file (str changelog-directory (changelog/render-filename new-entry))] - (impl/write-edn-file! new-file new-entry configuration) + (io/write-edn-file! new-file new-entry configuration) (println (format "Created new changelog entry: %s" new-file)))) (defn insert-entry "Insert a note of a specified change type into the most current change file." - [opts] - (let [change-type (first opts) - change-notes (rest opts)] + [project options] + (let [change-type (first options) + change-notes (rest options)] (if (commands/valid-insert-command? change-type change-notes) - (let [configuration (impl/load-config!) + (let [configuration (impl/load-config! project) changelog-directory (:changelog-entry-directory configuration) changelog (impl/load-changelog-entry-directory! configuration) latest-changelog-entry (changelog/max-version changelog) updated-changelog-entry (changelog/insert latest-changelog-entry change-type change-notes) updated-entry-filename (str changelog-directory (changelog/render-filename updated-changelog-entry))] - (impl/write-edn-file! updated-entry-filename updated-changelog-entry configuration) - (println (format "Updated changelog entries: %s" updated-entry-filename))) - (System/exit 1)))) + (io/write-edn-file! updated-entry-filename updated-changelog-entry configuration) + (main/info (format "Updated changelog entries: %s" updated-entry-filename))) + (main/exit 1)))) (defn display-version "Display information about the current version." - [project opts] - (let [version-source (first opts)] + [project options] + (let [version-source (first options)] (if (commands/valid-version-command? version-source) - (let [configuration (impl/load-config!) + (let [configuration (impl/load-config! project) changelog (impl/load-changelog-entry-directory! configuration) latest-changelog-entry (changelog/max-version changelog) sealog-version (changelog/render-version latest-changelog-entry) @@ -77,14 +78,14 @@ "sealog" (main/info sealog-version) (do (main/info (str "project.clj: " leiningen-version)) (main/info (str "sealog: " sealog-version))))) - (System/exit 1)))) + (main/exit 1)))) (defn check "Check the current configuration, changelog entries, and the current project version." [project _opts] - (if (impl/valid-configuration?) - (let [configuration (impl/load-config!) + (if (impl/valid-configuration? project) + (let [configuration (impl/load-config! project) some-changelog-entries? (impl/changelog-entry-directory-is-not-empty? configuration) all-changelog-entries-valid? (impl/changelog-directory-only-contains-valid-files? configuration) same-version-type? (impl/all-changelog-entries-use-same-version-type? configuration) @@ -97,6 +98,6 @@ changelog-rendered? (impl/rendered-changelog-contains-all-changelog-entries? configuration)] (if (and versions-match? changelog-rendered?) (main/info "All checks passed") - (System/exit 1))) - (System/exit 1))) - (System/exit 1))) + (main/exit 1))) + (main/exit 1))) + (main/exit 1))) diff --git a/src/leiningen/sealog/impl.clj b/src/leiningen/sealog/impl.clj index 970ec48..ed8fbd2 100644 --- a/src/leiningen/sealog/impl.clj +++ b/src/leiningen/sealog/impl.clj @@ -1,87 +1,54 @@ (ns leiningen.sealog.impl (:require [clojure.edn :as edn] - [clojure.java.io :as io] - [clojure.pprint :as pp] [clojure.spec.alpha :as spec] [clojure.string :as str] [com.wallbrew.spoon.core :as spoon] [leiningen.core.main :as main] + [leiningen.sealog.impl.io :as io] [leiningen.sealog.types.changelog :as changelog] [leiningen.sealog.types.config :as config] [spec-tools.core :as st])) -(defn list-all-files - "Recursively list all files within a directory." - [path] - (let [files (io/file path) - dir? (fn [f] (.isDirectory f)) - ->path (fn [f] (.getPath f))] - (if (dir? files) - (mapv ->path (filter (complement dir?) (file-seq files))) - (throw (ex-info "Not a directory!" {:path path}))))) - - -(defn file-exists? - "Returns true if `path` points to a valid file" - [path] - (and (string? path) - (.exists (io/file path)))) - - -(defn write-file! - "This is a wrapper around `spit` that logs the filename to the console." - [filename content] - (main/info (format "Writing to %s" filename)) - (spit filename content)) - - -(defn write-edn-file! - "Write the contents to a file as EDN." - [filename content {:keys [pretty-print-edn?]}] - (if pretty-print-edn? - (write-file! filename (with-out-str (pp/pprint content))) - (write-file! filename content))) - - -(defn read-file! - "This is a wrapper around `slurp` that logs the filename to the console." - [filename] - (main/info (format "Reading from %s" filename)) - (slurp filename)) - - -(defn read-edn-file! - "Reads an EDN file and returns the contents as a map. - Throws an exception if the file does not exist, or if the contents do not coerce" - [filename spec] - (if (file-exists? filename) - (let [file-content (edn/read-string (read-file! filename)) - contents (st/coerce spec file-content st/string-transformer)] - (if (spec/valid? spec contents) - contents - (throw (ex-info (str "Invalid file contents: " filename) - {:filename filename - :errors (spec/explain-data spec contents)})))) - (throw (ex-info "Not matching file exists!" - {:filename filename})))) +(defn select-config + "Select the configuration to use with the following precedence: + - The `:sealog` key in project.clj + - The configuration file in .sealog/config.edn + - The configuration file in .wallbrew/sealog/config.edn + - The default configuration" + [project] + (let [project-config (:sealog project) + config-file-exists? (io/file-exists? config/config-file) + backup-config-file-exists? (io/file-exists? config/backup-config-file)] + (cond + (map? project-config) project-config + config-file-exists? (io/read-edn-file! config/config-file ::config/config) + backup-config-file-exists? (io/read-edn-file! config/backup-config-file ::config/config) + :else (do (main/info "No configuration file found. Assuming default configuration.") + config/default-config)))) (defn load-config! - "Load the configuration file." - [] - (if (file-exists? config/config-file) - (read-edn-file! config/config-file ::config/config) - (do (main/info "No configuration file found. Assuming default configuration.") - config/default-config))) + "Load the configuration file with the following precedence: + - The `:sealog` key in project.clj + - The configuration file in .sealog/config.edn + - The default configuration + + If the configuration is invalid, print a warning and exit." + [project] + (let [config (select-config project)] + (if (spec/valid? ::config/config config) + config + (do (main/warn (format "Invalid configuration file contents: %s" (spec/explain-str ::config/config config))) + (main/exit 1))))) (defn load-changelog-entry-directory! "Load the changelog directory into a map of version to changelog entries." [{:keys [changelog-entry-directory] :as _config}] - (let [files (list-all-files changelog-entry-directory) + (let [files (io/list-all-files changelog-entry-directory) reducer (fn [acc file] - (let [content (read-edn-file! file ::changelog/entry)] + (let [content (io/read-edn-file! file ::changelog/entry)] (conj acc content)))] (reduce reducer [] files))) @@ -89,7 +56,7 @@ (defn sealog-initialized? "Returns true if the sealog directory exists." [{:keys [changelog-entry-directory] :as config}] - (if (file-exists? changelog-entry-directory) + (if (io/file-exists? changelog-entry-directory) (boolean (seq (load-changelog-entry-directory! config))) false)) @@ -182,29 +149,29 @@ (let [initial-entry (changelog/initialize version-scheme) initial-entry-filename (str changelog-entry-directory (changelog/render-filename initial-entry)) entry (update initial-entry :timestamp str)] - (io/make-parents (io/file initial-entry-filename)) - (write-edn-file! initial-entry-filename entry config))) + (io/create-file initial-entry-filename) + (io/write-edn-file! initial-entry-filename entry config))) (defn sealog-configured? - "Returns true if the sealog configuration file exists." - [] - (file-exists? config/config-file)) + "Returns true if the sealog configuration exists either in project.clj or in the configuration file." + [project] + (or (:sealog project) + (io/file-exists? config/config-file) + (io/file-exists? config/backup-config-file))) (defn configure! "Create a new configuration file." [_opts] - (io/make-parents (io/file config/config-file)) - (write-file! config/config-file config/default-config)) + (io/create-file config/config-file) + (io/write-file! config/config-file config/default-config)) (defn valid-configuration? "Returns true if the configuration is valid." - [] - (let [configuration-contents (if (file-exists? config/config-file) - (edn/read-string (read-file! config/config-file)) - config/default-config) + [project] + (let [configuration-contents (select-config project) contents (st/coerce ::config/config configuration-contents st/string-transformer) valid? (spec/valid? ::config/config contents)] (if valid? @@ -217,7 +184,7 @@ (defn changelog-entry-directory-is-not-empty? "Returns true if the changelog entry directory is not empty." [{:keys [changelog-entry-directory] :as _configuration}] - (let [has-files? (boolean (seq (list-all-files changelog-entry-directory)))] + (let [has-files? (boolean (seq (io/list-all-files changelog-entry-directory)))] (if has-files? (do (main/info "Changelog entry directory contains at least one file.") true) @@ -228,9 +195,9 @@ (defn changelog-directory-only-contains-valid-files? "Returns true if the changelog entry directory only contains valid files." [{:keys [changelog-entry-directory] :as _configuration}] - (let [files (list-all-files changelog-entry-directory) + (let [files (io/list-all-files changelog-entry-directory) valid? (fn [filepath] - (let [file-content (edn/read-string (read-file! filepath)) + (let [file-content (edn/read-string (io/read-file! filepath)) contents (st/coerce ::changelog/entry file-content st/string-transformer)] (if (spec/valid? ::changelog/entry contents) true @@ -248,9 +215,9 @@ (defn all-changelog-entries-use-same-version-type? "Returns true if all changelog entries use the same version type." [{:keys [changelog-entry-directory] :as _configuration}] - (let [files (list-all-files changelog-entry-directory) + (let [files (io/list-all-files changelog-entry-directory) reducer (fn [acc filepath] - (let [content (edn/read-string (read-file! filepath))] + (let [content (edn/read-string (io/read-file! filepath))] (conj acc (:version-type content)))) version-types (vec (distinct (reduce reducer [] files)))] (if (= 1 (count version-types)) @@ -263,9 +230,9 @@ (defn all-changelog-entries-have-distinct-versions? "Returns true if all changelog entries have distinct versions." [{:keys [changelog-entry-directory] :as _configuration}] - (let [files (list-all-files changelog-entry-directory) + (let [files (io/list-all-files changelog-entry-directory) reducer (fn [acc filepath] - (let [content (edn/read-string (read-file! filepath))] + (let [content (edn/read-string (io/read-file! filepath))] (conj acc (:version content)))) versions (vec (distinct (reduce reducer [] files)))] (if (= (count files) (count versions)) diff --git a/src/leiningen/sealog/impl/io.clj b/src/leiningen/sealog/impl/io.clj new file mode 100644 index 0000000..9179826 --- /dev/null +++ b/src/leiningen/sealog/impl/io.clj @@ -0,0 +1,69 @@ +(ns leiningen.sealog.impl.io + "This namespace contains functions for reading and writing files." + (:require [clojure.edn :as edn] + [clojure.java.io :as io] + [clojure.pprint :as pp] + [clojure.spec.alpha :as spec] + [leiningen.core.main :as main] + [spec-tools.core :as st])) + + +(def create-file + "Create a file with all of its parent directories." + (comp io/make-parents io/file)) + + +(defn list-all-files + "List all files within a directory." + [path] + (let [files (io/file path) + dir? (fn [f] (.isDirectory f)) + ->path (fn [f] (.getPath f))] + (if (dir? files) + (mapv ->path (remove dir? (file-seq files))) + (throw (ex-info "Not a directory!" {:path path}))))) + + +(defn file-exists? + "Returns true if `path` points to a valid file" + [path] + (and (string? path) + (.exists (io/file path)))) + + +(defn write-file! + "This is a wrapper around `spit` that logs the filename to the console." + [filename content] + (main/info (format "Writing to %s" filename)) + (spit filename content)) + + +(defn write-edn-file! + "Write the contents to a file as EDN." + [filename content {:keys [pretty-print-edn?]}] + (if pretty-print-edn? + (write-file! filename (with-out-str (pp/pprint content))) + (write-file! filename content))) + + +(defn read-file! + "This is a wrapper around `slurp` that logs the filename to the console." + [filename] + (main/info (format "Reading from %s" filename)) + (slurp filename)) + + +(defn read-edn-file! + "Reads an EDN file and returns the contents as a map. + Throws an exception if the file does not exist, or if the contents do not coerce" + [filename spec] + (if (file-exists? filename) + (let [file-content (edn/read-string (read-file! filename)) + contents (st/coerce spec file-content st/string-transformer)] + (if (spec/valid? spec contents) + contents + (throw (ex-info (str "Invalid file contents: " filename) + {:filename filename + :errors (spec/explain-data spec contents)})))) + (throw (ex-info "Not matching file exists!" + {:filename filename})))) diff --git a/src/leiningen/sealog/types/config.clj b/src/leiningen/sealog/types/config.clj index 8d57586..1bd2e44 100644 --- a/src/leiningen/sealog/types/config.clj +++ b/src/leiningen/sealog/types/config.clj @@ -9,6 +9,11 @@ ".sealog/config.edn") +(def ^:const backup-config-file + "The secondary name of the configuration file." + ".wallbrew/sealog/config.edn") + + (spec/def ::changelog-filename (st/spec {:type :string