From d027ec9108b81a95d5ddc38837de808340825566 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Thu, 28 Aug 2025 11:18:59 +0200 Subject: [PATCH] Add a --write and --diff flag; require --write to be set before making any changes, for safety --- go.mod | 5 ++ go.sum | 4 ++ tesseract/genconf/env.go | 10 ++-- tesseract/genconf/html.go | 10 ++-- tesseract/genconf/key.go | 7 ++- tesseract/genconf/main.go | 115 +++++++++++++++++++++++++++++++++---- tesseract/genconf/nginx.go | 4 +- tesseract/genconf/roots.go | 4 +- 8 files changed, 133 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index e4166cc..a35c65e 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,8 @@ module cheese go 1.24.4 require gopkg.in/yaml.v3 v3.0.1 + +require ( + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/go.sum b/go.sum index a62c313..4eb7ae3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tesseract/genconf/env.go b/tesseract/genconf/env.go index 9e21921..620af3a 100644 --- a/tesseract/genconf/env.go +++ b/tesseract/genconf/env.go @@ -8,7 +8,7 @@ import ( "strings" ) -func generateEnv(yamlFile string) { +func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { config := loadConfig(yamlFile) // Check that all local directories exist @@ -24,7 +24,7 @@ func generateEnv(yamlFile string) { // Create combined roots.pem file rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem") - err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath) + err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath, wantDiff, allowWrite, useColor) if err != nil { log.Fatalf("Failed to create %s: %v", rootsPemPath, err) } @@ -53,14 +53,14 @@ func generateEnv(yamlFile string) { tesseractArgs := strings.Join(args, " ") envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs) - err = writeFileWithStatus(envPath, []byte(envContent)) + err = writeFileWithStatus(envPath, []byte(envContent), wantDiff, allowWrite, useColor) if err != nil { log.Fatalf("Failed to write %s: %v", envPath, err) } } } -func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string) error { +func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error { // Read main roots file var combinedContent []byte if rootsFile != "" { @@ -80,5 +80,5 @@ func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath stri combinedContent = append(combinedContent, extraRootsData...) } - return writeFileWithStatus(outputPath, combinedContent) + return writeFileWithStatus(outputPath, combinedContent, wantDiff, allowWrite, useColor) } diff --git a/tesseract/genconf/html.go b/tesseract/genconf/html.go index 4426629..4e26c08 100644 --- a/tesseract/genconf/html.go +++ b/tesseract/genconf/html.go @@ -110,7 +110,7 @@ type TemporalInterval struct { EndExclusive string `json:"end_exclusive"` } -func generateHTML(yamlFile string) { +func generateHTML(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { config := loadConfig(yamlFile) // Check that all local directories exist @@ -145,14 +145,14 @@ func generateHTML(yamlFile string) { } // Write file with status - err = writeFileWithStatus(indexPath, buf.Bytes()) + err = writeFileWithStatus(indexPath, buf.Bytes(), wantDiff, allowWrite, useColor) if err != nil { log.Fatalf("Failed to write HTML to %s: %v", indexPath, err) } // Generate log.v3.json for this log jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json") - err = generateLogJSONWithStatus(logEntry, jsonPath) + err = generateLogJSONWithStatus(logEntry, jsonPath, wantDiff, allowWrite, useColor) if err != nil { log.Fatalf("Failed to generate %s: %v", jsonPath, err) } @@ -209,7 +209,7 @@ func computeKeyInfo(logEntry *Log) error { return nil } -func generateLogJSONWithStatus(logEntry Log, outputPath string) error { +func generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error { logJSON := LogV3JSON{ Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName), SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix), @@ -228,5 +228,5 @@ func generateLogJSONWithStatus(logEntry Log, outputPath string) error { return fmt.Errorf("failed to marshal JSON: %v", err) } - return writeFileWithStatus(outputPath, jsonData) + return writeFileWithStatus(outputPath, jsonData, wantDiff, allowWrite, useColor) } diff --git a/tesseract/genconf/key.go b/tesseract/genconf/key.go index f49d1dc..b3755fd 100644 --- a/tesseract/genconf/key.go +++ b/tesseract/genconf/key.go @@ -12,7 +12,12 @@ import ( "path/filepath" ) -func generateKeys(yamlFile string) { +func generateKeys(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { + if !allowWrite { + fmt.Printf("Key generation requires --write flag\n") + return + } + config := loadConfig(yamlFile) // Generate keys for each log diff --git a/tesseract/genconf/main.go b/tesseract/genconf/main.go index b872806..d7daa4b 100644 --- a/tesseract/genconf/main.go +++ b/tesseract/genconf/main.go @@ -5,8 +5,12 @@ import ( "fmt" "log" "os" + "strings" "time" + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" + "github.com/hexops/gotextdiff/span" "gopkg.in/yaml.v3" ) @@ -41,6 +45,9 @@ type Log struct { func main() { configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file") + wantDiff := flag.Bool("diff", false, "Show unified diff of changes") + allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)") + noColor := flag.Bool("no-color", false, "Disable colored diff output") flag.Parse() args := flag.Args() @@ -51,15 +58,15 @@ func main() { switch args[0] { case "gen-html": - generateHTML(*configFile) + generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-env": - generateEnv(*configFile) + generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-key": - generateKeys(*configFile) + generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-nginx": - generateNginx(*configFile) + generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-roots": - generateRoots(args[1:]) + generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) showHelp() @@ -97,17 +104,94 @@ func loadConfig(yamlFile string) Config { return config } -func writeFileWithStatus(filename string, content []byte) error { +// ANSI color codes +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" +) + +// colorizeUnifiedDiff adds ANSI color codes to unified diff output +func colorizeUnifiedDiff(diff string) string { + lines := strings.Split(diff, "\n") + var colorizedLines []string + + for _, line := range lines { + switch { + case strings.HasPrefix(line, "---"): + // File deletion header in cyan + colorizedLines = append(colorizedLines, colorCyan+line+colorReset) + case strings.HasPrefix(line, "+++"): + // File addition header in cyan + colorizedLines = append(colorizedLines, colorCyan+line+colorReset) + case strings.HasPrefix(line, "@@"): + // Hunk header in yellow + colorizedLines = append(colorizedLines, colorYellow+line+colorReset) + case strings.HasPrefix(line, "-"): + // Deleted lines in red + colorizedLines = append(colorizedLines, colorRed+line+colorReset) + case strings.HasPrefix(line, "+"): + // Added lines in green + colorizedLines = append(colorizedLines, colorGreen+line+colorReset) + default: + // Context lines unchanged + colorizedLines = append(colorizedLines, line) + } + } + + return strings.Join(colorizedLines, "\n") +} + +func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error { existingContent, err := os.ReadFile(filename) - if os.IsNotExist(err) { - fmt.Printf("Creating %s\n", filename) + isNew := os.IsNotExist(err) + isUnchanged := false + + if isNew { + if allowWrite { + fmt.Printf("Creating %s\n", filename) + } else { + fmt.Printf("Would create %s\n", filename) + } } else if err != nil { return fmt.Errorf("failed to read existing file %s: %v", filename, err) } else if string(existingContent) == string(content) { fmt.Printf("Unchanged %s\n", filename) - return nil + isUnchanged = true } else { - fmt.Printf("Updating %s\n", filename) + if allowWrite { + fmt.Printf("Updating %s\n", filename) + } else { + fmt.Printf("Would update %s\n", filename) + } + } + + if wantDiff && !isUnchanged { + if isNew { + // For new files, show the entire content as added + edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content)) + diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits)) + if useColor { + fmt.Print(colorizeUnifiedDiff(diff)) + } else { + fmt.Print(diff) + } + } else { + // For existing files, show the diff + edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content)) + diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits)) + if useColor { + fmt.Print(colorizeUnifiedDiff(diff)) + } else { + fmt.Print(diff) + } + } + } + + if isUnchanged || !allowWrite { + return nil } err = os.WriteFile(filename, content, 0644) @@ -119,8 +203,17 @@ func writeFileWithStatus(filename string, content []byte) error { func showHelp() { fmt.Printf("Usage: %s [options] \n\n", os.Args[0]) + fmt.Printf("Note: Flags must come before the command name.\n\n") fmt.Printf("Options:\n") - fmt.Printf(" -c Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\n") + fmt.Printf(" -c Path to YAML configuration file (default: ./tesseract-staging.yaml)\n") + fmt.Printf(" --diff Show unified diff of changes without writing files\n") + fmt.Printf(" --write Allow writing files (required for actual file modifications)\n") + fmt.Printf(" --no-color Disable colored diff output\n\n") + fmt.Printf("Examples:\n") + fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0]) + fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0]) + fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0]) + fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0]) fmt.Printf("Commands:\n") fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n") fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n") diff --git a/tesseract/genconf/nginx.go b/tesseract/genconf/nginx.go index 090eac7..0d40e7b 100644 --- a/tesseract/genconf/nginx.go +++ b/tesseract/genconf/nginx.go @@ -76,7 +76,7 @@ type NginxTemplateData struct { ListenPort string } -func generateNginx(yamlFile string) { +func generateNginx(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { config := loadConfig(yamlFile) // Extract port from first listen address @@ -123,7 +123,7 @@ func generateNginx(yamlFile string) { } // Write file with status - err = writeFileWithStatus(outputPath, buf.Bytes()) + err = writeFileWithStatus(outputPath, buf.Bytes(), wantDiff, allowWrite, useColor) if err != nil { fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err) continue diff --git a/tesseract/genconf/roots.go b/tesseract/genconf/roots.go index 89a1c4c..07eaf83 100644 --- a/tesseract/genconf/roots.go +++ b/tesseract/genconf/roots.go @@ -16,7 +16,7 @@ type CTLogRootsResponse struct { Certificates []string `json:"certificates"` } -func generateRoots(args []string) { +func generateRoots(args []string, wantDiff bool, allowWrite bool, useColor bool) { sourceURL := "https://rennet2027h2.log.ct.ipng.ch/" outputFile := "roots.pem" @@ -107,7 +107,7 @@ func generateRoots(args []string) { } // Write all certificates to file with status - err = writeFileWithStatus(outputFile, pemBuffer.Bytes()) + err = writeFileWithStatus(outputFile, pemBuffer.Bytes(), wantDiff, allowWrite, useColor) if err != nil { log.Fatalf("Failed to write output file %s: %v", outputFile, err) }