From e8b0359d4d8677b1bfe4fd0d15d7c23ceed2f4ed Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 24 Feb 2021 15:25:29 -0600 Subject: [PATCH 1/9] Add --extend-exclude parameter --- README.md | 25 ++++------ docs/change_log.md | 2 + docs/installation_and_usage.md | 5 ++ docs/pyproject_toml.md | 19 +------- pyproject.toml | 13 +---- src/black/__init__.py | 75 +++++++++++++++++++---------- tests/test_black.py | 86 ++++++++++++++++++++++++++++------ 7 files changed, 140 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 178f763c32d..df21d2b1f00 100644 --- a/README.md +++ b/README.md @@ -135,11 +135,17 @@ Options: hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu ild|buck-out|build|dist)/] + --extend-exclude TEXT Like --exclude, but adds additional files + and directories on top of the excluded + ones. (Useful if you simply want to add to + the default) + --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even when they are passed explicitly as arguments. + --stdin-filename TEXT The name of the file when passing it through stdin. Useful to make sure Black will respect --force-exclude option on some @@ -313,25 +319,10 @@ expressions by Black. Use `[ ]` to denote a significant space character. line-length = 88 target-version = ['py37'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories # in the root of the project. -^/( - ( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | foo.py # also separately exclude a file named foo.py in - # the root of the project -) +^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults) ''' ``` diff --git a/docs/change_log.md b/docs/change_log.md index 066be76c06c..01c27553fca 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -28,6 +28,8 @@ - use lowercase hex strings (#1692) +- added `--extend-exclude` argument (#1571) + #### _Packaging_ - Self-contained native _Black_ binaries are now provided for releases via GitHub diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index ee45c934da8..5b4adb547ec 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -95,6 +95,11 @@ Options: when they are passed explicitly as arguments. + --extend-exclude TEXT Like --exclude, but adds additional files + and directories on top of the excluded + ones. (Useful if you simply want to add to + the default) + --stdin-filename TEXT The name of the file when passing it through stdin. Useful to make sure Black will respect --force-exclude option on some diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 453f533bf96..e71185cc036 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -54,25 +54,10 @@ expressions by Black. Use `[ ]` to denote a significant space character. line-length = 88 target-version = ['py37'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories # in the root of the project. -^/( - ( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | foo.py # also separately exclude a file named foo.py in - # the root of the project -) +^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults) ''' ``` diff --git a/pyproject.toml b/pyproject.toml index 9d4da0bf692..7f632f2839d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,19 +9,8 @@ line-length = 88 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - # The following are specific to Black, you probably don't want those. | blib2to3 | tests/data diff --git a/src/black/__init__.py b/src/black/__init__.py index 6919468609c..6d597520e93 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -461,6 +461,14 @@ def target_version_option_callback( ), show_default=True, ) +@click.option( + "--extend-exclude", + type=str, + help=( + "Like --exclude, but adds additional files and directories on top of the" + " excluded ones. (Useful if you simply want to add to the default)" + ), +) @click.option( "--force-exclude", type=str, @@ -537,6 +545,7 @@ def main( verbose: bool, include: str, exclude: str, + extend_exclude: Optional[str], force_exclude: Optional[str], stdin_filename: Optional[str], src: Tuple[str, ...], @@ -570,6 +579,7 @@ def main( verbose=verbose, include=include, exclude=exclude, + extend_exclude=extend_exclude, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, @@ -602,6 +612,18 @@ def main( ctx.exit(report.return_code) +def test_regex( + ctx: click.Context, + regex_name: str, + regex: Optional[str], +) -> Optional[Pattern]: + try: + return re_compile_maybe_verbose(regex) if regex else None + except re.error: + err(f"Invalid regular expression for {regex_name} given: {regex!r}") + ctx.exit(2) + + def get_sources( *, ctx: click.Context, @@ -610,28 +632,17 @@ def get_sources( verbose: bool, include: str, exclude: str, + extend_exclude: Optional[str], force_exclude: Optional[str], report: "Report", stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" - try: - include_regex = re_compile_maybe_verbose(include) - except re.error: - err(f"Invalid regular expression for include given: {include!r}") - ctx.exit(2) - try: - exclude_regex = re_compile_maybe_verbose(exclude) - except re.error: - err(f"Invalid regular expression for exclude given: {exclude!r}") - ctx.exit(2) - try: - force_exclude_regex = ( - re_compile_maybe_verbose(force_exclude) if force_exclude else None - ) - except re.error: - err(f"Invalid regular expression for force_exclude given: {force_exclude!r}") - ctx.exit(2) + + include_regex = test_regex(ctx, "include", include) + exclude_regex = test_regex(ctx, "exclude", exclude) + extend_exclude_regex = test_regex(ctx, "extend_exclude", extend_exclude) + force_exclude_regex = test_regex(ctx, "force_exclude", force_exclude) root = find_project_root(src) sources: Set[Path] = set() @@ -672,6 +683,7 @@ def get_sources( root, include_regex, exclude_regex, + extend_exclude_regex, force_exclude_regex, report, gitignore, @@ -6110,17 +6122,27 @@ def normalize_path_maybe_ignore( return normalized_path +def path_is_excluded( + normalized_path: str, + pattern: Pattern[str], +): + match = pattern.search(normalized_path) if pattern else None + return match and match.group(0) + + def gen_python_files( paths: Iterable[Path], root: Path, include: Optional[Pattern[str]], exclude: Pattern[str], + extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: "Report", gitignore: PathSpec, ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the - `exclude_regex` or `force_exclude` regexes, but are included by the `include` regex. + `exclude_regex`, `extend_exclude`, or `force_exclude` regexes, + but are included by the `include` regex. Symbolic links pointing outside of the `root` directory are ignored. @@ -6137,20 +6159,22 @@ def gen_python_files( report.path_ignored(child, "matches the .gitignore file content") continue - # Then ignore with `--exclude` and `--force-exclude` options. + # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. normalized_path = "/" + normalized_path if child.is_dir(): normalized_path += "/" - exclude_match = exclude.search(normalized_path) if exclude else None - if exclude_match and exclude_match.group(0): + if path_is_excluded(normalized_path, exclude): report.path_ignored(child, "matches the --exclude regular expression") continue - force_exclude_match = ( - force_exclude.search(normalized_path) if force_exclude else None - ) - if force_exclude_match and force_exclude_match.group(0): + if path_is_excluded(normalized_path, extend_exclude): + report.path_ignored( + child, "matches the --extend-exclude regular expression" + ) + continue + + if path_is_excluded(normalized_path, force_exclude): report.path_ignored(child, "matches the --force-exclude regular expression") continue @@ -6160,6 +6184,7 @@ def gen_python_files( root, include, exclude, + extend_exclude, force_exclude, report, gitignore, diff --git a/tests/test_black.py b/tests/test_black.py index 5d14ceda8f4..b0510e55068 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1337,7 +1337,14 @@ def test_include_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1361,6 +1368,7 @@ def test_exclude_for_issue_1572(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1383,6 +1391,7 @@ def test_get_sources_with_stdin(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1406,6 +1415,7 @@ def test_get_sources_with_stdin_filename(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1433,6 +1443,35 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None: verbose=False, include=include, exclude=exclude, + extend_exclude=None, + force_exclude=None, + report=report, + stdin_filename=stdin_filename, + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: + # Extend exclude shouldn't exclude stdin_filename since it is mimicing the + # file being passed directly. This is the same as + # test_exclude_for_issue_1572 + path = THIS_DIR / "data" / "include_exclude_tests" + include = "" + extend_exclude = r"/exclude/|a\.py" + src = "-" + report = black.Report() + stdin_filename = str(path / "b/exclude/a.py") + expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")] + sources = list( + black.get_sources( + ctx=FakeContext(), + src=(src,), + quiet=True, + verbose=False, + include=include, + exclude="", + extend_exclude=None, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1458,6 +1497,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None: verbose=False, include=include, exclude="", + extend_exclude=None, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, @@ -1542,7 +1582,14 @@ def test_gitignore_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, exclude, None, report, gitignore + path.iterdir(), + this_abs, + include, + exclude, + None, + None, + report, + gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1572,25 +1619,21 @@ def test_empty_include(self) -> None: empty, re.compile(black.DEFAULT_EXCLUDES), None, + None, report, gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) - def test_empty_exclude(self) -> None: + def test_extend_exclude(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report() gitignore = PathSpec.from_lines("gitwildmatch", []) - empty = re.compile(r"") sources: List[Path] = [] expected = [ - Path(path / "b/dont_exclude/a.py"), - Path(path / "b/dont_exclude/a.pyi"), Path(path / "b/exclude/a.py"), - Path(path / "b/exclude/a.pyi"), - Path(path / "b/.definitely_exclude/a.py"), - Path(path / "b/.definitely_exclude/a.pyi"), + Path(path / "b/dont_exclude/a.py"), ] this_abs = THIS_DIR.resolve() sources.extend( @@ -1598,7 +1641,8 @@ def test_empty_exclude(self) -> None: path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - empty, + re.compile(r"\.pyi$"), + re.compile(r"\.definitely_exclude"), None, report, gitignore, @@ -1606,8 +1650,8 @@ def test_empty_exclude(self) -> None: ) self.assertEqual(sorted(expected), sorted(sources)) - def test_invalid_include_exclude(self) -> None: - for option in ["--include", "--exclude"]: + def test_invalid_cli_regex(self) -> None: + for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]: self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2) def test_preserves_line_endings(self) -> None: @@ -1656,7 +1700,14 @@ def test_symlink_out_of_root_directory(self) -> None: try: list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, ) ) except ValueError as ve: @@ -1670,7 +1721,14 @@ def test_symlink_out_of_root_directory(self) -> None: with self.assertRaises(ValueError): list( black.gen_python_files( - path.iterdir(), root, include, exclude, None, report, gitignore + path.iterdir(), + root, + include, + exclude, + None, + None, + report, + gitignore, ) ) path.iterdir.assert_called() From ab9345ca5b1801073b2fb6a2f4a54523df25d0ec Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 24 Feb 2021 15:45:38 -0600 Subject: [PATCH 2/9] Fix flake8 issues --- src/black/__init__.py | 9 +++++---- tests/test_black.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6d597520e93..8f6e3949600 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -618,7 +618,7 @@ def test_regex( regex: Optional[str], ) -> Optional[Pattern]: try: - return re_compile_maybe_verbose(regex) if regex else None + return re_compile_maybe_verbose(regex) if regex is not None else None except re.error: err(f"Invalid regular expression for {regex_name} given: {regex!r}") ctx.exit(2) @@ -641,6 +641,7 @@ def get_sources( include_regex = test_regex(ctx, "include", include) exclude_regex = test_regex(ctx, "exclude", exclude) + assert exclude_regex is not None extend_exclude_regex = test_regex(ctx, "extend_exclude", extend_exclude) force_exclude_regex = test_regex(ctx, "force_exclude", force_exclude) @@ -6124,10 +6125,10 @@ def normalize_path_maybe_ignore( def path_is_excluded( normalized_path: str, - pattern: Pattern[str], -): + pattern: Optional[Pattern[str]], +) -> bool: match = pattern.search(normalized_path) if pattern else None - return match and match.group(0) + return bool(match and match.group(0)) def gen_python_files( diff --git a/tests/test_black.py b/tests/test_black.py index b0510e55068..dbfb1622a4c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1471,7 +1471,7 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: verbose=False, include=include, exclude="", - extend_exclude=None, + extend_exclude=extend_exclude, force_exclude=None, report=report, stdin_filename=stdin_filename, From 0a8db152719670b80bc89367c8d1c60b70da514d Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Sun, 28 Feb 2021 15:02:24 -0600 Subject: [PATCH 3/9] Look ma! I contribute to open source! --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df21d2b1f00..98890d721de 100644 --- a/README.md +++ b/README.md @@ -606,7 +606,8 @@ Multiple contributions by: - [Jose Nazario](mailto:jose.monkey.org@gmail.com) - [Joseph Larson](mailto:larson.joseph@gmail.com) - [Josh Bode](mailto:joshbode@fastmail.com) -- [Josh Holland](mailto:anowlcalledjosh@gmail.com) +- [Josh Bode](mailto:joshbode@fastmail.com) +- [Joshua Cannon](mailto:joshdcannon@gmail.com) - [José Padilla](mailto:jpadilla@webapplicate.com) - [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) - [kaiix](mailto:kvn.hou@gmail.com) From 5adb59000ac542992ef042b6e5e3bc45d4a38e91 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Sun, 28 Feb 2021 15:02:30 -0600 Subject: [PATCH 4/9] Update tests/test_black.py Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index dbfb1622a4c..7717a70be5d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1453,7 +1453,7 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None: @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None: - # Extend exclude shouldn't exclude stdin_filename since it is mimicing the + # Extend exclude shouldn't exclude stdin_filename since it is mimicking the # file being passed directly. This is the same as # test_exclude_for_issue_1572 path = THIS_DIR / "data" / "include_exclude_tests" From f2e834dd1fda42203b177645fbdca83c2cf8a3af Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Sun, 28 Feb 2021 15:02:45 -0600 Subject: [PATCH 5/9] Update README.md Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df21d2b1f00..bf13fb707f6 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,8 @@ Options: --extend-exclude TEXT Like --exclude, but adds additional files and directories on top of the excluded - ones. (Useful if you simply want to add to - the default) + ones (useful if you simply want to add to + the default). --force-exclude TEXT Like --exclude, but files and directories matching this regex will be excluded even From 2ba5cb88bcceb5780d08016e1fe6cd948597c360 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Sun, 28 Feb 2021 15:10:03 -0600 Subject: [PATCH 6/9] Update references to --exclude --- README.md | 4 ++-- docs/installation_and_usage.md | 4 ++-- docs/pyproject_toml.md | 2 +- src/black/__init__.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fdcc2bb3d55..64638f92bc8 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Options: -v, --verbose Also emit messages to stderr about files that were not changed or were ignored due to - --exclude=. + exclusion patterns. --version Show the version and exit. --config FILE Read configuration from FILE path. @@ -269,7 +269,7 @@ above. What seems like a bug might be intended behaviour. _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude` patterns for your project. +`--include` and `--exclude`/`--extend-exclude` patterns for your project. **Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is "No". _Black_ is all about sensible defaults. diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index 5b4adb547ec..bb554eb6744 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -97,7 +97,7 @@ Options: --extend-exclude TEXT Like --exclude, but adds additional files and directories on top of the excluded - ones. (Useful if you simply want to add to + ones. (useful if you simply want to add to the default) --stdin-filename TEXT The name of the file when passing it through @@ -111,7 +111,7 @@ Options: -v, --verbose Also emit messages to stderr about files that were not changed or were ignored due to - --exclude=. + exclusion patterns. --version Show the version and exit. --config FILE Read configuration from FILE path. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index e71185cc036..96883bbe325 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -4,7 +4,7 @@ _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude` patterns for your project. +`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your project. **Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is "No". _Black_ is all about sensible defaults. diff --git a/src/black/__init__.py b/src/black/__init__.py index 8f6e3949600..15e2af3ceb3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -501,7 +501,7 @@ def target_version_option_callback( is_flag=True, help=( "Also emit messages to stderr about files that were not changed or were ignored" - " due to --exclude=." + " due to exclusion patterns." ), ) @click.version_option(version=__version__) From 94debf8054abbf111c5d9eb16983663b7ec683d4 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Sun, 28 Feb 2021 15:13:53 -0600 Subject: [PATCH 7/9] Update pyproject_toml.md --- docs/pyproject_toml.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 96883bbe325..9acc4c03d7c 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -4,7 +4,8 @@ _Black_ is able to read project-specific default values for its command line options from a `pyproject.toml` file. This is especially useful for specifying custom -`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your project. +`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your +project. **Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is "No". _Black_ is all about sensible defaults. From 63e783b1bfad9009875f73ad48806046a918bafd Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 1 Mar 2021 11:38:58 -0600 Subject: [PATCH 8/9] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 64638f92bc8..dadf9192bb7 100644 --- a/README.md +++ b/README.md @@ -607,6 +607,7 @@ Multiple contributions by: - [Joseph Larson](mailto:larson.joseph@gmail.com) - [Josh Bode](mailto:joshbode@fastmail.com) - [Josh Bode](mailto:joshbode@fastmail.com) +- [Josh Holland](mailto:anowlcalledjosh@gmail.com) - [Joshua Cannon](mailto:joshdcannon@gmail.com) - [José Padilla](mailto:jpadilla@webapplicate.com) - [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) From 3a8f4beee1de321d12f7fdc4dd39dd808673bc6a Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 1 Mar 2021 11:39:20 -0600 Subject: [PATCH 9/9] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index dadf9192bb7..5f8b52cb823 100644 --- a/README.md +++ b/README.md @@ -606,7 +606,6 @@ Multiple contributions by: - [Jose Nazario](mailto:jose.monkey.org@gmail.com) - [Joseph Larson](mailto:larson.joseph@gmail.com) - [Josh Bode](mailto:joshbode@fastmail.com) -- [Josh Bode](mailto:joshbode@fastmail.com) - [Josh Holland](mailto:anowlcalledjosh@gmail.com) - [Joshua Cannon](mailto:joshdcannon@gmail.com) - [José Padilla](mailto:jpadilla@webapplicate.com)