-
Notifications
You must be signed in to change notification settings - Fork 0
Add cfgx diff command #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,256 @@ | ||||||||||||||||||||||
| package main | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import ( | ||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||
| "os" | ||||||||||||||||||||||
| "sort" | ||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| "github.com/BurntSushi/toml" | ||||||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var ( | ||||||||||||||||||||||
| keysOnly bool | ||||||||||||||||||||||
| diffFormat string | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var diffCmd = &cobra.Command{ | ||||||||||||||||||||||
| Use: "diff <file1> <file2>", | ||||||||||||||||||||||
| Short: "Compare two TOML files and highlight differences", | ||||||||||||||||||||||
| Long: `Compare two TOML configuration files and show what's different. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This is useful for understanding changes between environments (dev vs prod) | ||||||||||||||||||||||
| or between base and override configurations.`, | ||||||||||||||||||||||
| Example: ` # Compare two config files | ||||||||||||||||||||||
| cfgx diff config.dev.toml config.prod.toml | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Show only changed keys | ||||||||||||||||||||||
| cfgx diff config.dev.toml config.prod.toml --keys-only | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Output as JSON for scripting | ||||||||||||||||||||||
| cfgx diff base.toml override.toml --format json`, | ||||||||||||||||||||||
| Args: cobra.ExactArgs(2), | ||||||||||||||||||||||
| Run: runDiff, | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||
| diffCmd.Flags().BoolVar(&keysOnly, "keys-only", false, "Show only the keys that differ, not their values") | ||||||||||||||||||||||
| diffCmd.Flags().StringVar(&diffFormat, "format", "text", "Output format: text or json") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func runDiff(cmd *cobra.Command, args []string) { | ||||||||||||||||||||||
| file1, file2 := args[0], args[1] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Parse both files | ||||||||||||||||||||||
| data1, err := parseTomlFile(file1) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", file1, err) | ||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| data2, err := parseTomlFile(file2) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", file2, err) | ||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Compute differences | ||||||||||||||||||||||
| diffs := computeDiffs(data1, data2, "") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Output based on format | ||||||||||||||||||||||
| switch diffFormat { | ||||||||||||||||||||||
| case "json": | ||||||||||||||||||||||
| outputJSON(diffs, file1, file2) | ||||||||||||||||||||||
| case "text": | ||||||||||||||||||||||
| outputText(diffs, file1, file2) | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| fmt.Fprintf(os.Stderr, "Unknown format: %s (use 'text' or 'json')\n", diffFormat) | ||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Exit successfully - differences are not errors | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // parseTomlFile parses a TOML file into a map | ||||||||||||||||||||||
| func parseTomlFile(filename string) (map[string]any, error) { | ||||||||||||||||||||||
| var data map[string]any | ||||||||||||||||||||||
| _, err := toml.DecodeFile(filename, &data) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return data, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // DiffType represents the type of difference | ||||||||||||||||||||||
| type DiffType string | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ( | ||||||||||||||||||||||
| DiffChanged DiffType = "changed" | ||||||||||||||||||||||
| DiffAdded DiffType = "added" | ||||||||||||||||||||||
| DiffRemoved DiffType = "removed" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Diff represents a difference between two configs | ||||||||||||||||||||||
| type Diff struct { | ||||||||||||||||||||||
| Key string `json:"key"` | ||||||||||||||||||||||
| Type DiffType `json:"type"` | ||||||||||||||||||||||
| Value1 any `json:"value1,omitempty"` | ||||||||||||||||||||||
| Value2 any `json:"value2,omitempty"` | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // computeDiffs recursively compares two maps and returns differences | ||||||||||||||||||||||
| func computeDiffs(data1, data2 map[string]any, prefix string) []Diff { | ||||||||||||||||||||||
| var diffs []Diff | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Get all keys from both maps | ||||||||||||||||||||||
| allKeys := make(map[string]bool) | ||||||||||||||||||||||
| for k := range data1 { | ||||||||||||||||||||||
| allKeys[k] = true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| for k := range data2 { | ||||||||||||||||||||||
| allKeys[k] = true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Sort keys for consistent output | ||||||||||||||||||||||
| keys := make([]string, 0, len(allKeys)) | ||||||||||||||||||||||
| for k := range allKeys { | ||||||||||||||||||||||
| keys = append(keys, k) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| sort.Strings(keys) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for _, key := range keys { | ||||||||||||||||||||||
| fullKey := key | ||||||||||||||||||||||
| if prefix != "" { | ||||||||||||||||||||||
| fullKey = prefix + "." + key | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val1, exists1 := data1[key] | ||||||||||||||||||||||
| val2, exists2 := data2[key] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Key only in data2 (added) | ||||||||||||||||||||||
| if !exists1 && exists2 { | ||||||||||||||||||||||
| diffs = append(diffs, Diff{ | ||||||||||||||||||||||
| Key: fullKey, | ||||||||||||||||||||||
| Type: DiffAdded, | ||||||||||||||||||||||
| Value2: val2, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Key only in data1 (removed) | ||||||||||||||||||||||
| if exists1 && !exists2 { | ||||||||||||||||||||||
| diffs = append(diffs, Diff{ | ||||||||||||||||||||||
| Key: fullKey, | ||||||||||||||||||||||
| Type: DiffRemoved, | ||||||||||||||||||||||
| Value1: val1, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Key exists in both - check if values differ | ||||||||||||||||||||||
| if exists1 && exists2 { | ||||||||||||||||||||||
| // If both are maps, recurse | ||||||||||||||||||||||
| map1, isMap1 := val1.(map[string]any) | ||||||||||||||||||||||
| map2, isMap2 := val2.(map[string]any) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if isMap1 && isMap2 { | ||||||||||||||||||||||
| // Recursively compare nested maps | ||||||||||||||||||||||
| nestedDiffs := computeDiffs(map1, map2, fullKey) | ||||||||||||||||||||||
| diffs = append(diffs, nestedDiffs...) | ||||||||||||||||||||||
|
||||||||||||||||||||||
| diffs = append(diffs, nestedDiffs...) | |
| diffs = append(diffs, nestedDiffs...) | |
| } else if isMap1 != isMap2 { | |
| // Type mismatch: one is a map, the other is not | |
| diffs = append(diffs, Diff{ | |
| Key: fullKey, | |
| Type: DiffChanged, | |
| Value1: val1, | |
| Value2: val2, | |
| }) |
Copilot
AI
Oct 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using fmt.Sprintf for deep equality comparison can produce false positives. For example, arrays and maps with different internal representations may produce the same string output. Consider using reflect.DeepEqual or implementing proper type-specific comparisons for arrays, maps, and primitive types.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| [server] | ||
| addr = ":8080" | ||
| debug = true | ||
| timeout = "30s" | ||
|
|
||
| [database] | ||
| dsn = "postgresql://localhost/dev" | ||
| max_conns = 10 | ||
| timeout = "5s" | ||
|
|
||
| [logging] | ||
| level = "debug" | ||
| format = "json" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [server] | ||
| addr = ":443" | ||
| tls_enabled = true | ||
| timeout = "30s" | ||
|
|
||
| [database] | ||
| dsn = "postgresql://prod-db/production" | ||
| max_conns = 100 | ||
| timeout = "10s" | ||
|
|
||
| [logging] | ||
| level = "info" | ||
| format = "json" | ||
|
|
||
| [monitoring] | ||
| enabled = true | ||
| interval = "60s" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a key exists only in data2 and its value is a nested map, the entire map is stored in Value2 without recursing into it. This means added nested structures are not expanded in the diff output, making it difficult to see what was added in deeply nested configurations. Consider recursing into added/removed maps to provide detailed diffs.