diff --git a/README.md b/README.md index 03ef84d..a927cd1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Looking for a high-level overview of OpenType table differences rather than low- - Display the first n lines of the diff output with the `--head` option - Display the last n lines of the diff output with the `--tail` option - Execute the diff with an external diff tool using the `--external` option +- Use in Git as a diff-driver to be the default diff tool for fonts Run `fdiff --help` to view all available options. @@ -103,6 +104,34 @@ $ fdiff [OPTIONS] [PRE-FONT FILE URL] [POST-FONT FILE FILE PATH] ⭐ **Tip**: Remote git repository hosting services (like Github) support access to files on different git branches by URL. Use these repository branch URL to compare fonts across git branches in your repository. +#### As Git's diff driver for fonts + +Git can be configured to automatically use a specific tool to diff specific file types. These are the steps to use `fdiff` as the default diff output for font files. + +1. Tell Git that the tool is available and how to run it. As with most git configuration options this may be set for a repository (`--local`), for your user (`--global`), or system (`--system`). We recommend setting this at the repository or user level. Assuming `fdiff` is available in your path, this setting should do the trick: + + # Repository level + git config --local diff.fdiff.command 'fdiff -c --git' + + # User level + git config --global diff.fdiff.command 'fdiff -c --git' + + This will write something like the following to either the current repository's `.git/config` or your user's `$HOME/.gitconfig` file looking like this: + + ```gitconfig + [diff "fdiff"] + command = fdiff -c --git + ``` + + Of course you may also edit the appropriate file and place that configuration manually. + +2. Tell Git to actually use that specific tool for supported file types. This may also be done at multiple places. Each repository may have it's own `.gitattributes` file, a user may have one setting global defaults in `$XDG_HOME/git/attributes`, or there may be a system wide default file. Wherever you choose to place this, the lines are the same: + + ```gitattributes + *.otf diff=fdiff + *.ttf diff=fdiff + ``` + ### Options #### Color diffs diff --git a/lib/fdiff/__main__.py b/lib/fdiff/__main__.py index f413148..60de656 100644 --- a/lib/fdiff/__main__.py +++ b/lib/fdiff/__main__.py @@ -8,7 +8,7 @@ from fdiff.color import color_unified_diff_line from fdiff.diff import external_diff, u_diff from fdiff.textiter import head, tail -from fdiff.utils import file_exists, get_tables_argument_list +from fdiff.utils import path_exists, get_tables_argument_list def main(): # pragma: no cover @@ -45,13 +45,14 @@ def run(argv): parser.add_argument( "-l", "--lines", type=int, default=3, help="Number of context lines (default 3)" ) - parser.add_argument( + filters = parser.add_mutually_exclusive_group() + filters.add_argument( "--include", type=str, default=None, help="Comma separated list of tables to include", ) - parser.add_argument( + filters.add_argument( "--exclude", type=str, default=None, @@ -63,10 +64,43 @@ def run(argv): "--nomp", action="store_true", help="Do not use multi process optimizations" ) parser.add_argument("--external", type=str, help="Run external diff tool command") - parser.add_argument("PREFILE", help="Font file path/URL 1") - parser.add_argument("POSTFILE", help="Font file path/URL 2") - args = parser.parse_args(argv) + parser.add_argument( + "--git", + type=str, + nargs=7, + help="Act as a diff driver for git (takes 7 parameters)", + metavar=( + "PATH", + "OLD-FILE", + "OLD-HEX", + "OLD-MODE", + "NEW-FILE", + "NEW-HEX", + "NEW-MODE", + ), + ) + # parser.add_argument("PREFILE", help="Font file path/URL 1") + # parser.add_argument("POSTFILE", help="Font file path/URL 2") + + args, positionals = parser.parse_known_args(argv) + + inputs = argparse.Namespace() + include_dir_paths = False + if args.git: + inputs.PREFILE = args.git[1] + inputs.POSTFILE = args.git[4] + # If the --git flag is used, we need to accept arguments + # that do not meet the definition of a file path using + # os.path.exists instead of os.path.isfile + # See https://github.com/source-foundry/fdiff/pull/48#discussion_r410424497 + # for additional details + include_dir_paths = True + else: + inputparser = argparse.ArgumentParser() + inputparser.add_argument("PREFILE", help="Font file path/URL 1") + inputparser.add_argument("POSTFILE", help="Font file path/URL 2") + inputparser.parse_args(positionals, namespace=inputs) # ///////////////////////////////////////////////////////// # @@ -74,29 +108,22 @@ def run(argv): # # ///////////////////////////////////////////////////////// - # ---------------------------------- - # Incompatible argument validations - # ---------------------------------- - # --include and --exclude are mutually exclusive options - if args.include and args.exclude: - sys.stderr.write( - f"[*] Error: --include and --exclude are mutually exclusive options. " - f"Please use ONLY one of these options in your command.{os.linesep}" - ) - sys.exit(1) - # ------------------------------- # File path argument validations # ------------------------------- - if not args.PREFILE.startswith("http") and not file_exists(args.PREFILE): + if not inputs.PREFILE.startswith("http") and not path_exists( + inputs.PREFILE, include_dir_paths=include_dir_paths + ): sys.stderr.write( - f"[*] ERROR: The file path '{args.PREFILE}' can not be found.{os.linesep}" + f"[*] ERROR: The file path '{inputs.PREFILE}' can not be found.{os.linesep}" ) sys.exit(1) - if not args.PREFILE.startswith("http") and not file_exists(args.POSTFILE): + if not inputs.POSTFILE.startswith("http") and not path_exists( + inputs.POSTFILE, include_dir_paths=include_dir_paths + ): sys.stderr.write( - f"[*] ERROR: The file path '{args.POSTFILE}' can not be found.{os.linesep}" + f"[*] ERROR: The file path '{inputs.POSTFILE}' can not be found.{os.linesep}" ) sys.exit(1) @@ -138,8 +165,8 @@ def run(argv): try: diff = external_diff( args.external, - args.PREFILE, - args.POSTFILE, + inputs.PREFILE, + inputs.POSTFILE, include_tables=include_list, exclude_tables=exclude_list, use_multiprocess=use_mp, @@ -164,8 +191,8 @@ def run(argv): # perform the unified diff analysis try: diff = u_diff( - args.PREFILE, - args.POSTFILE, + inputs.PREFILE, + inputs.POSTFILE, context_lines=args.lines, include_tables=include_list, exclude_tables=exclude_list, diff --git a/lib/fdiff/utils.py b/lib/fdiff/utils.py index 3b2aec6..6edfbaa 100644 --- a/lib/fdiff/utils.py +++ b/lib/fdiff/utils.py @@ -5,9 +5,13 @@ from datetime import datetime, timezone -def file_exists(path): - """Validates file path as existing local file""" - return os.path.isfile(path) +def path_exists(path, include_dir_paths=False): + """Validates existing paths. The include_dir_paths parameter + toggles acceptance of dir paths in addition to file paths.""" + if include_dir_paths: + return os.path.exists(path) + else: + return os.path.isfile(path) def get_file_modtime(path): diff --git a/tests/test_main.py b/tests/test_main.py index 999d0dc..bb24709 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,12 +9,24 @@ ROBOTO_BEFORE_PATH = os.path.join("tests", "testfiles", "Roboto-Regular.subset1.ttf") ROBOTO_AFTER_PATH = os.path.join("tests", "testfiles", "Roboto-Regular.subset2.ttf") -ROBOTO_UDIFF_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_expected.txt") -ROBOTO_UDIFF_COLOR_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_color_expected.txt") -ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_1context_expected.txt") -ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headonly_expected.txt") -ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headpostonly_expected.txt") -ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_ex_headpost_expected.txt") +ROBOTO_UDIFF_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_expected.txt" +) +ROBOTO_UDIFF_COLOR_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_color_expected.txt" +) +ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_1context_expected.txt" +) +ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_headonly_expected.txt" +) +ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_headpostonly_expected.txt" +) +ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH = os.path.join( + "tests", "testfiles", "roboto_udiff_ex_headpost_expected.txt" +) ROBOTO_BEFORE_URL = "https://github.com/source-foundry/fdiff/raw/master/tests/testfiles/Roboto-Regular.subset1.ttf" ROBOTO_AFTER_URL = "https://github.com/source-foundry/fdiff/raw/master/tests/testfiles/Roboto-Regular.subset2.ttf" @@ -80,21 +92,32 @@ def test_main_filepath_validations_false_secondfont(capsys): # Mutually exclusive argument tests # + def test_main_include_exclude_defined_simultaneously(capsys): - args = ["--include", "head", "--exclude", "head", ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH] + args = [ + "--include", + "head", + "--exclude", + "head", + ROBOTO_BEFORE_PATH, + ROBOTO_AFTER_PATH, + ] with pytest.raises(SystemExit) as exit_info: run(args) captured = capsys.readouterr() - assert captured.err.startswith("[*] Error: --include and --exclude are mutually exclusive options") - assert exit_info.value.code == 1 + assert captured.err.endswith( + "error: argument --exclude: not allowed with argument --include\n" + ) + assert exit_info.value.code == 2 # # Unified diff integration tests # + def test_main_run_unified_default_local_files_no_diff(capsys): """Test default behavior when there is no difference in font files under evaluation""" args = [ROBOTO_BEFORE_PATH, ROBOTO_BEFORE_PATH] diff --git a/tests/test_utils.py b/tests/test_utils.py index 8927172..690a307 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,17 +1,40 @@ import os import re -from fdiff.utils import get_file_modtime, get_tables_argument_list, file_exists +from fdiff.utils import get_file_modtime, get_tables_argument_list, path_exists import pytest -def test_file_exists_true(): - assert file_exists(os.path.join("tests", "testfiles", "test.txt")) is True +def test_path_exists_default_true(): + assert ( + path_exists( + os.path.join("tests", "testfiles", "test.txt"), include_dir_paths=False + ) + is True + ) -def test_file_exists_false(): - assert file_exists(os.path.join("tests", "testfiles", "bogus.jpg")) is False +def test_path_exists_default_false(): + assert ( + path_exists( + os.path.join("tests", "testfiles", "bogus.jpg"), include_dir_paths=False + ) + is False + ) + + +def test_path_exists_default_dirpath_fails(): + assert ( + path_exists(os.path.join("tests", "testfiles"), include_dir_paths=False) + is False + ) + + +def test_path_exists_default_dirpath_toggle_succeeds(): + assert ( + path_exists(os.path.join("tests", "testfiles"), include_dir_paths=True) is True + ) def test_get_file_modtime(): diff --git a/tests/test_utils_unix_only.py b/tests/test_utils_unix_only.py new file mode 100644 index 0000000..2304172 --- /dev/null +++ b/tests/test_utils_unix_only.py @@ -0,0 +1,13 @@ +import os +import sys + +import pytest + +from fdiff.utils import path_exists + +if sys.platform.startswith("win"): + pytest.skip("skipping Unix only tests", allow_module_level=True) + + +def test_path_exists_default_dirpath_toggle_succeeds(): + assert path_exists("/dev/null", include_dir_paths=True) is True