Skip to content

Instantly share code, notes, and snippets.

@crazytaxii
Last active March 11, 2025 16:07
Show Gist options
  • Save crazytaxii/0428496ac65b8e2d09141aa039b5651c to your computer and use it in GitHub Desktop.
Save crazytaxii/0428496ac65b8e2d09141aa039b5651c to your computer and use it in GitHub Desktop.
Diff 2 directories.
//go:build linux || darwin
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"syscall"
)
func main() {
args := os.Args
if len(args) != 3 {
log.Fatalf("Usage: %s directory1 directory2", args[0])
}
if err := compare(filepath.Clean(args[1]), filepath.Clean(args[2])); err != nil {
log.Fatal(err)
}
log.Println("same")
}
func relativePath(path, dir1 string, dir2 string) string {
return filepath.Clean(strings.Replace(path, dir1, dir2, 1))
}
func isSymlink(info os.FileInfo) bool {
return info.Mode()&os.ModeSymlink != 0
}
func compare(dir1, dir2 string) error {
return filepath.Walk(dir1, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// ignore the root directory
if path == dir1 {
return nil
}
_path := relativePath(path, dir1, dir2) // hypothetical file path in dir2
_info, err := os.Lstat(_path)
if err != nil {
return err
}
if !compareMetadata(info, _info) {
return fmt.Errorf("file %s is different from %s", _path, path)
}
if info.IsDir() {
return nil
}
if isSymlink(info) {
if !isSymlink(_info) {
return fmt.Errorf("file %s is different from %s", _path, path)
}
target, err := os.Readlink(path)
if err != nil {
return err
}
_target, err := os.Readlink(_path)
if err != nil {
return err
}
if target != _target {
return fmt.Errorf("symlink %s is different from %s", _path, path)
}
return nil
}
digest1, err := compute(path)
if err != nil {
return err
}
digest2, err := compute(_path)
if err != nil {
return err
}
if digest1 != digest2 {
return fmt.Errorf("file %s is different from %s", _path, path)
}
return nil
})
}
// compareMetadata returns true if the metadata of two files are the same
func compareMetadata(meta1, meta2 os.FileInfo) bool {
stat1, ok := meta1.Sys().(*syscall.Stat_t)
if !ok {
return false
}
stat2, ok := meta2.Sys().(*syscall.Stat_t)
if !ok {
return false
}
return meta1.Mode() == meta2.Mode() &&
meta1.Size() == meta2.Size() &&
meta1.ModTime() == meta2.ModTime() &&
meta1.IsDir() == meta2.IsDir() &&
stat1.Uid == stat2.Uid &&
stat1.Gid == stat2.Gid
}
// compute returns the sha256 hash of the file
func compute(file string) (digest string, err error) {
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
hasher := sha256.New()
if _, err = io.Copy(hasher, f); err != nil {
return
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
@crazytaxii
Copy link
Author

$ go run diff.go dir1 dir2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment