|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +package component |
| 5 | + |
| 6 | +import ( |
| 7 | + "encoding/json" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + "os" |
| 11 | + |
| 12 | + "github.com/mattn/go-isatty" |
| 13 | + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev" |
| 14 | + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components" |
| 15 | + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources" |
| 16 | + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/workdir" |
| 17 | + "github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders" |
| 18 | + "github.com/microsoft/azure-linux-dev-tools/internal/utils/dirdiff" |
| 19 | + "github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms" |
| 20 | + "github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils" |
| 21 | + "github.com/spf13/cobra" |
| 22 | +) |
| 23 | + |
| 24 | +// DiffSourcesOptions holds the options for the diff-sources command. |
| 25 | +type DiffSourcesOptions struct { |
| 26 | + ComponentFilter components.ComponentFilter |
| 27 | + |
| 28 | + OutputFile string |
| 29 | +} |
| 30 | + |
| 31 | +func diffSourcesOnAppInit(_ *azldev.App, parentCmd *cobra.Command) { |
| 32 | + parentCmd.AddCommand(NewDiffSourcesCmd()) |
| 33 | +} |
| 34 | + |
| 35 | +// NewDiffSourcesCmd constructs a [cobra.Command] for the "component diff-sources" CLI subcommand. |
| 36 | +func NewDiffSourcesCmd() *cobra.Command { |
| 37 | + var options DiffSourcesOptions |
| 38 | + |
| 39 | + cmd := &cobra.Command{ |
| 40 | + Use: "diff-sources", |
| 41 | + Short: "Show the diff that overlays apply to a component's sources", |
| 42 | + Long: `Computes a unified diff showing the changes that overlays apply to a |
| 43 | +component's sources. Fetches the sources once, copies them, then applies |
| 44 | +overlays to the copy and displays the resulting diff between the two trees.`, |
| 45 | + RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) { |
| 46 | + options.ComponentFilter.ComponentNamePatterns = append(args, options.ComponentFilter.ComponentNamePatterns...) |
| 47 | + |
| 48 | + return DiffComponentSources(env, &options) |
| 49 | + }), |
| 50 | + ValidArgsFunction: components.GenerateComponentNameCompletions, |
| 51 | + } |
| 52 | + |
| 53 | + components.AddComponentFilterOptionsToCommand(cmd, &options.ComponentFilter) |
| 54 | + |
| 55 | + cmd.Flags().StringVar(&options.OutputFile, "output-file", "", |
| 56 | + "write the diff output to a file instead of stdout") |
| 57 | + |
| 58 | + azldev.ExportAsMCPTool(cmd) |
| 59 | + |
| 60 | + return cmd |
| 61 | +} |
| 62 | + |
| 63 | +// DiffComponentSources computes the diff between original and overlaid sources for a single component. |
| 64 | +// When color is enabled and the output format is not JSON, the returned value is a pre-colorized |
| 65 | +// string. Otherwise it is [*dirdiff.DiffResult] for structured output. |
| 66 | +func DiffComponentSources(env *azldev.Env, options *DiffSourcesOptions) (interface{}, error) { |
| 67 | + resolver := components.NewResolver(env) |
| 68 | + |
| 69 | + comps, err := resolver.FindComponents(&options.ComponentFilter) |
| 70 | + if err != nil { |
| 71 | + return nil, fmt.Errorf("failed to resolve components:\n%w", err) |
| 72 | + } |
| 73 | + |
| 74 | + if comps.Len() == 0 { |
| 75 | + return nil, errors.New("no components were selected; " + |
| 76 | + "please use command-line options to indicate which components you would like to diff", |
| 77 | + ) |
| 78 | + } |
| 79 | + |
| 80 | + if comps.Len() != 1 { |
| 81 | + return nil, fmt.Errorf("expected exactly one component, got %d", comps.Len()) |
| 82 | + } |
| 83 | + |
| 84 | + component := comps.Components()[0] |
| 85 | + |
| 86 | + event := env.StartEvent("Diffing sources", "component", component.GetName()) |
| 87 | + defer event.End() |
| 88 | + |
| 89 | + sourceManager, err := sourceproviders.NewSourceManager(env) |
| 90 | + if err != nil { |
| 91 | + return nil, fmt.Errorf("failed to create source manager:\n%w", err) |
| 92 | + } |
| 93 | + |
| 94 | + preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env) |
| 95 | + if err != nil { |
| 96 | + return nil, fmt.Errorf("failed to create source preparer:\n%w", err) |
| 97 | + } |
| 98 | + |
| 99 | + // Create a per-component work directory for the diff operation's temp files. |
| 100 | + workDirFactory, err := workdir.NewFactory(env.FS(), env.WorkDir(), env.ConstructionTime()) |
| 101 | + if err != nil { |
| 102 | + return nil, fmt.Errorf("failed to create work dir factory:\n%w", err) |
| 103 | + } |
| 104 | + |
| 105 | + baseDir, err := workDirFactory.Create(component.GetName(), "diff-sources") |
| 106 | + if err != nil { |
| 107 | + return nil, fmt.Errorf("failed to create work dir for component %#q:\n%w", component.GetName(), err) |
| 108 | + } |
| 109 | + |
| 110 | + result, err := preparer.DiffSources(env, component, baseDir) |
| 111 | + if err != nil { |
| 112 | + return nil, fmt.Errorf("failed to diff sources for component %#q:\n%w", component.GetName(), err) |
| 113 | + } |
| 114 | + |
| 115 | + // If an output file was specified, write the diff there (JSON or plain text depending on format). |
| 116 | + if options.OutputFile != "" { |
| 117 | + if err := writeDiffOutput(env, options.OutputFile, result); err != nil { |
| 118 | + return nil, err |
| 119 | + } |
| 120 | + |
| 121 | + env.Event("Diff written to file", "file", options.OutputFile) |
| 122 | + |
| 123 | + return "", nil |
| 124 | + } |
| 125 | + |
| 126 | + // For JSON output, return the structured form. [FileDiff.MarshalJSON] parses |
| 127 | + // the raw unified diff into per-line change records automatically. |
| 128 | + if env.DefaultReportFormat() == azldev.ReportFormatJSON { |
| 129 | + return result, nil |
| 130 | + } |
| 131 | + // For non-JSON text formats, colorize the diff output when color is enabled. |
| 132 | + if shouldColorize(env) { |
| 133 | + return result.ColorString(), nil |
| 134 | + } |
| 135 | + |
| 136 | + return result.String(), nil |
| 137 | +} |
| 138 | + |
| 139 | +// writeDiffOutput writes the diff result to the specified output file in the appropriate format. |
| 140 | +func writeDiffOutput(env *azldev.Env, outputFile string, result *dirdiff.DiffResult) error { |
| 141 | + var fileContent []byte |
| 142 | + |
| 143 | + if env.DefaultReportFormat() == azldev.ReportFormatJSON { |
| 144 | + jsonBytes, jsonErr := json.MarshalIndent(result, "", " ") |
| 145 | + if jsonErr != nil { |
| 146 | + return fmt.Errorf("failed to marshal diff to JSON:\n%w", jsonErr) |
| 147 | + } |
| 148 | + |
| 149 | + jsonBytes = append(jsonBytes, '\n') |
| 150 | + fileContent = jsonBytes |
| 151 | + } else { |
| 152 | + fileContent = []byte(result.String()) |
| 153 | + } |
| 154 | + |
| 155 | + if writeErr := fileutils.WriteFile(env.FS(), outputFile, fileContent, fileperms.PublicFile); writeErr != nil { |
| 156 | + return fmt.Errorf("failed to write diff to %#q:\n%w", outputFile, writeErr) |
| 157 | + } |
| 158 | + |
| 159 | + return nil |
| 160 | +} |
| 161 | + |
| 162 | +// shouldColorize determines whether the current session should produce colorized output, |
| 163 | +// based on the environment's [azldev.ColorMode] and whether stdout is a terminal. |
| 164 | +func shouldColorize(env *azldev.Env) bool { |
| 165 | + switch env.ColorMode() { |
| 166 | + case azldev.ColorModeAlways: |
| 167 | + return true |
| 168 | + case azldev.ColorModeNever: |
| 169 | + return false |
| 170 | + case azldev.ColorModeAuto: |
| 171 | + return isatty.IsTerminal(os.Stdout.Fd()) |
| 172 | + } |
| 173 | + |
| 174 | + return false |
| 175 | +} |
0 commit comments