package com.jdblabs.file.treediff import groovy.io.FileType import groovy.swing.SwingBuilder import com.jdbernard.util.LightOptionParser import org.apache.commons.codec.digest.DigestUtils import com.fasterxml.jackson.databind.ObjectMapper public class TreeDiff { public static final String VERSION = "1.0"; private ObjectMapper objectMapper public static void main(String[] args) { def cliDef = [ 'h': [longName: 'help'], 'v': [longName: 'version'], 'g': [longName: 'gui'], 'i': [longName: 'analysis-in'], 'o': [longName: 'analysis-out'], 's': [longName: 'same'], 'S': [longName: 'exclude-same'], 'c': [longName: 'content-mismatch'], 'C': [longName: 'exclude-content-mismatch'], 'p': [longName: 'path-mismatch'], 'P': [longName: 'exclude-path-mismatch'], 'l': [longName: 'left-only'], 'L': [longName: 'exclude-left-only'], 'r': [longName: 'right-only'], 'R': [longName: 'exclude-right-only'], 'rd': [longName: 'directory', arguments: 1] ] def opts = LightOptionParser.parseOptions(cliDef, args) if (opts.h) { /* TODO */ return } if (opts.v) { println "JDB Labs TreeDiff v${VERSION}" return } if (opts.g) { gui(opts) } else cli(opts) } public static void cli(def opts) { def show = [ same: false, content: false, path: false, left: false, right: false] // If none of the explicit selectors are given, assume all are expeced. if (!opts.s && !opts.c && !opts.p && !opts.l && !opts.r) { show = [ same: true, content: true, path: true, left: true, right: true] } if (opts.s) show.same = true; if (opts.S) show.same = false if (opts.c) show.content = true; if (opts.C) show.content = false if (opts.p) show.path = true; if (opts.P) show.path = false if (opts.l) show.left = true; if (opts.L) show.left = false if (opts.r) show.right = true; if (opts.R) show.right = false if (opts.args.size() < 2) { /* TODO: print usage */ println "TreeDiff v${VERSION}: exactly two directory paths are required to compare." System.exit(1) } File rootDir, leftDir, rightDir if (opts.rd) rootDir = new File(opts.rd[0] ?: '.') else rootDir = new File('.') if (opts.args[0].startsWith('/')) leftDir = new File(opts.args[0]) else leftDir = new File(rootDir, opts.args[0]) if (opts.args[1].startsWith('/')) rightDir = new File(opts.args[1]) else rightDir = new File(rootDir, opts.args[1]) if (!leftDir.exists() || !leftDir.isDirectory()) { println "TreeDiff v${VERSION}: '${opts.args[0]}' cannot be found or is not a directory" System.exit(2) } if (!rightDir.exists() || !rightDir.isDirectory()) { println "TreeDiff v${VERSION}: '${opts.args[1]}' cannot be found or is not a directory" System.exit(2) } DirAnalysis left = analyzeDir(leftDir) DirAnalysis right = analyzeDir(rightDir) if (show.same) same(left, right).each { println "same: ${it.relativePath}" } if (show.content) samePathDifferentContents(left, right).each { println "contents differ: $it" } if (show.path) sameContentsDifferentPaths(left, right).each { println "paths differ: ${it.first.relativePath} ${it.second.relativePath}" } if (show.left) firstSideOnly(left, right).each { println "left only: ${it.relativePath}" } if (show.right) firstSideOnly(right, left).each { println "right only: ${it.relativePath}" } } public static gui(def opts) { frame(title: "TreeDif v${VERSION}", show: true) { boxLayout() } } public static List same(DirAnalysis left, DirAnalysis right) { return left.allEntries.findAll { l -> FileEntry match = right.byRelativePath[l.relativePath] return match != null && l.checksum == match.checksum } } public static Set samePathDifferentContents(DirAnalysis left, DirAnalysis right) { return left.allEntries.findAll { l -> FileEntry match = right.byRelativePath[l.relativePath] return match != null && l.checksum != match.checksum } .collect { it.relativePath } } public static List > sameContentsDifferentPaths(DirAnalysis left, DirAnalysis right) { return left.allEntries.inject([]) { acc, l -> List matches = right.byChecksum[l.checksum] if (matches) { acc.addAll(matches.findAll { l.relativePath != it.relativePath } .collect { r -> new Tuple2(l, r) }) } return acc }.sort { it.first.checksum } } public static List firstSideOnly(DirAnalysis first, DirAnalysis second) { return first.allEntries.findAll { !second.byRelativePath.containsKey(it.relativePath) && !second.byChecksum.containsKey(it.checksum) } } public static DirAnalysis analyzeDir(File root) { DirAnalysis analysis = new DirAnalysis() root.eachFileRecurse(FileType.FILES) { file -> FileEntry entry = new FileEntry( file: file, relativePath: getRelativePath(root, file), checksum: file.withInputStream { DigestUtils.md5Hex(it) }) analysis.allEntries << entry; analysis.byRelativePath[entry.relativePath] = entry if (!analysis.byChecksum.containsKey(entry.checksum)) { analysis.byChecksum[entry.checksum] = [] } analysis.byChecksum[entry.checksum] << entry } return analysis } /** #### `getRelativePath` * Given a parent path and a child path, assuming the child path is * contained within the parent path, return the relative path from the * parent to the child. */ public static String getRelativePath(File parent, File child) { def parentPath = parent.canonicalPath.split("[\\\\/]") def childPath = child.canonicalPath.split("[\\\\/]") /// If the parent path is longer it cannot contain the child path and /// we cannot construct a relative path without backtracking. if (parentPath.length > childPath.length) return "" /// Compare the parent and child path up until the end of the parent /// path. int b = 0 while (b < parentPath.length && parentPath[b] == childPath[b] ) b++; /// If we stopped before reaching the end of the parent path it must be /// that the paths do not match. The parent cannot contain the child and /// we cannot build a relative path without backtracking. if (b != parentPath.length) return "" return (['.'] + childPath[b..