Skip to content

Commit

Permalink
[clang-format] Fix a serious bug in git clang-format -f (#102629)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
owenca authored Aug 10, 2024
1 parent c5a4291 commit 986bc3d
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 3 deletions.
60 changes: 60 additions & 0 deletions clang/test/Format/list-ignored.cpp
Original file line number Diff line number Diff line change
@@ -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
// 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
// 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
// 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
// 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
// CHECK6: foo.cc
// CHECK6-NEXT: bar.cc
// CHECK6-NEXT: baz.c
// CHECK6-NOT: foo.c
// CHECK6-NEXT: foo.js

// RUN: rm .clang-format-ignore
// RUN: clang-format -list-ignored *.cc level1/*.c* level1/level2/foo.* \
// RUN: | FileCheck %s -check-prefix=CHECK7
// CHECK7-NOT: foo.cc
// CHECK7-NOT: bar.cc
// CHECK7-NOT: baz.c
// CHECK7-NOT: foo.c
// CHECK7: foo.js

// RUN: cd ..
// RUN: rm -r %t.dir
12 changes: 11 additions & 1 deletion clang/tools/clang-format/ClangFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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() << "] "
Expand Down
15 changes: 13 additions & 2 deletions clang/tools/clang-format/git-clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -399,6 +400,16 @@ 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())
if not ignored_files:
return
ignored_files = ignored_files.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')
Expand Down

0 comments on commit 986bc3d

Please sign in to comment.