2018-08-05 16:19:52 +02:00
|
|
|
|
;;; db-projects.el -- Simple Directory-Based Project Management -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
2020-09-20 14:48:59 +02:00
|
|
|
|
;; A project is simply a directory under `projects-main-project-directory'
|
|
|
|
|
;; containing either .git or .projectile. This little collection of functions
|
|
|
|
|
;; helps to manage these project directories and also integration them
|
|
|
|
|
;; consistently with the projectile package.
|
|
|
|
|
|
|
|
|
|
;; To start, first customize `projects-main-project-directory' and
|
|
|
|
|
;; `projects-archive-directory' as needed. Then use `projects-add-project' to
|
|
|
|
|
;; add new projects and `projects-archive-project' to archive them (i.e., move
|
|
|
|
|
;; them to `projects-archive-directory'). This package does not offer to remove
|
|
|
|
|
;; projects; this has to be done manually.
|
|
|
|
|
|
|
|
|
|
;; XXX: check known projects for missing bookmarks
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
2018-08-18 10:15:05 +02:00
|
|
|
|
(require 'subr-x)
|
2018-08-18 10:50:55 +02:00
|
|
|
|
(require 'cl-lib)
|
|
|
|
|
(require 'dash)
|
2020-06-26 21:53:05 +02:00
|
|
|
|
(require 'bookmark)
|
2020-09-20 14:10:46 +02:00
|
|
|
|
(require 'projectile)
|
2018-08-18 10:15:05 +02:00
|
|
|
|
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(defgroup projects nil
|
|
|
|
|
"Simple directory-based project management"
|
|
|
|
|
:tag "Project Management"
|
|
|
|
|
:group 'projects)
|
|
|
|
|
|
|
|
|
|
(defcustom projects-main-project-directory "~/Documents/projects/"
|
|
|
|
|
"Main directory to host projects."
|
2020-06-26 21:53:05 +02:00
|
|
|
|
:group 'projects
|
|
|
|
|
:type 'directory)
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
2020-06-26 21:53:05 +02:00
|
|
|
|
(defcustom projects-archive-directory "~/Documents/projects/.archive/"
|
2020-09-20 14:12:50 +02:00
|
|
|
|
"Directory to archive projects into."
|
2020-06-26 21:53:05 +02:00
|
|
|
|
:group 'projects
|
|
|
|
|
:type 'directory)
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
|
|
|
|
(defun projects-project-exists-p (short-name)
|
2020-09-20 14:12:50 +02:00
|
|
|
|
"Check whether a project named SHORT-NAME already exists."
|
2018-08-18 10:49:59 +02:00
|
|
|
|
(or
|
|
|
|
|
(file-exists-p (expand-file-name (concat (file-name-as-directory short-name)
|
|
|
|
|
".git")
|
|
|
|
|
projects-main-project-directory))
|
|
|
|
|
(file-exists-p (expand-file-name (concat (file-name-as-directory short-name)
|
|
|
|
|
".projectile")
|
|
|
|
|
projects-main-project-directory))))
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
|
|
|
|
(defun projects-existing-projects ()
|
2020-09-20 14:12:50 +02:00
|
|
|
|
"Return list of all short-names of existing projects."
|
2018-08-18 10:49:59 +02:00
|
|
|
|
(cl-remove-if-not #'projects-project-exists-p
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(directory-files projects-main-project-directory)))
|
|
|
|
|
|
2020-09-20 14:24:38 +02:00
|
|
|
|
;;;###autoload
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(defun projects-add-project (short-name long-name)
|
2020-09-20 14:12:50 +02:00
|
|
|
|
"Add new project with SHORT-NAME and LONG-NAME.
|
|
|
|
|
The project directory will be located under
|
|
|
|
|
`projects-main-project-directory' within a directory named
|
|
|
|
|
SHORT-NAME. A bookmark to the project diary will be created,
|
|
|
|
|
using the given LONG-NAME. The project diary will be pre-filled
|
|
|
|
|
with some standard information like title and creation date."
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(interactive "sShort Name: \nsLong Name: ")
|
|
|
|
|
(when (projects-project-exists-p short-name)
|
2018-08-05 16:35:44 +02:00
|
|
|
|
(user-error "Project %s already exists, exiting" short-name))
|
2020-09-20 14:23:52 +02:00
|
|
|
|
(when (file-exists-p (expand-file-name short-name
|
|
|
|
|
projects-archive-directory))
|
|
|
|
|
(user-error "Project %s already exists as archived project, exiting" short-name))
|
2018-08-18 10:15:05 +02:00
|
|
|
|
(let* ((project-directory (expand-file-name short-name
|
|
|
|
|
projects-main-project-directory))
|
|
|
|
|
(default-directory project-directory))
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(make-directory project-directory)
|
2018-08-18 10:15:05 +02:00
|
|
|
|
(make-directory (expand-file-name "scripts"))
|
|
|
|
|
(make-directory (expand-file-name "data"))
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(with-temp-buffer
|
|
|
|
|
(insert (format "#+title: %s\n" long-name))
|
|
|
|
|
(insert (format "#+created: %s\n\n"
|
|
|
|
|
(format-time-string "[%Y-%m-%d %a %H:%M]" (current-time))))
|
2018-08-18 10:15:05 +02:00
|
|
|
|
(write-file (expand-file-name "projekttagebuch.org"))
|
2018-08-18 09:57:08 +02:00
|
|
|
|
(bookmark-set (format "Projekttagebuch %s" long-name)))
|
2018-08-18 10:15:05 +02:00
|
|
|
|
(if-let ((git-executable (executable-find "git")))
|
|
|
|
|
(call-process git-executable nil nil nil "init")
|
|
|
|
|
(write-region "" nil (expand-file-name ".projectile")))
|
2018-08-05 16:29:19 +02:00
|
|
|
|
(when (require 'projectile nil 'no-error)
|
|
|
|
|
(projectile-add-known-project project-directory))))
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
2020-09-20 14:24:38 +02:00
|
|
|
|
;;;###autoload
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(defun projects-archive-project (short-name)
|
2020-09-20 14:12:50 +02:00
|
|
|
|
"Archive existing project identified by SHORT-NAME.
|
|
|
|
|
This amounts to moving the project directory SHORT-NAME under
|
|
|
|
|
`projects-main-project-directory' to
|
|
|
|
|
`projects-archive-directory', deleting the bookmark to the
|
|
|
|
|
project diary, and updating projectile's cache."
|
2018-08-18 10:15:21 +02:00
|
|
|
|
(interactive
|
|
|
|
|
(list (completing-read "Short Name: " (projects-existing-projects) nil t)))
|
2018-08-05 16:29:03 +02:00
|
|
|
|
(unless (projects-project-exists-p short-name)
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(user-error "Project %s does not exist, exiting" short-name))
|
2018-08-18 10:50:55 +02:00
|
|
|
|
|
|
|
|
|
;; Remove bookmark first
|
|
|
|
|
(let* ((notebook-path (expand-file-name (concat
|
|
|
|
|
(file-name-as-directory short-name)
|
|
|
|
|
"projekttagebuch.org")
|
|
|
|
|
projects-main-project-directory))
|
|
|
|
|
(bookmark-entry (cl-find-if (lambda (entry)
|
|
|
|
|
(let ((filename (->> entry
|
|
|
|
|
cdr
|
|
|
|
|
(cl-assoc 'filename)
|
|
|
|
|
cdr)))
|
|
|
|
|
(and (not (file-remote-p filename))
|
|
|
|
|
(file-equal-p notebook-path
|
|
|
|
|
filename))))
|
|
|
|
|
bookmark-alist)))
|
|
|
|
|
(if (null bookmark-entry)
|
|
|
|
|
(warn "No bookmark for project notebook of %s found." short-name)
|
|
|
|
|
(bookmark-delete (car bookmark-entry))))
|
|
|
|
|
|
|
|
|
|
;; Move project directory into archive
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(unless (file-exists-p projects-archive-directory)
|
|
|
|
|
(make-directory projects-archive-directory))
|
|
|
|
|
(rename-file (expand-file-name short-name projects-main-project-directory)
|
|
|
|
|
(expand-file-name short-name projects-archive-directory)
|
|
|
|
|
nil)
|
2018-08-18 10:50:55 +02:00
|
|
|
|
|
|
|
|
|
;; Update projectile’s cache
|
2020-09-20 14:11:01 +02:00
|
|
|
|
(projectile-remove-known-project
|
|
|
|
|
(expand-file-name short-name
|
|
|
|
|
projects-main-project-directory)))
|
2018-08-05 16:19:52 +02:00
|
|
|
|
|
2020-09-20 14:48:18 +02:00
|
|
|
|
(defun projects--org-files ()
|
|
|
|
|
"Return all Org Mode files in all known projects, recursively."
|
|
|
|
|
(mapcan #'(lambda (dir)
|
|
|
|
|
(directory-files-recursively (expand-file-name dir projects-main-project-directory)
|
|
|
|
|
".*\\.org" nil))
|
|
|
|
|
(projects-existing-projects)))
|
|
|
|
|
|
|
|
|
|
(defvar org-agenda-text-search-extra-files) ; to keep the byte-compiler happy
|
|
|
|
|
|
|
|
|
|
(defun projects-find-unsearched-org-files ()
|
|
|
|
|
"Find Org Mode files in known projects that are not searched by default.
|
|
|
|
|
This is done by checking all org Mode files in every project
|
|
|
|
|
whether it is included in `org-agenda-text-search-extra-files'."
|
|
|
|
|
(require 'org)
|
|
|
|
|
(let ((extra-files (make-hash-table :test #'equal)))
|
|
|
|
|
(mapc #'(lambda (entry)
|
|
|
|
|
(when (stringp entry)
|
|
|
|
|
(puthash (file-truename entry) t extra-files)))
|
|
|
|
|
org-agenda-text-search-extra-files)
|
|
|
|
|
(cl-remove-if #'(lambda (org-file)
|
|
|
|
|
(gethash (file-truename org-file) extra-files nil))
|
|
|
|
|
(projects--org-files))))
|
2018-08-18 10:49:30 +02:00
|
|
|
|
|
2018-08-05 16:19:52 +02:00
|
|
|
|
(provide 'db-projects)
|
|
|
|
|
|
|
|
|
|
;;; db-projects.el ends here
|