In Emacs, when I’m writing a commit message for a repository backed by version control, and I type #123
the bug-reference package overlays that #123
with links to the remote issue. I can then “click” on #123
and jump to the issue at the remote repository; a convenient feature!
However that convenience comes with a cost, namely those terse references do two things:
If I were to change the host of that repository or transfer ownership, the #123
becomes disconnected from what it once referenced.
I prefer, instead, to use full Uniform Resource Locators (URLs 📖). This way there is no ambiguity about what I’m referencing. Unless of course the remote service breaks links or goes away.
Adding further nuance, due to the nature of my work, I’m often referencing other repository’s issues and pull requests during the writing of a commit.
I’ve been playing with Completion at Point Functions (CaPFs 📖) and decided the explore automatically creating those URLs.
See Completion at Point Function (CAPF) for Org-Mode Links.
The end goal is to have a full URL.
For example https://github.com/samvera/hyrax/issues/6056
.
I broke this into two steps:
I settled on the following feature:
Given that I have typed “/hyr”
When I then type {{{kbd(TAB)}}}
Then auto-complete options should include “/hyrax”
I went a step further in my implementation, when I select the project completion candidate I append a # to that. I end up with /hyrax#
and the cursor is right after the #
character. From which I then have my second CaPF.
Given that the text before <point> is "/hyrax#123"
When I type {{{kbd(TAB)}}}
Then auto-complete will convert "/hyrax#123"
to "https://github.com/samvera/hyrax/issues/123"
First let’s look at the part for finding a project. I do this via jf/version-control/project-capf.
(defun jf/version-control/project-capf ()
"Complete project links."
;; While I'm going to replace "/project" I want to make
;; sure that I don't have any odd hits (for example
;; "/path/to/file")
(when (looking-back "[^[:word:]]/[[:word:][:digit:]_\-]+"
(jf/capf-max-bounds))
(let ((right (point))
(left (save-excursion
;; First check for the project
(search-backward-regexp
"/[[:word:][:digit:]_\-]+"
(jf/capf-max-bounds) t)
(point))))
(list left right
(jf/version-control/known-project-names)
:exit-function
(lambda (text _status)
(delete-char (- (length text)))
(insert text "#"))
:exclusive 'no))))
The above function looks backwards from point, using jf/capf-max-bounds
as the bounds of how far back to look. If there’s a match the function then gets the left and right boundaries and calls jf/version-control/known-project-names
to get a list of all possible projects that I have on my machine.
The jf/capf-max-bounds function ensures that we don’t attempt to look at a position outside of the buffer. See the below definition:
(cl-defun jf/capf-max-bounds (&key (window-size 40))
"Return the max bounds for `point' based on given WINDOW-SIZE."
(let ((boundary (- (point) window-size)))
(if (> 0 boundary) (point-min) boundary)))
The jf/version-control/known-project-names leverages the projectile package to provides a list of known projects.
I’ve been working at moving away from projectile but the projectile-known-projects
variable just works, so I’m continuing my dependency on projectile. I want to migrate towards the built-in project package, but there are a few points that I haven’t resolved.
(cl-defun jf/version-control/known-project-names (&key (prefix "/"))
"Return a list of project, prepending PREFIX to each."
(mapcar (lambda (proj)
(concat prefix (f-base proj)))
projectile-known-projects))
I then add jf/version-control/project-capf
to the completion-at-point-functions
variable.
I also need to incorporate that elsewhere, based on various modes. But that’s a different exercise.
(add-to-list 'completion-at-point-functions #'jf/version-control/project-capf)
The above code delivers on the first feature; namely auto completion for projects that sets me up to deliver on the second feature.
The jf/version-control/issue-capf function below builds on jf/version-control/project-capf
convention, working then from having an issue number appended to the text.
(defun jf/version-control/issue-capf ()
"Complete project issue links."
;; While I'm going to replace "/project" I want to make sure that I don't
;; have any odd hits (for example /path/to/file)
(when (looking-back "[^[:word:]]/[[:word:][:digit:]_\-]+#[[:digit:]]+"
(jf/capf-max-bounds))
(let ((right (point))
(left (save-excursion
(search-backward-regexp
"/[[:word:][:digit:]_\-]+#[[:digit:]]+"
(jf/capf-max-bounds) t)
(point))))
(list left right
(jf/version-control/text)
:exit-function
#'jf/version-control/unfurl-issue-to-url
:exclusive 'no))))
I continue to leverage jf/capf-max-bounds
querying for all matching version control text within the buffer (via jf/version-control/text):
(defun jf/version-control/text ()
"Find all matches for project and issue."
(s-match-strings-all "/[[:word:][:digit:]_\-]+#[[:digit:]]+" (buffer-string)))
Once we have a match, I use jf/version-control/unfurl-issue-to-url to convert the text into a URL.
I had originally tried to get #123
to automatically unfurl the issue URL for the current project. But I set that aside as it wasn’t quite working.
(defun jf/version-control/unfurl-issue-to-url (text _status)
"Unfurl the given TEXT to a URL.
Ignoring _STATUS."
(delete-char (- (length text)))
(let* ((parts (s-split "#" text))
(issue (cadr parts))
(project (or (car parts) (cdr (project-current)))))
(insert (format
(jf/version-control/unfurl-project-as-issue-url-template project)
issue))))
That function relies on jf/version-control/unfurl-project-as-issue-url-template which takes a project and determines the correct template for the project.
(cl-defun jf/version-control/unfurl-project-as-issue-url-template (project &key (prefix "/"))
"Return the issue URL template for the given PROJECT.
Use the provided PREFIX to help compare against
`projectile-known-projects'."
(let* ((project-path
(car (seq-filter
(lambda (el)
(or
(s-ends-with? (concat project prefix) el)
(s-ends-with? project el)))
projectile-known-projects)))
(remote
(s-trim (shell-command-to-string
(format
"cd %s && git remote get-url origin"
project-path)))))
(s-replace ".git" "/issues/%s" remote)))
And last, I add jf/version-control/issue-capf
to my list of completion-at-point-functions
.
(add-to-list 'completion-at-point-functions #'jf/version-control/issue-capf)
While demonstrating these functions to a co-worker, I said the following:
“The purpose of these URL unfurling functions is to make it easier to minimize the risk of losing information that might be helpful in understanding how we got here.”
In other words, information is scattered across many places, and verbose URLs are more likely to be relevant than terse short-hand references.
A future refactor would be to use the bug-reference
logic to create the template; but what I have works because I mostly work on Github projects and it’s time to ship it. Also, these CaPFs are available in other contexts, which helps with writing more expressive inline comments.
Earlier this week my team members began talking prefixing our commit title with the type of commit. The idea being that with consistent prefixing, we can more scan the commit titles to get an overview of what that looks like.
We cribbed our initial list from Udacity Nanodegree Style Guide:
Our proposal was that at the start of next sprint we’d adopt this pattern for one sprint and then assess. We also had a conversation about the fact that those “labels” consume precious space in the 50 character or so title.
So we adjusted our recommendation to use emojis. We established the following:
Which means we were only surrendering 2 characters instead of a possible 8 or so.
Given that we were going to be practicing this, I wanted to have Emacs prompt me to use this new approach.
The jf/version-control/valid-commit-title-prefixes
defines the glossary of emojis and their meanings:
(defvar jf/version-control/valid-commit-title-prefixes
'("🎁: feature (A new feature)"
"🐛: bug fix (A bug fix)"
"📚: docs (Changes to documentation)"
"💄: style (Formatting, missing semi colons, etc; no code change)"
"♻️: refactor (Refactoring production code)"
"☑️: tests (Adding tests, refactoring test; no production code change)"
"🧹: chore (Updating build tasks, package manager configs, etc; no production code change)")
"Team 💜 Violet 💜 's commit message guidelines on <2023-05-12 Fri>.")
I then added jf/git-commit-mode-hook
which is added as find-file-hook
This hook is fired anytime we find a file and load it into a buffer.
.
(cl-defun jf/git-commit-mode-hook (&key (splitter ":") (padding " "))
"If the first line is empty, prompt for commit type and insert it.
Add PADDING between inserted commit type and start of title. For
the `completing-read' show the whole message. But use the
SPLITTER to determine the prefix to include."
(when (and (eq major-mode 'text-mode)
(string= (buffer-name) "COMMIT_EDITMSG")
;; Is the first line empty?
(save-excursion
(goto-char (point-min))
(beginning-of-line-text)
(looking-at-p "^$")))
(let ((commit-type (completing-read "Commit title prefix: "
jf/version-control/valid-commit-title-prefixes nil t)))
(goto-char (point-min))
(insert (car (s-split splitter commit-type)) padding))))
(add-hook 'find-file-hook 'jf/git-commit-mode-hook)
The jf/git-commit-mode-hook
function delivers on the following two scenarios:
Given I am editing a commit message
When I start from an empty message
Then Emacs will prompt me to select the commit type
And will insert an emoji representing that type
Given I am editing a commit message
When I start from a non-empty message
Then Emacs will not prompt me to select the commit type
This function took about 20 minutes to write and helps me create habits around a new process. And if we agree to stop doing it, I’ll remove the hook (maybe keeping the function).
In this practice time, before we commit as a team to doing this, I am already appreciating the improved scanability of the various project’s short-logs. Further this prompt helps remind me to write small commits.
Also, in exploring how to do this function, I continue to think about how my text editor reflects my personal workflows and conventions.
In Completing Org Links, the author mentioned the following: “I try never to link to something more than once in a single post.”
And I agree!
In a single blog post, I like all of my article’s A-tags
to have unique href
attributes.
See <a>: The Anchor element - HTML: HyperText Markup Language
And I also like to use semantic Hypertext Markup Language (HTML 📖), such as the CITE-tag
See <cite>: The Citation element - HTML: HyperText Markup Language
or the ABBR-tag
.
See <abbr>: The Abbreviation element - HTML: HyperText Markup Language
In my Org-Mode writing, I frequently link to existing Denote documents. Some of those documents do not have a public Uniform Resource Locator (URL 📖) and others do. During the export from Org-Mode to Hugo, via Ox-Hugo, linked documents that have public URLs will be written up as Hugo shortcodes. And linked documents without public URLs will be rendered as plain text.
The shortcode logic
See glossary.html shortcode for implementation details.
ensures that each page does not have duplicate A-tags
. And in the case of abbreviations, the short code ensures that the first time I render the abbreviation, it renders as: Full Term (Abbreviation)
then the next time as Abbreviation
; always using the correct ABBR
tag and corresponding title
attribute.
I also have date links
Here I add “date” to the org-link-set-parameters
, which export as TIME-tags
.
See <time>: The (Date) Time element - HTML: HyperText Markup Language
And someday, I might get around to writing a function to find the nodes that reference a date’s same year, year/month, and year/month/day.
Another advantage of multiple links in my Org-Mode is that when I shuffle my notes to different files, the backlink utility of Denote and Org-Roam will pick up these new documents
All of this means that my Org-Mode document is littered with links, but on export the resulting to my blog, things become tidier.
So yes, don’t repeat links in blog posts; that’s just a lot of clutter. But for Personal Knowledge Management (PKM 📖), spamming the links helps me ensure that I’m able to find when and where I mention things.
Which is another reason I have an extensive Glossary of Terms for Take on Rules. All in service of helping me find things.