How to manage Emacs packages effectively

I developed a few package managing techniques after reading Steve Purcell's Emacs setup.

The techniques are compatible with other plugin (use-package, for example) based on package.el.

Emacs Lisp knowledge is required to read this article.

1 Technique 1, Do NOT use package.el for certain packages

Create directory site-lisp at ~/.emacs.d, then insert below code into ~/.emacs.d/init.el,

(if (fboundp 'normal-top-level-add-to-load-path)
    (let* ((my-lisp-dir "~/.emacs.d/site-lisp/")
           (default-directory my-lisp-dir))
      (progn
        (setq load-path
              (append
               (loop for dir in (directory-files my-lisp-dir)
                     unless (string-match "^\\." dir)
                     collecting (expand-file-name dir))
               load-path)))))

You can create a sub-directory under ~/.emacs.d/site-lisp/ and place your package's source code inside that sub-directory.

2 Technique 2, Create your own package repository

Step 1, Place two files "archive-contents" and "hello-1.0.0.el" in any directory. Say ~/.emacs.d/localelpa.

Content of archive-contents:

(1
 (hello . [(1 0 0) nil "Say hello" single])
)

Content of hello-1.0.0.el:

;;;###autoload
(defun hello-say ()
  (interactive)
  (message "Hi, hello!"))

(provide 'hello)

Step 2, insert below code into ~/.emacs,

(add-to-list 'package-archives '("localelpa" . "~/.emacs.d/localelpa"))

Step 3, restart Emacs and press M-x list-packages. As you can see, you can install package hello from repository ~/.emacs.d/localelpa.

I'm using rainbow-mode from ELPA(https://elpa.gnu.org/). But ELPA which shuts down sometimes. With above technique, my setup is never dependent on the reliability of GNU's website.

I also create plugin elpa-mirror which creates a local repository from all the installed packages. The local repository could also be converted to remote repository easily using Dropbox and Github.

It also solve the issue of orphan package. In order to get a clean setup without orphan packages, you only need delete everything from ~/.emacs.d/elpa and set the repository to the local one build by elpa-mirror. It takes 30 seconds to install 300 packages.

3 Technique 3, advice package--add-to-archive-contents to filter packages

Insert below code into ~/.emacs,

;; List of VISIBLE packages from melpa-unstable (http://melpa.org)
;; Feel free to add more packages!
(defvar melpa-include-packages
  '(bbdb
    color-theme
    company-c-headers)
  "Don't install any mELPA packages except these packages")

(defvar package-filter-function nil
  "Optional predicate function used to internally filter packages used by package.el.

The function is called with the arguments PACKAGE VERSION ARCHIVE, where
PACKAGE is a symbol, VERSION is a vector as produced by `version-to-list', and
ARCHIVE is the string name of the package archive.")

;; Don't take MELPA versions of certain packages
(setq package-filter-function
      (lambda (package version archive)
        (or (not (string-equal archive "melpa"))
            ;; install package in whitelist
            (memq package melpa-include-packages)
            ;; use all color themes
            (string-match (format "%s" package) "-theme"))))

(defadvice package--add-to-archive-contents
  (around filter-packages (package archive) activate)
  "Add filtering of available packages using `package-filter-function', if non-nil."
  (when (or (null package-filter-function)
      (funcall package-filter-function
         (car package)
         (funcall (if (fboundp 'package-desc-version)
          'package--ac-desc-version
        'package-desc-vers)
            (cdr package))
         archive))
    ad-do-it))

Above code hide the packages which don't meet our criteria.

The criteria is just a simple boolean expression defined in package-filter-function.

We install packages if it's NOT from melpa-unstable OR it's listed in melpa-include-packages OR its name contains "-theme" (it's color theme package).

Feel free to use any boolean expression to filter packages.

The initial version of above code was copied Steve Purcell's setup years ago.

I developed my own strategy based on his code. The first thing I did is reverse his logic. In his code melpa-unstable take the highest priority while in my code melpa-unstable takes the lowest priority.

4 Summary

You can combine my techniques to solve any package problem.

For example, package A is dependent on package B. Both A and B have two versions, 1.0 and 2.0:

  • A 2.0 can use B 1.0 and B 2.0, but A 1.0 can ONLY use B 1.0
  • A 2.0 can ONLY use B 2.0, and A 1.0 can only use B 1.0

The solution is simple. We create a local repository to host B 1.0 and A 1.0 (Technique 2), then in package-filter-function (Technique 3), we will decide which version of to use the with information collected from B. As I said, package-filter-function only returns a boolean expression. So you can design any strategy.

I know somebody believed Emacs is worse than IDE because its package manager after "studying" it for seven years.

It took me fifteen minutes to reach the opposite conclusion when I was still absolute Emacs dummy.

It's possibly I learned the Emacs by cloning and reading code. You can read my guide "Master Emacs In One Year" for more details.

New git-timemachine UI based on ivy-mode

UPDATED: <2016-09-23 Fri>

CREATED: <2016-06-05>

When using git-timemachine, I prefer start from my selected revision instead of HEAD.

Here is my code based on ivy-mode,

(defun my-git-timemachine-show-selected-revision ()
  "Show last (current) revision of file."
  (interactive)
  (let* ((collection (mapcar (lambda (rev)
                    ;; re-shape list for the ivy-read
                    (cons (concat (substring-no-properties (nth 0 rev) 0 7) "|" (nth 5 rev) "|" (nth 6 rev)) rev))
                  (git-timemachine--revisions))))
    (ivy-read "commits:"
              collection
              :action (lambda (rev)
                        ;; compatible with ivy 9+ and ivy 8
                        (unless (string-match-p "^[a-z0-9]*$" (car rev))
                          (setq rev (cdr rev)))
                        (git-timemachine-show-revision rev)))))

(defun my-git-timemachine ()
  "Open git snapshot with the selected version.  Based on ivy-mode."
  (interactive)
  (unless (featurep 'git-timemachine)
    (require 'git-timemachine))
  (git-timemachine--start #'my-git-timemachine-show-selected-revision))

Screenshot after M-x my-git-timemachine,

my-git-timemachine-nq8.png