Chris Maiorana: Git changes and commits in Emacs modeline

Wait 5 sec.

In a previous post, I mentioned a little bit of modeline hacking to display git changes and today’s commit count. In this article, I thought I would go through those snippets from my modeline code that accomplish this. At the bottom, I’ve also included a standalone minor mode for it.Basically, this code will show you a format like “250/5” where 250 represents the number of uncommitted changes and 5 represents the number of commits made today.If you’re interested in topics like these, particularly how you can leverage programs like git for technical and creative writing, I’d highly recommend my downloadable handbooks:Emacs For WritersGit For WritersSo let’s get into it.Table of ContentsGit branch displayGit changes and commit count displayCaching variablesGetting Change CountGetting today’s commit CountModeline display variableSimplified minor modeMinor mode featuresGit branch displayThis section displays the current git branch name in brackets in the modeline. I like seeing the branch name just to make sure I’m in the right place.(defun csm-modeline--git-branch () "Return the current git branch name, wrapped in brackets.If not in a git repository, return an empty string." (when buffer-file-name (let* ((default-directory (file-name-directory buffer-file-name)) (branch (string-trim (shell-command-to-string "git rev-parse --abbrev-ref HEAD 2>/dev/null")))) (if (and branch (not (string-empty-p branch))) (format "[%s]" branch) ""))))(defvar-local csm-modeline-directory '(:eval (when-let ((branch-name (csm-modeline--git-branch))) (propertize branch-name 'face 'magit-branch-local))) "Mode line construct to display the git branch name.")(put 'csm-modeline-directory 'risky-local-variable t)csm-modeline--git-branch uses git rev-parse --abbrev-ref HEAD to catch the current branch name.It checks if buffer-file-name exists to ensure we’re in a file-based buffer.Sets default-directory to the file’s directory so git commands run in the correct location.Redirects errors to /dev/null to silently handle cases in which we’re not in a git repository.Formats the branch name in brackets like [master] or [drafting] (my preferred default branch).csm-modeline-directory is the modeline variable that displays the branch with magit-branch-local face styling.The risky-local-variable property is set to t because it evaluates code dynamically.Git changes and commit count displayThis is the main functionality that shows uncommitted changes and today’s commit count.Caching variables(defvar csm-modeline-csmchange-cache nil "Cache for csmchange command output.")(defvar csm-modeline-csmchange-last-update 0 "Timestamp of last csmchange update.")(defvar csm-modeline-csmchange-update-interval 30 "Update interval in seconds for csmchange output.")These variables implement a caching mechanism to avoid running shell commands too frequently:csm-modeline-csmchange-cache stores the last result from the csmchange commandcsm-modeline-csmchange-last-update tracks when the cache was last refreshed using a Unix timestamp.csm-modeline-csmchange-update-interval sets how often to refresh (30 seconds by default)This caching is crucial for performance since the modeline updates frequently, but we don’t need to run shell commands every time.Getting Change Count(defun csm-modeline--csmchange-output () "Return the output of csmchange command." (let ((output (shell-command-to-string "csmchange 2>/dev/null"))) (string-trim output)))(defun csm-modeline--cached-csmchange-output () "Return cached csmchange output, updating if necessary." (let ((now (float-time))) (when (or (null csm-modeline-csmchange-cache) (> (- now csm-modeline-csmchange-last-update) csm-modeline-csmchange-update-interval)) (setq csm-modeline-csmchange-cache (csm-modeline--csmchange-output) csm-modeline-csmchange-last-update now))) csm-modeline-csmchange-cache)csm-modeline--csmchange-output runs the csmchange command (a custom shell script) and returns the trimmed output.csm-modeline--cached-csmchange-output implements the caching logic:Gets the current time with float-time.Checks if the cache is null (first run) or if enough time has passed since the last update.If an update is needed, it runs csmchange and updates both the cache and timestamp.Returns the cached value.Here is the csmchange shell script:#!/bin/bash# Run the command in the current working directorygit diff --word-diff=porcelain HEAD | grep -e '^+[^+]' -e '^-[^-]' | wc -wGetting today’s commit Count(defun csm-modeline--today-commits-count () "Return the number of commits made today." (when buffer-file-name (let* ((default-directory (file-name-directory buffer-file-name)) (count (string-trim (shell-command-to-string "git log --since='00:00:00' --oneline --no-merges 2>/dev/null | wc -l")))) (if (and count (not (string-empty-p count))) count "0"))))This function counts commits made since midnight today:Checks buffer-file-name exists to ensure we’re in a file buffer.Sets default-directory to the file’s directory for correct git context.Uses git log --since='00:00:00' to get commits since midnight.--oneline formats each commit as a single line for easy counting.--no-merges excludes merge commits to count only direct commits.Pipes to wc -l to count the number of lines (commits).Returns “0” if the result is empty or invalid.Errors are silently discarded with 2>/dev/null.Modeline display variable(defvar-local csm-modeline-csmchange '(:eval (when (mode-line-window-selected-p) (let ((output (csm-modeline--cached-csmchange-output)) (commits (csm-modeline--today-commits-count))) (when (and output (not (string-empty-p output))) (let* ((change-count (string-to-number output)) (face (if (>= change-count 250) 'warning 'font-lock-string-face))) (concat " " (propertize output 'face face) (when commits (propertize (format "/%s" commits) 'face face)))))))) "Mode line construct to display csmchange output with today's commit count.")(put 'csm-modeline-csmchange 'risky-local-variable t)This is the main modeline variable that brings it all together:Uses :eval to dynamically evaluate the code each time the modeline updates.mode-line-window-selected-p ensures the display only appears in the active window.Retrieves both the cached change count and today’s commit count.Converts the change count to a number to compare against 250.Selects a face: 'warning (usually red/orange), depending on theme, if changes >~ 250, otherwise 'font-lock-string-face (usually green/blue).Formats the output as changes over commits (e.g. “250/5”).Setting risky-local-variable to t allows the dynamic evaluation.Simplified minor modeOn my GitHub, I have included a self-contained minor mode that implements only the git changes and commit count functionality. I figured this would be easier for individual analysis. If you have any suggestions on how to improve it please let me know. The csmchange shell script logic has been integrated directly into the minor mode, making it fully self-contained without requiring external commands. Minor mode featuresThe simplified minor mode includes:Built-in Change Counting: The csmchange shell script logic is integrated directly into the package, so no external commands are required (though you can still use an external command if you prefer by setting git-stats-modeline-use-builtin-diff to nil).Customizable Settings: Users can customize the update interval, warning threshold, whether to use built-in diff counting, and optionally the command to use for external counting.Same Core Logic: Uses the identical caching and counting logic from the original modeline.Easy Activation: Simply call (git-stats-modeline-mode 1) to enable or (git-stats-modeline-mode 0) to disable.Global Minor Mode: Affects all buffers, not just specific ones.Self-Contained: Can be distributed as a standalone functionality with proper headers and documentation.How the minor could be used and modified:;; Load the file(load-file "/path/to/git-stats-modeline.el");; Enable the mode (uses built-in change counting by default)(git-stats-modeline-mode 1);; Optional: customize settings(setq git-stats-modeline-warning-threshold 300)(setq git-stats-modeline-update-interval 60);; Optional: use external command instead of built-in counting(setq git-stats-modeline-use-builtin-diff nil)(setq git-stats-modeline-change-command "csmchange")The post Git changes and commits in Emacs modeline appeared first on Chris Maiorana.