#!/usr/bin/env lumo (ns project-checkup.core (:gen-class) (:require [clojure.java.shell :as shell] [clojure.set :as set] [clojure.string :as string])) (defn get-extension [path] "Extracts the extension of a path. Returns the extension with the period, e.g., '.txt' because that's the format people are used to seeing extensions in." (re-find #"\.[a-zA-Z0-9]+$" path)) (defn gather-untracked [vcs-systems] "Gather untracked files in Git or Mercurial." (filter (comp not empty?) (reduce into [ (if (contains? vcs-systems ".hg") (string/split (:out (shell/sh "chg" "st" "-u" "-n")) #"\n")) (if (contains? vcs-systems ".git") (string/split (:out (shell/sh "git" "ls-files" "--others" "--exclude-standard")) #"\n"))]))) (defn gather-project-info "Creates a dictionary of project information." [] (let [all-files (map str (file-seq (clojure.java.io/file "."))) files (map #(clojure.string/replace % #"./(.*)" "$1") all-files ) vcs-systems (set/intersection #{".git" ".hg"} (set files))] {:files files :extensions (frequencies (map get-extension files )) :path (System/getProperty "user.dir") :untracked-files (gather-untracked vcs-systems) :readme (if-let [filename (some #{"README.md" "README.txt" "README.mkd"} files)] (slurp filename) "") }) ) (def doc-extensions ["" ".txt" ".mkd" ".md" ".rst"]) (defn create-valid-names [exts names] "Creates valid file names based on a sequence of exts and names." (set (for [ext exts name names] (str name ext)))) (defn color [color string] (let [color-sequence (case color :green "\u001B[32m" :yellow "\u001B[33m" :blue "\u001B[34m" :red "\u001B[31m" :cyan "\u001B[36m" :magenta "\u001B[35m") reset "\u001B[m"] (str color-sequence string reset))) (defn check-vcs [project] (let [{files :files } project] (boolean (some #{".git" ".hg"} files)))) (defn check-readme [project] (let [{files :files } project] (boolean (some #{"README.md" "README.txt" "README.mkd" "README"} files)))) (defn check-changelog [project] (let [{files :files } project changelog-names #{"CHANGELOG" "HISTORY" "NEWS" "RELEASES"}] (boolean (some (create-valid-names doc-extensions changelog-names) files)))) (defn check-untracked [project] (let [{untracked :untracked-files } project] (= (count untracked) 0))) (defn check-taskpaper [project] (let [{extensions :extensions files :files } project] (or (>= (get extensions ".taskpaper" 0) 1) (some #{"TODO" "TODO.txt" } files)))) (defn check-readme-placeholders [project] (= (count (re-find #"(FIXME|TODO)" (:readme project))) 0)) (defn check-license [project] (let [{files :files } project] (boolean (some #{"LICENSE" "LICENSE.txt" "LICENSE.md" "LICENSE.mkd"} files)))) (def checks [{:name "Project is checked into revision control" :description "" :function check-vcs :level :error :follow-up "Initialize a repository."} {:name "Always True" :function #(or true %) :level :error :follow-up "This is a bug."} {:name "All files are tracked or ignored" :description "" :function check-untracked :level :warning :follow-up "Commit or ignore files from 'hg st -u' or 'git ls-files --others --exclude-standard'." } {:name "Project has a todo file" :function check-taskpaper :description "" :level :suggestion :follow-up "Add a todo file using Taskpaper."} {:name "Project has a README" :function check-readme :description "Readme exists" :level :warning :follow-up "Add a README."} {:name "Project has a CHANGELOG" :function check-changelog :description "Changelog exists" :level :warning :follow-up "Add a CHANGELOG. Consider refering to keepachangelog.com for guidance." } {:name "README has no placeholders" :function check-readme-placeholders :description "No placeholders in README" :level :error :follow-up "Address placeholders or convert them to tasks."} { :name "Project has a license" :function check-license :description "Project has a LICENSE file." :level :warning ;going with warning because a project might not have a license before release. :follow-up "Add a license to LICENSE. Consider using https://choosealicense.com/ for guidance." }]) (def false-colors {:suggestion :blue :warning :yellow :error :red}) (def level-prefixes {:suggestion "Suggested " :warning "Recommended " :error "Required "}) (defn perform-check [check project] (let [{check-name :name :keys [function follow-up level]} check result (function project) false-color (level false-colors :red) prefix (level level-prefixes "Follow-up")] {:name check-name :result result :output (if result (color :green (str "✔" check-name "…passed!")) (str (color false-color (str "❌" check-name "…failed!")) "\n\t" prefix " Follow up: " follow-up))})) (defn -main "Run checks." [& args] (try (let [project-info (gather-project-info)] (doseq [check checks] (println (:output (perform-check check project-info ))))) (catch Exception ex ; Not expecting there to be a lot of exceptions, hence the catch-all. (.printStackTrace ex) (str "caught exception: " (.getMessage ex))) (finally (shutdown-agents))))