Skip to content

Conversation

@fxenik
Copy link
Member

@fxenik fxenik commented Dec 31, 2025

This pull request introduces support for detailed, nested diff reporting when comparing complex resource structures. It adds a new experimental flag to enable this feature, implements recursive diffing logic for nested maps and arrays, and updates the plan reporter to display these granular differences. The changes also include comprehensive tests for both flat and nested diff modes.

Nested diff reporting support:

  • Added a new experimental flag NestedDiffs to ExperimentalConfig to enable detailed diff reports for nested structures.
  • Implemented the ComputeNestedDiffs function and supporting helpers in cli/internal/syncer/reporters/diff.go to recursively compare and flatten differences between nested maps and arrays.

Plan reporter enhancements:

  • Updated the plan reporter (cli/internal/syncer/reporters/plan.go) to use the nested diff logic when the experimental flag is enabled, displaying property diffs at the most granular changed path using dot notation. [1] [2]
  • Ensured consistent ordering of updated resources and diff output for readability.

Type consistency and code modernization:

  • Refactored diffing logic in cli/internal/syncer/differ/diff.go to use any instead of interface{} for improved Go 1.18+ compatibility and consistency. [1] [2] [3] [4] [5]

Testing improvements:

  • Expanded and added tests in cli/internal/syncer/reporters/plan_test.go to cover both flat and nested diff output, including complex structures and property references. [1] [2] [3]

These changes make it easier to understand exactly what has changed in deeply nested resource structures, improving the clarity and usefulness of diff reports with minimal impact on existing workflows.

🔒 Scanned for secrets using gitleaks 8.28.0

@fxenik fxenik requested a review from Copilot December 31, 2025 10:45
@fxenik fxenik marked this pull request as ready for review December 31, 2025 10:46
@codecov
Copy link

codecov bot commented Dec 31, 2025

Codecov Report

❌ Patch coverage is 84.86842% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.48%. Comparing base (38f2176) to head (b37350a).

Files with missing lines Patch % Lines
cli/internal/syncer/differ/diff.go 36.84% 12 Missing ⚠️
cli/internal/syncer/reporters/plan.go 83.33% 6 Missing and 1 partial ⚠️
cli/internal/syncer/reporters/diff.go 95.60% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #328      +/-   ##
==========================================
+ Coverage   44.02%   44.48%   +0.46%     
==========================================
  Files         192      193       +1     
  Lines       13908    14027     +119     
==========================================
+ Hits         6123     6240     +117     
- Misses       7212     7214       +2     
  Partials      573      573              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request introduces a sophisticated nested diff reporting system for complex resource structures. When the experimental NestedDiffs flag is enabled, instead of showing entire nested objects as changed, the system recursively compares structures and reports only the specific leaf-level differences using dot notation (e.g., config.servers.1.port: 443 => 8443).

Key changes:

  • Added experimental NestedDiffs flag to enable granular diff reporting for nested structures
  • Implemented recursive diff computation with support for nested maps, arrays, and mixed structures
  • Enhanced plan reporter to display property-level changes with full path notation when nested diffs are enabled

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
cli/internal/config/experimental.go Adds NestedDiffs boolean field to ExperimentalConfig for enabling nested diff reporting
cli/internal/syncer/reporters/diff.go Implements new file with ComputeNestedDiffs function and supporting helpers for recursive diff computation with dot-notation paths
cli/internal/syncer/reporters/plan.go Updates plan reporter to use nested diff logic when experimental flag is enabled, adds PropertyRef display handling, and sorts output for consistency
cli/internal/syncer/differ/diff.go Modernizes code by replacing interface{} with any type alias throughout for Go 1.18+ compatibility
cli/internal/syncer/reporters/diff_test.go Adds comprehensive test suite covering maps, arrays, mixed structures, type changes, nil handling, primitives, and multiple array element changes
cli/internal/syncer/reporters/plan_test.go Expands tests to cover both flat and nested diff modes, including complex structures and PropertyRef handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return nil, false
}

// toSlice attempts to convert a value to []any
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function toSlice is missing documentation. It should have a doc comment explaining that it attempts to convert various slice types to []any using both type assertion and reflection.

Suggested change
// toSlice attempts to convert a value to []any
// toSlice attempts to convert various slice-typed values to []any, first using
// a direct type assertion and then using reflection for other slice types.

Copilot uses AI. Check for mistakes.
return basePath + "." + key
}

// toMap attempts to convert a value to map[string]any
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function toMap is missing documentation. It should have a doc comment explaining its purpose as a type assertion helper that converts values to map[string]any.

Suggested change
// toMap attempts to convert a value to map[string]any
// toMap is a type assertion helper that converts a value to map[string]any.

Copilot uses AI. Check for mistakes.
}
return lines
}

Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function formattedLine is missing documentation. It should have a doc comment explaining that it formats a diff line with property name, old/new values, and color coding.

Suggested change
// formattedLine returns a single formatted diff line with the given property name,
// showing the old and new values and applying appropriate color coding.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +72
// Primitive comparison
if !reflect.DeepEqual(source, target) {
result[basePath] = ValuePair{Source: source, Target: target}
}
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The PropertyRef struct contains a function field (Resolve), which cannot be compared using reflect.DeepEqual. This will cause a panic at runtime when comparing PropertyRef values. The code should handle PropertyRef as a special case, comparing only the comparable fields (URN, Property, IsResolved, Value) while ignoring the Resolve function field.

Copilot uses AI. Check for mistakes.
return flattenDiffs("", source, target)
}

// flattenDiffs is the recursive implementation that compares values and builds paths
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function flattenDiffs is missing documentation. As an internal recursive helper function that's central to the diff computation algorithm, it should have a doc comment explaining its parameters, return value, and behavior.

Suggested change
// flattenDiffs is the recursive implementation that compares values and builds paths
// flattenDiffs is the internal recursive engine used by ComputeNestedDiffs.
// It compares the given source and target values at the current nesting level
// and returns a map of dot-notation paths to ValuePair instances for all
// leaf-level differences found under the provided basePath.
//
// Parameters:
// - basePath: the current dot-notation prefix representing the path to
// source and target within the overall structure. For top-level calls
// this is an empty string, and for nested map keys or slice indices it
// is extended by flattenMapDiffs/flattenSliceDiffs (for example "a.b.0").
// - source: the source value at the current path segment; may be a map,
// slice, primitive, or nil.
// - target: the target value at the current path segment; may be a map,
// slice, primitive, or nil.
//
// Return value:
// A map keyed by full dot-notation paths (including basePath and any
// nested segments) whose values are ValuePair instances representing all
// leaf-level differences between source and target. When source and target
// are equal (including both nil) an empty map is returned.
//
// Behavior:
// - If one of source/target is nil or their types differ, a single entry
// is recorded at basePath.
// - If both are maps with string keys, comparison is delegated to
// flattenMapDiffs, which recurses into each key.
// - If both are slices, comparison is delegated to flattenSliceDiffs,
// which recurses into each index.
// - Otherwise, source and target are compared as primitives using
// reflect.DeepEqual, and a diff is recorded at basePath when they differ.

Copilot uses AI. Check for mistakes.
return result
}

// flattenMapDiffs recursively compares two maps
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function flattenMapDiffs is missing documentation. It should have a doc comment explaining its parameters, return value, and behavior, especially noting that it handles keys present in either map and sorts them for consistent output.

Suggested change
// flattenMapDiffs recursively compares two maps
// flattenMapDiffs compares two map[string]any values and returns a flat map of
// dot-notation paths to ValuePair entries for all differing leaf values.
//
// basePath is the current path prefix (using dot notation) under which keys in
// sourceMap and targetMap will be resolved. sourceMap and targetMap are the two
// maps being compared.
//
// The function computes the union of keys present in either map, sorts those
// keys to ensure deterministic output order, and recursively invokes
// flattenDiffs for each key. For keys that exist in only one of the maps, the
// missing value is treated as nil in the resulting ValuePair. The returned map
// is keyed by full dot-notation paths and contains only entries where the
// source and target values differ.

Copilot uses AI. Check for mistakes.
return result
}

// flattenSliceDiffs recursively compares two slices
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function flattenSliceDiffs is missing documentation. It should have a doc comment explaining its parameters, return value, and behavior, especially noting how it handles slices of different lengths.

Suggested change
// flattenSliceDiffs recursively compares two slices
// flattenSliceDiffs recursively compares two slices and flattens their differences
// into a map keyed by dot-notation paths.
//
// Parameters:
// - basePath: the dot-notation path prefix for all indices in these slices.
// - sourceSlice: the slice representing the "source" value.
// - targetSlice: the slice representing the "target" value.
//
// Behavior:
// - The function iterates up to the maximum length of the two slices.
// - For indices beyond the end of either slice, the missing element is treated as nil.
// - If both elements at an index are present and are themselves maps or slices,
// the function recurses to compare their nested contents.
// - If the elements differ (including one being nil while the other is not),
// an entry is added to the result map with a key of the form
// basePath.<index> and a ValuePair holding the source and target values.
//
// The returned map[string]ValuePair contains one entry for each leaf-level
// difference between sourceSlice and targetSlice, with keys representing the
// full path to the differing index.

Copilot uses AI. Check for mistakes.
return result
}

// buildPath constructs a dot-notation path
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The function buildPath is missing documentation. It should have a doc comment explaining that it constructs dot-notation paths, with special handling for empty base paths.

Suggested change
// buildPath constructs a dot-notation path
// buildPath constructs a dot-notation path from a base path and key.
// If the base path is empty, it returns the key without a leading dot.

Copilot uses AI. Check for mistakes.
🔒 Scanned for secrets using gitleaks 8.28.0
@fxenik fxenik force-pushed the feature/dex-205-revamped-diff-reporting-of-plans-during-apply branch from 6130e4a to b37350a Compare December 31, 2025 11:20
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