Created
August 22, 2021 23:36
-
-
Save olivergeorge/e278b638c604cc5d9d4f5d4b03e229d0 to your computer and use it in GitHub Desktop.
Create calendars from your git commits. Produces a CSV which Google Calendar can import.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{:deps {clj-jgit/clj-jgit {:mvn/version "1.0.1"} | |
org.clojure/data.csv {:mvn/version "1.0.0"}}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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