From 4b19e9e4dda1edd6db99119c4ac11e14629843b8 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 18 Feb 2026 09:19:09 +0200 Subject: [PATCH] tools: support .vcheckignore pattern files for `v check-md .` (#26623) --- cmd/tools/vcheck-md.v | 178 +++++++++++++++- cmd/tools/vcheck_test.v | 366 +++++++++++++++++++++++++++++++++ vlib/v/help/other/check-md.txt | 5 + 3 files changed, 545 insertions(+), 4 deletions(-) create mode 100644 cmd/tools/vcheck_test.v diff --git a/cmd/tools/vcheck-md.v b/cmd/tools/vcheck-md.v index a2b32cb7e..68b561af1 100644 --- a/cmd/tools/vcheck-md.v +++ b/cmd/tools/vcheck-md.v @@ -35,6 +35,25 @@ pub mut: errors int } +struct VCheckIgnoreRule { + base_dir string + pattern string +} + +struct VCheckIgnoreContext { + repo_root string +} + +struct VCheckIgnoreMatch { + ignore_file string + pattern string +} + +struct MDPathScanResult { + files []string + skipped int +} + fn (v1 CheckResult) + (v2 CheckResult) CheckResult { return CheckResult{ files: v1.files + v2.files @@ -67,10 +86,13 @@ fn main() { os.rmdir_all(vcheckfolder) or {} } mut all_mdfiles := []MDFile{} + mut skipped_mdfiles := 0 for i := 0; i < files_paths.len; i++ { file_path := files_paths[i] if os.is_dir(file_path) { - files_paths << md_file_paths(file_path) + scan_result := md_file_paths(file_path) + files_paths << scan_result.files + skipped_mdfiles += scan_result.skipped continue } real_path := os.real_path(file_path) @@ -85,7 +107,12 @@ fn main() { lines: lines } } - println('> Found: ${all_mdfiles.len} .md files.') + println('> Found: ${all_mdfiles.len} .md files. Skipped by .vcheckignore: ${skipped_mdfiles}.') + if is_verbose { + for idx, mdfile in all_mdfiles { + println('> file ${idx + 1} is ${mdfile.path}') + } + } if show_progress { // this is intended to be replaced by the progress lines println('') @@ -110,17 +137,160 @@ fn main() { } } -fn md_file_paths(dir string) []string { +fn md_file_paths(dir string) MDPathScanResult { mut files_to_check := []string{} + mut skipped := 0 + vcheckignore := collect_vcheckignore_context(dir) md_files := os.walk_ext(dir, '.md') for file in md_files { nfile := file.replace('\\', '/') if nfile.contains_any_substr(['/thirdparty/', 'CHANGELOG', '/testdata/']) { continue } + if skip_match := vcheckignore.skip_match(file) { + if is_verbose { + println('SKIP: ${vcheckignore.repo_relative_path(file)} (from ${vcheckignore.repo_relative_path(skip_match.ignore_file)}: ${skip_match.pattern})') + } + skipped++ + continue + } files_to_check << file } - return files_to_check + return MDPathScanResult{ + files: files_to_check + skipped: skipped + } +} + +fn collect_vcheckignore_context(cwd string) VCheckIgnoreContext { + repo_root := find_repo_root(cwd) + return VCheckIgnoreContext{ + repo_root: repo_root + } +} + +fn find_repo_root(cwd string) string { + mut dir := os.real_path(cwd) + for { + if os.exists(os.join_path(dir, '.git')) { + return dir + } + parent := os.dir(dir) + if parent == dir || parent == '' { + return dir + } + dir = parent + } + return dir +} + +fn (ctx VCheckIgnoreContext) skip_match(file_path string) ?VCheckIgnoreMatch { + file := os.real_path(file_path).replace('\\', '/') + mut dir := os.dir(file) + repo_root := ctx.repo_root.replace('\\', '/') + for { + ignore_path := os.join_path(dir, '.vcheckignore') + if os.is_file(ignore_path) { + lines := os.read_lines(ignore_path) or { []string{} } + for line in lines { + pattern := normalize_vcheckignore_line(line) + if pattern == '' || pattern.starts_with('#') { + continue + } + if matches_vcheckignore_rule(file, VCheckIgnoreRule{ + base_dir: dir + pattern: pattern + }) + { + return VCheckIgnoreMatch{ + ignore_file: ignore_path + pattern: pattern + } + } + } + } + if dir.replace('\\', '/') == repo_root { + break + } + parent := os.dir(dir) + if parent == dir || parent == '' { + break + } + dir = parent + } + return none +} + +fn normalize_vcheckignore_line(line string) string { + trimmed := line.trim_space() + if trimmed == '' { + return '' + } + if comment_idx := trimmed.index('#') { + return trimmed[..comment_idx].trim_space() + } + return trimmed +} + +fn (ctx VCheckIgnoreContext) repo_relative_path(file_path string) string { + file := os.real_path(file_path).replace('\\', '/') + root := ctx.repo_root.replace('\\', '/') + root_prefix := root + '/' + if file.starts_with(root_prefix) { + return file.all_after(root_prefix) + } + return file +} + +fn matches_vcheckignore_rule(file string, rule VCheckIgnoreRule) bool { + base := rule.base_dir.replace('\\', '/') + base_prefix := base + '/' + if !file.starts_with(base_prefix) { + return false + } + relative_file := file.all_after(base_prefix) + mut pattern := rule.pattern.replace('\\', '/') + if pattern.starts_with('!') { + return false + } + mut anchored := false + if pattern.starts_with('/') { + anchored = true + pattern = pattern.trim_left('/') + } + if pattern.ends_with('/') { + pattern = pattern.trim_right('/') + return matches_vcheckignore_directory_pattern(relative_file, pattern, anchored) + } + if anchored { + return relative_file.match_glob(pattern) + } + if pattern.contains('/') { + return relative_file.match_glob(pattern) + } + return os.file_name(relative_file).match_glob(pattern) +} + +fn matches_vcheckignore_directory_pattern(relative_file string, pattern string, anchored bool) bool { + mut relative_dir := os.dir(relative_file).replace('\\', '/') + if relative_dir == '.' || relative_dir == '' { + return false + } + if anchored { + return relative_dir.match_glob(pattern) || relative_dir.match_glob(pattern + '/*') + } + mut candidate := relative_dir + for { + if candidate.match_glob(pattern) || candidate.match_glob(pattern + '/*') { + return true + } + if slash_idx := candidate.index('/') { + candidate = candidate[slash_idx + 1..] + continue + } + break + } + return false } fn wprintln(s string) { diff --git a/cmd/tools/vcheck_test.v b/cmd/tools/vcheck_test.v new file mode 100644 index 000000000..89e33b755 --- /dev/null +++ b/cmd/tools/vcheck_test.v @@ -0,0 +1,366 @@ +import os + +const vexe = @VEXE +const git_exe = os.find_abs_path_of_executable('git') or { '' } + +fn test_check_md_respects_vcheckignore() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + mut repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_test_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'README.md'), '# Root\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'cwd_skip.md'), '# CWD skip\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'skip.md'), '# Skip me\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'skip2.md'), '# Skip me too\n')! + write_text_file(os.join_path(repo_dir, 'notes', 'keep2.md'), '# Keep 2\n')! + + write_text_file(os.join_path(repo_dir, '.vcheckignore'), 'docs/skip.md\ndocs/sub/*.md\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'cwd_skip.md\n')! + + check_cmd := '${os.quoted_path(vexe)} check-md -hide-warnings -silent .' + + root_res := run_in_dir(repo_dir, check_cmd, true)! + assert root_res.exit_code == 0, root_res.output + assert root_res.output.contains('SKIP: docs/cwd_skip.md'), root_res.output + assert root_res.output.contains('SKIP: docs/skip.md'), root_res.output + assert root_res.output.contains('SKIP: docs/sub/skip2.md'), root_res.output + assert root_res.output.contains('from docs/.vcheckignore: cwd_skip.md'), root_res.output + assert root_res.output.contains('from .vcheckignore: docs/skip.md'), root_res.output + assert root_res.output.contains('from .vcheckignore: docs/sub/*.md'), root_res.output + assert root_res.output.contains('> Found: 2 .md files.'), root_res.output + assert root_res.output.contains('Skipped by .vcheckignore: 3.'), root_res.output + assert root_res.output.contains('Checked .md files: 2 |'), root_res.output + root_non_verbose_res := run_in_dir(repo_dir, check_cmd, false)! + assert root_non_verbose_res.exit_code == 0, root_non_verbose_res.output + assert !root_non_verbose_res.output.contains('SKIP:'), root_non_verbose_res.output + + docs_res := run_in_dir(os.join_path(repo_dir, 'docs'), check_cmd, true)! + assert docs_res.exit_code == 0, docs_res.output + assert docs_res.output.contains('SKIP: docs/cwd_skip.md'), docs_res.output + assert docs_res.output.contains('SKIP: docs/skip.md'), docs_res.output + assert docs_res.output.contains('SKIP: docs/sub/skip2.md'), docs_res.output + assert docs_res.output.contains('from docs/.vcheckignore: cwd_skip.md'), docs_res.output + assert docs_res.output.contains('> Found: 0 .md files.'), docs_res.output + assert docs_res.output.contains('Skipped by .vcheckignore: 3.'), docs_res.output + assert docs_res.output.contains('Checked .md files: 0 |'), docs_res.output + + sub_res := run_in_dir(os.join_path(repo_dir, 'docs', 'sub'), check_cmd, true)! + assert sub_res.exit_code == 0, sub_res.output + assert sub_res.output.contains('SKIP: docs/sub/skip2.md'), sub_res.output + assert !sub_res.output.contains('SKIP: docs/cwd_skip.md'), sub_res.output + assert sub_res.output.contains('from .vcheckignore: docs/sub/*.md'), sub_res.output + assert sub_res.output.contains('> Found: 0 .md files.'), sub_res.output + assert sub_res.output.contains('Skipped by .vcheckignore: 1.'), sub_res.output + assert sub_res.output.contains('Checked .md files: 0 |'), sub_res.output +} + +fn test_check_md_respects_vcheckignore_glob_in_scanned_dir() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_glob_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored1.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored2.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'ignored3.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep1.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep2.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', 'keep3.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'doc', 'plans', '.vcheckignore'), 'ignored*.md\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent doc/plans', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: doc/plans/ignored1.md'), res.output + assert res.output.contains('SKIP: doc/plans/ignored2.md'), res.output + assert res.output.contains('SKIP: doc/plans/ignored3.md'), res.output + assert res.output.contains('from doc/plans/.vcheckignore: ignored*.md'), res.output + assert res.output.contains('> Found: 3 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 3.'), res.output + assert res.output.contains('Checked .md files: 3 |'), res.output +} + +fn test_check_md_respects_vcheckignore_anchored_pattern() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'root_only.md'), '# ignored by /root_only.md\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'root_only.md'), '# should be kept\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), '/root_only.md\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs/root_only.md'), res.output + assert res.output.contains('from docs/.vcheckignore: /root_only.md'), res.output + assert !res.output.contains('SKIP: docs/sub/root_only.md'), res.output + assert res.output.contains('> Found: 2 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 1.'), res.output + assert res.output.contains('Checked .md files: 2 |'), res.output +} + +fn test_check_md_respects_vcheckignore_anchored_directory_pattern() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_dir_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'ignored.md'), '# ignored by /sub/\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'nested', 'sub', 'keep.md'), '# kept\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), '/sub/\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs/sub/ignored.md'), res.output + assert res.output.contains('from docs/.vcheckignore: /sub/'), res.output + assert !res.output.contains('SKIP: docs/nested/sub/keep.md'), res.output + assert res.output.contains('> Found: 2 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 1.'), res.output + assert res.output.contains('Checked .md files: 2 |'), res.output +} + +fn test_check_md_respects_vcheckignore_non_anchored_directory_pattern() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_non_anchored_dir_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'sub', 'ignored1.md'), '# ignored 1\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'nested', 'sub', 'ignored2.md'), '# ignored 2\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'sub/\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs/sub/ignored1.md'), res.output + assert res.output.contains('SKIP: docs/nested/sub/ignored2.md'), res.output + assert res.output.contains('from docs/.vcheckignore: sub/'), res.output + assert res.output.contains('> Found: 1 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output + assert res.output.contains('Checked .md files: 1 |'), res.output +} + +fn test_check_md_respects_vcheckignore_anchored_directory_glob_pattern() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_anchored_dir_glob_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs-a', 'sub', 'ignored1.md'), '# ignored 1\n')! + write_text_file(os.join_path(repo_dir, 'docs-b', 'sub', 'ignored2.md'), '# ignored 2\n')! + write_text_file(os.join_path(repo_dir, 'other', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, '.vcheckignore'), '/docs-*/\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent .', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs-a/sub/ignored1.md'), res.output + assert res.output.contains('SKIP: docs-b/sub/ignored2.md'), res.output + assert res.output.contains('from .vcheckignore: /docs-*/'), res.output + assert !res.output.contains('SKIP: other/keep.md'), res.output + assert res.output.contains('> Found: 1 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output + assert res.output.contains('Checked .md files: 1 |'), res.output +} + +fn test_check_md_respects_vcheckignore_comments_and_blank_lines() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_comments_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'ignored2.md'), '# ignored2\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), '# comment\n\nignored.md # inline comment\nignored2.md\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs/ignored.md'), res.output + assert res.output.contains('SKIP: docs/ignored2.md'), res.output + assert res.output.contains('from docs/.vcheckignore: ignored.md'), res.output + assert res.output.contains('from docs/.vcheckignore: ignored2.md'), res.output + assert !res.output.contains('SKIP: docs/keep.md'), res.output + assert res.output.contains('> Found: 1 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output + assert res.output.contains('Checked .md files: 1 |'), res.output +} + +fn test_check_md_file_argument_does_not_use_vcheckignore_directory_filtering() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_file_argument_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored by dir scan\n')! + write_text_file(os.join_path(repo_dir, 'docs', '.vcheckignore'), 'ignored.md\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs/ignored.md', + true)! + assert res.exit_code == 0, res.output + assert !res.output.contains('SKIP: docs/ignored.md'), res.output + assert res.output.contains('> Found: 1 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 0.'), res.output + assert res.output.contains('Checked .md files: 1 |'), res.output +} + +fn test_check_md_uses_scanned_dir_repo_root_for_vcheckignore() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + base_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_repo_root_${os.getpid()}') + repo_a := os.join_path(base_dir, 'repo_a') + repo_b := os.join_path(base_dir, 'repo_b') + os.rmdir_all(base_dir) or {} + os.mkdir_all(os.join_path(repo_b, 'docs'))! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(base_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_a)}') + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_b)}') + + write_text_file(os.join_path(base_dir, '.vcheckignore'), 'outside*.md\n')! + write_text_file(os.join_path(repo_b, 'docs', 'outside1.md'), '# outside but should not be skipped\n')! + write_text_file(os.join_path(repo_b, 'docs', 'keep1.md'), '# keep\n')! + + res := run_in_dir(repo_a, '${os.quoted_path(vexe)} check-md -hide-warnings -silent ${os.quoted_path(os.join_path(repo_b, + 'docs'))}', true)! + assert res.exit_code == 0, res.output + assert !res.output.contains('SKIP: '), res.output + assert res.output.contains('> Found: 2 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 0.'), res.output + assert res.output.contains('Checked .md files: 2 |'), res.output +} + +fn test_check_md_multiple_directories_accumulate_skipped_count() { + if git_exe == '' { + eprintln('git is required for this test; skipping') + return + } + original_wd := os.getwd() + repo_dir := os.join_path(os.vtmp_dir(), 'vcheckignore_multi_dirs_${os.getpid()}') + os.rmdir_all(repo_dir) or {} + os.mkdir_all(repo_dir)! + defer { + os.chdir(original_wd) or {} + os.rmdir_all(repo_dir) or {} + } + os.execute_or_exit('${os.quoted_path(git_exe)} init ${os.quoted_path(repo_dir)}') + + write_text_file(os.join_path(repo_dir, 'docs', 'ignored.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'docs', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, 'notes', 'ignored.md'), '# ignored\n')! + write_text_file(os.join_path(repo_dir, 'notes', 'keep.md'), '# keep\n')! + write_text_file(os.join_path(repo_dir, '.vcheckignore'), 'docs/ignored.md\nnotes/ignored.md\n')! + + res := run_in_dir(repo_dir, '${os.quoted_path(vexe)} check-md -hide-warnings -silent docs notes', + true)! + assert res.exit_code == 0, res.output + assert res.output.contains('SKIP: docs/ignored.md'), res.output + assert res.output.contains('SKIP: notes/ignored.md'), res.output + assert res.output.contains('> Found: 2 .md files.'), res.output + assert res.output.contains('Skipped by .vcheckignore: 2.'), res.output + assert res.output.contains('Checked .md files: 2 |'), res.output +} + +fn run_in_dir(path string, cmd string, verbose bool) !os.Result { + original_wd := os.getwd() + os.chdir(path)! + if verbose { + os.setenv('VERBOSE', '1', true) + } else { + os.unsetenv('VERBOSE') + } + res := os.execute(cmd) + os.unsetenv('VERBOSE') + os.chdir(original_wd)! + return res +} + +fn write_text_file(path string, content string) ! { + os.mkdir_all(os.dir(path))! + os.write_file(path, content)! +} diff --git a/vlib/v/help/other/check-md.txt b/vlib/v/help/other/check-md.txt index 7578f781c..1c80193e6 100644 --- a/vlib/v/help/other/check-md.txt +++ b/vlib/v/help/other/check-md.txt @@ -5,6 +5,11 @@ Usage: v check-md [flags] <...files> - Check the given .md files. v check-md [flags] <...dirs> - Check *all* files in the given directories. Note: You can also combine files and directories. + When scanning directories, patterns from `.vcheckignore` files (one glob per line) are + used to skip `.md` files. For each `.md` file, `.vcheckignore` files in that file's + directory and parent directories are applied up to the repo root. Each skipped file is + reported (when `VERBOSE=1`) as + `SKIP: (from : )`. Options: -silent Do not show a progress bar. -- 2.39.5