diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1ed6056 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/imrajdas/diffr + +go 1.20 + +require ( + github.com/pmezard/go-difflib v1.0.0 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b015290 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..04bb25b --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/imrajdas/diffr/pkg/cmd/root" +) + +func main() { + root.Execute() +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go new file mode 100644 index 0000000..eba1901 --- /dev/null +++ b/pkg/cmd/root/root.go @@ -0,0 +1,28 @@ +package root + +import ( + "github.com/imrajdas/diffr/pkg/cmd/version" + "github.com/imrajdas/diffr/pkg/diffr" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "diffr [dir1] [dir2]", + Example: "diffr /path/to/dir1 /path/to/dir2", + Short: "A web-based content difference analyzer", + Long: `A web-based tool to compare content differences between two directories ` + "\n" + `Find more information at: https://github.com/imrajdas/diffr`, + Args: cobra.ExactArgs(2), + Run: diffr.RunWebServer, +} + +func Execute() { + cobra.CheckErr(rootCmd.Execute()) +} + +func init() { + rootCmd.Flags().IntVarP(&diffr.Port, "port", "p", 8080, "Set the port for the web server to listen on, default is 8080") + rootCmd.Flags().StringVarP(&diffr.Address, "address", "a", "http://localhost", "Set the address for the web server to listen on, default is http://localhost") + + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.AddCommand(version.VersionCmd) +} diff --git a/pkg/cmd/version/version.go b/pkg/cmd/version/version.go new file mode 100644 index 0000000..a4c60d7 --- /dev/null +++ b/pkg/cmd/version/version.go @@ -0,0 +1,16 @@ +package version + +import ( + "os" + + "github.com/spf13/cobra" +) + +var VersionCmd = &cobra.Command{ + Use: "version", + Short: "Displays the version of diffr", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + cmd.Printf("Diffr Version: %s", os.Getenv("VERSION")) + }, +} diff --git a/pkg/diffr/config.go b/pkg/diffr/config.go new file mode 100644 index 0000000..73cca08 --- /dev/null +++ b/pkg/diffr/config.go @@ -0,0 +1,6 @@ +package diffr + +var ( + Port int + Address string +) diff --git a/pkg/diffr/diff.go b/pkg/diffr/diff.go new file mode 100644 index 0000000..3332fff --- /dev/null +++ b/pkg/diffr/diff.go @@ -0,0 +1,87 @@ +package diffr + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/pmezard/go-difflib/difflib" +) + +func compareFiles(file1, file2 string) (string, error) { + content1, err := ioutil.ReadFile(file1) + if err != nil { + return "", err + } + + content2, err := ioutil.ReadFile(file2) + if err != nil { + return "", err + } + + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(string(content1)), + B: difflib.SplitLines(string(content2)), + FromFile: file1, + ToFile: file2, + Context: 3, + } + + diffs, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + return "", err + } + + return diffs, nil +} + +func CompareDirectories(dir1, dir2 string, diffChan chan<- string, errorChan chan<- error, wg *sync.WaitGroup) { + defer wg.Done() + + filepath.Walk(dir1, func(path1 string, info os.FileInfo, err error) error { + if err != nil { + errorChan <- fmt.Errorf("error accessing %s: %s", path1, err) + return nil + } + + relPath, err := filepath.Rel(dir1, path1) + if err != nil { + errorChan <- fmt.Errorf("error getting relative path of %s: %s", path1, err) + return nil + } + + path2 := filepath.Join(dir2, relPath) + + if info.IsDir() { + return nil + } + + if _, err := os.Stat(path2); err == nil { + diff, err := compareFiles(path1, path2) + if err != nil { + errorChan <- fmt.Errorf("error comparing files %s and %s: %s", path1, path2, err) + return nil + } + + if diff != "" { + diffChan <- fmt.Sprintf("Differences in file: %s\n%s", relPath, diff) + } + } else if os.IsNotExist(err) { + diff, err := compareFiles(path1, "/dev/null") + if err != nil { + errorChan <- fmt.Errorf("error comparing files %s and /dev/null: %s", path1, err) + return nil + } + + if diff != "" { + diffChan <- fmt.Sprintf("Differences in file: %s (present in %s but not in %s)\n%s", relPath, dir1, dir2, diff) + } + } else { + errorChan <- fmt.Errorf("error accessing %s: %s", path2, err) + } + + return nil + }) +} diff --git a/pkg/diffr/handler.go b/pkg/diffr/handler.go new file mode 100644 index 0000000..04bee4d --- /dev/null +++ b/pkg/diffr/handler.go @@ -0,0 +1,134 @@ +package diffr + +import ( + "fmt" + "html/template" + "net/http" + "os" + "os/exec" + "os/signal" + "runtime" + "sync" + "syscall" + + "github.com/spf13/cobra" +) + +var ( + dir1 string + dir2 string +) + +func openBrowser(url string) error { + var cmd *exec.Cmd + + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + default: + return fmt.Errorf("unsupported platform") + } + + return cmd.Start() +} + +func RunWebServer(cmd *cobra.Command, args []string) { + if len(args) != 2 { + fmt.Errorf("Error: Usage: \n diffr /path/to/dir1 /path/to/dir2") + return + } + + dir1 = args[0] + dir2 = args[1] + + serverURL := fmt.Sprintf("%s:%d", Address, Port) + + http.HandleFunc("/", handler) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + server := &http.Server{Addr: fmt.Sprintf(":%d", Port)} + + // Channel to receive signals (e.g., interrupt or termination) + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + + go func() { + fmt.Printf("Server started at %s\n", serverURL) + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + fmt.Println("Error starting server:", err) + os.Exit(1) + } + }() + + fmt.Println("Opening browser...") + err := openBrowser(serverURL) + if err != nil { + fmt.Println("Error opening browser:", err) + } + + // Wait for a termination signal + <-signalCh + + fmt.Println("Shutting down server...") + err = server.Shutdown(nil) + if err != nil { + fmt.Println("Error shutting down server:", err) + } +} + +type PageData struct { + Title string + Diff string +} + +func handler(w http.ResponseWriter, r *http.Request) { + var ( + wg sync.WaitGroup + finalStr = "" + diffChan = make(chan string) + errorChan = make(chan error) + ) + + go func() { + for diff := range diffChan { + finalStr += diff + } + }() + + go func() { + for err := range errorChan { + fmt.Printf("error: %v", err) + } + }() + + wg.Add(1) + go CompareDirectories(dir1, dir2, diffChan, errorChan, &wg) + wg.Wait() + + close(diffChan) + close(errorChan) + + tmpl, err := template.ParseFiles("static/templates/template.html") + if err != nil { + fmt.Printf("error: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := PageData{ + Title: "Diffr - A web-based content difference analyzer", + Diff: finalStr, + } + + // Execute the template with the data and write the output to the response writer + err = tmpl.Execute(w, data) + if err != nil { + fmt.Printf("error: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..e5e30bb --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,17 @@ +.navbar { + background-color: #010b18; /* Dark blue color */ +} +.navbar-brand { + color: #fff; + font-weight: bold; +} +.github-star { + font-size: 24px; + color: #fff; + margin-right: 20px; +} + +#myDiffElement { + margin-left: 10%; + margin-right: 10%; +} diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..ee0d30c --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,27 @@ +var diffString = document.getElementById("diff-data").getAttribute("data-diff"); +document.addEventListener('DOMContentLoaded', function () { + var targetElement = document.getElementById('myDiffElement'); + var configuration = { + drawFileList: true, + fileListToggle: true, + fileListStartVisible: true, + fileContentToggle: true, + matching: 'lines', + outputFormat: 'side-by-side', + synchronisedScroll: true, + highlight: true, + highlightLanguages: true, + renderNothingWhenEmpty: false, + }; + var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration); + diff2htmlUi.draw(); + diff2htmlUi.highlightCode(); + + // Dark mode toggle functionality + const darkModeToggle = document.getElementById('darkModeToggle'); + const body = document.body; + + darkModeToggle.addEventListener('click', () => { + body.classList.toggle('dark-mode'); + }); +}); diff --git a/static/templates/template.html b/static/templates/template.html new file mode 100644 index 0000000..1544f26 --- /dev/null +++ b/static/templates/template.html @@ -0,0 +1,40 @@ + + +
+