[clang] [clang-format] Fix a serious bug in `git clang-format -f` (PR #102629)

Owen Pan via cfe-commits cfe-commits at lists.llvm.org
Fri Aug 9 09:21:40 PDT 2024


https://github.com/owenca updated https://github.com/llvm/llvm-project/pull/102629

>From 9b71b289feb75cdc6d67a6ac696ff0ba7969549d Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Fri, 9 Aug 2024 08:15:43 -0700
Subject: [PATCH 1/3] [clang-format] Fix a serious bug in `git clang-format -f`

With the --force (or -f) option, git-clang-format wipes out input files
excluded by a .clang-format-ignore file if they have unstaged changes.

This patch adds a hidden clang-format option --list-ignored that lists such
excluded files for git-clang-format to filter out.

Fixes #102459.
---
 clang/test/Format/list-ignored.cpp        | 60 +++++++++++++++++++++++
 clang/tools/clang-format/ClangFormat.cpp  | 12 ++++-
 clang/tools/clang-format/git-clang-format | 12 ++++-
 3 files changed, 81 insertions(+), 3 deletions(-)
 create mode 100644 clang/test/Format/list-ignored.cpp

diff --git a/clang/test/Format/list-ignored.cpp b/clang/test/Format/list-ignored.cpp
new file mode 100644
index 00000000000000..91f2b5175d6395
--- /dev/null
+++ b/clang/test/Format/list-ignored.cpp
@@ -0,0 +1,60 @@
+// RUN: rm -rf %t.dir
+// RUN: mkdir -p %t.dir/level1/level2
+
+// RUN: cd %t.dir
+// RUN: echo "*" > .clang-format-ignore
+// RUN: echo "level*/*.c*" >> .clang-format-ignore
+// RUN: echo "*/*2/foo.*" >> .clang-format-ignore
+
+// RUN: touch foo.cc
+// RUN: clang-format -list-ignored .clang-format-ignore foo.cc \
+// RUN:   | FileCheck %s -match-full-lines
+// CHECK: .clang-format-ignore
+// CHECK-NEXT: foo.cc
+
+// RUN: cd level1
+// RUN: touch bar.cc baz.c
+// RUN: clang-format -list-ignored bar.cc baz.c \
+// RUN:   | FileCheck %s -check-prefix=CHECK2 -match-full-lines
+// CHECK2: bar.cc
+// CHECK2-NEXT: baz.c
+
+// RUN: cd level2
+// RUN: touch foo.c foo.js
+// RUN: clang-format -list-ignored foo.c foo.js \
+// RUN:   | FileCheck %s -check-prefix=CHECK3 -match-full-lines
+// CHECK3: foo.c
+// CHECK3-NEXT: foo.js
+
+// RUN: touch .clang-format-ignore
+// RUN: clang-format -list-ignored foo.c foo.js \
+// RUN:   | FileCheck %s -allow-empty -check-prefix=CHECK4
+// CHECK4-NOT: foo.c
+// CHECK4-NOT: foo.js
+
+// RUN: echo "*.js" > .clang-format-ignore
+// RUN: clang-format -list-ignored foo.c foo.js \
+// RUN:   | FileCheck %s -check-prefix=CHECK5 -match-full-lines
+// CHECK5-NOT: foo.c
+// CHECK5: foo.js
+
+// RUN: cd ../..
+// RUN: clang-format -list-ignored *.cc level1/*.c* level1/level2/foo.* \
+// RUN:   | FileCheck %s -check-prefix=CHECK6 -match-full-lines
+// CHECK6-NOT: level1/levle2/foo.c
+// CHECK6: foo.cc
+// CHECK6-NEXT: level1/bar.cc
+// CHECK6-NEXT: level1/baz.c
+// CHECK6-NEXT: level1/level2/foo.js
+
+// RUN: rm .clang-format-ignore
+// RUN: clang-format -list-ignored *.cc level1/*.c* level1/level2/foo.* \
+// RUN:   | FileCheck %s -check-prefix=CHECK7 -match-full-lines
+// CHECK7-NOT: foo.cc
+// CHECK7-NOT: level1/bar.cc
+// CHECK7-NOT: level1/baz.c
+// CHECK7-NOT: level1/level2/foo.c
+// CHECK7: level1/level2/foo.js
+
+// RUN: cd ..
+// RUN: rm -r %t.dir
diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index 6582d73eae40ea..50dd5345a6db33 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -210,6 +210,10 @@ static cl::opt<bool> FailOnIncompleteFormat(
     cl::desc("If set, fail with exit code 1 on incomplete format."),
     cl::init(false), cl::cat(ClangFormatCategory));
 
+static cl::opt<bool> ListIgnored("list-ignored",
+                                 cl::desc("List ignored files."),
+                                 cl::cat(ClangFormatCategory), cl::Hidden);
+
 namespace clang {
 namespace format {
 
@@ -715,7 +719,13 @@ int main(int argc, const char **argv) {
   unsigned FileNo = 1;
   bool Error = false;
   for (const auto &FileName : FileNames) {
-    if (isIgnored(FileName))
+    const bool Ignored = isIgnored(FileName);
+    if (ListIgnored) {
+      if (Ignored)
+        outs() << FileName << "\n";
+      continue;
+    }
+    if (Ignored)
       continue;
     if (Verbose) {
       errs() << "Formatting [" << FileNo++ << "/" << FileNames.size() << "] "
diff --git a/clang/tools/clang-format/git-clang-format b/clang/tools/clang-format/git-clang-format
index d33fd478d77fd9..8f7eb38d72e738 100755
--- a/clang/tools/clang-format/git-clang-format
+++ b/clang/tools/clang-format/git-clang-format
@@ -173,11 +173,12 @@ def main():
   # those files.
   cd_to_toplevel()
   filter_symlinks(changed_lines)
+  filter_ignored_files(changed_lines, binary=opts.binary)
   if opts.verbose >= 1:
     ignored_files.difference_update(changed_lines)
     if ignored_files:
-      print(
-        'Ignoring changes in the following files (wrong extension or symlink):')
+      print('Ignoring the following files (wrong extension, symlink, or '
+            'ignored by clang-format):')
       for filename in ignored_files:
         print('    %s' % filename)
     if changed_lines:
@@ -399,6 +400,13 @@ def filter_symlinks(dictionary):
       del dictionary[filename]
 
 
+def filter_ignored_files(dictionary, binary):
+  """Delete every key in `dictionary` that is ignored by clang-format."""
+  ignored_files = run(binary, '-list-ignored', *dictionary.keys()).split('\n')
+  for filename in ignored_files:
+    del dictionary[filename]
+
+
 def cd_to_toplevel():
   """Change to the top level of the git repository."""
   toplevel = run('git', 'rev-parse', '--show-toplevel')

>From d081b62432ad00130481272044fc28440e6b08fe Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Fri, 9 Aug 2024 08:30:13 -0700
Subject: [PATCH 2/3] Update ClangFormat.cpp

Changes the `"\n"` to `'\n'`.
---
 clang/tools/clang-format/ClangFormat.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index 50dd5345a6db33..54b1dacbbe854a 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -722,7 +722,7 @@ int main(int argc, const char **argv) {
     const bool Ignored = isIgnored(FileName);
     if (ListIgnored) {
       if (Ignored)
-        outs() << FileName << "\n";
+        outs() << FileName << '\n';
       continue;
     }
     if (Ignored)

>From 7a0ac9f6d4e6906284b585701e2896be626cb27e Mon Sep 17 00:00:00 2001
From: Owen Pan <owenpiano at gmail.com>
Date: Fri, 9 Aug 2024 09:21:31 -0700
Subject: [PATCH 3/3] Update git-clang-format

Handles the case when `clang-format -list-ignored` outputs nothing.
---
 clang/tools/clang-format/git-clang-format | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/clang/tools/clang-format/git-clang-format b/clang/tools/clang-format/git-clang-format
index 8f7eb38d72e738..714ba8a6e77d51 100755
--- a/clang/tools/clang-format/git-clang-format
+++ b/clang/tools/clang-format/git-clang-format
@@ -402,7 +402,10 @@ def filter_symlinks(dictionary):
 
 def filter_ignored_files(dictionary, binary):
   """Delete every key in `dictionary` that is ignored by clang-format."""
-  ignored_files = run(binary, '-list-ignored', *dictionary.keys()).split('\n')
+  ignored_files = run(binary, '-list-ignored', *dictionary.keys())
+  if not ignored_files:
+    return
+  ignored_files = ignored_files.split('\n')
   for filename in ignored_files:
     del dictionary[filename]
 



More information about the cfe-commits mailing list