Quickly switching between git status files buffers in Emacs

When I'm deep into developing a feature, I don't want to think about whether a file is already open in a buffer, or if I need to search the whole project to find it.
I just want to jump straight to the files I'm actively working on, the ones that Git tells me are modified, untracked, or even renamed.
Emacs already gives us great defaults for different contexts:
➡ C-x b
(switch-to-buffer
) → jump between open buffers.
➡ C-x p b
(project-switch-to-buffer
) → jump between buffers in the
current project.
➡ C-x p f
(project-find-file
) → find any file inside the project.
Each of these has its value and time. But in the middle of a coding session, when iterating fast on a feature, none of them give me what I want: a list of just the files I've touched.
That's the itch that led me to write this command.
The Function and Binding
(defun emacs-solo/switch-git-status-buffer ()
"Parse git status from an expanded path and switch to a file.
The completion candidates include the Git status of each file."
(interactive)
(require 'vc-git)
(let ((repo-root (vc-git-root default-directory)))
(if (not repo-root)
(message "Not inside a Git repository.")
(let* ((expanded-root (expand-file-name repo-root))
(command-to-run (format "git -C %s status --porcelain=v1"
(shell-quote-argument expanded-root)))
(cmd-output (shell-command-to-string command-to-run))
(target-files
(let (files)
(dolist (line (split-string cmd-output "\n" t) (nreverse files))
(when (> (length line) 3)
(let ((status (substring line 0 2))
(path-info (substring line 3)))
;; Handle rename specially
(if (string-match "^R" status)
(let* ((paths (split-string path-info " -> " t))
(new-path (cadr paths)))
(when new-path
(push (cons (format "R %s" new-path) new-path) files)))
;; Modified or untracked
(when (or (string-match "M" status)
(string-match "\?\?" status))
(push (cons (format "%s %s" status path-info) path-info) files)))))))))
(if (not target-files)
(message "No modified or renamed files found.")
(let* ((candidates target-files)
(selection (completing-read "Switch to buffer (Git modified): "
(mapcar #'car candidates) nil t)))
(when selection
(let ((file-path (cdr (assoc selection candidates))))
(when file-path
(find-file (expand-file-name file-path expanded-root)))))))))))
(global-set-key (kbd "C-x C-g") 'emacs-solo/switch-git-status-buffer)
The command parses git status --porcelain=v1
, collects the
interesting files, and offers them through completing-read
. This
means it works with whichever completion frontend you already use, and
it switches to the selected file's buffer (opening it first if it
isn't already open).
Completion in Action
Here's how it looks in different completion UIs:
➡ Plain completion buffer
➡ Fido
➡ Fido vertical
➡ Icomplete vertical
➡ Vertico
No matter which you prefer, you get a filtered list of just the files that matter right now.
Why emacs-solo?
This command is part of my emacs-solo project, a collection of Emacs enhancements that stick to built-in tools and minimal glue code.
The goal is to show how far we can go without depending on a huge package ecosystem, while still making day-to-day workflows smoother.
Conclusion
When I'm developing a feature, I care about the files I've just modified, created, or moved.
This command scratches exactly that itch: fast navigation to Git-relevant files, without worrying if they're open or buried somewhere in the project tree.
Give it a try, and check out emacs-solo for more small, practical commands like this one. 🚀
Edit:
2025-09-04: Fixed the global binding to
emacs-solo/switch-git-status-buffer
, as pointed by u/jplindstrom
on r/emacs
(reddit).