|
|
#!/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))))
|
|
|
|