Magit

Magit is a complete text-based user interface to Git. It fills the glaring gap between the Git command-line interface and various GUIs, letting you perform trivial as well as elaborate version control tasks with just a couple of mnemonic key presses. Magit looks like a prettified version of what you get after running a few Git commands but in Magit every bit of visible information is also actionable to an extent that goes far beyond what any Git GUI provides and it takes care of automatically refreshing this output when it becomes outdated. In the background Magit just runs Git commands and if you wish you can see what exactly is being run, making it possible for you to learn the git command-line by using Magit1.

Actually, Emacs already has a built-in version control package (aptly named vc). But to me the UI is rather cryptic. I’ve used magit since forever, and old habit die hard.

Figure 1: Emacs magit status

Figure 1: Emacs magit status

Installation

use-package

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(use-package magit
  :ensure
  :demand t
  :config
  (evil-set-initial-state #'git-commit-mode 'insert)
  (with-eval-after-load 'general
    (+config/leader-go
      "g" 'magit-status))
  :custom
  (magit-revision-show-gravatars '("^Author:     " . "^Commit:     "))
  (magit-diff-refine-hunk 'all)
  (magit-log-arguments '("-n100" "--graph" "--decorate")))
Code Snippet 1: magit suggested installation

Notes:

  • magit-revision-show-gravatars will enable gravatars when viewing commits. The service used by default is Libgravatar.

extensions

There are lots of extensions for magit which adds more features or functionalities. These are the ones I use:

Interactive Emacs functions that create URLs for files and commits in GitHub/Bitbucket/GitLab/… repositories2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(use-package git-link
  :ensure
  :commands (git-link git-link-commit git-link-homepage)
  :config
  (with-eval-after-load 'general
    (+config/leader-go
      "G" '(:ignore t :wk "git")
      "Gl" 'git-link
      "Gh" 'git-link-homepage
      "Gc" 'git-link-commit)))
Code Snippet 2: my git-link setup

Notes:

  • git-link returns the URL for the current buffer’s file location at the current line number or active region.

  • git-link-commit returns the URL for the commit at point.

  • git-link-homepage returns the URL for the repository’s homepage.

    All the URLs are added to the kill ring automatically.

git-messenger

git-messenger.el is Emacs port of git-messenger.vim3.

git-messenger.el provides function that popup commit message at current line. This is useful when you want to know why this line was changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(use-package git-messenger
  :ensure
  :config
  (with-eval-after-load 'general
    (+config/leader-go
      "Gm" 'git-messenger:popup-message))
  :custom
  ;; Enable magit-show-commit instead of pop-to-buffer
  (git-messenger:use-magit-popup t)
  (git-messenger:show-detail t))
Code Snippet 3: my git-messenger setup
Figure 2: image from the github README

Figure 2: image from the github README

  • git-timemachine

    Walk through git revisions of a file4.

    1
    2
    3
    4
    5
    6
    7
    
    (use-package git-timemachine
      :ensure
      :after magit
      :config
      (with-eval-after-load 'general
        (+config/leader-go
          "Gt" 'git-timemachine-toggle)))
    
    Code Snippet 4: My git-timemachine configuration
  • magit-todos

    This package displays keyword entries from source code comments and Org files in the Magit status buffer. Activating an item jumps to it in its file. By default, it uses keywords from hl-todo, minus a few (like NOTE).

Projectile

Projectile is a project interaction library for Emacs. Its goal is to provide a nice set of features operating on a project level without introducing external dependencies (when feasible). For instance - finding project files has a portable implementation written in pure Emacs Lisp without the use of GNU find (but for performance sake an indexing mechanism backed by external commands exists as well).

Installation

1
2
3
4
5
6
7
(use-package projectile
  :ensure t
  :init
  (projectile-mode +1)
  :bind (:map projectile-mode-map
              ("s-p" . projectile-command-map)
              ("C-c p" . projectile-command-map)))
Code Snippet 5: projectile suggested installation

This is the basic configuration, however, projectile is highly configurable. This is how I setup my configuration for projectile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(use-package projectile
  :ensure t
  :demand t
  :bind (([remap evil-jump-to-tag] . projectile-find-tag)
         ([remap find-tag] . projectile-find-tag))
  :hook (dired-before-readin . projectile-track-known-projects-find-file-hook)
  :custom
  (projectile-cache-file (expand-file-name ".projects" user-emacs-directory))
  (projectile-auto-discover nil)
  (projectile-enable-caching (not noninteractive))
  (projectile-globally-ignored-files '("DS_Store" "TAGS"))
  (projectile-globally-ignored-file-suffixes '(".elc" ".pyc" ".o"))
  (projectile-kill-buffers-filter 'kill-only-files)
  (projectile-known-projects-file (expand-file-name ".projectile_projects.eld" user-emacs-directory))
  (projectile-ignored-projects '("~/"))
  (projectile-project-root-files-bottom-up
   (append '(".projectile" ".project" ".git")
           (when (executable-find "hg")
             '(".hg"))
           (when (executable-find "bzr")
             '(".bzr"))))
  (projectile-project-root-files-top-down-recurring '("Makefile"))
  (compilation-buffer-name-function #'projectile-compilation-buffer-name)
  (compilation-save-buffers-predicate #'projectile-current-project-buffer-p)
  (projectile-git-submodule-command nil)
  (projectile-indexing-method 'hybrid)
  :config
  (projectile-mode +1)
  (put 'projectile-git-submodule-command 'initial-value projectile-git-submodule-command)
  (with-eval-after-load 'general
  (+config/leader-key
    "SPC" 'projectile-find-file
    "p" '(:keymap projectile-command-map :package projectile :wk "projectile"))))
Code Snippet 6: my projectile configuration

Notes:

External tools

Projectile will work without any external dependencies out of the box. However, if you have various tools installed, they will be automatically used when appropriate to improve performance. If you use git, install the system package as well.

fd

File searching tool, if available, will be use as an alternative to git ls-files.

ag / ripgrep

To benefit from the projectile-ag and projectile-ripgrep commands to perform file search, it’s recommended to install ag (the_silver_searcher) and/or rg (ripgrep). You should also install the Emacs packages ag, ripgrep or rg if you want to make sure of Projectile’s commands projectile-ag and projectile-ripgrep.

1
2
3
4
5
(use-package ripgrep
  :ensure
  :init
  (with-eval-after-load 'evil-collection
    (evil-collection-ripgrep-setup)))
Code Snippet 7: ripgrep package

Useful commands

  • projectile-find-file, bound to SPC p f in my configuration. Find file in current project.
  • projectile-switch-project, bound to SPC p p, you can also switch to between open projects with SPC p q.
  • projectile-grep, bound to SPC p s g, search for text/regexp in project.
  • projectile-replace, bound to SPC p r, replace in project.
  • projectile-commander, execute any projectile command with a single letter. See the variable projectile-commander-methods to see the list of methods used.
  • projectile-find-other-file, switch between files with the same name but different extensions (e.g. foo.h to foo.c).
  • projectile-run-shell-command-in-root and projectile-run-async-shell-comand-in-root, bound to SPC p ! and SPC p &, Run a shell command in the root of the project.
  • other commands described in the documentation.

Packages that interacts with projectile

diff-hl

diff-hl-mode highlights uncommitted changes on the left side of the window (area also known as the “gutter”), allows you to jump between and revert them selectively5.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(use-package diff-hl
  :ensure
  :hook (find-file . diff-hl-mode)
  :hook (vc-dir-mode . diff-hl-dir-mode)
  :hook (dired-mode . diff-hl-dired-mode)
  :hook (diff-hl-mode . diff-hl-show-hunk-mouse-mode)
  :hook (diff-hl-mode . diff-hl-flydiff-mode)
  :hook (magit-pre-refresh-hook . diff-hl-magit-pre-refresh)
  :hook (magit-post-refresh-hook . diff-hl-magit-post-refresh)
  :init
  (global-diff-hl-mode)
  :custom
  (vc-git-diff-switches '("--histogram")
                        diff-hl-flydiff-delay 0.5
                        diff-hl-show-staged-changes nil)
  :config
  (when (featurep 'flycheck)
    (setq flycheck-indication-mode 'right-fringe)))
Code Snippet 8: my diff-hl setup

Perspective

The Perspective package provides multiple named workspaces (or “perspectives”) in Emacs, similar to multiple desktops in window managers like Awesome and XMonad, and Spaces on the Mac. Each perspective has its own buffer list and its own window layout, along with some other isolated niceties, like the xref ring. This makes it easy to work on many separate projects without getting lost in all the buffers. Switching to a perspective activates its window configuration, and when in a perspective, only its buffers are available (by default). Each Emacs frame has a distinct list of perspectives6.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(use-package perspective
  :ensure
  :config
  (setq persp-initial-frame-name "Main"
        persp-suppress-no-prefix-key-warning t)
  (if (featurep 'no-littering)
      (setq persp-state-default-file (expand-file-name ".perspective-state" no-littering-var-directory))
    (setq persp-state-default-file (expand-file-name ".perspective-state" user-emacs-directory)))
  (global-set-key [remap switch-to-buffer] #'persp-switch-to-buffer*)
  (when (featurep 'consult)
    (require 'consult)
    (unless (boundp 'persp-consult-source)
      (defvar persp-consult-source
        (list :name     "Perspective"
              :narrow   ?s
              :category 'buffer
              :state    #'consult--buffer-state
              :history  'buffer-name-history
              :default  t
              :items
              #'(lambda () (consult--buffer-query :sort 'visibility
                                                  :predicate '(lambda (buf) (persp-is-current-buffer buf t))
                                                  :as #'buffer-name)))))
    (consult-customize consult--source-buffer :hidden t :default nil)
    (add-to-list 'consult-buffer-sources persp-consult-source))
  (with-eval-after-load 'general
    (general-def
      :keymaps 'perspective-map
      "P" 'projectile-persp-switch-project)
    (+config/leader-key
      "TAB" '(:keymap perspective-map
                      :package perspective
                      :which-key "perspective")
      "TAB TAB" '(persp-switch-last :wk "switch to last perspective")
      "C-x" '(persp-switch-to-scratch-buffer :wk "switch to scratch buffer")))
  :init
  (customize-set-variable 'persp-mode-prefix-key (kbd "C-c TAB"))
  (unless (equal persp-mode t)
    (persp-mode 1))
  :bind (([remap switch-to-buffer] . persp-switch-to-buffer*)
         ([remap kill-buffer] . persp-kill-buffer*))
  :hook (kill-emacs . persp-state-save))

(use-package persp-projectile
  :ensure t
  :after perspective
  :commands projectile-persp-switch-project)
Code Snippet 9: my perspective configuration

Notes: The persp-consult-source is added to the consult-buffer-sources for the consult-buffer command. The prefix keybinding I use is SPC TAB.