Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/intentumdiff/_differ_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,12 @@ def _token_fallback_diff(
changes.append(
Change(
change_type=ChangeType.MODIFICATION,
description=f"token-level fallback: {i2 - i1} token(s) → {j2 - j1} token(s)",
# ASCII arrow deliberately. A Windows console defaults to cp1252, which
# cannot encode U+2192, and Rich raised UnicodeEncodeError while rendering
# the row — so the fallback path printed "Error: 'charmap' codec can't
# encode character" INSIDE the results table, on exactly the files that had
# already failed to parse.
description=f"token-level fallback: {i2 - i1} token(s) -> {j2 - j1} token(s)",
confidence=0.5,
)
)
Expand Down
33 changes: 33 additions & 0 deletions src/intentumdiff/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,40 @@
]


#: The pre-rebrand distribution. Installing IntentumDiff does not remove it, because pip
#: treats a renamed project as an unrelated package.
_RETIRED_DISTRIBUTION = "intentdiff"
Comment on lines +194 to +196

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the actual legacy distribution before warning

When the unrelated bare intentdiff project is installed, this lookup emits a false collision warning and directs the user to uninstall that package. The repository’s released pre-rebrand package was named intentdiff-python and installed the intentdiff import package, whereas the current distribution installs intentumdiff; therefore the checked distribution neither represents the retired release nor shares this import package. Remove this warning or base collision detection on distributions that actually own intentumdiff.

Useful? React with 👍 / 👎.



def _warn_if_retired_distribution_installed() -> None:
"""
Warn when the pre-rebrand ``intentdiff`` distribution is installed alongside this one.

Both projects install the same ``intentumdiff`` import package, so whichever pip laid down
last wins and the resolved distribution name may be the retired one — which is NOT in the
first-party trust allowlist. Every bundled parser is then rejected as untrusted third-party
code, and the user sees a stream of ``native_fallback`` errors and no diff at all.

Nothing in that chain names the actual cause, and the failure is total rather than partial,
so it reads as "the tool is broken" rather than "you have two installs". This is the same
defect class that made 0.0.1 unusable: a package failing its own trust check because of how
its distribution name resolves.
"""
try:
from importlib.metadata import distribution

distribution(_RETIRED_DISTRIBUTION)
except Exception: # noqa: BLE001 - absence is the normal case, and any lookup failure is fine
return
_err.print(
f"[yellow]Warning:[/yellow] the retired '{_RETIRED_DISTRIBUTION}' distribution is "
"installed alongside IntentumDiff. They share an import package, so parsers may be "
f"rejected as untrusted and diffs may fail. Run: pip uninstall {_RETIRED_DISTRIBUTION}"
)


def main(argv: list[str] | None = None) -> NoReturn:
_warn_if_retired_distribution_installed()
_legacy_click_main(argv)


Expand Down
17 changes: 16 additions & 1 deletion src/intentumdiff/cli/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,11 @@ def _cmd_plugins_list(_args: argparse.Namespace) -> None:

# Resolve the core package provenance string once for built-in rows
import importlib.metadata as _meta
for _dist_name in ("intentumdiff",):
# The DISTRIBUTION name, which is not the import name: this package publishes as
# `intentumdiff-python` while `import intentumdiff` is the package. Looking up only the
# import name silently fell through to the bare "IntentumDiff" fallback, so every
# built-in plugin row lost its version.
for _dist_name in ("intentumdiff-python", "intentumdiff_python", "intentumdiff"):
try:
_core_dist = _meta.distribution(_dist_name)
_core_prov = (
Expand Down Expand Up @@ -1939,6 +1943,17 @@ def _add_output_args(p: argparse.ArgumentParser) -> None:
metavar="FORMAT",
help="Output format: terminal (default), json, patch, html, llm",
)
# `--json` is the spelling people reach for first, and reaching for it used to fail with
# "unrecognized arguments: --json" even though JSON output existed the whole time behind
# `--format json`. An error that denies a feature you actually ship is worse than a missing
# feature, because it teaches the user the tool cannot do it.
p.add_argument(
"--json",
dest="format",
action="store_const",
const="json",
help="Shorthand for --format json.",
)
p.add_argument(
"--output", "-o",
metavar="FILE",
Expand Down
12 changes: 11 additions & 1 deletion src/intentumdiff/cli/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ def _build_parser() -> argparse.ArgumentParser:
assets_git_p.set_defaults(func=_cmd_assets_git)

# ── file ─────────────────────────────────────────────────────────────────
file_p = sub.add_parser("file", help="Diff two local files.")
# `diff` is the first thing anyone types, and it used to fail with
# "No such command 'diff'. Did you mean 'gist-diff'?" — a suggestion that points away from
# the two commands that actually diff things. Aliasing it to `file` costs nothing and
# removes a dead end from the very first interaction with the tool.
file_p = sub.add_parser(
"file", aliases=["diff"], help="Diff two local files (alias: diff)."
)
file_p.add_argument("old_file", metavar="OLD", help="Path to the old file.")
file_p.add_argument("new_file", metavar="NEW", help="Path to the new file.")
_add_output_args(file_p)
Expand Down Expand Up @@ -995,6 +1001,10 @@ def _click_cli(ctx: click.Context, no_banner: bool) -> None:
("git", "Diff files or commits in a git repository."),
("assets", "Perceptual diffs for non-text assets."),
("file", "Diff two local files."),
# Registered here as well as aliased on the argparse parser: this Click group is what
# rejects unknown commands, so without an entry here `diff` still fails before argparse
# is ever consulted — and fails suggesting `gist-diff`.
("diff", "Diff two local files (alias for 'file')."),
("patch", "Diff from a unified diff patch."),
("string", "Diff two in-memory strings."),
("github-pr", "Parse a GitHub pull request URL into a review target."),
Expand Down
2 changes: 1 addition & 1 deletion src/intentumdiff/differ.py
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,7 @@ def _run_stages_1_to_11(
_has_error_node(old_tree) or _has_error_node(new_tree)
):
logger.warning(
"Parse errors detected in %r falling back to token-level diff",
"Parse errors detected in %r - falling back to token-level diff",
filename,
)
_raise_rust_only_gate_error("parse errors require Rust token-level fallback")
Expand Down
Loading