[emacs][clang-format] Add elisp API for clang-format on git diffs (#112792)
New proposed function `clang-format-vc-diff`.
It is the same as calling `clang-format-region` on all diffs between
the content of a buffer-file and the content of the file at git
revision HEAD. This is essentially the same thing as:
`git-clang-format -f {filename}`
If the current buffer is saved.
The motivation is many project (LLVM included) both have code that is
non-compliant with there clang-format style and disallow unrelated
format diffs in PRs. This means users can't just run
`clang-format-buffer` on the buffer they are working on, and need to
manually go through all the regions by hand to get them
formatted. This is both an error prone and annoying workflow.
NOKEYCHECK=True
GitOrigin-RevId: 32be90db269a6dbb876b99f6ef3df6563f66315a
diff --git a/clang-format.el b/clang-format.el
index fb943b7..54ab2e1 100644
--- a/clang-format.el
+++ b/clang-format.el
@@ -146,24 +146,133 @@
(lambda (byte &optional _quality _coding-system)
(byte-to-position (1+ byte)))))
-;;;###autoload
-(defun clang-format-region (start end &optional style assume-file-name)
- "Use clang-format to format the code between START and END according to STYLE.
-If called interactively uses the region or the current statement if there is no
-no active region. If no STYLE is given uses `clang-format-style'. Use
-ASSUME-FILE-NAME to locate a style config file, if no ASSUME-FILE-NAME is given
-uses the function `buffer-file-name'."
- (interactive
- (if (use-region-p)
- (list (region-beginning) (region-end))
- (list (point) (point))))
+(defmacro clang-format--with-delete-files-guard (bind-files-to-delete &rest body)
+ "Execute BODY which may add temp files to BIND-FILES-TO-DELETE."
+ (declare (indent 1))
+ `(let ((,bind-files-to-delete nil))
+ (unwind-protect
+ (progn
+ ,@body)
+ (while ,bind-files-to-delete
+ (with-demoted-errors "failed to remove file: %S"
+ (delete-file (pop ,bind-files-to-delete)))))))
+
+(defun clang-format--vc-diff-get-diff-lines (file-orig file-new)
+ "Return all line regions that contain diffs between FILE-ORIG and
+FILE-NEW. If there is no diff ‘nil’ is returned. Otherwise the return
+is a ‘list’ of line ranges to format. The list of line ranges can be
+passed to ‘clang-format--region-impl’"
+ ;; Use temporary buffer for output of diff.
+ (with-temp-buffer
+ ;; We could use diff.el:diff-no-select here. The reason we don't
+ ;; is diff-no-select requires extra copies on the buffers which
+ ;; induces noticeable slowdowns, especially on larger files.
+ (let ((status (call-process
+ diff-command
+ nil
+ (current-buffer)
+ nil
+ ;; Binary diff has different behaviors that we
+ ;; aren't interested in.
+ "-a"
+ ;; Get minimal diff (copy diff config for git-clang-format).
+ "-U0"
+ file-orig
+ file-new))
+ (stderr (concat (if (zerop (buffer-size)) "" ": ")
+ (buffer-substring-no-properties
+ (point-min) (line-end-position))))
+ (diff-lines '()))
+ (cond
+ ((stringp status)
+ (error "clang-format: (diff killed by signal %s%s)" status stderr))
+ ;; Return of 0 indicates no diff.
+ ((= status 0) nil)
+ ;; Return of 1 indicates found diffs and no error.
+ ((= status 1)
+ ;; Find and collect all diff lines.
+ ;; We are matching something like:
+ ;; "@@ -80 +80 @@" or "@@ -80,2 +80,2 @@"
+ (goto-char (point-min))
+ (while (re-search-forward
+ "^@@[[:blank:]]-[[:digit:],]+[[:blank:]]\\+\\([[:digit:]]+\\)\\(,\\([[:digit:]]+\\)\\)?[[:blank:]]@@$"
+ nil
+ t
+ 1)
+ (let ((match1 (string-to-number (match-string 1)))
+ (match3 (let ((match3_or_nil (match-string 3)))
+ (if match3_or_nil
+ (string-to-number match3_or_nil)
+ nil))))
+ (push (cons match1 (if match3 (+ match1 match3) match1)) diff-lines)))
+ (nreverse diff-lines))
+ ;; Any return != 0 && != 1 indicates some level of error.
+ (t
+ (error "clang-format: (diff returned unsuccessfully %s%s)" status stderr))))))
+
+(defun clang-format--vc-diff-get-vc-head-file (tmpfile-vc-head)
+ "Stores the contents of ‘buffer-file-name’ at vc revision HEAD into
+‘tmpfile-vc-head’. If the current buffer is either not a file or not
+in a vc repo, this results in an error. Currently git is the only
+supported vc."
+ ;; We need the current buffer to be a file.
+ (unless (buffer-file-name)
+ (error "clang-format: Buffer is not visiting a file"))
+
+ (let ((base-dir (vc-root-dir))
+ (backend (vc-backend (buffer-file-name))))
+ ;; We need to be able to find version control (git) root.
+ (unless base-dir
+ (error "clang-format: File not known to git"))
+ (cond
+ ((string-equal backend "Git")
+ ;; Get the filename relative to git root.
+ (let ((vc-file-name (substring
+ (expand-file-name (buffer-file-name))
+ (string-width (expand-file-name base-dir))
+ nil)))
+ (let ((status (call-process
+ vc-git-program
+ nil
+ `(:file ,tmpfile-vc-head)
+ nil
+ "show" (concat "HEAD:" vc-file-name)))
+ (stderr (with-temp-buffer
+ (unless (zerop (cadr (insert-file-contents tmpfile-vc-head)))
+ (insert ": "))
+ (buffer-substring-no-properties
+ (point-min) (line-end-position)))))
+ (when (stringp status)
+ (error "clang-format: (git show HEAD:%s killed by signal %s%s)"
+ vc-file-name status stderr))
+ (unless (zerop status)
+ (error "clang-format: (git show HEAD:%s returned unsuccessfully %s%s)"
+ vc-file-name status stderr)))))
+ (t
+ (error
+ "Version control %s isn't supported, currently supported backends: git"
+ backend)))))
+
+
+(defun clang-format--region-impl (start end &optional style assume-file-name lines)
+ "Common implementation for ‘clang-format-buffer’,
+‘clang-format-region’, and ‘clang-format-vc-diff’. START and END
+refer to the region to be formatter. STYLE and ASSUME-FILE-NAME are
+used for configuring the clang-format. And LINES is used to pass
+specific locations for reformatting (i.e diff locations)."
(unless style
(setq style clang-format-style))
(unless assume-file-name
(setq assume-file-name (buffer-file-name (buffer-base-buffer))))
+ ;; Convert list of line ranges to list command for ‘clang-format’ executable.
+ (when lines
+ (setq lines (mapcar (lambda (range)
+ (format "--lines=%d:%d" (car range) (cdr range)))
+ lines)))
+
(let ((file-start (clang-format--bufferpos-to-filepos start 'approximate
'utf-8-unix))
(file-end (clang-format--bufferpos-to-filepos end 'approximate
@@ -190,8 +299,12 @@
(list "--assume-filename" assume-file-name))
,@(and style (list "--style" style))
"--fallback-style" ,clang-format-fallback-style
- "--offset" ,(number-to-string file-start)
- "--length" ,(number-to-string (- file-end file-start))
+ ,@(and lines lines)
+ ,@(and (not lines)
+ (list
+ "--offset" (number-to-string file-start)
+ "--length" (number-to-string
+ (- file-end file-start))))
"--cursor" ,(number-to-string cursor))))
(stderr (with-temp-buffer
(unless (zerop (cadr (insert-file-contents temp-file)))
@@ -216,9 +329,62 @@
(if incomplete-format
(message "(clang-format: incomplete (syntax errors)%s)" stderr)
(message "(clang-format: success%s)" stderr))))
- (delete-file temp-file)
+ (with-demoted-errors
+ "clang-format: Failed to delete temporary file: %S"
+ (delete-file temp-file))
(when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
+
+;;;###autoload
+(defun clang-format-vc-diff (&optional style assume-file-name)
+ "The same as ‘clang-format-buffer’ but only operates on the vc
+diffs from HEAD in the buffer. If no STYLE is given uses
+‘clang-format-style’. Use ASSUME-FILE-NAME to locate a style config
+file. If no ASSUME-FILE-NAME is given uses the function
+‘buffer-file-name’."
+ (interactive)
+ (clang-format--with-delete-files-guard tmp-files
+ (let ((tmpfile-vc-head nil)
+ (tmpfile-curbuf nil))
+ (setq tmpfile-vc-head
+ (make-temp-file "clang-format-vc-tmp-head-content"))
+ (push tmpfile-vc-head tmp-files)
+ (clang-format--vc-diff-get-vc-head-file tmpfile-vc-head)
+ ;; Move the current buffer to a temporary file to take a
+ ;; diff. Even if current-buffer is backed by a file, we
+ ;; want to diff the buffer contents which might not be
+ ;; saved.
+ (setq tmpfile-curbuf (make-temp-file "clang-format-vc-tmp"))
+ (push tmpfile-curbuf tmp-files)
+ (write-region nil nil tmpfile-curbuf nil 'nomessage)
+ ;; Get a list of lines with a diff.
+ (let ((diff-lines
+ (clang-format--vc-diff-get-diff-lines
+ tmpfile-vc-head tmpfile-curbuf)))
+ ;; If we have any diffs, format them.
+ (when diff-lines
+ (clang-format--region-impl
+ (point-min)
+ (point-max)
+ style
+ assume-file-name
+ diff-lines))))))
+
+
+;;;###autoload
+(defun clang-format-region (start end &optional style assume-file-name)
+ "Use clang-format to format the code between START and END according
+to STYLE. If called interactively uses the region or the current
+statement if there is no no active region. If no STYLE is given uses
+`clang-format-style'. Use ASSUME-FILE-NAME to locate a style config
+file, if no ASSUME-FILE-NAME is given uses the function
+`buffer-file-name'."
+ (interactive
+ (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list (point) (point))))
+ (clang-format--region-impl start end style assume-file-name))
+
;;;###autoload
(defun clang-format-buffer (&optional style assume-file-name)
"Use clang-format to format the current buffer according to STYLE.
@@ -226,7 +392,11 @@
to locate a style config file. If no ASSUME-FILE-NAME is given uses
the function `buffer-file-name'."
(interactive)
- (clang-format-region (point-min) (point-max) style assume-file-name))
+ (clang-format--region-impl
+ (point-min)
+ (point-max)
+ style
+ assume-file-name))
;;;###autoload
(defalias 'clang-format 'clang-format-region)