;;; code-compass.el --- Navigate software aided by metrics and visualization -*- lexical-binding: t; -*-

;; Copyright (C) 2023 Andrea

;; Author: Andrea <andrea-dev@hotmail.com>
;; Package-Version: 20260724.1822
;; Package-Revision: 9004c885ac20
;; Package-Requires: ((emacs "26.1") (s "1.12.0") (dash "2.13") (async "1.9.7") (simple-httpd "1.5.1"))
;; Keywords: tools, extensions, help
;; Homepage: https://github.com/ag91/code-compass

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>

;;; Commentary:

;; Make Emacs your compass in a sea of software complexity.
;;
;; This tool puts the power and knowledge of your repository history in your hands.
;; You can find what analyses are supported with `code-compass-cheatsheet'.
;; A good way to start is:
;;   - `code-compass-show-hotspots':
;;     show hotspots in code repository as a circle diagram.
;;     Circles are packages or modules.
;;     The redder the circle, the more it has been modified lately. The bigger the more code it contains.
;;
;; If you are having trouble with dependencies, try `code-compass-doctor' to get some clarity.
;;
;; See documentation at https://github.com/ag91/code-compass

;;; Code:
(require 'dash)
(require 's)
(require 'simple-httpd)
(require 'async)
(require 'url)
(require 'vc)

(defun code-compass--python-script (script)
  "Return the command to run a script with code-compass' python. "
  (concat code-compass-download-directory "/venv/bin/python " script))

(defgroup code-compass nil
  "Options specific to code-compass."
  :tag "code-compass"
  :group 'code-compass)

(defcustom code-compass-default-port 8888
  "Default port on which to serve analyses files."
  :type 'int
  :group 'code-compass)

(defcustom code-compass-default-periods
  '("beginning" "1d" "2d" "3d" "6d" "12d" "18d" "24d" "1m" "2m" "6m")
  "Starting date to reduce the Git log for analysis.
'beginning' is a keyword to say to not reduce.
'Nd' means to start after N days, where N is a positive number.
'Nm' means to start after N months, where N is a positive number."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-snapshot-periods
  '("1d" "3m" "6m" "9m" "12m" "15m")
  "A list of snapshots periods to show evolution of analyses over time."
  :group 'code-compass
  :type 'string)

(defconst code-compass-path-to-code-compass (file-name-directory (or load-file-name (buffer-file-name)))
  "The directory from where code compass was loaded.")

(defun code-compass--expand-file-name (file-name)
  "Expand FILE-NAME with `code-compass-path-to-code-compass'."
  (expand-file-name file-name code-compass-path-to-code-compass))

(defcustom code-compass-tmp-directory "/tmp"
  "Directory to store temporary files generated by code-compass."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-docker-data-directory "/data"
  "Directory to store temporary docker files generated by code-compass."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-download-directory (code-compass--expand-file-name "dependencies")
  "Directory to store downloaded dependencies."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-code-maat-command
  (format "java -jar %s/code-maat-1.0.1-standalone.jar" (code-compass--expand-file-name "dependencies"))
  "Command to run Code-maat (https://github.com/adamtornhill/code-maat).
Currently defaults to use docker because easier to setup."
  :group 'code-compass
  :type 'string
  :options `(,(format "java -jar %s/code-maat-1.0.1-standalone.jar" code-compass-download-directory)
             ,(format "docker run -v %s/:%s code-maat-app" code-compass-tmp-directory code-compass-docker-data-directory)))

(defcustom code-compass-preferred-browser
  "chromium"
  "Browser to use to open graphs served by webserver."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-exclude-directories
  '("node_modules" "bower_components" "vendor" "tmp")
  "A list of directory patterns to exclude from reports.
Contents are passed to the cloc executable via its --exclude-dir argument."
  :group 'code-compass
  :type 'list)

(defcustom code-compass-respect-gitignore t
  "If non-nil, parse `.gitignore' files and exclude matching directories.
This supplements `code-compass-exclude-directories' so that directories
ignored by Git are also excluded from hotspot and coupling analyses."
  :group 'code-compass
  :type 'boolean)

(defcustom code-compass-calculate-coupling-project-key-fn
  (lambda (repository)
    (concat
     repository
     "-"
     (s-trim
      (shell-command-to-string
       (format "cd %s; git rev-parse --short HEAD" repository)))))
  "Function taking a REPOSITORY path and returning a string."
  :group 'code-compass
  :type 'function)

(defcustom code-compass-authors-colors (list
                                        "red"
                                        "blue"
                                        "orange"
                                        "gray"
                                        "green"
                                        "violet"
                                        "pink"
                                        "brown"
                                        "aquamarine"
                                        "blueviolet"
                                        "burlywood"
                                        "cadetblue"
                                        "chartreuse"
                                        "chocolate"
                                        "coral"
                                        "cornflowerblue"
                                        "cyan"
                                        "darkblue"
                                        "darkcyan"
                                        "darkgoldenrod"
                                        "darkgray"
                                        "darkgreen"
                                        "darkkhaki"
                                        "darkmagenta"
                                        "darkolivegreen"
                                        "darkorange"
                                        "darkorchid"
                                        "darkred"
                                        "darksalmon"
                                        "darkseagreen"
                                        "darkslateblue"
                                        "darkslategray"
                                        "darkturquoise"
                                        "darkviolet"
                                        "deeppink"
                                        "deepskyblue"
                                        "dimgray"
                                        "dodgerblue"
                                        "firebrick"
                                        "forestgreen"
                                        "fuchsia"
                                        "gold"
                                        "goldenrod"
                                        "greenyellow"
                                        "hotpink"
                                        "indianred"
                                        "indigo"
                                        "lawngreen"
                                        "lightcoral"
                                        "lightgray"
                                        "lightgreen"
                                        "lightpink"
                                        "lightsalmon"
                                        "lightseagreen"
                                        "lightskyblue"
                                        "lightslategray"
                                        "lightsteelblue"
                                        "lime"
                                        "limegreen"
                                        "linen"
                                        "magenta"
                                        "maroon"
                                        "mediumaquamarine"
                                        "mediumblue"
                                        "mediumorchid"
                                        "mediumpurple"
                                        "mediumseagreen"
                                        "mediumslateblue"
                                        "mediumspringgreen"
                                        "mediumturquoise"
                                        "mediumvioletred"
                                        "midnightblue"
                                        "mintcream"
                                        "mistyrose"
                                        "moccasin"
                                        "navajowhite"
                                        "navy"
                                        "oldlace"
                                        "olive"
                                        "olivedrab"
                                        "orangered"
                                        "orchid"
                                        "palegoldenrod"
                                        "palegreen"
                                        "paleturquoise"
                                        "palevioletred"
                                        "papayawhip"
                                        "peachpuff"
                                        "peru"
                                        "plum"
                                        "powderblue"
                                        "purple"
                                        "rosybrown"
                                        "royalblue"
                                        "saddlebrown"
                                        "salmon"
                                        "sandybrown"
                                        "seagreen"
                                        "seashell"
                                        "sienna"
                                        "silver"
                                        "skyblue"
                                        "slateblue"
                                        "slategray"
                                        "snow"
                                        "springgreen"
                                        "steelblue"
                                        "tan"
                                        "teal"
                                        "thistle"
                                        "tomato"
                                        "turquoise"
                                        "wheat"
                                        "whitesmoke"
                                        "yellow"
                                        "yellowgreen")
  "Colors to use for authors."
  :group 'code-compass
  :type 'list)

(defcustom code-compass-pie-or-bar-chart-command (code-compass--python-script "csv-to-pie-graph.py %s")
  "Command to visualize chart."
  :group 'code-compass
  :type 'string
  :options '((code-compass--python-script "csv-to-pie-graph.py %s") "graph %s --bar --width 0.4 --offset='-0.2,0.2'"))

(defcustom code-compass-gource-command
  "gource"
  "Command to the gource utility. See https://gource.io/ for more information on how to install."
  :group 'code-compass
  :type 'string)

(defcustom code-compass-gource-seconds-per-day
  0.5
  "How long each Git history day should take."
  :type 'float
  :group 'code-compass)

(defcustom code-compass-display-icon
  't
  "Display an icon in modeline showing growth trend of code.
A pointing up icon means the code has been growing,
   a pointing down arrow has been decreasing."
  :group 'code-compass
  :type 'bool)

(defcustom code-compass-icon-trends
  '(
    :period "3"
    :always-additions (propertize "⬆" 'face `(:background "DarkOrange"))
    :always-deletions (propertize "⬇" 'face `(:background "SpringGreen"))
    :more-additions   (propertize "↗" 'face `(:background "Gold"))
    :more-deletions   (propertize "↘" 'face `(:background "GreenYellow")))
  "Icon and period of evaluation for trend."
  :group 'code-compass
  :type 'plist)

(defcustom code-compass-display-file-contributors 't
  "Enable the listing of contributors for a file in the *Messages* buffer."
  :group 'code-compass
  :type 'bool)

(defcustom code-compass-cache-file (concat user-emacs-directory ".code-compass/cache")
  "Directory to store a cache of coupling files."
  :group 'code-compass
  :type 'string)

;; these definitions are just to satisfy the linting tools
(defvar browse-url-generic-program)
(defvar slack-current-team)
(declare-function slack-team-users "ext:slack.el")
(declare-function slack-create-user-profile-buffer "ext:slack.el")
(declare-function slack-buffer-display-im "ext:slack.el")

;;;###autoload
(defun code-compass-install (&optional no-query-p)
  (interactive)
  (let ((venv-dir (file-name-concat code-compass-download-directory "venv/")))
    (when (and (not (file-exists-p venv-dir)) (or no-query-p
                                                  (y-or-n-p "Need to install code-compass python dependencies, do it now ?")))
      (code-compass--in-directory code-compass-download-directory
        (mkdir code-compass-download-directory t)
        (let ((compilation-buffer (compilation-start (format "cd %s; python3 -m venv venv && ./venv/bin/pip3 install -r ../requirements.txt" code-compass-download-directory))))
          (if (get-buffer-window compilation-buffer)
              (select-window (get-buffer-window compilation-buffer))
            (pop-to-buffer compilation-buffer))
          )))))

;;;###autoload
(defun code-compass-doctor ()
  "Report if and what dependencies are missing."
  (interactive)
  (let ((git-p (executable-find "git"))
        (python-p (executable-find "python3"))
        (python-venv-p (file-exists-p (file-name-concat code-compass-download-directory "/venv/")))
        (java-p (executable-find "java"))
        (graph-cli-p (file-exists-p (file-name-concat code-compass-download-directory "/venv/bin/graph")))
        (cloc-p (executable-find "cloc"))
        (gource-p (executable-find "gource"))
        (docker-p (executable-find "docker"))
        (doctor-buffer (get-buffer-create "code-compass dependencies check")))
    (with-current-buffer doctor-buffer
      (read-only-mode -1)
      (erase-buffer)
      (insert "Welcome to Code Compass doctor!\n\n")
      (insert "Required dependencies for minimal functionality:\n")
      (insert (format "- Git: %s\n" (if git-p "OK" "MISSING")))
      (insert (format "- Python: %s\n" (if python-p "OK" "MISSING")))
      (insert (format "- Python venv: %s\n" (if python-venv-p "OK" "MISSING")))
      (insert (format "- Java: %s\n" (if java-p "OK" "MISSING")))
      (insert (format "- Cloc: %s\n" (if cloc-p "OK" "MISSING")))
      (insert "\n\nOptional dependencies:\n")
      (insert (format "- Graph-cli: %s\n" (if graph-cli-p "OK" "MISSING")))
      (insert (format "- Gource: %s\n" (if gource-p "OK" "MISSING")))
      (insert (format "- Docker: %s\n" (if docker-p "OK" "MISSING")))
      (read-only-mode))
    (switch-to-buffer-other-window doctor-buffer)))
(define-obsolete-function-alias 'c/doctor #'code-compass-doctor "0.1.2")

(defun code-compass--subtract-to-now (n month|day &optional time)
  "Subtract N * MONTH|DAY to current time.
Optionally give TIME from which to start."
  (time-subtract
   (or time (current-time))
   (seconds-to-time (* 60 60 month|day n))))

(defun code-compass-request-date (days|months &optional time)
  "Request date in days or months by asking how many DAYS|MONTHS ago.
Optionally give TIME from which to start.

>> (code-compass-request-date \"1d\" '(25610 2072 776840 400000))
=> \"2023-03-08\"

>> (code-compass-request-date \"1m\" '(25610 2072 776840 400000))
=> \"2023-02-06\""
  (interactive
   (list (completing-read "From how long ago? " code-compass-default-periods)))
  (when (not (string= days|months "beginning"))
    (format-time-string
     "%F"
     (apply
      #'code-compass--subtract-to-now
      (-concat
       (if (s-contains-p "m" days|months)
           (list (string-to-number (s-replace "m" "" days|months)) (* 24 31))
         (list (string-to-number (s-replace "d" "" days|months)) 24))
       (list time))))))
(define-obsolete-function-alias 'c/request-date #'code-compass-request-date "0.1.2")

(defun code-compass--first (l)
  "Get first element of L."
  (car l))

(defun code-compass--second (l)
  "Get second element of L."
  (nth 1 l))

(defun code-compass--third (l)
  "Get third element of L."
  (nth 2 l))

(defun code-compass--filename (file)
  "Get filename of FILE.

>> (code-compass--filename \"some/file.txt\")
=> \"file.txt\""
  (file-name-nondirectory (directory-file-name file)))

(defun code-compass--temp-dir (repository)
  "Format temporary directory in which store analyses assets for REPOSITORY.

>> (code-compass--temp-dir \"~/some-repo/\")
=> \"/tmp/code-compass-some-repo/\""
  (format "%s/code-compass-%s/" code-compass-tmp-directory (code-compass--filename repository)))

(defmacro code-compass--in-directory (directory &rest body)
  "Execute BODY in DIRECTORY.
Temporarily changes current buffer's default directory to DIRECTORY."
  (declare (indent defun))
  `(let ((default-directory ,directory))
     (unwind-protect
         ,@body)))

(defmacro code-compass--in-temp-directory (repository &rest body)
  "Execute BODY in temporary directory created for analysed REPOSITORY."
  (declare (indent defun))
  `(progn
     (mkdir (code-compass--temp-dir ,repository) t)
     (code-compass--in-directory
      (code-compass--temp-dir ,repository)
      ,@body)))

(defun code-compass--shell-command-error-handler (command buffer-name)
  "Run COMMAND with `shell-command' but check if errors are output in BUFFER-NAME."
  (ignore-errors (kill-buffer buffer-name))
  (shell-command
   command
   nil
   (get-buffer-create buffer-name))
  (let ((contents
         (with-current-buffer buffer-name
           (buffer-string))))
    (when (> (length contents) 1) (error (concat buffer-name "\n\n" contents)))))

(defun code-compass--parse-gitignore (repository)
  "Parse `.gitignore' in REPOSITORY and return a list of directory names to exclude.
Only simple directory patterns are extracted (lines with no wildcards).
Patterns with leading/trailing slashes are stripped to bare names so they
can be passed to cloc's `--exclude-dir' and git's `:(exclude)' pathspec."
  (let ((gitignore-file (expand-file-name ".gitignore" repository)))
    (when (file-exists-p gitignore-file)
      (with-temp-buffer
        (insert-file-contents gitignore-file)
        (let ((lines (split-string (buffer-string) "\n" t)))
          (-map (lambda (line)
                  (string-trim line "/" "/"))
                (-filter (lambda (line)
                           (and (not (s-blank-p line))
                                (not (s-prefix-p "#" line))
                                (not (s-contains-p "*" line))
                                (not (s-contains-p "[" line))))
                         (-map #'string-trim lines))))))))

(defun code-compass--exclude-directories (repository)
  "Return the combined list of directories to exclude for REPOSITORY.
This merges `code-compass-exclude-directories' with patterns parsed from
`.gitignore' when `code-compass-respect-gitignore' is non-nil."
  (let ((base code-compass-exclude-directories))
    (if code-compass-respect-gitignore
        (-uniq (-concat base (code-compass--parse-gitignore repository)))
      base)))

(defun code-compass-produce-git-report (repository date &optional before-date authors)
  "Create git report for REPOSITORY with a Git log starting at DATE.
Define optionally a BEFORE-DATE.
The knowledge analysis allow to filter by AUTHORS when set."
  (interactive
   (list (call-interactively #'code-compass-request-date)))
  (message "Producing git report...")
  (let* ((exclude-dirs (code-compass--exclude-directories repository))
         (git-command
          (s-concat
           (format "git -C %s" repository)
           " log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN' --no-renames "
           (when authors
             (format "--perl-regexp --author='%s' " authors))
           (when date
             (format "--after=%s " date))
           (when before-date
             (format "--before=%s " before-date))
           (when exclude-dirs
             (s-join " "  (--map (format "':(exclude)%s'" it) exclude-dirs)))
           " > gitreport.log")))
    (message "Running %s" git-command)
    (code-compass--shell-command-error-handler git-command "*code-compass-produce-git-report-errors*"))
  repository)
(define-obsolete-function-alias 'c/produce-git-report #'code-compass-produce-git-report "0.1.2")

(defun code-compass--run-code-maat (command repository)
  "Run code-maat's COMMAND on REPOSITORY."
  (message "Producing code-maat %s report for %s..." command repository)
  (let ((source-file (format "%s/code-maat-1.0.1-standalone.jar" code-compass-download-directory))
        (maat-jar-p (s-contains-p "jar" code-compass-code-maat-command)))
    (when (and maat-jar-p (not (file-exists-p (code-compass--expand-file-name source-file))))
      (mkdir code-compass-download-directory t)
      (url-copy-file "https://github.com/smontanari/code-forensics/raw/v3.0.0/lib/analysers/code_maat/code-maat-1.0.1-standalone.jar" (code-compass--expand-file-name source-file) t))
    (code-compass--shell-command-error-handler
     (format
      "%1$s -l %4$s/code-compass-%2$s/gitreport.log -c git2 -a %3$s > %3$s.csv"
      code-compass-code-maat-command
      (code-compass--filename repository)
      command
      (if maat-jar-p code-compass-tmp-directory code-compass-docker-data-directory))
     "*code-compass--run-code-maat-errors*")))

(defun code-compass--produce-code-maat-revisions-report (repository)
  "Create code-maat revisions report for REPOSITORY."
  (code-compass--run-code-maat "revisions" repository)
  repository)

(defun code-compass--produce-cloc-report (repository)
  "Create cloc report for REPOSITORY.
To filter specific subdirectories out of this report,
edit the variable `code-compass-exclude-directories'.
When `code-compass-respect-gitignore' is non-nil, directories listed in
`.gitignore' are also excluded."
  (let* ((exclude-dirs (code-compass--exclude-directories repository))
         (cloc-command (format "(cd %s; PERL_BADLANG=0 cloc ./ --timeout 0 --by-file --csv --quiet --exclude-dir=%s) > cloc.csv" repository (string-join exclude-dirs ","))))
    (message (concat
              "Producing cloc report with "
              cloc-command
              "..."))
    (code-compass--shell-command-error-handler
     cloc-command
     "*code-compass--produce-cloc-report-errors*"))
  repository)

(defun code-compass--copy-file (file-name directory)
  "Copy FILE-NAME to DIRECTORY."
  (copy-file (code-compass--expand-file-name file-name) directory t)
  (set-file-modes (concat directory "/" (file-name-nondirectory file-name)) (file-modes-symbolic-to-number "u=rw,go=r")))

(defun code-compass--generate-merger-script (repository)
  "Generate a Python script to give weights to the circle diagram of REPOSITORY."
  (code-compass--copy-file "./scripts/csv_as_enclosure_json.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--generate-d3-v3-lib (repository)
  "Make available the D3 library for REPOSITORY.
This is just to not depend on a network connection."
  (mkdir "d3" t)
  (let ((source-file (format "%s/d3.v3.min.js" code-compass-download-directory)))
    (unless (file-exists-p (code-compass--expand-file-name source-file))
      (mkdir code-compass-download-directory t)
      (url-copy-file "http://d3js.org/d3.v3.min.js" (code-compass--expand-file-name source-file) t))
    (code-compass--copy-file source-file "d3/"))
  repository)

(defun code-compass--generate-d3-v4-lib (repository)
  "Make available the D3 v4 library for REPOSITORY.
This is just to not depend on a network connection."
  (mkdir "d3" t)
  (let ((source-file (format "%s/d3.v4.min.js" code-compass-download-directory)))
    (unless (file-exists-p (code-compass--expand-file-name source-file))
      (mkdir code-compass-download-directory t)
      (url-copy-file "http://d3js.org/d3.v4.min.js" (code-compass--expand-file-name source-file) t))
    (code-compass--copy-file source-file "d3/"))
  repository)

(defun code-compass--produce-json (repository)
  "Produce json for REPOSITORY."
  (message "Produce json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "csv_as_enclosure_json.py --structure cloc.csv --weights revisions.csv > hotspot_proto.json")
   "*code-compass--produce-json-errors*")
  repository)

(defun code-compass--generate-host-enclosure-diagram-html (repository)
  "Generate host html from REPOSITORY."
  (code-compass--copy-file "./pages/enclosure-diagram/style.css" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/enclosure-diagram/script.js" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/enclosure-diagram/zoomable.html" (code-compass--temp-dir repository))
  repository)

(defun code-compass--navigate-to-localhost (repository &optional port)
  "Navigate to served directory for REPOSITORY, optionally at specified PORT."
  (let ((port (or port code-compass-default-port))
        (browse-url-browser-function #'browse-url-generic)
        (browse-url-generic-program code-compass-preferred-browser))
    (browse-url (format "http://localhost:%s/zoomable.html" port)))
  repository)

(defvar code-compass--served-repository nil
  "Absolute path of the repository currently served by `simple-httpd'.
Used by the `/open' servlet to resolve relative paths clicked in the
enclosure-diagram visualization.")

(defun code-compass--run-server (repository &optional port)
  "Serve directory for REPOSITORY, optionally at PORT."
  (let ((httpd-host 'local)
        (httpd-port (or port code-compass-default-port)))
    (httpd-stop)
    (setq code-compass--served-repository (expand-file-name repository))
    (ignore-errors (httpd-serve-directory (code-compass--temp-dir repository))))
  repository)

(defservlet* open text/plain (path)
  "Open PATH inside the repository currently served by code-compass in
the current Emacs.  PATH is a slash-separated repository-relative path
sent from the enclosure-diagram visualization."
  (let ((repo code-compass--served-repository))
    (cond
     ((null repo)
      (insert "no repository is currently served"))
     ((or (null path) (string-empty-p path))
      (insert "missing path"))
     (t
      (let* ((clean (replace-regexp-in-string "\\`/+" "" path))
             (full (expand-file-name clean repo)))
        (if (not (string-prefix-p (file-name-as-directory repo) full))
            (insert (format "refusing to open path outside repository: %s" full))
          (if (not (file-exists-p full))
              (insert (format "file not found: %s" full))
            ;; With `run-at-time 0 nil`, the servlet finishes writing
            ;; its 200 response and closes the connection first; then
            ;; Emacs returns to its main command loop and runs
            ;; `find-file` cleanly from there.
            (run-at-time 0 nil
                         (lambda (f)
                           (find-file f)
                           (when (fboundp 'select-frame-set-input-focus)
                             (select-frame-set-input-focus (selected-frame))))
                         full)
            (insert (format "opening %s" full)))))))))

(defun code-compass--run-server-and-navigate (repository &optional port)
  "Serve and navigate to REPOSITORY, optionally at PORT."
  (when port
    (code-compass--run-server repository port)
    (code-compass--navigate-to-localhost repository port)))

(defun code-compass--async-run (command repository date &optional port do-not-serve authors)
  "Run asynchronously COMMAND taking a REPOSITORY and a DATE, optionally at PORT.
Optional argument DO-NOT-SERVE skips serving contents on localhost.
Optional argument AUTHORS to filter AUTHORS for knowledge analysis."
  (async-start
   `(lambda ()
      (setq load-path ',load-path)
      (load-file ,(symbol-file command))
      (setq code-compass-code-maat-command ,code-compass-code-maat-command)
      (setq code-compass-pie-or-bar-chart-command ,code-compass-pie-or-bar-chart-command)
      (setq code-compass-calculate-coupling-project-key-fn ',code-compass-calculate-coupling-project-key-fn)
      (setq code-compass-authors-colors ',code-compass-authors-colors)
      (setq code-compass-exclude-directories ',code-compass-exclude-directories)
      (setq code-compass-respect-gitignore ,code-compass-respect-gitignore)
      (setq code-compass-preferred-browser ,code-compass-preferred-browser)
      (setq code-compass-snapshot-periods ',code-compass-snapshot-periods)
      (setq code-compass-default-periods ',code-compass-default-periods)
      (setq code-compass-tmp-directory ',code-compass-tmp-directory)
      (setq code-compass-docker-data-directory ',code-compass-docker-data-directory)
      (setq code-compass-download-directory ',code-compass-download-directory)
      (setq code-compass-default-port ',code-compass-default-port)
      (let ((browse-url-browser-function #'browse-url-generic)
            (browse-url-generic-program ,code-compass-preferred-browser))
        (condition-case err
            (funcall ',command ,repository ,date ',authors)
          (error
           (funcall ',command ,repository ,date)))))
   `(lambda (result)
      (when (not ,do-not-serve) (code-compass--run-server-and-navigate  ,(expand-file-name repository) (or ,port code-compass-default-port))))))

;;;###autoload
(defun code-compass-show-hotspots-sync (repository date &optional port)
  "Show REPOSITORY enclosure diagram for hotspots starting at DATE.
Optionally served at PORT."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)
    code-compass-default-port))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it date)
        code-compass--produce-code-maat-revisions-report
        code-compass--produce-cloc-report
        code-compass--generate-merger-script
        code-compass--generate-d3-v3-lib
        code-compass--produce-json
        code-compass--generate-host-enclosure-diagram-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-hotspots-sync #'code-compass-show-hotspots-sync "0.1.2")

;;;###autoload
(defun code-compass-show-hotspots (repository date &optional port)
  "Show REPOSITORY enclosure diagram for hotspots.
Starting DATE reduces scope of Git log and
PORT define where the html is served."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-hotspots-sync repository date port))
(define-obsolete-function-alias 'c/show-hotspots #'code-compass-show-hotspots "0.1.2")

(defun code-compass-show-hotspot-snapshot-sync (repository)
  "Snapshot COMMAND over REPOSITORY over the last year every three months."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))))
  (--each code-compass-snapshot-periods (code-compass-show-hotspots-sync repository (code-compass-request-date it) code-compass-default-port)))

(define-obsolete-function-alias 'c/show-hotspot-snapshot-sync #'code-compass-show-hotspot-snapshot-sync "0.1.2")

;; BEGIN indentation

(defun code-compass--split-on-newlines (code)
  "Split CODE over newlines."
  (s-split "\n" code))

(defun code-compass--remove-empty-lines (lines)
  "Remove empty LINES.

>> (code-compass--remove-empty-lines '(\"line\" \" \" \"   \" \"\"))
=> (\"line\")\""
  (--remove (eq (length (s-trim it)) 0) lines))

(defun code-compass--remove-text-after-indentation (lines)
  "Remove text in LINES that is not indentation characters..

>> (code-compass--remove-text-after-indentation '(\" one indent\"))
=> (\" \")"
  (--map
   (apply #'string (--take-while (or (eq ?\s  it) (eq ?\t it)) (string-to-list it)))
   lines))

(defun code-compass--find-indentation (lines-without-text)
  "Infer indentation level in LINES-WITHOUT-TEXT.
If no indentation present in file, defaults to 2.

>> (code-compass--find-indentation '(\"   \" \"   \" \"    \"))
=> 3

>> (code-compass--find-indentation '(\" \"))
=> 1"
  (or (--> lines-without-text
           (--map (list (s-count-matches "\s" it) (s-count-matches "\t" it)) it)
           (let ((spaces-ind (-sort #'< (--remove (eq 0 it) (-map 'code-compass--first it))))
                 (tabs-ind (-sort #'< (--remove (eq 0 it) (-map 'code-compass--second it)))))
             (if (> (length spaces-ind) (length tabs-ind))
                 (code-compass--first spaces-ind)
               (code-compass--first tabs-ind))))
      2))

(defun code-compass--convert-tabs-to-spaces (line-without-text n)
  "Replace tabs in LINE-WITHOUT-TEXT with N spaces."
  (s-replace "\t" (make-string n ?\s) line-without-text))

(defun code-compass--calculate-complexity (line-without-text indentation)
  "Calculate indentation complexity.
This divides length of LINE-WITHOUT-TEXT by INDENTATION.

>> (code-compass--calculate-complexity \"    \" 2)
=> 2.0"
  (/ (+ 0.0 (length line-without-text)) indentation))

(defun code-compass--as-logical-indents (lines &optional opts)
  "Calculate logical indentations of LINES.
Try to infer how many space is an indent unless OPTS provides it."
  (let ((indentation (or opts (code-compass--find-indentation lines))))
    (list
     (--map
      (--> it
        (code-compass--convert-tabs-to-spaces it indentation)
        (code-compass--calculate-complexity it indentation))
      lines)
     indentation)))

(defun code-compass--stats-from (complexities-indentation)
  "Return stats from COMPLEXITIES-INDENTATION."
  (let* ((complexities (code-compass--first complexities-indentation))
         (mean (/ (-sum complexities) (length complexities)))
         (sd (sqrt (/ (-sum (--map (expt (- it mean) 2) complexities)) (length complexities)))))
    `((total . ,(-sum complexities))
      (n-lines . ,(length complexities))
      (max . ,(-max complexities))
      (mean . ,mean)
      (standard-deviation . ,sd)
      (used-indentation . ,(code-compass--second complexities-indentation)))))

(defun code-compass-calculate-complexity-stats (code &optional opts)
  "Return complexity of CODE based on indentation.
If OPTS is provided, use these settings to define what is the indentation.
Return nil for empty CODE.

>> (-take 3 (code-compass-calculate-complexity-stats \"1\"))
=> ((total . 0.0) (n-lines . 1) (max . 0.0))"
  (ignore-errors
    (--> code
         ;; TODO maybe add line numbers, so that I can also open the most troublesome (max-c) line automatically?
         code-compass--split-on-newlines
         code-compass--remove-empty-lines
         code-compass--remove-text-after-indentation
         (code-compass--as-logical-indents it opts)
         code-compass--stats-from)))
(define-obsolete-function-alias 'c/calculate-complexity-stats #'code-compass-calculate-complexity-stats "0.1.2")

;;;###autoload
(defun code-compass-calculate-complexity-current-buffer (&optional indentation)
  "Calculate complexity of the current buffer contents.
Optionally you can provide the INDENTATION level of the file. The
code can infer it automatically."
  (interactive)
  (code-compass-calculate-complexity-stats
   (buffer-substring-no-properties (point-min) (point-max)) indentation))

(define-obsolete-function-alias 'c/calculate-complexity-current-buffer #'code-compass-calculate-complexity-current-buffer "0.1.2")

;; END indentation

;; BEGIN complexity over commits

(defun code-compass--retrieve-commits-up-to-date-touching-file (file &optional date)
  "Retrieve list of commits touching FILE from DATE."
  (s-split
   "\n"
   (shell-command-to-string
    (s-concat
     "git log --format=format:%H --reverse "
     (if date
         (s-concat "--after=" date " ")
       "")
     file))))

(defun code-compass--retrieve-file-at-commit-with-git (file commit)
  "Retrieve FILE contents at COMMIT."
  (let* ((git-dir (with-current-buffer (find-file-noselect file)
                    (expand-file-name (vc-root-dir))))
         (git-file
          (string-remove-prefix
           git-dir
           (expand-file-name file))))
    (shell-command-to-string (format "git show %s:\"%s\"" commit git-file))))

(defun code-compass--git-hash-to-date (commit)
  "Return the date of the COMMIT.
Note this is the date of merging in, not of the code change."
  (s-replace "\n" "" (shell-command-to-string (s-concat "git show --no-patch --no-notes --pretty='%cd' --date=short " commit))))

(defun code-compass--calculate-complexity-over-commits (file &optional opts)
  "Calculate complexity of FILE over commits.
Optional argument OPTS defines things like the indentation to use."
  (--> (call-interactively #'code-compass-request-date)
       (code-compass--retrieve-commits-up-to-date-touching-file file it)
       (--map
        (--> it
             (list it (code-compass--retrieve-file-at-commit-with-git file it))
             (list (code-compass--first it) (code-compass-calculate-complexity-stats (code-compass--second it) opts)))
        it)))
(define-obsolete-function-alias 'c/calculate-complexity-over-commits #'code-compass--calculate-complexity-over-commits "0.1.2")

(defun code-compass--plot-csv-file-with-graph-cli (file)
  "Plot CSV FILE with graph-cli."
  (async-shell-command
   (format "%s/venv/bin/graph --xtick-angle 90 %s" code-compass-download-directory file)))

(defun code-compass--plot-lines-with-graph-cli (data)
  "Plot DATA from lists as a graph."
  (let ((tmp-file (format "%s/data-file-graph-cli.csv" code-compass-tmp-directory)))
    (with-temp-file tmp-file
      (insert "commit-date,total-complexity,loc\n")
      (insert (s-join "\n" (--map (s-replace-all '((" " . ",") ("(" . "") (")" . "")) (format "%s" it)) data))))
    (code-compass--plot-csv-file-with-graph-cli tmp-file)))

;;;###autoload
(defun code-compass-show-complexity-over-commits (file &optional opts)
  "Make a graph plotting complexity out of a FILE.
Optionally give file indentation in OPTS."
  (interactive (list (read-file-name "Select file:" nil nil nil (buffer-file-name))))
  (code-compass--plot-lines-with-graph-cli
   (--map
    (list (code-compass--git-hash-to-date (code-compass--first it))
          (alist-get 'total (code-compass--second it))
          (alist-get 'n-lines (code-compass--second it)))
    (code-compass--calculate-complexity-over-commits file opts))))
(define-obsolete-function-alias 'c/show-complexity-over-commits #'code-compass-show-complexity-over-commits "0.1.2")

;; END complexity over commits

;; BEGIN code churn
(defun code-compass--produce-code-maat-abs-churn-report (repository)
  "Create code-maat abs-churn report for REPOSITORY."
  (code-compass--run-code-maat "abs-churn" repository)
  repository)

(defun code-compass-show-code-churn-sync (repository date)
  "Show how much code was added and removed from REPOSITORY from a DATE."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--in-temp-directory
   repository
   (progn
     (--> repository
          (code-compass-produce-git-report it date)
          code-compass--produce-code-maat-abs-churn-report)
     (code-compass--plot-csv-file-with-graph-cli "abs-churn.csv"))))
(define-obsolete-function-alias 'c/show-code-churn-sync #'code-compass-show-code-churn-sync "0.1.2")

;;;###autoload
(defun code-compass-show-code-churn (repository date)
  "Show how much code was added and removed from REPOSITORY from a DATE."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-code-churn-sync repository date nil 't))
(define-obsolete-function-alias 'c/show-code-churn #'code-compass-show-code-churn "0.1.2")
;; END complexity over commits

;; BEGIN change coupling
(defun code-compass--produce-code-maat-coupling-report (repository)
  "Create code-maat coupling report for REPOSITORY."
  (code-compass--run-code-maat "coupling" repository)
  repository)

(defun code-compass--generate-coupling-json-script (repository)
  "Generate script to produce a weighted graph for REPOSITORY."
  (code-compass--copy-file "./scripts/coupling_csv_as_edge_bundling.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--produce-coupling-json (repository)
  "Produce coupling json needed by d3 for REPOSITORY."
  (message "Produce coupling json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "coupling_csv_as_edge_bundling.py --coupling coupling.csv > edgebundling.json")
   "*code-compass--produce-coupling-json-errors*")
  repository)

(defun code-compass--generate-host-edge-bundling-html (repository)
  "Generate host html from REPOSITORY."
  (code-compass--copy-file "./pages/edge-bundling/script.js" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/edge-bundling/style.css" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/edge-bundling/zoomable.html" (code-compass--temp-dir repository))
  repository)

(defun code-compass-show-coupling-graph-sync (repository date &optional port)
  "Show REPOSITORY edge bundling synchronously for code coupling up to DATE.
Serve graph on PORT."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)
                8888))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it nil date)
        code-compass--produce-code-maat-coupling-report
        code-compass--generate-coupling-json-script
        code-compass--generate-d3-v4-lib
        code-compass--produce-coupling-json
        code-compass--generate-host-edge-bundling-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-coupling-graph-sync #'code-compass-show-coupling-graph-sync "0.1.2")

;;;###autoload
(defun code-compass-show-coupling-graph (repository date &optional port)
  "Show REPOSITORY edge bundling for code coupling up to DATE. Serve graph on PORT."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-coupling-graph-sync repository date port))
(define-obsolete-function-alias 'c/show-coupling-graph #'code-compass-show-coupling-graph "0.1.2")
;; END change coupling

;; BEGIN find coupled files
(defun code-compass--add-filename-to-analysis-columns (repository analysis)
  "Add filepath from REPOSITORY to ANALYSIS columns."
  (--> analysis
       (s-split "\n" it 't)
                                        ;(--remove (s-blank? (s-trim it)) it)
       (-concat
        (list (car it))
        (--map
         (--> (s-split "," it)
              (-concat
               (list (s-concat repository "/" (code-compass--first it)))
               (list
                (if (or (null (code-compass--second it)) (not (s-contains-p "/" (code-compass--second it))))
                    (code-compass--second it)
                  (s-concat repository "/" (code-compass--second it))))
               (cdr (cdr it)))
              (s-join "," it))
         (cdr it)))))

(defun code-compass--get-coupling-alist-sync (repository)
  "Get list of coupled files in REPOSITORY async."
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it nil)
        code-compass--produce-code-maat-coupling-report)
   (--> (code-compass--get-analysis-as-string-from-csv "coupling")
        (code-compass--add-filename-to-analysis-columns repository it)
        (--map (s-split "," it) (cdr it)))))

(defun code-compass--get-coupling-alist (repository fun)
  "FUN takes a list of coupled files in REPOSITORY."
  (async-start
   `(lambda ()
      (setq load-path ',load-path)
      (load-file ,(symbol-file 'code-compass--get-coupling-alist))
      (code-compass--get-coupling-alist-sync ,repository))
   fun))

(defvar code-compass-coupling-project-map
  (make-hash-table :test 'equal)
  "Hash table to contain coupling files list.")

(defun code-compass--get-coupled-files-alist (repository fun)
  "Run FUN on the coupled files for REPOSITORY."
  (let* ((key (funcall code-compass-calculate-coupling-project-key-fn repository))
         (code-compass-files (gethash key code-compass-coupling-project-map)))
    (if code-compass-files
        (funcall fun code-compass-files)
      (message "Building coupling cache asynchronously...")
      (code-compass--get-coupling-alist
       repository
       `(lambda (result-files)
          (message "Coupling Cache Built")
          (puthash ,key result-files code-compass-coupling-project-map)
          (funcall ,fun result-files)
          ;; Save cache to local storage to access it faster next time if nothing has changed
          (with-temp-file ,code-compass-cache-file
            (prin1 code-compass-coupling-project-map (current-buffer))))))))

(defun code-compass-clear-coupling-project-map ()
  "Clear `code-compass-coupling-project-map' and deletes cache file."
  (interactive)
  (clrhash code-compass-coupling-project-map)

  (delete-file code-compass-cache-file))
(define-obsolete-function-alias 'c/clear-coupling-project-map #'code-compass-clear-coupling-project-map "0.1.2")

(defun code-compass-get-coupled-files-alist-hook-fn ()
  "Calculate coupled files asynchronously."
  (let ((git-root (ignore-errors (vc-root-dir))))
    (when git-root
      (code-compass--get-coupled-files-alist
       git-root
       `(lambda (x)
          (message
           "Finished to update coupled files for %s and found %s coupled files."
           ,git-root
           (length x)))))))
(define-obsolete-function-alias 'c/get-coupled-files-alist-hook-fn #'code-compass-get-coupled-files-alist-hook-fn "0.1.2")

(defun code-compass--coupling-completions (file-name coupled-files root)
  "Get a list of files coupled to FILE-NAME.
The coupling information is provided by COUPLED-FILES.
ROOT is the VCS project path.

>> (code-compass--coupling-completions \"\" nil \"\")
=> nil

>> (code-compass--coupling-completions
    (concat code-compass-path-to-code-compass \"code-compass.el\")
    (list
      (list
        (concat code-compass-path-to-code-compass \"/README.org\")
        \"code-compass.el\"
        31
        65))
    code-compass-path-to-code-compass)
=> (\"README.org\")"
  (let ((default-directory root)
        (root (s-chop-suffixes '("/" "/" "/") root)))
    (--> coupled-files
       (--sort (> (string-to-number (nth 3 it)) (string-to-number (nth 3 other))) it) ;; sort by number of commits
       (--sort (> (string-to-number (nth 2 it)) (string-to-number (nth 2 other))) it) ;; sort then by how often this file has changed
       (-keep
        (lambda (file)
          (let ((src-coupled-file-name (expand-file-name (car file)))
                (target-coupled-file-name-src (expand-file-name (nth 1 file)))
                (file-name (expand-file-name file-name)))
            (when (and
                   (file-exists-p src-coupled-file-name)
                   (file-exists-p target-coupled-file-name-src)
                   (or (string= file-name src-coupled-file-name)
                       (string= file-name target-coupled-file-name-src)))
              (s-replace ; this replace is just removing the root prefix, so the completions are human readable
               (concat root "/")
               ""
               (expand-file-name
                (car
                 ;; this picks only the coupled files, ignoring the file we are matching against (file-name)
                 (--remove
                  (string= (expand-file-name file-name) (expand-file-name it))
                  (-take 2 file))))))))
        it))))

(defun code-compass--show-coupled-files (files file-name)
  "Gather coupled files to FILE-NAME from all FILES."
  (let* ((root (ignore-errors (car (s-split "//" (caar files)))))
         (completions (ignore-errors
                        (-non-nil
                         (code-compass--coupling-completions
                          file-name
                          files
                          (expand-file-name root))))))
    (if completions
        (let ((open-file (completing-read "Find coupled file: " completions nil 't)))
          (find-file (concat root "/" open-file)))
      (error "No coupled file found!"))))

;;;###autoload
(defun code-compass-find-coupled-files ()
  "Allow user to choose files coupled according to previous modifications."
  (interactive)

  (when (file-exists-p code-compass-cache-file)
    (with-temp-buffer
      (insert-file-contents code-compass-cache-file)
      (goto-char (point-min))
      (set 'code-compass-coupling-project-map (read (current-buffer)))))

  (code-compass--get-coupled-files-alist
   (vc-root-dir)
   `(lambda (files) (code-compass--show-coupled-files files ,(buffer-file-name)))))

(define-obsolete-function-alias 'c/find-coupled-files #'code-compass-find-coupled-files "0.1.2")
;; END find coupled files

;; BEGIN code communication
(defun code-compass--produce-code-maat-communication-report (repository)
  "Create code-maat communication report for REPOSITORY."
  (code-compass--run-code-maat "communication" repository)
  repository)

(defun code-compass--generate-communication-json-script (repository)
  "Generate script to produce a weighted graph for REPOSITORY."
  (code-compass--copy-file "./scripts/communication_csv_as_edge_bundling.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--produce-communication-json (repository)
  "Generate REPOSITORY age json."
  (message "Produce age json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "communication_csv_as_edge_bundling.py --communication communication.csv > edgebundling.json")
   "*code-compass--produce-communication-json-errors*")
  repository)

(defun code-compass-show-code-communication-sync (repository date &optional port)
  "Show REPOSITORY edge bundling for code communication from DATE.
Green edges is incoming (dependant) and red outgoing (dependencies).
Optionally set the PORT on which to serve the graph."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)
    8888))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it date)
        code-compass--produce-code-maat-communication-report
        code-compass--generate-communication-json-script
        code-compass--generate-d3-v4-lib
        code-compass--produce-communication-json
        code-compass--generate-host-edge-bundling-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-code-communication-sync #'code-compass-show-code-communication-sync "0.1.2")

;;;###autoload
(defun code-compass-show-code-communication (repository date &optional port)
  "Show REPOSITORY edge bundling for code communication from DATE.
Optionally define PORT on which to serve graph."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-code-communication-sync repository date port))
(define-obsolete-function-alias 'c/show-code-communication #'code-compass-show-code-communication "0.1.2")
;; END code communication

;; BEGIN code knowledge
(defun code-compass--produce-code-maat-main-dev-report (repository)
  "Create code-maat main-dev report for REPOSITORY."
  (code-compass--run-code-maat "main-dev" repository)
  repository)

(defun code-compass--generate-knowledge-json-script (repository)
  "Generate python script.
Argument REPOSITORY defines for which repo to run this."
  (code-compass--copy-file "./scripts/knowledge_csv_as_enclosure_diagram.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--produce-knowledge-json (repository)
  "Generate REPOSITORY age json."
  (message "Produce knowledge json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "knowledge_csv_as_enclosure_diagram.py --structure cloc.csv --owners main-dev.csv --authors authors.csv > knowledge.json")
   "*code-compass--produce-knowledge-json-errors*")
  repository)

(defun code-compass--insert-authors-colors-in-file (authors-colors)
  "Insert a csv of AUTHORS-COLORS in the temporary asset directory for REPOSITORY."
  (with-temp-file "authors.csv"
    (insert "author,color\n")
    (apply #'insert (--map (s-concat (car it) "," (cdr it) "\n") authors-colors))))

(defun code-compass--get-authors (repository)
  "Retrieve authors in REPOSITORY."
  (--> (s-concat "cd " repository "; git shortlog HEAD -s -n | uniq | cut -f 2")
       shell-command-to-string
       (s-split "\n" it)
       (--remove (s-blank? (s-trim it)) it)))

(defun code-compass--generate-list-authors-colors (repository)
  "Generate list of authors of REPOSITORY."
  (--> (code-compass--get-authors repository)
       (-zip-pair it code-compass-authors-colors)
       (code-compass--insert-authors-colors-in-file it))
  repository)

(defun code-compass--generate-host-knowledge-enclosure-diagram-html (repository)
  "Generate host html from REPOSITORY."
  (code-compass--copy-file "./pages/knowledge-enclosure-diagram/script.js" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/knowledge-enclosure-diagram/style.css" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/knowledge-enclosure-diagram/zoomable.html" (code-compass--temp-dir repository))
  repository)

(defun code-compass-show-knowledge-graph-sync (repository date authors &optional port)
  "Show REPOSITORY enclosure diagram for code knowledge.
Filter by DATE and AUTHORS.
Optionally define PORT on which to serve graph."
  (interactive (let ((repository (read-directory-name "Choose git repository directory:" (vc-root-dir))))
                 (list
                  repository
                  (call-interactively #'code-compass-request-date)
                  (completing-read-multiple "Filter by authors (TAB-completion) or leave empty for all: " (code-compass--get-authors repository))
                  8888)))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it date nil (if (> (length authors) 1)
                                                         (s-concat "(" (s-join "|" authors) ")")
                                                       authors))
        code-compass--produce-code-maat-main-dev-report
        code-compass--produce-cloc-report
        code-compass--generate-knowledge-json-script
        code-compass--generate-d3-v3-lib
        code-compass--generate-list-authors-colors
        code-compass--produce-knowledge-json
        code-compass--generate-host-knowledge-enclosure-diagram-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-knowledge-graph-sync #'code-compass-show-knowledge-graph-sync "0.1.2")

;;;###autoload
(defun code-compass-show-knowledge-graph (repository date &optional authors  port)
  "Show REPOSITORY enclosure diagram for code knowledge.
Filter by DATE and AUTHORS.
Optionally define PORT on which to serve graph."
  (interactive (let ((repository (read-directory-name "Choose git repository directory:" (vc-root-dir))))
                 (list
                  repository
                  (call-interactively #'code-compass-request-date)
                  (completing-read-multiple "Filter by authors (TAB-completion) or leave empty for all: " (code-compass--get-authors repository))
                  8888)))
  (code-compass--async-run #'code-compass-show-knowledge-graph-sync repository date port nil authors))
(define-obsolete-function-alias 'c/show-knowledge-graph #'code-compass-show-knowledge-graph "0.1.2")
;; END code knowledge

;; BEGIN code refactoring
(defun code-compass--produce-code-maat-refactoring-main-dev-report (repository)
  "Create code-maat refactoring-main-dev report for REPOSITORY."
  (code-compass--run-code-maat "refactoring-main-dev" repository)
  repository)

(defun code-compass--generate-refactoring-json-script (repository)
  "Generate python script for REPOSITORY."
  (code-compass--copy-file "./scripts/refactoring_csv_as_enclosure_diagram.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--produce-refactoring-json (repository)
  "Generate REPOSITORY age json."
  (message "Produce refactoring json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "refactoring_csv_as_enclosure_diagram.py --structure cloc.csv --owners refactoring-main-dev.csv --authors authors.csv > knowledge.json") ; TODO should be refactoring.json, but leaving knowledge.json for code reuse
   "*code-compass--produce-refactoring-json-errors*")
  repository)

(defun code-compass-show-refactoring-graph-sync (repository date &optional port)
  "Show REPOSITORY enclosure diagram for code knowledge up to DATE.
Optionally define PORT on which to serve graph."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)
    8888))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it date)
        code-compass--produce-code-maat-refactoring-main-dev-report
        code-compass--produce-cloc-report
        code-compass--generate-refactoring-json-script ;; added,total-added, vs removed,total-removed
        code-compass--generate-d3-v3-lib
        code-compass--generate-list-authors-colors
        code-compass--produce-refactoring-json
        code-compass--generate-host-knowledge-enclosure-diagram-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-refactoring-graph-sync #'code-compass-show-refactoring-graph-sync "0.1.2")

;;;###autoload
(defun code-compass-show-refactoring-graph (repository date &optional port)
  "Show REPOSITORY enclosure diagram for code refactoring.
Filter by DATE.
 Optionally define PORT on which to serve graph."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-refactoring-graph-sync repository date port))
(define-obsolete-function-alias 'c/show-refactoring-graph #'code-compass-show-refactoring-graph "0.1.2")
;; END code refactoring

;; BEGIN code stability
(defun code-compass-produce-code-maat-age-report (repository)
  "Create code-maat age report for REPOSITORY."
  (code-compass--run-code-maat "age" repository)
  repository)

(defun code-compass--generate-age-json-script (repository)
  "Generate python script for REPOSITORY."
  (code-compass--copy-file "./scripts/code_age_csv_as_enclosure_json.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--produce-age-json (repository)
  "Generate REPOSITORY age json."
  (message "Produce age json...")
  (code-compass--shell-command-error-handler
   (code-compass--python-script "code_age_csv_as_enclosure_json.py --structure cloc.csv --weights age.csv > age.json")
   "*code-compass--produce-age-json-errors*")
  repository)

(defun code-compass--generate-host-age-enclosure-diagram-html (repository)
  "Generate host html from REPOSITORY."
  (code-compass--copy-file "./pages/age-enclosure-diagram/script.js" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/age-enclosure-diagram/style.css" (code-compass--temp-dir repository))
  (code-compass--copy-file "./pages/age-enclosure-diagram/zoomable.html" (code-compass--temp-dir repository))
  repository)

(defun code-compass-show-code-age-sync (repository date &optional port)
  "Show REPOSITORY enclosure diagram for code stability/age.
Filter by DATE.
Optionally define PORT on which to serve graph."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)
    8888))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it date)
        code-compass-produce-code-maat-age-report
        code-compass--produce-cloc-report
        code-compass--generate-age-json-script
        code-compass--generate-d3-v3-lib
        code-compass--produce-age-json
        code-compass--generate-host-age-enclosure-diagram-html
        (code-compass--run-server-and-navigate it port))))
(define-obsolete-function-alias 'c/show-code-age-sync #'code-compass-show-code-age-sync "0.1.2")

;;;###autoload
(defun code-compass-show-stability-graph (repository date &optional port)
  "Show REPOSITORY enclosure diagram for code stability.
Filter by DATE.
 Optionally define PORT on which to serve graph."
  (interactive (list
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-code-age-sync repository date port))
(define-obsolete-function-alias 'c/show-stability-graph #'code-compass-show-stability-graph "0.1.2")
;; END code stability

;; BEGIN code fragmentation
(defun code-compass--produce-code-maat-entity-ownership-report (repository)
  "Create code-maat entity-ownership report for REPOSITORY."
  (code-compass--run-code-maat "entity-ownership" repository)
  repository)

(defun code-compass--slurp (file)
  "Get string from FILE contents."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (buffer-substring-no-properties (point-min) (point-max))))

(defun code-compass--get-analysis-as-string-from-csv (analysis)
  "Get ANALYSIS in csv as text."
  (code-compass--slurp (s-concat analysis ".csv")))

(defun code-compass--generate-pie-script (repository)
  "Generate python script for REPOSITORY."
  (code-compass--copy-file "./scripts/csv-to-pie-graph.py" (code-compass--temp-dir repository))
  repository)

(defun code-compass--show-pie-chart-command (file)
  "Show pie chart of CSV FILE."
  (format code-compass-pie-or-bar-chart-command file))

(defun code-compass--sum-by-first-column (rows)
  "Utility to sum ROWS by first column.

>> (code-compass--sum-by-first-column '((a . 1) (a . 2)))
=> ((a . 3))"
  (let (result)
    (dolist (row rows)
      (let* ((key (car row))
             (value (cdr row))
             (mapping (assoc key result)))
        (if mapping
            (setcdr mapping (+ (cdr mapping) value))
          (push (cons key value) result))))
    result))

(defun code-compass-show-fragmentation-sync (path &optional date)
  "Show knowledge fragmentation for PATH.
Optional argument DATE to reduce time window."
  (interactive "fShow fragmentation for:")
  (let* ((path (file-truename path))
         (repository (s-trim (shell-command-to-string (format "cd %s; git rev-parse --show-toplevel" (file-name-directory path))))))
    (code-compass--in-temp-directory
     repository
     (--> repository
          (code-compass-produce-git-report it date)
          code-compass--produce-code-maat-entity-ownership-report
          code-compass--generate-pie-script)
     (--> (code-compass--get-analysis-as-string-from-csv "entity-ownership")
          (code-compass--add-filename-to-analysis-columns repository it)
          (--filter (s-starts-with-p path it) it)
          (--map
           (--> (s-split "," it)
                (cons (nth 1 it) (+ (string-to-number (nth 2 it)) (string-to-number (nth 3 it)))))
           it)
          (code-compass--sum-by-first-column it)
          (--map
           (--> it (format "%s,%s\n" (car it) (cdr it)))
           it)
          (cons "author,+&-lines\n" it)
          (with-temp-file "fragmentation.csv"
            (apply #'insert it)))
     (code-compass--shell-command-error-handler
      (code-compass--show-pie-chart-command "fragmentation.csv")
      "*code-compass-show-fragmentation-sync-errors*"))))
(define-obsolete-function-alias 'c/show-fragmentation-sync #'code-compass-show-fragmentation-sync "0.1.2")

;;;###autoload
(defun code-compass-show-fragmentation (path)
  "Show knowledge fragmentation for PATH."
  (interactive "fShow fragmentation for:")
  (code-compass--async-run #'code-compass-show-fragmentation-sync path nil nil 't))
(define-obsolete-function-alias 'c/show-fragmentation #'code-compass-show-fragmentation "0.1.2")
;; END code fragmentation

;; BEGIN word analysis
;; taken from: https://emacs.stackexchange.com/questions/13514/how-to-obtain-the-statistic-of-the-the-frequency-of-words-in-a-buffer
(defvar code-compass-punctuation-marks '(","
                                         "."
                                         "'"
                                         "&"
                                         "\"")
  "List of Punctuation Marks that you want to count.")

(defun code-compass--count-raw-word-list (raw-word-list)
  "Produce a dictionary of RAW-WORD-LIST.
This contains the number of occurrences for each word."
  (--> raw-word-list
       (--reduce-from
        (progn
          (cl-incf (cdr (or (assoc it acc)
                            (code-compass--first (push (cons it 0) acc)))))
          acc)
        nil
        it)
       (sort it (lambda (a b) (string< (car a) (car b))))))

(defun code-compass--word-stats (string)
  "Return word (as a token between spaces) frequency in STRING."
  (let* ((words (split-string
                 (downcase string)
                 (format "[ %s\f\t\n\r\v]+"
                         (mapconcat #'identity code-compass-punctuation-marks ""))
                 t))
         (punctuation-marks (--filter
                             (member it code-compass-punctuation-marks)
                             (split-string string "" t)))
         (raw-word-list (append punctuation-marks words))
         (word-list (code-compass--count-raw-word-list raw-word-list)))
    (sort word-list (lambda (a b) (> (cdr a) (cdr b))))))

(defun code-compass--word-stats-to-csv-string (string &optional order-fn)
  "Produce occurrences csv table for words in STRING.
Optionally sorting the table according to ORDER-FN."
  (--> string
    code-compass--word-stats
    (--filter (> (length (car it)) 2) it)
    (sort it (or order-fn (lambda (a b) (> (cdr a) (cdr b)))))
    (--map (format "'%s',%d\n" (car it) (cdr it)) it)
    (apply #'s-concat it)
    (s-concat "word,occurences\n\n" it)))

;;;###autoload
(defun code-compass-word-statistics (string &optional order-fn)
  "Produce a buffer with word statistics from STRING.
Optionally define ORDER-FN
\(for example to see the ones appearing less first\)."
  (interactive
   (list (buffer-substring-no-properties (point-min) (point-max))))
  (with-current-buffer (get-buffer-create "*word-statistics*")
    (erase-buffer)
    (insert (code-compass--word-stats-to-csv-string string order-fn)))
  (pop-to-buffer "*word-statistics*")
  (goto-char (point-min)))
(define-obsolete-function-alias 'c/word-statistics #'code-compass-word-statistics "0.1.2")

;;;###autoload
(defun code-compass-word-semantics (string)
  "Produce a buffer with the words least used.
This could contain the most semantically relevant from STRING."
  (interactive
   (list (buffer-substring-no-properties (point-min) (point-max))))
  (code-compass-word-statistics string (lambda (a b) (< (cdr a) (cdr b)))))
(define-obsolete-function-alias 'c/word-semantics #'code-compass-word-semantics "0.1.2")

;;;###autoload
(defun code-compass-word-analysis-commits (arg)
  "Show the frequency of words used in commits messages.
When ARG is set show only history for given file."
  (interactive "P")
  (--> (shell-command-to-string (s-concat
                                 "git log --pretty=format:\"%s\""
                                 (when arg (format " %s" (read-file-name "Analyze history of:")))))
       code-compass-word-statistics))
(define-obsolete-function-alias 'c/word-analysis-commits #'code-compass-word-analysis-commits "0.1.2")

;;;###autoload
(defun code-compass-word-analysis-region ()
  "Show the frequency of words in a region."
  (interactive)
  (when (region-active-p)
    (--> (buffer-substring-no-properties (region-beginning) (region-end))
         code-compass-word-statistics)))
(define-obsolete-function-alias 'c/word-analysis-region #'code-compass-word-analysis-region "0.1.2")

;;;###autoload
(defun code-compass-word-analysis-region-graph ()
  "Show the frequency graph for words in region."
  (interactive)
  (when (region-active-p)
    (--> (buffer-substring-no-properties (region-beginning) (region-end))
         code-compass--word-stats-to-csv-string
         (with-temp-file (format "%s/word-stats.csv" code-compass-tmp-directory)
           (insert it)))
    (shell-command (format "graph --bar --xtick-angle 90 %s/word-stats.csv" code-compass-tmp-directory))))
(define-obsolete-function-alias 'c/word-analysis-region-graph #'code-compass-word-analysis-region-graph "0.1.2")


;; END word analysis

;; BEGIN churn icon

(defun code-compass--calculate-added-delete-lines (file n-weeks-ago)
  "Calculate added and deleted lines for FILE from N-WEEKS-AGO."
  (--> "git log --since \"%s weeks ago\" --numstat --oneline %s "
       (format it n-weeks-ago file)
       (shell-command-to-string it)
       (s-split "\n" it)
       (--map (s-split "\t" it) it)
       (--filter (> (length it) 2) it)
       (--reduce-from
        (list
         :additions (+ (string-to-number (nth 0 it)) (plist-get acc :additions))
         :deletions (+ (string-to-number (nth 1 it)) (plist-get acc :deletions)))
        (list :additions 0 :deletions 0)
        it)))

(defun code-compass--async-start (start-func &optional finish-func)
  "Call `async-start' with START-FUNC and FINISH-FUNC."
  (async-start
   `(lambda ()
      (setq load-path ',load-path)
      (load-file ,(symbol-file 'code-compass--async-start))
      (funcall ,start-func))
   finish-func))

(defun code-compass--display-icon ()
  "Display icon for buffer according to the previous history."
  (when (and code-compass-display-icon (vc-root-dir))
    (let ((current-buffer (current-buffer))
          (current-file (buffer-file-name)))
      (code-compass--async-start
       `(lambda ()
          (let* ((additions-deletions
                  (code-compass--calculate-added-delete-lines ,current-file (plist-get ',code-compass-icon-trends :period)))

                 (additions (plist-get additions-deletions :additions))
                 (deletions (plist-get additions-deletions :deletions))
                 (icon-key (cond
                            ((eq additions 0) :always-deletions)
                            ((eq deletions 0) :always-additions)
                            ((> additions deletions) :more-additions)
                            ('otherwise :more-deletions))))
            (plist-get ',code-compass-icon-trends icon-key)))
       `(lambda (icon)
          (with-current-buffer ,current-buffer
            (code-compass--remove-icon)
            (setq-local mode-line-format (cons `(:eval ,icon) mode-line-format))))))))

(defun code-compass--remove-icon ()
  "Remove icon."
  (setq-local
   mode-line-format
   (--remove
    (-contains-p
     (--remove (symbolp it) code-compass-icon-trends)
     (plist-get it :eval))
    mode-line-format)))

(defun code-compass--display-icon-delayed ()
  "Display icon function for `prog-mode-hook'."
  (run-with-timer 0.1 nil 'code-compass--display-icon))

(defun code-compass-toggle-churn-status-icon ()
  "Enable churn status icon."
  (interactive)
  (if (-contains-p prog-mode-hook #'code-compass--display-icon-delayed)
      (remove-hook 'prog-mode-hook #'code-compass--display-icon-delayed)
    (add-hook 'prog-mode-hook #'code-compass--display-icon-delayed)))
;; END churn icon
;; BEGIN wrapper gource

;;;###autoload
(defun code-compass-show-gource (repository date)
  "Open gource for REPOSITORY from DATE."
  (interactive
   (list
    (read-directory-name "Choose git repository directory:" (vc-root-dir))
    (call-interactively #'code-compass-request-date)))
  (if (executable-find code-compass-gource-command)
      (async-shell-command
       (s-concat
        (format "cd %s; %s -seconds-per-day %s" repository code-compass-gource-command code-compass-gource-seconds-per-day)
        (when date (format " --start-date %s" date))))
    (message
     "Sorry, cannot find executable (%s). Try change the value of `code-compass-gource-command'"
     code-compass-gource-command)))
(define-obsolete-function-alias 'c/show-gource #'code-compass-show-gource "0.1.2")
;; END wrapper gource


;; BEGIN create todos for a coupled files
(defun code-compass--get-matching-coupled-files (files &optional file)
  "Get coupled FILES that match FILE or current buffer's file."
  (let ((coupled-file (file-truename (or file (buffer-file-name)))))
    (--> files
         (--sort (> (string-to-number (nth 3 it)) (string-to-number (nth 3 other))) it) ;; sort by number of commits
         (--sort (> (string-to-number (nth 2 it)) (string-to-number (nth 2 other))) it) ;; sort then by how often this file has changed
         (-map (lambda (file)
                 (when (or (string= coupled-file (file-truename (car file)))
                           (string= coupled-file (file-truename (nth 1 file))))
                   (car
                    (--remove (string= (buffer-file-name) it) (-take 2 file)))))
               it)
         (-remove 'null it))))

(defun code-compass--show-todo-buffer (files file)
  "Show a `org-mode' buffer for FILE with the left FILES to modify."
  (let ((buffer (get-buffer-create (concat (file-name-base file) "-todos"))))
    (switch-to-buffer buffer)
    (erase-buffer)
    (org-mode)
    (insert "* Files you need to modify after " (file-name-base file) ":\n")
    (let* ((modified-files (--filter
                            (-contains-p
                             (s-split "\n" (shell-command-to-string "git diff --name-only HEAD"))
                             (file-name-nondirectory it))
                            files))
           (files-to-modify (-difference files modified-files)))
      (--each files-to-modify
        (insert (format "** TODO [[%s][%s]]\n" it (file-name-base it))))
      (--each modified-files
        (insert (format "** DONE [[%s][%s]]\n" it (file-name-base it)))))
    buffer))

;;;###autoload
(defun code-compass-create-todos-from-coupled-files (&optional file)
  "Allow user to choose a coupled file to FILE or the current buffer's file."
  (interactive)
  (let ((create-todos
         (lambda (files file)
           (--> (code-compass--get-matching-coupled-files files file)
             (if (null it)
                 (message "Nothing left todo for this file in this commit!")
               (code-compass--show-todo-buffer it file))))))
    (code-compass--get-coupled-files-alist
     (vc-root-dir)
     `(lambda (files)
        (funcall
         ,create-todos
         files
         ,(or file (file-truename (or file (buffer-file-name)))))))))
(define-obsolete-function-alias 'c/create-todos-from-coupled-files #'code-compass-create-todos-from-coupled-files "0.1.2")
;; END create todos for a coupled files

;; BEGIN EXPERIMENTAL slack support
(defun code-compass--get-main-contributor-email (&optional file)
  "Find email of main contributor of buffer, or FILE."
  (--> (or file (buffer-file-name))
       (file-name-nondirectory it)
       (s-concat "./" it)
       (format "git shortlog HEAD -n -sne -- %s" it)
       (shell-command-to-string it)
       (s-split "\n" it)
       car ;; only the first contributor
       (s-split "<" it)
       (nth 1 it)
       (s-split ">" it)
       (nth 0 it)))

(defun code-compass--open-slack-from-email (email)
  "Open slack chat from EMAIL."
  (when (and (fboundp 'slack) slack-current-team)
    (slack-buffer-display-im
     (slack-create-user-profile-buffer
      slack-current-team
      (plist-get ;; TODO fall back to manual choice if the mail cannot be found?
       (--find
        (string=
         email
         (plist-get (plist-get it :profile) :email))
        (slack-team-users slack-current-team))
       :id)))))

;;;###autoload
(defun code-compass-slack-main-contributor ()
  "Open slack chat with main contributor of file."
  (interactive)
  ;; Allow slack only if user has it installed, and has set a current team
  (if (and (fboundp 'slack) slack-current-team)
      (code-compass--open-slack-from-email (code-compass--get-main-contributor-email))
    (message "Sorry, setup your emacs-slack to use this function. See https://github.com/yuya373/emacs-slack.")))
(define-obsolete-function-alias 'c/slack-main-contributor #'code-compass-slack-main-contributor "0.1.2")

;; END EXPERIMENTAL slack support

;; BEGIN Hotspots for microservices
(defun code-compass--get-repositories-from-file (file)
  "Extract the list the cluster list of directories.
We run `code-compass-show-hotspot-cluster-sync' from FILE."
  (when (file-exists-p file)
    (with-temp-buffer
      (insert-file-contents-literally file)
      (--> (buffer-substring-no-properties (point-min) (point-max))
           (s-split "\n" it)
           (--map (s-concat (file-name-directory file) "/" it) it)
           (--filter
            (and
             (file-directory-p it)
             (file-directory-p (concat it "/.git")))
            it)))))

(defun code-compass--directory-git-p (directory)
  "Check if DIRECTORY is a Git repository."
  (and
   (file-directory-p directory)
   (file-directory-p (concat directory "/.git"))))

(defun code-compass--add-prepended-reports (directory)
"Prepend DIRECTORY to gitreport and revisions."
(copy-file "gitreport.log" (format "%s-gitreport.log" (code-compass--filename directory)) 't)
(copy-file "revisions.csv" (format "%s-revisions.csv" (code-compass--filename directory)) 't)
directory)

(defun code-compass-show-hotspot-cluster-sync (repository date &optional port)
  "Show hotspot analysis for repositories in DIRECTORY.
Filter by DATE.
If a file `repos-cluster.txt' exists with a list of repositories
in the current REPOSITORY, this has priority over DIRECTORY."
  (interactive
   (list
    (read-directory-name "Choose repositories directory:" ".")
    (call-interactively #'code-compass-request-date)
    8888))
  (let* ((filepath (s-concat repository "/repos-cluster.txt"))
         (directories
          (or (code-compass--get-repositories-from-file filepath)
              (-filter
               #'code-compass--directory-git-p
               (--remove
                (or (s-ends-with? "/." it) (s-ends-with? "/.." it))
                (directory-files repository 't)))))
         (no-prefix-revisions-fn
          (lambda (repo)
            (code-compass--produce-code-maat-revisions-report repository)
            repo)))
    (message "Used directories: %s" directories)
    (code-compass--in-temp-directory
     repository
     (code-compass--produce-cloc-report repository)
     (--each directories
       (--> it
            (code-compass-produce-git-report it date)
            (funcall no-prefix-revisions-fn it)
            code-compass--add-prepended-reports
            ;; codemaat: prepend "repo-name/" to all entries apart first and last (empty line)
            (let* ((filename (code-compass--filename it))
                   (rev-file (s-concat filename "-revisions.csv")))
              (--> rev-file
                   (with-temp-buffer
                     (insert-file-contents-literally it)
                     (buffer-substring-no-properties (point-min) (point-max)))
                   (s-split "\n" it 'omit-nulls)
                   cdr
                   (--map (concat filename "/" it) it)
                   (s-join "\n" it)
                   (format "%s\n" it)
                   (write-region it nil rev-file)))))
     (code-compass--in-temp-directory
      repository
      ;; at this point I need to merge all *-revisions.csv and cloc-*.csv in something like "system"
      (shell-command "cat *-revisions.csv | sed '/^[[:space:]]*$/d' > revisions.csv;")
      (write-region
       (concat
        "entity,n-revs\n"
        (with-temp-buffer
          (insert-file-contents-literally "revisions.csv")
          (buffer-substring-no-properties (point-min) (point-max))))
       nil
       "revisions.csv")
      (--> repository
           code-compass--generate-merger-script
           code-compass--generate-d3-v3-lib
           code-compass--produce-json
           code-compass--generate-host-enclosure-diagram-html
           (code-compass--run-server-and-navigate it port))))))
(define-obsolete-function-alias 'c/show-hotspot-cluster-sync #'code-compass-show-hotspot-cluster-sync "0.1.2")

;;;###autoload
(defun code-compass-show-hotspot-cluster (directory date &optional port)
  "Show DIRECTORY enclosure diagram for hotspots.
Starting DATE reduces scope of Git log.
 PORT defines where the html is served."
  (interactive
   (list
    (read-directory-name "Choose repositories directory:" ".")
    (call-interactively #'code-compass-request-date)))
  (code-compass--async-run #'code-compass-show-hotspot-cluster-sync directory date port))
(define-obsolete-function-alias 'c/show-hotspot-cluster #'code-compass-show-hotspot-cluster "0.1.2")

;; END Hotspots for microservices

;; BEGIN display contributors


(defun code-compass--contributors-list-for-current-buffer ()
  "Return contributors of this file if it is in a git repository."
  (if (vc-root-dir)
      (shell-command-to-string
       (concat
        "git shortlog HEAD -n -s -- "
        (buffer-file-name)))
    "    No history yet"))

(defun code-compass-file-name-parent-directory (filename)
  "Get parent of FILENAME. This comes in Emacs 29."
  (let* ((expanded-filename (expand-file-name filename))
         (parent (file-name-directory (directory-file-name expanded-filename))))
    (cond
     ;; filename is at top-level, therefore no parent
     ((or (null parent)
          ;; `equal' is enough, we don't need to resolve symlinks here
          ;; with `file-equal-p', also for performance
          (equal parent expanded-filename))
      nil)
     ;; filename is relative, return relative parent
     ((not (file-name-absolute-p filename))
      (file-relative-name parent))
     (t
      parent))))

;;;###autoload
(defun code-compass-display-contributors ()
  "Show in minibuffer the main contributors of this file."
  (interactive)
  (when (and code-compass-display-file-contributors (buffer-file-name))
    (let ((file-path buffer-file-name))
      ;; When we have the ability to infer the project root, we will use that to display the relative file path
      ;; Otherwise, we will display the entire full path.
      ;; We can infer project root when file in question, is in VCS. If its a new file, the function won't
      ;; be able to pick it up, so it will display the full file path.
      (when (vc-root-dir)
        (setq file-path (file-relative-name (buffer-file-name) (code-compass-file-name-parent-directory (vc-root-dir)))))
      (message "Contributors of %s:\n%s" file-path (code-compass--contributors-list-for-current-buffer)))))
(define-obsolete-function-alias 'c/display-contributors #'code-compass-display-contributors "0.1.2")

(defun code-compass-display-contributors-delayed ()
  "Delayed version of `code-compass--display-contributors' for use in hooks.
For example `prog-mode-hook'."
  (run-with-timer 0.1 nil 'code-compass-display-contributors))
(define-obsolete-function-alias 'c/display-contributors-delayed #'code-compass-display-contributors-delayed "0.1.2")


(defun code-compass-toggle-display-contributors ()
  "Show the contributors when opening files."
  (interactive)
  (if (-contains-p prog-mode-hook #'code-compass-display-contributors-delayed)
      (remove-hook 'prog-mode-hook #'code-compass-display-contributors-delayed)
    (add-hook 'prog-mode-hook #'code-compass-display-contributors-delayed)))
;; END display contributors

(defun code-compass-cheatsheet ()
  "Show cheatsheet for code compass."
  (interactive)
  (switch-to-buffer (get-buffer-create "*Code Compass Help*"))
  (org-mode)
  (insert "
      | Command                                          | Description                                                     |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-doctor                              | Check dependencies are satisfied.                               |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | Graphs                                           |                                                                 |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-show-hotspots                       | Show code that changed more frequently in the last period.      |
      | code-compass-show-hotspot-snapshot-sync          | Show hotspots over many intervals of times.                     |
      | code-compass-show-hotspot-cluster                | Show hotspots graph for a directory containing many projects.   |
      | code-compass-show-code-churn                     | Show code throughput/churn in the last period.                  |
      | code-compass-show-coupling-graph                 | Show which file is coupled to which in the last period.         |
      | code-compass-show-code-communication             | Show which contributors are/should likely chat with each other. |
      | code-compass-show-knowledge-graph                | Show who knows most about what code.                            |
      | code-compass-show-refactoring-graph              | Show who refactored most what code.                             |
      | code-compass-show-stability-graph                | Show the code that is most stable in last period.               |
      | code-compass-show-fragmentation                  | Show pie chart with how much people contributed to file.        |
      | code-compass-show-gource                         | Show gource video of repository contributions.                  |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | Complexity                                       |                                                                 |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-calculate-complexity-current-buffer | Prints complexity stats of current buffer.                      |
      | code-compass-show-complexity-over-commits        | Show line graph of complexity for current file from the start.  |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | Word analysis                                    |                                                                 |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-word-statistics                     | Show stats about words in buffer.                               |
      | code-compass-word-semantics                      | Show words least used in buffer.                                |
      | code-compass-word-analysis-commits               | Show frequency of words in commit messages.                     |
      | code-compass-word-analysis-region                | Show frequency of words in region (useful on functions)         |
      | code-compass-word-analysis-region-graph          | Show graph for words frequency in region.                       |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | Coupling extras                                  |                                                                 |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-create-todos-from-coupled-files     | Make TODO list from coupled files.                              |
      | code-compass-find-coupled-files                  | Jump to a file coupled to the current one, if available.        |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | Contributors extras                              |                                                                 |
      |--------------------------------------------------+-----------------------------------------------------------------|
      | code-compass-slack-main-contributor              | Chat with main contributor of file via emacs-slack.             |
      | code-compass-display-contributors                | Show contributors for current file in minibuffer.               |
      | code-compass-show-raw-csv                        | Show the raw csv file for code-maat analyses.                   |
      |--------------------------------------------------+-----------------------------------------------------------------|

"))
(define-obsolete-function-alias 'c/cheatsheet #'code-compass-cheatsheet "0.1.2")

;;;###autoload
(defun code-compass-show-raw-csv (analysis repository date)
  "Show REPOSITORY edge bundling synchronously for code coupling up to DATE.
Serve graph on PORT.
Argument ANALYSIS sets the anylysis command to run."
  (interactive (list
                (completing-read "Analysis:"
                                 '("authors" "revisions" "coupling" "soc" "summary" "identity" "abs-churn" "author-churn" "entity-churn" "entity-ownership" "main-dev" "refactoring-main-dev" "entity-effort" "main-dev-by-revs" "fragmentation" "communication" "messages" "age"))
                (read-directory-name "Choose git repository directory:" (vc-root-dir))
                (call-interactively #'code-compass-request-date)))
  (code-compass--in-temp-directory
   repository
   (--> repository
        (code-compass-produce-git-report it nil date)
        (code-compass--run-code-maat analysis it))
   (find-file (concat "./" analysis ".csv"))
   (when (progn (goto-char (point-min))
                (search-forward "entity," nil t))
     (while (search-forward-regexp "\\\n." nil t)
       (goto-char (- (point) 1))
       (insert repository))
     (goto-char (point-min)))))
(define-obsolete-function-alias 'c/show-raw-csv #'code-compass-show-raw-csv "0.1.2")

(provide 'code-compass)

;;; code-compass.el ends here
