Unfurling of Issue Reference Abbreviations in Github and other Git Forge Issues
17 May 2023 | 5:23 pm

Making it Easier to Minimize Risk of Information Loss

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:

  • create low-level lock-in
  • increase the risk of information loss

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.

Enter Automation

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:

  • Create a CaPF for finding the project
  • Create a CaPF for replacing the project and issue with the URL

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"

Code

Create a CaPF for finding the project

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.

Create a CaPF for replacing the project and issue with the URL

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)

Conclusion

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.

Reply by Email


Configuring Emacs to Automatically Prompt Me to Define the Type of Commit
17 May 2023 | 1:14 pm

Adding a Function to Help Establish a Habit

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:

feat
A new feature
fix
A bug fix
docs
Changes to documentation
style
Formatting, missing semi colons, etc; no code change
refactor
Refactoring production code
test
Adding tests, refactoring test; no production code change
chore
Updating build tasks, package manager configs, etc; no production code change

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:

🎁
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)

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

Conclusion

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.

Reply by Email


The Why of Linking to a Resource Multiple Times
8 May 2023 | 11:37 pm

Peeling Back the Curtain of Some Blogging Wizardy

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.

Reply by Email



More News from this Feed See Full Web Site