Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Create calendars from your git commits. Produces a CSV which Google Calendar can import.
{:deps {clj-jgit/clj-jgit {:mvn/version "1.0.1"}
org.clojure/data.csv {:mvn/version "1.0.0"}}}
(ns git-events.core
(:require [clojure.string :as string]
[clj-jgit.porcelain :as porcelain]
[clojure.data.csv :as csv]
[clojure.java.io :as io]
[clojure.spec.alpha :as s])
(:import (java.text SimpleDateFormat)
(java.util Calendar)
(java.util Date)
(java.text Normalizer Normalizer$Form)))
(s/check-asserts true)
(s/def ::repo (s/keys :req-un [::host ::owner ::project ::dir]))
(s/def ::commit (s/keys :req-un [::sha ::who ::when ::repo ::what]))
(s/def ::commits (s/coll-of ::commit))
(defn as-time [i] (.format (SimpleDateFormat. "HH:mm") i))
(defn as-date [i] (.format (SimpleDateFormat. "yyyy-MM-dd") i))
(defn get-year [i]
(let [cal (Calendar/getInstance)]
(.setTime cal i)
(.get cal Calendar/YEAR)))
(defn date-with-time
([i]
(date-with-time i 0))
([i min]
(let [cal (Calendar/getInstance)]
(.setTime cal i)
(.set cal Calendar/MINUTE 0)
(.set cal Calendar/SECOND 0)
(.set cal Calendar/MILLISECOND 0)
(.add cal Calendar/MINUTE min)
(.getTimeInMillis cal))))
(defn commit-info
[repo {:keys [id author msg]}]
(let [sha (subs (.getName id) 0 7)]
{:sha sha
:who (:email author)
:when (:date author)
:repo repo
:what (str (as-time (:date author))
" "
(first (string/split-lines msg))
" <" sha ">")}))
(defmulti commit-uri (fn [repo commits] (:host repo)))
(defmethod commit-uri "github.com"
[{:keys [host owner project]} commit]
(str "https://" host "/" owner "/" project "/commit/" (:sha commit)))
(defmethod commit-uri "bitbucket.org"
[{:keys [host owner project]} commit]
(str "https://" host "/" owner "/" project "/commits/" (:sha commit) "?w=1"))
(defmulti commits-diff (fn [repo commits] (:host repo)))
(defmethod commits-diff :default [_ _] "")
; NOTE: This isn't right. It doesn't include the first commit. Not sure how to build a better URL.
;(defmethod commits-diff "bitbucket.org"
; [repo commits]
; (let [commits (sort-by :when commits)
; commit1 (:sha (first commits))
; commit2 (:sha (last commits))
; {:keys [host owner project]} repo]
; (str "https://" host "/" owner "/" project "/branches/compare/" commit2 "%0D" commit1 "#diff")))
(defmethod commits-diff "github.com"
[repo commits]
(s/assert ::repo repo)
(s/assert ::commits commits)
(let [commits (sort-by :when commits)
commit1 (:sha (first commits))
commit2 (:sha (last commits))
{:keys [host owner project]} repo]
(str "https://" host "/" owner "/" project "/compare/" commit1 "..." commit2)))
(defn between-dates [s0 s1]
(fn [i] (and (>= (.compareTo i s0) 0)
(<= (.compareTo i s1) 0))))
(defn events
[{:keys [repo subject commits]}]
(s/assert ::repo repo)
(s/assert ::commits commits)
(for [[_ when-commits] (group-by (comp date-with-time :when) commits)]
(let [commits (sort-by :when when-commits)
when (:when (first commits))
s00 (Date. (date-with-time when 0))
s15 (Date. (date-with-time when 15))
s30 (Date. (date-with-time when 30))
s45 (Date. (date-with-time when 45))
s60 (Date. (date-with-time when 60))
start (cond
(some (comp (between-dates s00 s15) :when) commits) s00
(some (comp (between-dates s15 s30) :when) commits) s15
(some (comp (between-dates s30 s45) :when) commits) s30
(some (comp (between-dates s45 s60) :when) commits) s45)
end (cond
(some (comp (between-dates s45 s60) :when) commits) s60
(some (comp (between-dates s30 s45) :when) commits) s45
(some (comp (between-dates s15 s30) :when) commits) s30
(some (comp (between-dates s00 s15) :when) commits) s15)]
{:subject subject
:start-date (as-date start)
:start-time (as-time start)
:end-date (as-date end)
:end-time (as-time end)
:all-day-event "False"
:description (str (if (= 1 (count commits))
(commit-uri repo (first commits))
(commits-diff repo when-commits))
"\n"
(string/join "\n" (map :what commits)))
:private "True"})))
(defn generate-csv
[{:keys [events filename]}]
(with-open [writer (io/writer filename)]
(csv/write-csv writer
(into [["Subject"
"Start Date"
"Start Time"
"End Date"
"End Time"
"All Day Event"
"Description"
"Private"]]
(map (juxt :subject
:start-date
:start-time
:end-date
:end-time
:all-day-event
:description
:private)
events)))))
(defn- trim-to [string-to-trim trim-value]
(apply str (take trim-value string-to-trim)))
(defn- normalize [string-to-normalize]
(let [normalized (Normalizer/normalize string-to-normalize Normalizer$Form/NFD)
ascii (string/replace normalized #"[\P{ASCII}]+" "")]
(string/lower-case ascii)))
(defn slugify
"Returns a slugified string. Takes two optional parameters:
delimiter (str): string that interleaves valid words,
trim-value (int): max url value."
([string-to-slugify] (slugify string-to-slugify "-"))
([string-to-slugify delimiter] (slugify string-to-slugify delimiter 250))
([string-to-slugify delimiter trim-value]
(let [normalized (normalize string-to-slugify)
split-s (string/split (string/triml normalized) #"[\p{Space}\p{P}]+")
combined (string/join delimiter split-s)]
(trim-to combined trim-value))))
(defn repo-uri
[{:keys [host owner project]}]
(str "git@" host ":" owner "/" project ".git"))
(defn clone-or-fetch
[repo-list]
(doseq [{:keys [dir] :as cfg} repo-list]
(let [uri (repo-uri cfg)]
(if (.exists (io/file dir))
(do (println :fetching uri dir)
(doto (porcelain/load-repo dir)
(porcelain/git-fetch-all)))
(do (println :cloning uri dir)
(porcelain/git-clone uri :dir dir))))))
(defn repo-commits
[{:keys [dir] :as repo}]
(let [git-repo (porcelain/load-repo dir)
log (porcelain/git-log git-repo)]
(map (partial commit-info repo) log)))
(defn generate-user-calendars
[{:keys [users commits]}]
(let [commits-by-who (group-by (comp users :who) commits)]
(doseq [[who who-commits] commits-by-who]
(s/assert ::commits who-commits)
(let [commits-by-repo (group-by :repo who-commits)
events (mapcat (fn [[{:keys [project] :as repo} repo-commits]]
(s/assert ::repo repo)
(events {:repo repo
:subject (str project " commits by " who)
:commits repo-commits}))
commits-by-repo)]
(generate-csv {:events (sort-by :start-date events)
:filename (str (slugify who) ".csv")})))))
(def repos
[{:host "github.com"
:owner "clj-kondo"
:project "clj-kondo"
:dir "/tmp/repos/clj-kondo"}
{:host "github.com"
:owner "borkdude"
:project "sci"
:dir "/tmp/repos/sci"}
{:host "github.com"
:owner "babashka"
:project "babashka"
:dir "/tmp/repos/babashka"}
{:host "github.com"
:owner "borkdude"
:project "deps.clj"
:dir "/tmp/repos/deps-clj"}
{:host "github.com"
:owner "clj-kondo"
:project "clj-kondo.lsp"
:dir "/tmp/repos/clj-kondo-lsp"}])
; For mapping many emails into one user alias
(s/def ::users (s/map-of ::email ::alias))
(s/def ::email string?)
(s/def ::alias string?)
(def users
{"michielborkent@gmail.com" "borkdude"})
(comment
(s/assert (s/coll-of ::repo) repos)
(s/assert ::users users)
(clone-or-fetch repos)
(generate-user-calendars
{:users users
:commits (->> (mapcat repo-commits repos)
; subset of users
(filter (comp users :who))
; just this year
(filter (comp #{2021} get-year :when)))}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment