after-set-visited-file-name-hook
is a reliable way to detect changes to the filename.
This clears the cached value when a new filename if set.
;; Cache the vc-root relative name of the buffer.
(defvar-local buffer-file-name--vc-root-relative nil)
(defun buffer-file-name-vc-root-relative-fn (filepath)
;; Lazy initialize.
(when (eq buffer-file-name--vc-root-relative nil)
(let ((base-path
(let ((vc-backend (ignore-errors (vc-responsible-backend filepath))))
(when vc-backend
(vc-call-backend vc-backend 'root filepath)))))
(setq
buffer-file-name--vc-root-relative
(if base-path
(concat
"./"
;; Account for possibility of filepath containing a '%'.
(replace-regexp-in-string
"%" "%%" (file-relative-name filepath base-path)))
;; No base-path, set to 't',
;; initialized with no base (return nil).
t))))
(unless (eq buffer-file-name--vc-root-relative t)
buffer-file-name--vc-root-relative))
(add-hook 'after-set-visited-file-name-hook
(lambda () (setq buffer-file-name--vc-root-relative nil)))
Then in the mode-line, this can be used to show the filename.
(or (buffer-file-name-vc-root-relative-fn filepath) "%f")