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

via cfe-commits cfe-commits at lists.llvm.org
Sun Nov 3 08:31:51 PST 2024


https://github.com/goldsteinn updated https://github.com/llvm/llvm-project/pull/112792

>From 802764e879862541e205ba1a070824b71d2fef9a Mon Sep 17 00:00:00 2001
From: Noah Goldstein <goldstein.w.n at gmail.com>
Date: Thu, 17 Oct 2024 17:31:24 -0500
Subject: [PATCH 1/2] [emacs][clang-format] Add elisp API for clang-format on
 git diffs

New proposed function `clang-format-git-diffs`.

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.
---
 clang/tools/clang-format/clang-format.el | 159 ++++++++++++++++++++---
 1 file changed, 144 insertions(+), 15 deletions(-)

diff --git a/clang/tools/clang-format/clang-format.el b/clang/tools/clang-format/clang-format.el
index fb943b7b722f8a..d3f874de41c550 100644
--- a/clang/tools/clang-format/clang-format.el
+++ b/clang/tools/clang-format/clang-format.el
@@ -146,18 +146,97 @@ 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))))
-
+(defun clang-format--git-diffs-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 lines in the format '--lines=<start>:<end>'
+which can be passed directly to 'clang-format'"
+  ;; Temporary buffer for output of diff.
+  (with-temp-buffer
+    (let ((status (call-process
+                   "diff"
+                   nil
+                   (current-buffer)
+                   nil
+                   ;; Binary diff has different behaviors that we
+                   ;; aren't interested in.
+                   "-a"
+                   ;; Printout changes as only the line groups.
+                   "--changed-group-format=--lines=%dF:%dL "
+                   ;; Ignore unchanged content.
+                   "--unchanged-group-format="
+                   file-orig
+                   file-new
+                   )
+                  )
+          (stderr (concat (if (zerop (buffer-size)) "" ": ")
+                          (buffer-substring-no-properties
+                           (point-min) (line-end-position)))))
+      (when (stringp status)
+        (error "(diff killed by signal %s%s)" status stderr))
+      (unless (= status 0)
+        (unless (= status 1)
+          (error "(diff returned unsuccessfully %s%s)" status stderr)))
+
+
+      (if (= status 0)
+          ;; Status == 0 -> no Diff.
+          nil
+        (progn
+          ;; Split "--lines=<S0>:<E0>... --lines=<SN>:<SN>" output to
+          ;; a list for return.
+          (s-split
+           " "
+           (string-trim
+            (buffer-substring-no-properties
+             (point-min) (point-max)))))))))
+
+(defun clang-format--git-diffs-get-git-head-file ()
+  "Returns a temporary file with the content of 'buffer-file-name' at
+git revision HEAD. If the current buffer is either not a file or not
+in a git repo, this results in an error"
+  ;; Needs current buffer to be a file
+  (unless (buffer-file-name)
+    (error "Buffer is not visiting a file"))
+  ;; Need to be able to find version control (git) root
+  (unless (vc-root-dir)
+    (error "File not known to git"))
+  ;; Need version control to in fact be git
+  (unless (string-equal (vc-backend (buffer-file-name)) "Git")
+    (error "Not using git"))
+
+  (let ((tmpfile-git-head (make-temp-file "clang-format-tmp-git-head-content")))
+    ;; Get filename relative to git root
+    (let ((git-file-name (substring
+                          (expand-file-name (buffer-file-name))
+                          (string-width (expand-file-name (vc-root-dir)))
+                          nil)))
+      (let ((status (call-process
+                     "git"
+                     nil
+                     `(:file, tmpfile-git-head)
+                     nil
+                     "show" (concat "HEAD:" git-file-name)))
+            (stderr (with-temp-buffer
+                      (unless (zerop (cadr (insert-file-contents tmpfile-git-head)))
+                        (insert ": "))
+                      (buffer-substring-no-properties
+                       (point-min) (line-end-position)))))
+        (when (stringp status)
+          (error "(git show HEAD:%s killed by signal %s%s)"
+                 git-file-name status stderr))
+        (unless (zerop status)
+          (error "(git show HEAD:%s returned unsuccessfully %s%s)"
+                 git-file-name status stderr))))
+    ;; Return temporary file so we can diff it.
+    tmpfile-git-head))
+
+(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-git-diffs'. 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))
 
@@ -190,8 +269,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)))
@@ -219,6 +302,48 @@ uses the function `buffer-file-name'."
       (delete-file temp-file)
       (when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
 
+;;;###autoload
+(defun clang-format-git-diffs (&optional style assume-file-name)
+  "The same as 'clang-format-buffer' but only operates on the git
+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)
+  (let ((tmpfile-git-head
+         (clang-format--git-diffs-get-git-head-file))
+        (tmpfile-curbuf (make-temp-file "clang-format-git-tmp")))
+    ;; Move 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.
+    (write-region nil nil tmpfile-curbuf nil 'nomessage)
+    ;; Git list of lines with a diff.
+    (let ((diff-lines
+           (clang-format--git-diffs-get-diff-lines
+            tmpfile-git-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 +351,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)

>From 2c8395da26d2fb654ad26566fbd6db6b8523be69 Mon Sep 17 00:00:00 2001
From: Noah Goldstein <goldstein.w.n at gmail.com>
Date: Sun, 3 Nov 2024 10:31:27 -0600
Subject: [PATCH 2/2] Address V2 Comments

---
 clang/tools/clang-format/clang-format.el | 137 ++++++++++++++---------
 1 file changed, 83 insertions(+), 54 deletions(-)

diff --git a/clang/tools/clang-format/clang-format.el b/clang/tools/clang-format/clang-format.el
index d3f874de41c550..2a3d818e5db4e7 100644
--- a/clang/tools/clang-format/clang-format.el
+++ b/clang/tools/clang-format/clang-format.el
@@ -146,6 +146,20 @@ is a zero-based file offset, assuming ‘utf-8-unix’ coding."
     (lambda (byte &optional _quality _coding-system)
       (byte-to-position (1+ byte)))))
 
+(defun clang-format--git-diffs-match-diff-line (line)
+  ;; Matching something like:
+  ;; "@@ -80 +80 @@" or "@@ -80,2 +80,2 @@"
+  ;; Return as "<LineStart>:<LineEnd>"
+  (when (string-match "^@@\s-[0-9,]+\s\\+\\([0-9]+\\)\\(,\\([0-9]+\\)\\)?\s@@$" line)
+    ;; If we have multi-line diff
+    (if (match-string 3 line)
+        (concat (match-string 1 line)
+                ":"
+                (number-to-string
+                 (+ (string-to-number (match-string 1 line))
+                    (string-to-number (match-string 3 line)))))
+      (concat (match-string 1 line) ":" (match-string 1 line)))))
+
 (defun clang-format--git-diffs-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
@@ -161,55 +175,60 @@ which can be passed directly to 'clang-format'"
                    ;; Binary diff has different behaviors that we
                    ;; aren't interested in.
                    "-a"
-                   ;; Printout changes as only the line groups.
-                   "--changed-group-format=--lines=%dF:%dL "
-                   ;; Ignore unchanged content.
-                   "--unchanged-group-format="
+                   ;; Get minimal diff (copy diff config for git-clang-format)
+                   "-U0"
                    file-orig
-                   file-new
-                   )
-                  )
+                   file-new))
           (stderr (concat (if (zerop (buffer-size)) "" ": ")
                           (buffer-substring-no-properties
-                           (point-min) (line-end-position)))))
-      (when (stringp status)
+                           (point-min) (line-end-position))))
+          (diff-lines '()))
+      (cond
+       ((stringp status)
         (error "(diff killed by signal %s%s)" status stderr))
-      (unless (= status 0)
-        (unless (= status 1)
-          (error "(diff returned unsuccessfully %s%s)" status stderr)))
-
-
-      (if (= status 0)
-          ;; Status == 0 -> no Diff.
-          nil
-        (progn
-          ;; Split "--lines=<S0>:<E0>... --lines=<SN>:<SN>" output to
-          ;; a list for return.
-          (s-split
-           " "
-           (string-trim
-            (buffer-substring-no-properties
-             (point-min) (point-max)))))))))
-
-(defun clang-format--git-diffs-get-git-head-file ()
+       ;; Return of 0 indicates no diff
+       ((= status 0) nil)
+       ;; Return of 1 indicates found diffs and no error
+       ((= status 1)
+        ;; Iterate through all lines in diff buffer and collect all
+        ;; lines in current buffer that have a diff.
+        (goto-char (point-min))
+        (while (not (eobp))
+          (let ((diff-line (clang-format--git-diffs-match-diff-line
+                            (buffer-substring-no-properties
+                             (line-beginning-position)
+                             (line-end-position)))))
+            (when diff-line
+              ;; Create list line regions with diffs to pass to
+              ;; clang-format
+              (add-to-list 'diff-lines (concat "--lines=" diff-line) t)))
+          (forward-line 1))
+        diff-lines)
+       ;; Any return != 0 && != 1 indicates some level of error
+       (t
+        (error "(diff returned unsuccessfully %s%s)" status stderr))))))
+
+(defun clang-format--git-diffs-get-git-head-file (tmpfile-git-head)
   "Returns a temporary file with the content of 'buffer-file-name' at
 git revision HEAD. If the current buffer is either not a file or not
 in a git repo, this results in an error"
   ;; Needs current buffer to be a file
   (unless (buffer-file-name)
     (error "Buffer is not visiting a file"))
-  ;; Need to be able to find version control (git) root
-  (unless (vc-root-dir)
-    (error "File not known to git"))
   ;; Need version control to in fact be git
   (unless (string-equal (vc-backend (buffer-file-name)) "Git")
     (error "Not using git"))
 
-  (let ((tmpfile-git-head (make-temp-file "clang-format-tmp-git-head-content")))
+  (let ((base-dir (vc-root-dir)))
+    ;; Need to be able to find version control (git) root
+    (unless base-dir
+      (error "File not known to git"))
+
+
     ;; Get filename relative to git root
     (let ((git-file-name (substring
                           (expand-file-name (buffer-file-name))
-                          (string-width (expand-file-name (vc-root-dir)))
+                          (string-width (expand-file-name base-dir))
                           nil)))
       (let ((status (call-process
                      "git"
@@ -227,9 +246,7 @@ in a git repo, this results in an error"
                  git-file-name status stderr))
         (unless (zerop status)
           (error "(git show HEAD:%s returned unsuccessfully %s%s)"
-                 git-file-name status stderr))))
-    ;; Return temporary file so we can diff it.
-    tmpfile-git-head))
+                 git-file-name status stderr))))))
 
 (defun clang-format--region-impl (start end &optional style assume-file-name lines)
   "Common implementation for 'clang-format-buffer',
@@ -302,6 +319,7 @@ specific locations for reformatting (i.e diff locations)."
       (delete-file temp-file)
       (when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
 
+
 ;;;###autoload
 (defun clang-format-git-diffs (&optional style assume-file-name)
   "The same as 'clang-format-buffer' but only operates on the git
@@ -310,25 +328,36 @@ diffs from HEAD in the buffer. If no STYLE is given uses
 file. If no ASSUME-FILE-NAME is given uses the function
 `buffer-file-name'."
   (interactive)
-  (let ((tmpfile-git-head
-         (clang-format--git-diffs-get-git-head-file))
-        (tmpfile-curbuf (make-temp-file "clang-format-git-tmp")))
-    ;; Move 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.
-    (write-region nil nil tmpfile-curbuf nil 'nomessage)
-    ;; Git list of lines with a diff.
-    (let ((diff-lines
-           (clang-format--git-diffs-get-diff-lines
-            tmpfile-git-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)))))
+  (let ((tmpfile-git-head nil)
+        (tmpfile-curbuf nil))
+    (unwind-protect
+        (progn
+          (setq tmpfile-git-head
+                (make-temp-file "clang-format-git-tmp-head-content"))
+          (clang-format--git-diffs-get-git-head-file tmpfile-git-head)
+          ;; Move 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-git-tmp"))
+          (write-region nil nil tmpfile-curbuf nil 'nomessage)
+          ;; Git list of lines with a diff.
+          (let ((diff-lines
+                 (clang-format--git-diffs-get-diff-lines
+                  tmpfile-git-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))))
+      (progn
+        ;; Cleanup temporary files
+        (when tmpfile-git-head (delete-file tmpfile-git-head))
+        (when tmpfile-curbuf (delete-file tmpfile-curbuf))))))
+
 
 ;;;###autoload
 (defun clang-format-region (start end &optional style assume-file-name)



More information about the cfe-commits mailing list