[clang] 32be90d - [emacs][clang-format] Add elisp API for clang-format on git diffs (#112792)

via cfe-commits cfe-commits at lists.llvm.org
Tue Feb 4 15:17:47 PST 2025


Author: goldsteinn
Date: 2025-02-04T17:17:43-06:00
New Revision: 32be90db269a6dbb876b99f6ef3df6563f66315a

URL: https://github.com/llvm/llvm-project/commit/32be90db269a6dbb876b99f6ef3df6563f66315a
DIFF: https://github.com/llvm/llvm-project/commit/32be90db269a6dbb876b99f6ef3df6563f66315a.diff

LOG: [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.

Added: 
    

Modified: 
    clang/tools/clang-format/clang-format.el

Removed: 
    


################################################################################
diff  --git a/clang/tools/clang-format/clang-format.el b/clang/tools/clang-format/clang-format.el
index fb943b7b722f8a4..54ab2e1fd4c6128 100644
--- a/clang/tools/clang-format/clang-format.el
+++ b/clang/tools/clang-format/clang-format.el
@@ -146,24 +146,133 @@ is a zero-based file offset, assuming ‘utf-8-unix’ coding."
     (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
+           , at 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 
diff s 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 
diff erent 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 
diff s 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 @@ uses the function `buffer-file-name'."
                                       (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 @@ uses the function `buffer-file-name'."
             (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
+
diff s 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 
diff s, 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 @@ 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-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)


        


More information about the cfe-commits mailing list