← Back to team overview

launchpad-dev team mailing list archive

Automated help for writing log messages.

 

At the Bugs Sprint in Auburn, Alabama, USA last week, I showed the rest
of the team some customizations by which Emacs writes the boring parts
of my log messages for me.  The team was interested enough -- or polite
enough to feign interest -- to ask me to put it in the dev wiki, which I
have now done:

  https://dev.launchpad.net/EmacsTips#log-messages

I'll include the text below, for convenience, but the above is the
canonical reference.  For an example of the kind of log messages it
facilitates, see:

  http://bazaar.launchpad.net/~kfogel/launchpad/506018-patch-report/revision/10167

Graham commented that he found these kinds of log messages made
reviewing changes somewhat easier, because they describe not only what
happened but where it happened, in a consistently-formatted way.

If someone knows how to do this in vim or other customizable editors,
please put that in the dev wiki too.

-Karl

;;;; Partial automation for writing log messages in Emacs.
;;;
;;;    1) Put this code in your .emacs.
;;;
;;;    2) Reload your .emacs (by "M-x load-file", or just by restarting).
;;;
;;;    3) Bind the entry point to a key, for example to "C-c h":
;;;
;;;       (global-set-key "\C-ch" 'kf-log-message)
;;;
;;; Now whenever you're working on Launchpad code, Emacs will help you
;;; write the log message for the change you're working on.  Just type
;;; C-c h while inside, say, lib/lp/bugs/interfaces/bugtarget.py, in
;;; the class IHasBugs, in the method getBugCounts().  Emacs will
;;; bring up a file in which to accumulate a log message (by default,
;;; this is the file "msg" at the top of your Bazaar working tree).
;;;
;;; If neither the source file path and class/method information are
;;; currently in the log message file, Emacs will insert them, leaving
;;; point at the end so you can write something about the change.  If
;;; some of that information is already in the log message (because
;;; you're doing more work in the same class or method), Emacs will
;;; put point at what it thinks is the most appropriate place in the
;;; log message, and the kill ring (that is, the clipboard) should
;;; have anything else you need -- type C-y to paste in the method
;;; name, and if that's not quite right, type M-y immediately to paste
;;; it in surrounded by parentheses and followed by a colon, which is
;;; a traditional format for starting a new subsection for a given
;;; method in a log message.
;;;
;;; The result is log messages that look like this:
;;;
;;;   Working with Abel on bug #506018:
;;;
;;;   Use the view instead of the model to prepare data for display.
;;;   
;;;   * lib/lp/bugs/browser/bugtarget.py: Import datetime, timezone,
;;;     BugTaskSearchParams, and BugAttachmentType.
;;;     (BugsPatchesView.patch_tasks,
;;;      BugsPatchesView.context_can_have_different_bugtargets,
;;;      BugsPatchesView.youngest_patch,
;;;      BugsPatchesView.patch_age): New properties and methods.
;;;   
;;;   * lib/lp/bugs/templates/bugtarget-patches.pt: Rewrite.
;;;   
;;;   * lib/lp/bugs/model/bugtarget.py
;;;     (HasBugsBase.fish_patches): Remove this now-unused property.
;;;   
;;;   * lib/lp/bugs/interfaces/bugtarget.py
;;;     (IHasBugs.patches): Likewise remove.
;;;
;;; This format more or less adheres to the guidelines given at
;;; http://subversion.apache.org/docs/community-guide/#log-messages,
;;; which I think are pretty good, though of course every project may
;;; have their own guidelines, "your mileage may vary", "void where
;;; prohibited by law", etc.

(defun kf-log-path-derive (path &optional root)
  "If ROOT is a prefix of PATH, return the remainder; else return PATH."
  (save-match-data
    (if (and root (string-prefix-p root path))
        (substring path (length root))
      path)))

(defcustom kf-log-message-file-basename "msg"
  "*The basename of the file in which to accumulate a log message.
See `kf-log-message' for more.")

(defun kf-log-message-file (path)
  "Return the name of the log message accumulation file for PATH:
the file `kf-log-message-file-basename' in PATH's directory or in some
parent upwards from PATH."
  (interactive)
  (let* ((d (directory-file-name path))
         ;; If there's a .bzr directory here, that indicates the top
         ;; of a working tree, which is a good place for a log message.
         (b (concat d "/.bzr"))
         ;; Or if there's already a "msg" file here, then go with that.
         (m (concat d "/" kf-log-message-file-basename)))
    (save-match-data
      (while (and d (not (file-exists-p m)) (not (file-exists-p b)))
        (string-match "\\(.*\\)/[^/]+$" d)
        (setq d (match-string 1 d)
              m (concat d "/" kf-log-message-file-basename)
              b (concat d "/.bzr")))
      m)))

(defun kf-add-log-current-defun ()
  "Try to determine the current defun using `add-log-current-defun'
  first, falling back to various custom heuristics if that fails."
  (let* ((flavor (kf-markup-flavor))
         (default-defun (add-log-current-defun)))
    ;; Work around a bug in add-log-current-defun w.r.t. Subversion's code.
    (if (string-match "\\.h$" (buffer-file-name))
        (setq default-defun nil))
    (save-excursion
      (save-match-data
        (cond
         ((and (not default-defun) (eq major-mode 'c-mode))
          ;; Handle .h files as well as .c files.
          (progn
            (c-beginning-of-statement-1)
            (or (= (char-after (1- (point))) ?\( )
                (search-forward "(" nil t))
            (forward-char -1)
            (forward-sexp -1)
            (buffer-substring-no-properties
             (point)
             (progn (forward-sexp 1) (point)))))
         ((or (eq flavor 'xml) (eq flavor 'html))
          (let* ((section-open-re "\\(<sect[0-9]\\|<div\\)")
                 (section-close-re "</\\(sect[0-9]\\|div\\)>")
                 (title-open-re  "<\\(title\\|h[0-9]\\)>")
                 (title-close-re "</\\(title\\|h[0-9]\\)>")
                 (nearest-title-spot
                  (or (save-excursion (re-search-backward title-open-re nil t))
                      (point-min)))
                 (nearest-section-spot
                  (or (save-excursion
                        (re-search-backward section-open-re nil t))
                      (point-min)))
                 (title-grabber
                  (lambda ()
                    (when (re-search-backward title-open-re nil t)
                      (search-forward ">")
                      (buffer-substring-no-properties
                       (point)
                       (progn (re-search-forward title-close-re)
                              (search-backward "</")
                              (point)))))))
            (if (> nearest-title-spot nearest-section-spot)
                (funcall title-grabber)
              ;; Else we have a section or div with no title, so use
              ;; one of the usual attributes instead.
              (goto-char nearest-section-spot)
              (let ((opoint (point))
                    (bound (progn
                             (re-search-forward section-close-re) (point))))
                (goto-char opoint)
                (if (re-search-forward
                     "\\(id=\"\\|name=\"\\|label=\"\\|title=\"\\)" nil t)
                    (buffer-substring-no-properties
                     (point) (progn (search-forward "\"") (1- (point))))
                  (funcall title-grabber))))))
         (t
          (add-log-current-defun)))))))

(defun kf-current-defun-to-kill-ring ()
  "Put the name of the current defun into the kill-ring."
  (interactive)
  (kill-new (kf-add-log-current-defun)))

(defun kf-log-message (short-file-names)
  "Add to an in-progress log message, based on context around point.
If prefix arg SHORT-FILE-NAMES is non-nil, then use basenames only in
log messages, otherwise use full paths.  The current defun name is
always used.

If the log message already contains material about this defun, then put
point there, so adding to that material is easy.

Else if the log message already contains material about this file, put
point there, and push onto the kill ring the defun name with log
message dressing around it, plus the raw defun name, so yank and
yank-next are both useful.

Else if there is no material about this defun nor file anywhere in the
log message, then put point at the end of the message and insert a new
entry for file with defun.

See also the function `kf-log-message-file'."
  (interactive "P")
  (let* ((this-defun   (kf-add-log-current-defun))
         (log-file     (kf-log-message-file buffer-file-name))
         (log-file-dir (file-name-directory log-file))
         (this-file    (if short-file-names
                           (file-name-nondirectory buffer-file-name)
                         (kf-log-path-derive buffer-file-name log-file-dir))))
    (find-file log-file)
    (goto-char (point-min))
    ;; Strip text properties from strings
    (set-text-properties 0 (length this-file) nil this-file)
    (set-text-properties 0 (length this-defun) nil this-defun)
    ;; If log message for defun already in progress, add to it
    (if (and
         this-defun                        ;; we have a defun to work with
         (search-forward this-defun nil t) ;; it's in the log msg already
         (save-excursion                   ;; and it's about the same file
           (save-match-data
             (if (re-search-backward  ; Ick, I want a real filename regexp!
                  "^\\*\\s-+\\([a-zA-Z0-9-_.@=+^$/%!?(){}<>]+\\)" nil t)
                 (string-equal (match-string 1) this-file)
               t))))
        (if (re-search-forward ":" nil t)
            (if (looking-at " ") (forward-char 1)))
      ;; Else no log message for this defun in progress...
      (goto-char (point-min))
      ;; But if log message for file already in progress, add to it.
      (if (search-forward this-file nil t)
          (progn 
            (if this-defun (progn
                             (kill-new (format "(%s): " this-defun))
                             (kill-new this-defun)))
            (search-forward ")" nil t)
            (if (looking-at " ") (forward-char 1)))
        ;; Found neither defun nor its file, so create new entry.
        (goto-char (point-max))
        (if (not (bolp)) (insert "\n"))
        (insert (format "\n* %s (%s): " this-file (or this-defun "")))
        ;; Finally, if no derived defun, put point where the user can
        ;; type it themselves.
        (if (not this-defun) (forward-char -3))))))