Skip to content

Commit 32be90d

Browse files
authored
[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.
1 parent 0572580 commit 32be90d

File tree

1 file changed

+185
-15
lines changed

1 file changed

+185
-15
lines changed

clang/tools/clang-format/clang-format.el

Lines changed: 185 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,24 +146,133 @@ is a zero-based file offset, assuming ‘utf-8-unix’ coding."
146146
(lambda (byte &optional _quality _coding-system)
147147
(byte-to-position (1+ byte)))))
148148

149-
;;;###autoload
150-
(defun clang-format-region (start end &optional style assume-file-name)
151-
"Use clang-format to format the code between START and END according to STYLE.
152-
If called interactively uses the region or the current statement if there is no
153-
no active region. If no STYLE is given uses `clang-format-style'. Use
154-
ASSUME-FILE-NAME to locate a style config file, if no ASSUME-FILE-NAME is given
155-
uses the function `buffer-file-name'."
156-
(interactive
157-
(if (use-region-p)
158-
(list (region-beginning) (region-end))
159-
(list (point) (point))))
149+
(defmacro clang-format--with-delete-files-guard (bind-files-to-delete &rest body)
150+
"Execute BODY which may add temp files to BIND-FILES-TO-DELETE."
151+
(declare (indent 1))
152+
`(let ((,bind-files-to-delete nil))
153+
(unwind-protect
154+
(progn
155+
,@body)
156+
(while ,bind-files-to-delete
157+
(with-demoted-errors "failed to remove file: %S"
158+
(delete-file (pop ,bind-files-to-delete)))))))
159+
160+
161+
(defun clang-format--vc-diff-get-diff-lines (file-orig file-new)
162+
"Return all line regions that contain diffs between FILE-ORIG and
163+
FILE-NEW. If there is no diff ‘nil’ is returned. Otherwise the return
164+
is a ‘list’ of line ranges to format. The list of line ranges can be
165+
passed to ‘clang-format--region-impl’"
166+
;; Use temporary buffer for output of diff.
167+
(with-temp-buffer
168+
;; We could use diff.el:diff-no-select here. The reason we don't
169+
;; is diff-no-select requires extra copies on the buffers which
170+
;; induces noticeable slowdowns, especially on larger files.
171+
(let ((status (call-process
172+
diff-command
173+
nil
174+
(current-buffer)
175+
nil
176+
;; Binary diff has different behaviors that we
177+
;; aren't interested in.
178+
"-a"
179+
;; Get minimal diff (copy diff config for git-clang-format).
180+
"-U0"
181+
file-orig
182+
file-new))
183+
(stderr (concat (if (zerop (buffer-size)) "" ": ")
184+
(buffer-substring-no-properties
185+
(point-min) (line-end-position))))
186+
(diff-lines '()))
187+
(cond
188+
((stringp status)
189+
(error "clang-format: (diff killed by signal %s%s)" status stderr))
190+
;; Return of 0 indicates no diff.
191+
((= status 0) nil)
192+
;; Return of 1 indicates found diffs and no error.
193+
((= status 1)
194+
;; Find and collect all diff lines.
195+
;; We are matching something like:
196+
;; "@@ -80 +80 @@" or "@@ -80,2 +80,2 @@"
197+
(goto-char (point-min))
198+
(while (re-search-forward
199+
"^@@[[:blank:]]-[[:digit:],]+[[:blank:]]\\+\\([[:digit:]]+\\)\\(,\\([[:digit:]]+\\)\\)?[[:blank:]]@@$"
200+
nil
201+
t
202+
1)
203+
(let ((match1 (string-to-number (match-string 1)))
204+
(match3 (let ((match3_or_nil (match-string 3)))
205+
(if match3_or_nil
206+
(string-to-number match3_or_nil)
207+
nil))))
208+
(push (cons match1 (if match3 (+ match1 match3) match1)) diff-lines)))
209+
(nreverse diff-lines))
210+
;; Any return != 0 && != 1 indicates some level of error.
211+
(t
212+
(error "clang-format: (diff returned unsuccessfully %s%s)" status stderr))))))
213+
214+
(defun clang-format--vc-diff-get-vc-head-file (tmpfile-vc-head)
215+
"Stores the contents of ‘buffer-file-name’ at vc revision HEAD into
216+
‘tmpfile-vc-head’. If the current buffer is either not a file or not
217+
in a vc repo, this results in an error. Currently git is the only
218+
supported vc."
219+
;; We need the current buffer to be a file.
220+
(unless (buffer-file-name)
221+
(error "clang-format: Buffer is not visiting a file"))
222+
223+
(let ((base-dir (vc-root-dir))
224+
(backend (vc-backend (buffer-file-name))))
225+
;; We need to be able to find version control (git) root.
226+
(unless base-dir
227+
(error "clang-format: File not known to git"))
228+
(cond
229+
((string-equal backend "Git")
230+
;; Get the filename relative to git root.
231+
(let ((vc-file-name (substring
232+
(expand-file-name (buffer-file-name))
233+
(string-width (expand-file-name base-dir))
234+
nil)))
235+
(let ((status (call-process
236+
vc-git-program
237+
nil
238+
`(:file ,tmpfile-vc-head)
239+
nil
240+
"show" (concat "HEAD:" vc-file-name)))
241+
(stderr (with-temp-buffer
242+
(unless (zerop (cadr (insert-file-contents tmpfile-vc-head)))
243+
(insert ": "))
244+
(buffer-substring-no-properties
245+
(point-min) (line-end-position)))))
246+
(when (stringp status)
247+
(error "clang-format: (git show HEAD:%s killed by signal %s%s)"
248+
vc-file-name status stderr))
249+
(unless (zerop status)
250+
(error "clang-format: (git show HEAD:%s returned unsuccessfully %s%s)"
251+
vc-file-name status stderr)))))
252+
(t
253+
(error
254+
"Version control %s isn't supported, currently supported backends: git"
255+
backend)))))
256+
160257

258+
(defun clang-format--region-impl (start end &optional style assume-file-name lines)
259+
"Common implementation for ‘clang-format-buffer’,
260+
‘clang-format-region’, and ‘clang-format-vc-diff’. START and END
261+
refer to the region to be formatter. STYLE and ASSUME-FILE-NAME are
262+
used for configuring the clang-format. And LINES is used to pass
263+
specific locations for reformatting (i.e diff locations)."
161264
(unless style
162265
(setq style clang-format-style))
163266

164267
(unless assume-file-name
165268
(setq assume-file-name (buffer-file-name (buffer-base-buffer))))
166269

270+
;; Convert list of line ranges to list command for ‘clang-format’ executable.
271+
(when lines
272+
(setq lines (mapcar (lambda (range)
273+
(format "--lines=%d:%d" (car range) (cdr range)))
274+
lines)))
275+
167276
(let ((file-start (clang-format--bufferpos-to-filepos start 'approximate
168277
'utf-8-unix))
169278
(file-end (clang-format--bufferpos-to-filepos end 'approximate
@@ -190,8 +299,12 @@ uses the function `buffer-file-name'."
190299
(list "--assume-filename" assume-file-name))
191300
,@(and style (list "--style" style))
192301
"--fallback-style" ,clang-format-fallback-style
193-
"--offset" ,(number-to-string file-start)
194-
"--length" ,(number-to-string (- file-end file-start))
302+
,@(and lines lines)
303+
,@(and (not lines)
304+
(list
305+
"--offset" (number-to-string file-start)
306+
"--length" (number-to-string
307+
(- file-end file-start))))
195308
"--cursor" ,(number-to-string cursor))))
196309
(stderr (with-temp-buffer
197310
(unless (zerop (cadr (insert-file-contents temp-file)))
@@ -216,17 +329,74 @@ uses the function `buffer-file-name'."
216329
(if incomplete-format
217330
(message "(clang-format: incomplete (syntax errors)%s)" stderr)
218331
(message "(clang-format: success%s)" stderr))))
219-
(delete-file temp-file)
332+
(with-demoted-errors
333+
"clang-format: Failed to delete temporary file: %S"
334+
(delete-file temp-file))
220335
(when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
221336

337+
338+
;;;###autoload
339+
(defun clang-format-vc-diff (&optional style assume-file-name)
340+
"The same as ‘clang-format-buffer’ but only operates on the vc
341+
diffs from HEAD in the buffer. If no STYLE is given uses
342+
‘clang-format-style’. Use ASSUME-FILE-NAME to locate a style config
343+
file. If no ASSUME-FILE-NAME is given uses the function
344+
‘buffer-file-name’."
345+
(interactive)
346+
(clang-format--with-delete-files-guard tmp-files
347+
(let ((tmpfile-vc-head nil)
348+
(tmpfile-curbuf nil))
349+
(setq tmpfile-vc-head
350+
(make-temp-file "clang-format-vc-tmp-head-content"))
351+
(push tmpfile-vc-head tmp-files)
352+
(clang-format--vc-diff-get-vc-head-file tmpfile-vc-head)
353+
;; Move the current buffer to a temporary file to take a
354+
;; diff. Even if current-buffer is backed by a file, we
355+
;; want to diff the buffer contents which might not be
356+
;; saved.
357+
(setq tmpfile-curbuf (make-temp-file "clang-format-vc-tmp"))
358+
(push tmpfile-curbuf tmp-files)
359+
(write-region nil nil tmpfile-curbuf nil 'nomessage)
360+
;; Get a list of lines with a diff.
361+
(let ((diff-lines
362+
(clang-format--vc-diff-get-diff-lines
363+
tmpfile-vc-head tmpfile-curbuf)))
364+
;; If we have any diffs, format them.
365+
(when diff-lines
366+
(clang-format--region-impl
367+
(point-min)
368+
(point-max)
369+
style
370+
assume-file-name
371+
diff-lines))))))
372+
373+
374+
;;;###autoload
375+
(defun clang-format-region (start end &optional style assume-file-name)
376+
"Use clang-format to format the code between START and END according
377+
to STYLE. If called interactively uses the region or the current
378+
statement if there is no no active region. If no STYLE is given uses
379+
`clang-format-style'. Use ASSUME-FILE-NAME to locate a style config
380+
file, if no ASSUME-FILE-NAME is given uses the function
381+
`buffer-file-name'."
382+
(interactive
383+
(if (use-region-p)
384+
(list (region-beginning) (region-end))
385+
(list (point) (point))))
386+
(clang-format--region-impl start end style assume-file-name))
387+
222388
;;;###autoload
223389
(defun clang-format-buffer (&optional style assume-file-name)
224390
"Use clang-format to format the current buffer according to STYLE.
225391
If no STYLE is given uses `clang-format-style'. Use ASSUME-FILE-NAME
226392
to locate a style config file. If no ASSUME-FILE-NAME is given uses
227393
the function `buffer-file-name'."
228394
(interactive)
229-
(clang-format-region (point-min) (point-max) style assume-file-name))
395+
(clang-format--region-impl
396+
(point-min)
397+
(point-max)
398+
style
399+
assume-file-name))
230400

231401
;;;###autoload
232402
(defalias 'clang-format 'clang-format-region)

0 commit comments

Comments
 (0)