Skip to content

fix(diff): make --parent default to current version and skip non-meaningful ancestors#10526

Open
davidfirst wants to merge 2 commits into
masterfrom
fix-diff-parent-flag
Open

fix(diff): make --parent default to current version and skip non-meaningful ancestors#10526
davidfirst wants to merge 2 commits into
masterfrom
fix-diff-parent-flag

Conversation

@davidfirst

Copy link
Copy Markdown
Member

Fixes several issues with bit diff --parent:

  • when the parent is a snap, the diff silently fell back to a wrong comparison and showed "no diff" (a tag name was looked up for the parent ref; snaps have none).
  • --parent without a version threw an error; it now defaults to the component's current version.
  • when a tag is created by tag-from-scope (_tag) on a merged lane snap, the tag is identical to that snap, so diffing against the immediate parent showed "no diff". The diff now walks up the history, skipping hidden ancestors and snap ancestors with identical content (the latter covers releases with rebuilt artifacts, where the merged snap is not marked hidden). A tag ancestor is never skipped by content, so a legit tag created with --unmodified still shows no diff.
  • --parent on the first version now shows all files as added.

Added e2e coverage for all the above.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix bit diff --parent defaulting and skip non-meaningful ancestors

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Make --parent default to the component’s current version when omitted.
• Fix parent resolution for snaps and first-version diffs (show all files added).
• Skip hidden/identical snap ancestors so --parent compares against a meaningful baseline.
Diagram

graph TD
  U["User: bit diff"] --> C["diff-cmd (CLI)"] --> M["ComponentCompareMain"] --> W{"--parent?"}
  W -->|"yes"| P["Parent selection"] --> I["Scope importer"] --> R[("Model repo")]
  P --> D["Compute diff"]
  W -->|"no"| D --> O["Diff output"]

  subgraph Legend
    direction LR
    _u[User] ~~~ _cmd[CLI command] ~~~ _svc[Service/logic] ~~~ _db[(Repository/store)] ~~~ _dec{Decision}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Move parent-walk logic into ModelComponent/Version APIs
  • ➕ Centralizes ancestry semantics (hidden/identical/tag-aware) for reuse across features
  • ➕ Reduces command-layer complexity and makes behavior easier to test in isolation
  • ➖ Larger refactor surface area and potential ripple effects for other consumers
  • ➖ Requires careful API design to avoid leaking CLI-specific policy
2. Use cheap content hashes to skip identical ancestors (avoid full diffs)
  • ➕ Potentially faster than computing diffs repeatedly while walking ancestors
  • ➕ Clearer intent: compare snapshots by fingerprint, not by diff output
  • ➖ Requires reliable hash definition (files + metadata) aligned with what diff considers meaningful
  • ➖ Adds/depends on additional stored metadata or recalculation cost
3. Add an explicit flag (e.g. `--skip-identical-ancestors`) instead of default behavior
  • ➕ Avoids surprising behavior changes for users expecting the immediate parent only
  • ➕ Keeps default --parent semantics simpler and more predictable
  • ➖ More CLI surface area and documentation burden
  • ➖ Doesn’t fix the existing ‘no diff’ UX for common merge/tag-from-scope flows unless users discover the flag

Recommendation: The PR’s approach is appropriate for improving --parent usability in common lane-merge/tag-from-scope scenarios, and the tag-aware rule (never skipping tag ancestors by content) preserves expected semantics for legitimate no-change tags. If performance becomes an issue on deep histories, consider replacing the repeated diff calls during ancestor-walk with a hash/fingerprint check.

Files changed (3) +131 / -4

Bug fix (1) +39 / -3
component-compare.main.runtime.tsDefault '--parent' to current version and walk meaningful ancestors +39/-3

Default '--parent' to current version and walk meaningful ancestors

• Fixes '--parent' to default to 'component.id.version' when no version is provided and handles first-version comparisons by treating all files as added. Reworks parent resolution to traverse ancestors, skipping hidden versions and snap ancestors with identical content, while never skipping tag ancestors by content to preserve legitimate no-diff tags.

scopes/component/component-compare/component-compare.main.runtime.ts

Tests (1) +83 / -0
diff.e2e.tsAdd e2e coverage for 'diff --parent' edge cases +83/-0

Add e2e coverage for 'diff --parent' edge cases

• Adds a dedicated test suite validating '--parent' behavior for tags, snaps, missing version defaulting, first-version diffs, invalid argument combinations, and skipping hidden/identical snap ancestors while preserving tag no-diff semantics.

e2e/commands/diff.e2e.ts

Documentation (1) +9 / -1
diff-cmd.tsClarify '--parent' help text and add example for omitted version +9/-1

Clarify '--parent' help text and add example for omitted version

• Updates the CLI option description to reflect defaulting to the current version and adds an explicit usage example ('diff foo --parent').

scopes/component/component-compare/diff-cmd.ts

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Default version not imported 🐞 Bug ☼ Reliability
Description
In ComponentCompareMain.computeDiff(), the initial importWithoutDeps() runs before --parent
defaults version to component.id.version, so loadVersion(version) can run without importing
that resolved version first. If the current version object isn’t already present in the local object
store, modelComponent.loadVersion() will throw and bit diff <comp> --parent will fail.
Code

scopes/component/component-compare/component-compare.main.runtime.ts[R239-245]

    const idList = ComponentIdList.fromArray(idsToImport);
    await this.scope.legacyScope.scopeImporter.importWithoutDeps(idList, { cache: true, reason: 'to show diff' });
    if (diffOpts.compareToParent) {
-      if (!version) throw new BitError('--parent flag expects to get version');
      if (toVersion) throw new BitError('--parent flag expects to get only one version');
+      if (!version) version = component.id.version;
      const versionObject = await modelComponent.loadVersion(version, repository);
-      const parent = versionObject.parents[0];
+      let parentRef = versionObject.parents[0];
Evidence
computeDiff() constructs idsToImport only from the CLI-provided version/toVersion, calls
importWithoutDeps(), and only then defaults version for --parent and immediately calls
loadVersion(version). ModelComponent.loadVersion() throws if the ref/object isn’t available in
the repository.

scopes/component/component-compare/component-compare.main.runtime.ts[235-245]
scopes/scope/objects/models/model-component.ts[908-913]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`computeDiff()` imports `idsToImport` before defaulting `version` for `--parent`. When `--parent` is used without an explicit version, the resolved `version` may not be imported, and `modelComponent.loadVersion(version, repository)` can throw if the object is missing locally.

### Issue Context
`ModelComponent.loadVersion()` throws when the ref/object can’t be found/loaded from the repository. Because the defaulting happens after the initial import step, the resolved version is not guaranteed to exist locally.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[235-245]
- scopes/scope/objects/models/model-component.ts[908-913]

### Suggested fix
- Resolve the effective `version`/`toVersion` (including the `--parent` defaulting) **before** building `idsToImport` and calling `importWithoutDeps()`.
 - Alternatively, after `if (!version) version = component.id.version;` add an `importWithoutDeps()` call for `component.id.changeVersion(version)` before calling `loadVersion(version, ...)`.
- Keep the existing per-parent import inside the ancestor loop as-is (or batch import if desired), but ensure the initially diffed `toVersion` is present first.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Repeated parent diff computation 🐞 Bug ➹ Performance
Description
The new --parent ancestor walk can compute a full diff against multiple successive ancestors (load
versions, materialize files, run getFilesDiff) before finding a meaningful parent. On components
with long chains of non-hidden, untagged ancestors with identical content, this can make `bit diff
--parent` unexpectedly slow.
Code

scopes/component/component-compare/component-compare.main.runtime.ts[R261-283]

+      while (parentRef) {
+        const parentTag = modelComponent.getTagOfRefIfExists(parentRef);
+        const parentVersion: string = parentTag || parentRef.toString();
+        await this.scope.legacyScope.scopeImporter.importWithoutDeps(
+          ComponentIdList.fromArray([component.id.changeVersion(parentVersion)]),
+          { cache: true, reason: 'to show diff' }
+        );
+        const parentObject = await modelComponent.loadVersion(parentVersion, repository);
+        version = parentVersion;
+        if (!parentObject.hidden) {
+          if (parentTag) break;
+          const parentDiff = await this.diffBetweenVersionsObjects(
+            modelComponent,
+            parentObject,
+            versionObject,
+            parentVersion,
+            toVersion,
+            diffOpts
+          );
+          if (parentDiff.hasDiff) break;
+        }
+        parentRef = parentObject.parents[0];
+      }
Evidence
The loop calls diffBetweenVersionsObjects() to detect whether an ancestor has meaningful changes,
and that function generates file diffs via getFilesDiff(), which writes temp files and runs diff
for each file path. Repeating this per ancestor can be costly in worst-case histories.

scopes/component/component-compare/component-compare.main.runtime.ts[253-283]
scopes/component/component-compare/component-compare.main.runtime.ts[319-352]
components/legacy/component-diff/components-diff.ts[60-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `--parent` walk-up loop may invoke `diffBetweenVersionsObjects()` multiple times, doing full file diff generation (temp writes + git diff) per ancestor just to answer the yes/no question “is content identical?”.

### Issue Context
This is only triggered when:
- the ancestor is not hidden,
- the ancestor is a snap (no tag), and
- the content is identical across multiple ancestors.
In that shape of history, the loop performs repeated expensive work.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[253-283]
- scopes/component/component-compare/component-compare.main.runtime.ts[319-352]
- components/legacy/component-diff/components-diff.ts[60-101]

### Suggested fix
- Add a lightweight “content equality” check for the ancestor-walk decision:
 - e.g., compare per-file hashes and paths from `modelFilesToSourceFiles()` (without calling `getOneFileDiff()` / `git diff`) to decide if the parent is identical.
 - Only when a non-identical ancestor is found, compute the full diff once for output.
- Keep the “never skip tag ancestor by content” rule by applying the fast equality check only when `parentTag` is falsy.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 239 to +245
const idList = ComponentIdList.fromArray(idsToImport);
await this.scope.legacyScope.scopeImporter.importWithoutDeps(idList, { cache: true, reason: 'to show diff' });
if (diffOpts.compareToParent) {
if (!version) throw new BitError('--parent flag expects to get version');
if (toVersion) throw new BitError('--parent flag expects to get only one version');
if (!version) version = component.id.version;
const versionObject = await modelComponent.loadVersion(version, repository);
const parent = versionObject.parents[0];
let parentRef = versionObject.parents[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Default version not imported 🐞 Bug ☼ Reliability

In ComponentCompareMain.computeDiff(), the initial importWithoutDeps() runs before --parent
defaults version to component.id.version, so loadVersion(version) can run without importing
that resolved version first. If the current version object isn’t already present in the local object
store, modelComponent.loadVersion() will throw and bit diff <comp> --parent will fail.
Agent Prompt
### Issue description
`computeDiff()` imports `idsToImport` before defaulting `version` for `--parent`. When `--parent` is used without an explicit version, the resolved `version` may not be imported, and `modelComponent.loadVersion(version, repository)` can throw if the object is missing locally.

### Issue Context
`ModelComponent.loadVersion()` throws when the ref/object can’t be found/loaded from the repository. Because the defaulting happens after the initial import step, the resolved version is not guaranteed to exist locally.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[235-245]
- scopes/scope/objects/models/model-component.ts[908-913]

### Suggested fix
- Resolve the effective `version`/`toVersion` (including the `--parent` defaulting) **before** building `idsToImport` and calling `importWithoutDeps()`.
  - Alternatively, after `if (!version) version = component.id.version;` add an `importWithoutDeps()` call for `component.id.changeVersion(version)` before calling `loadVersion(version, ...)`.
- Keep the existing per-parent import inside the ancestor loop as-is (or batch import if desired), but ensure the initially diffed `toVersion` is present first.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +261 to +283
while (parentRef) {
const parentTag = modelComponent.getTagOfRefIfExists(parentRef);
const parentVersion: string = parentTag || parentRef.toString();
await this.scope.legacyScope.scopeImporter.importWithoutDeps(
ComponentIdList.fromArray([component.id.changeVersion(parentVersion)]),
{ cache: true, reason: 'to show diff' }
);
const parentObject = await modelComponent.loadVersion(parentVersion, repository);
version = parentVersion;
if (!parentObject.hidden) {
if (parentTag) break;
const parentDiff = await this.diffBetweenVersionsObjects(
modelComponent,
parentObject,
versionObject,
parentVersion,
toVersion,
diffOpts
);
if (parentDiff.hasDiff) break;
}
parentRef = parentObject.parents[0];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Repeated parent diff computation 🐞 Bug ➹ Performance

The new --parent ancestor walk can compute a full diff against multiple successive ancestors (load
versions, materialize files, run getFilesDiff) before finding a meaningful parent. On components
with long chains of non-hidden, untagged ancestors with identical content, this can make `bit diff
--parent` unexpectedly slow.
Agent Prompt
### Issue description
The `--parent` walk-up loop may invoke `diffBetweenVersionsObjects()` multiple times, doing full file diff generation (temp writes + git diff) per ancestor just to answer the yes/no question “is content identical?”.

### Issue Context
This is only triggered when:
- the ancestor is not hidden,
- the ancestor is a snap (no tag), and
- the content is identical across multiple ancestors.
In that shape of history, the loop performs repeated expensive work.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[253-283]
- scopes/component/component-compare/component-compare.main.runtime.ts[319-352]
- components/legacy/component-diff/components-diff.ts[60-101]

### Suggested fix
- Add a lightweight “content equality” check for the ancestor-walk decision:
  - e.g., compare per-file hashes and paths from `modelFilesToSourceFiles()` (without calling `getOneFileDiff()` / `git diff`) to decide if the parent is identical.
  - Only when a non-identical ancestor is found, compute the full diff once for output.
- Keep the “never skip tag ancestor by content” rule by applying the fast equality check only when `parentTag` is falsy.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Default version not imported 🐞 Bug ☼ Reliability ⭐ New
Description
In computeDiff(), when --parent is used without an explicit version, version is defaulted to
component.id.version only after importWithoutDeps() runs, so the current version may not be
imported before modelComponent.loadVersion(version) executes and can throw VersionNotFoundOnFS
if the object is missing locally.
Code

scopes/component/component-compare/component-compare.main.runtime.ts[R240-244]

    await this.scope.legacyScope.scopeImporter.importWithoutDeps(idList, { cache: true, reason: 'to show diff' });
    if (diffOpts.compareToParent) {
-      if (!version) throw new BitError('--parent flag expects to get version');
      if (toVersion) throw new BitError('--parent flag expects to get only one version');
+      if (!version) version = component.id.version;
      const versionObject = await modelComponent.loadVersion(version, repository);
-      const parent = versionObject.parents[0];
Evidence
computeDiff() imports only the explicit version/toVersion values, but in --parent mode it may
later set version from component.id.version and immediately call loadVersion();
loadVersion() throws if the object is not on disk.

scopes/component/component-compare/component-compare.main.runtime.ts[235-244]
scopes/scope/objects/models/model-component.ts[908-913]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`bit diff <comp> --parent` now defaults the version to `component.id.version`, but this happens **after** the initial `scopeImporter.importWithoutDeps()` call (which imports only explicit CLI versions). If the current version object is not already in the local object store, `modelComponent.loadVersion(version, repository)` may throw (e.g. `VersionNotFoundOnFS`).

### Issue Context
- `computeDiff()` imports versions from `idsToImport` before entering the `compareToParent` branch.
- When the user omits the version, `idsToImport` is empty, so nothing is imported.
- `ModelComponent.loadVersion()` throws when the ref exists but the object file is missing locally.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[235-244]

### Proposed fix
Either:
1) Move the `--parent` defaulting logic earlier (before building `idsToImport` and calling `importWithoutDeps`), so the defaulted `version` is included in `idsToImport`, or
2) After `version` is defaulted inside the `compareToParent` branch, explicitly `importWithoutDeps([component.id.changeVersion(version)])` before calling `loadVersion(version, repository)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Repeated parent diff computation 🐞 Bug ➹ Performance
Description
The new --parent ancestor walk can compute a full diff against multiple successive ancestors (load
versions, materialize files, run getFilesDiff) before finding a meaningful parent. On components
with long chains of non-hidden, untagged ancestors with identical content, this can make `bit diff
--parent` unexpectedly slow.
Code

scopes/component/component-compare/component-compare.main.runtime.ts[R261-283]

+      while (parentRef) {
+        const parentTag = modelComponent.getTagOfRefIfExists(parentRef);
+        const parentVersion: string = parentTag || parentRef.toString();
+        await this.scope.legacyScope.scopeImporter.importWithoutDeps(
+          ComponentIdList.fromArray([component.id.changeVersion(parentVersion)]),
+          { cache: true, reason: 'to show diff' }
+        );
+        const parentObject = await modelComponent.loadVersion(parentVersion, repository);
+        version = parentVersion;
+        if (!parentObject.hidden) {
+          if (parentTag) break;
+          const parentDiff = await this.diffBetweenVersionsObjects(
+            modelComponent,
+            parentObject,
+            versionObject,
+            parentVersion,
+            toVersion,
+            diffOpts
+          );
+          if (parentDiff.hasDiff) break;
+        }
+        parentRef = parentObject.parents[0];
+      }
Evidence
The loop calls diffBetweenVersionsObjects() to detect whether an ancestor has meaningful changes,
and that function generates file diffs via getFilesDiff(), which writes temp files and runs diff
for each file path. Repeating this per ancestor can be costly in worst-case histories.

scopes/component/component-compare/component-compare.main.runtime.ts[253-283]
scopes/component/component-compare/component-compare.main.runtime.ts[319-352]
components/legacy/component-diff/components-diff.ts[60-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `--parent` walk-up loop may invoke `diffBetweenVersionsObjects()` multiple times, doing full file diff generation (temp writes + git diff) per ancestor just to answer the yes/no question “is content identical?”.
### Issue Context
This is only triggered when:
- the ancestor is not hidden,
- the ancestor is a snap (no tag), and
- the content is identical across multiple ancestors.
In that shape of history, the loop performs repeated expensive work.
### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[253-283]
- scopes/component/component-compare/component-compare.main.runtime.ts[319-352]
- components/legacy/component-diff/components-diff.ts[60-101]
### Suggested fix
- Add a lightweight “content equality” check for the ancestor-walk decision:
- e.g., compare per-file hashes and paths from `modelFilesToSourceFiles()` (without calling `getOneFileDiff()` / `git diff`) to decide if the parent is identical.
- Only when a non-identical ancestor is found, compute the full diff once for output.
- Keep the “never skip tag ancestor by content” rule by applying the fast equality check only when `parentTag` is falsy.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 240 to 244
await this.scope.legacyScope.scopeImporter.importWithoutDeps(idList, { cache: true, reason: 'to show diff' });
if (diffOpts.compareToParent) {
if (!version) throw new BitError('--parent flag expects to get version');
if (toVersion) throw new BitError('--parent flag expects to get only one version');
if (!version) version = component.id.version;
const versionObject = await modelComponent.loadVersion(version, repository);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Default version not imported 🐞 Bug ☼ Reliability

In computeDiff(), when --parent is used without an explicit version, version is defaulted to
component.id.version only after importWithoutDeps() runs, so the current version may not be
imported before modelComponent.loadVersion(version) executes and can throw VersionNotFoundOnFS
if the object is missing locally.
Agent Prompt
### Issue description
`bit diff <comp> --parent` now defaults the version to `component.id.version`, but this happens **after** the initial `scopeImporter.importWithoutDeps()` call (which imports only explicit CLI versions). If the current version object is not already in the local object store, `modelComponent.loadVersion(version, repository)` may throw (e.g. `VersionNotFoundOnFS`).

### Issue Context
- `computeDiff()` imports versions from `idsToImport` before entering the `compareToParent` branch.
- When the user omits the version, `idsToImport` is empty, so nothing is imported.
- `ModelComponent.loadVersion()` throws when the ref exists but the object file is missing locally.

### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[235-244]

### Proposed fix
Either:
1) Move the `--parent` defaulting logic earlier (before building `idsToImport` and calling `importWithoutDeps`), so the defaulted `version` is included in `idsToImport`, or
2) After `version` is defaulted inside the `compareToParent` branch, explicitly `importWithoutDeps([component.id.changeVersion(version)])` before calling `loadVersion(version, repository)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant