Home whoami

Create changeset entries - without leaving Emacs

2023-06-07 22:30

I recently got frustrated by the changeset CLI - as far as I know, there is no option to create changesets that are not empty without using the annoying prompt and arrows to select patch/minor/major. And I don't have arrows in my small keyboard ;)

Let's fix it - without leaving Emacs.

Implementation

A disclaimer first: I'm not saying that this small snippet has feature parity with the CLI - for instance, the creation of the final changelog is not done (yet). Yet, I found out that in my single repo project, this approach works like a charm.

The changeset format is extremely simple and described in the link above. Basically, there are 2 things we need to care about: the change level, and its description. In our case, the "name" of the object is just the name of the repository.

(require 'projectile)
(require 'f)

(defun my/changeset-create-entry (&optional lvl description)
  "Generate changeset file for the current project.

This helper function can be used instead of the annoying `changeset'
CLI command to create valid entries for the PRs.

If LVL is given, it must be one of `patch', `minor', `major', or
`empty'. If not, the user can input the choice in the minibuffer.

Similarly, DESCRIPTION may be given - if not, the user is required to
provide it.
"
  (interactive)
  (let* (
         ;; Helper: get a random alphanumeric value
         (random-alnum (lambda ()(let* ((alnum "abcdefghijklmnopqrstuvwxyz0123456789")
                                        (i (% (abs (random)) (length alnum))))
                                   (substring alnum i (1+ i)))))
         ;; Helper: create a n letter random string
         (random-n-letter-string (lambda (n)
                                   (let ((rem n) (res ""))
                                     (while (>= rem 0)
                                       (setq res (concat (funcall random-alnum) res))
                                       (setq rem (- rem 1)))
                                     res)))
         ;; The root of the project, and the .changeset folder
         (pj-root (projectile-project-root))
         (changeset-dir (concat  pj-root ".changeset"))
         ;; The name of the folder containing the project
         (app-folder (file-name-nondirectory (directory-file-name pj-root)))
         ;; Random name for the file
         (filename (format "%s-%s.md"
                           (funcall random-n-letter-string 5)
                           (funcall random-n-letter-string 5)))
         ;; The path of the changeset file
         (complete-path (expand-file-name filename changeset-dir))
         ;; empty/patch/minor/major change
         (level))
    (unless (f-directory-p changeset-dir) (error "No .changeset folder?"))
    (setq level (or lvl (nth 1 (read-multiple-choice
                        "Level?"
                        '((?0 "empty" "Not customer facing, no description")
                          (?1 "patch" "Patch change")
                          (?2 "minor" "Minor change")
                          (?3 "major" "Major (breaking) change"))))))
    (with-current-buffer (find-file-noselect complete-path)
      (insert (if (string-equal level "empty")
                  "---\n---"
                (format "---\n'%s': %s\n---\n\n%s"
                        app-folder
                        level
                        (or description (read-string "Add a short description: ")))))
      (save-buffer))
    (message "Created %s" complete-path)))

The let* part

The command needs to generate some random file names. I've found the solution on StackOverflow (not citing the sources, as I forgot to save the links), and it is as simple as randomly selecting some alphanumeric values from a list, and then combine them together.

Then, we leverage projectile to give us the project root folder, and we generate some paths that will be used later on. Little rant here: I found finding the functions to isolate the dir-name and such quite hard, even though it is not the first time I'm doing that. Maybe better next time?

As a side note: I did not want to have defun laying around and littering my/ namespace, so I just inlined the helper functions. I find the lack of "private" things in elisp a bit annoying, but I guess this is as good as I can get.

The actual function

The only interesting part is the use of or to allow reading values from the minibuffer only if these are not sent as parameters. It is, again, quite nice to read in my opinion.

Otherwise, we just verify we are in a project that contains the .changeset folder, we read (if needed) data from the minibuffer, and write them in a buffer.

Hope it helps someone else!