Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a73b7f248 | |||
ce821d8f53 | |||
de9ff2b54a | |||
bd41e2d2f5 | |||
|
c59a700cf9 | ||
|
641a562563 | ||
|
e49bd4e9c9 | ||
|
6cc4bf390f | ||
|
3f829ef69e | ||
|
92d8ed61fc | ||
|
2277dd0828 | ||
|
9728055c45 | ||
|
c16a5a684d | ||
|
ab99661720 | ||
|
c0e3818520 | ||
|
e33586c9f0 | ||
|
660bf35540 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
nimcache/
|
nimcache/
|
||||||
build/
|
build/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
/treediff
|
||||||
|
12
build.gradle
12
build.gradle
@ -3,19 +3,17 @@ apply plugin: "application"
|
|||||||
apply plugin: "maven"
|
apply plugin: "maven"
|
||||||
|
|
||||||
group = "com.jdblabs"
|
group = "com.jdblabs"
|
||||||
version = "1.1"
|
version = "1.4.4"
|
||||||
mainClassName = "com.jdblabs.file.treediff.TreeDiff"
|
mainClassName = "com.jdblabs.file.treediff.TreeDiff"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral() }
|
mavenCentral()
|
||||||
|
maven { url "http://mvn.jdb-labs.com/repo" } }
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'org.codehaus.groovy:groovy-all:2.4.3'
|
compile localGroovy()
|
||||||
compile 'org.slf4j:slf4j-api:1.7.10'
|
compile 'com.jdbernard:jdb-util:4.+'
|
||||||
compile 'ch.qos.logback:logback-core:1.1.2'
|
|
||||||
compile 'ch.qos.logback:logback-classic:1.1.2'
|
|
||||||
compile 'com.jdbernard:jdb-util:3.5'
|
|
||||||
compile 'commons-codec:commons-codec:1.10'
|
compile 'commons-codec:commons-codec:1.10'
|
||||||
compile 'com.fasterxml.jackson.core:jackson-databind:2.4.4'
|
compile 'com.fasterxml.jackson.core:jackson-databind:2.4.4'
|
||||||
|
|
||||||
|
@ -1,216 +0,0 @@
|
|||||||
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.1";
|
|
||||||
|
|
||||||
private static ObjectMapper objectMapper = new ObjectMapper()
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
|
|
||||||
def cliDef = [
|
|
||||||
'h': [longName: 'help'],
|
|
||||||
'v': [longName: 'version'],
|
|
||||||
'g': [longName: 'gui'],
|
|
||||||
'i': [longName: 'analysis-in', arguments: 2],
|
|
||||||
'o': [longName: 'analysis-out', arguments: 1],
|
|
||||||
'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'],
|
|
||||||
'q': [longName: 'quiet'],
|
|
||||||
'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) {
|
|
||||||
|
|
||||||
File rootDir, leftFile, rightFile
|
|
||||||
|
|
||||||
if (opts.rd) rootDir = new File(opts.rd[0] ?: '.')
|
|
||||||
else rootDir = new File('.')
|
|
||||||
|
|
||||||
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 && !opts.q) {
|
|
||||||
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
|
|
||||||
|
|
||||||
DirAnalysis left, right;
|
|
||||||
|
|
||||||
if (opts.i) {
|
|
||||||
leftFile = resolvePath(opts.i[0], rootDir)
|
|
||||||
rightFile = resolvePath(opts.i[1], rootDir)
|
|
||||||
left = objectMapper.readValue(leftFile, DirAnalysis)
|
|
||||||
right = objectMapper.readValue(rightFile, DirAnalysis)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if (opts.args.size() < 2) {
|
|
||||||
/* TODO: print usage */
|
|
||||||
println "TreeDiff v${VERSION}: exactly two directory paths are required to compare."
|
|
||||||
System.exit(1) }
|
|
||||||
|
|
||||||
leftFile = resolvePath(opts.args[0], rootDir)
|
|
||||||
rightFile = resolvePath(opts.args[0], rootDir)
|
|
||||||
|
|
||||||
if (!leftFile.isDirectory()) {
|
|
||||||
println "TreeDiff v${VERSION}: '${opts.args[1]}' is not a directory"
|
|
||||||
System.exit(2) }
|
|
||||||
|
|
||||||
if (!rightFile.isDirectory()) {
|
|
||||||
println "TreeDiff v${VERSION}: '${opts.args[1]}' is not a directory"
|
|
||||||
System.exit(2) }
|
|
||||||
|
|
||||||
left = analyzeDir(leftFile)
|
|
||||||
right = analyzeDir(rightFile) }
|
|
||||||
|
|
||||||
|
|
||||||
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}" }
|
|
||||||
|
|
||||||
if (opts.o) {
|
|
||||||
String rootName = opts.o[0]
|
|
||||||
File leftOut, rightOut
|
|
||||||
|
|
||||||
if (rootName.startsWith('/')) leftOut = new File(rootName + '.left')
|
|
||||||
else leftOut = new File(rootDir, rootName + '.left')
|
|
||||||
|
|
||||||
if (rootName.startsWith('/')) rightOut = new File(rootName + '.right')
|
|
||||||
else rightOut = new File(rootDir, rootName + '.right')
|
|
||||||
|
|
||||||
objectMapper.writeValue(leftOut, left)
|
|
||||||
objectMapper.writeValue(rightOut, right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static gui(def opts) {
|
|
||||||
frame(title: "TreeDif v${VERSION}", show: true) {
|
|
||||||
boxLayout()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<FileEntry> 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<String> 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<Tuple2<FileEntry, FileEntry> > sameContentsDifferentPaths(DirAnalysis left, DirAnalysis right) {
|
|
||||||
return left.allEntries.inject([]) { acc, l ->
|
|
||||||
List<FileEntry> matches = right.byChecksum[l.checksum]
|
|
||||||
if (matches) {
|
|
||||||
acc.addAll(matches.findAll { l.relativePath != it.relativePath }
|
|
||||||
.collect { r -> new Tuple2<FileEntry, FileEntry>(l, r) }) }
|
|
||||||
return acc }.sort { it.first.checksum }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<FileEntry> 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..<childPath.length]).join('/') }
|
|
||||||
|
|
||||||
public static File resolvePath(String path, File rootDir) {
|
|
||||||
File f
|
|
||||||
if (path.startsWith('/')) f = new File(path)
|
|
||||||
else f = new File(rootDir, path)
|
|
||||||
|
|
||||||
if (!f.exists()) {
|
|
||||||
println "TreeDiff v${VERSION}: '${f.canonicalPath}' cannot be found"
|
|
||||||
System.exit(2) }
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.jdblabs.file.treediff
|
||||||
|
|
||||||
|
import com.jdbernard.util.ConsoleProgressBar
|
||||||
|
|
||||||
|
public class ConsoleProgressListener implements ProgressListener {
|
||||||
|
|
||||||
|
private ConsoleProgressBar consoleProgressBar
|
||||||
|
private def out
|
||||||
|
private boolean verbose
|
||||||
|
|
||||||
|
public ConsoleProgressListener(def out, boolean verbose) {
|
||||||
|
this.consoleProgressBar = new ConsoleProgressBar(out: out)
|
||||||
|
this.out = out
|
||||||
|
this.verbose = verbose }
|
||||||
|
|
||||||
|
public void init(File root, int total) {
|
||||||
|
out.println "-- ${root.canonicalPath}"
|
||||||
|
out.println " $total files"
|
||||||
|
consoleProgressBar.max = total
|
||||||
|
consoleProgressBar.update(0, root.name) }
|
||||||
|
|
||||||
|
public void update(File curFile, int curCount) {
|
||||||
|
if (verbose) {
|
||||||
|
consoleProgressBar.erase()
|
||||||
|
out.println " ${curFile.canonicalPath}" }
|
||||||
|
|
||||||
|
consoleProgressBar.update(curCount, curFile.name) }
|
||||||
|
|
||||||
|
public void finish() {
|
||||||
|
consoleProgressBar.erase()
|
||||||
|
out.println "" }
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.jdblabs.file.treediff;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public interface ProgressListener {
|
||||||
|
void init(File root, int total);
|
||||||
|
void update(File curFile, int curCount);
|
||||||
|
void finish();
|
||||||
|
}
|
382
src/main/groovy/com/jdblabs/file/treediff/TreeDiff.groovy
Normal file
382
src/main/groovy/com/jdblabs/file/treediff/TreeDiff.groovy
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
package com.jdblabs.file.treediff
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.jdbernard.util.LightOptionParser
|
||||||
|
import groovy.io.FileType
|
||||||
|
import groovy.swing.SwingBuilder
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
|
|
||||||
|
public class TreeDiff {
|
||||||
|
|
||||||
|
public static final String VERSION = "1.4.3"
|
||||||
|
|
||||||
|
private ObjectMapper objectMapper = new ObjectMapper()
|
||||||
|
private PrintStream stdout
|
||||||
|
private PrintStream stderr
|
||||||
|
private File relativeRoot
|
||||||
|
private Map displayFilter
|
||||||
|
private boolean verbose
|
||||||
|
private boolean quiet
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
TreeDiff inst = new TreeDiff(
|
||||||
|
stdout: System.out,
|
||||||
|
stderr: System.err)
|
||||||
|
|
||||||
|
inst.doDiff(args)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doDiff(String[] args) {
|
||||||
|
|
||||||
|
def cliDef = [
|
||||||
|
'h': [longName: 'help'],
|
||||||
|
'v': [longName: 'verbose'],
|
||||||
|
'V': [longName: 'version'],
|
||||||
|
'g': [longName: 'gui'],
|
||||||
|
'i': [longName: 'analysis-in', arguments: 2],
|
||||||
|
'o': [longName: 'analysis-out', arguments: 1],
|
||||||
|
'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'],
|
||||||
|
'q': [longName: 'quiet'],
|
||||||
|
'Q': [longName: 'very-quiet'],
|
||||||
|
'rd': [longName: 'directory', arguments: 1]
|
||||||
|
]
|
||||||
|
|
||||||
|
def opts = LightOptionParser.parseOptions(cliDef, args)
|
||||||
|
|
||||||
|
if (opts.h) { println this.usage; return }
|
||||||
|
|
||||||
|
if (opts.V) {
|
||||||
|
stdout.println "JDB Labs TreeDiff v${VERSION}"
|
||||||
|
return }
|
||||||
|
|
||||||
|
verbose = opts.v
|
||||||
|
|
||||||
|
if (opts.rd) relativeRoot = new File(opts.rd[0] ?: '.')
|
||||||
|
else relativeRoot = new File('.')
|
||||||
|
|
||||||
|
def progressListener
|
||||||
|
|
||||||
|
if (opts.g) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
else progressListener = new ConsoleProgressListener(stdout, verbose)
|
||||||
|
|
||||||
|
if (opts.q) quiet = true
|
||||||
|
if (opts.Q) {
|
||||||
|
quiet = true
|
||||||
|
progressListener = null }
|
||||||
|
|
||||||
|
DirAnalysis left, right
|
||||||
|
File leftFile, rightFile
|
||||||
|
|
||||||
|
if (opts.i) {
|
||||||
|
leftFile = resolvePath(opts.i[0], relativeRoot)
|
||||||
|
rightFile = resolvePath(opts.i[1], relativeRoot)
|
||||||
|
left = objectMapper.readValue(leftFile, DirAnalysis)
|
||||||
|
right = objectMapper.readValue(rightFile, DirAnalysis) }
|
||||||
|
|
||||||
|
else {
|
||||||
|
|
||||||
|
if (opts.args.size() < 2) {
|
||||||
|
/* TODO: print usage */
|
||||||
|
stderr.println "TreeDiff v${VERSION}: exactly two directory paths are required to compare."
|
||||||
|
System.exit(1) }
|
||||||
|
|
||||||
|
leftFile = resolvePath(opts.args[0], relativeRoot)
|
||||||
|
rightFile = resolvePath(opts.args[1], relativeRoot)
|
||||||
|
|
||||||
|
if (!leftFile.isDirectory()) {
|
||||||
|
stderr.println "TreeDiff v${VERSION}: '${opts.args[0]}' is not a directory"
|
||||||
|
System.exit(2) }
|
||||||
|
|
||||||
|
if (!rightFile.isDirectory()) {
|
||||||
|
stderr.println "TreeDiff v${VERSION}: '${opts.args[1]}' is not a directory"
|
||||||
|
System.exit(2) }
|
||||||
|
|
||||||
|
left = analyzeDir(leftFile, progressListener)
|
||||||
|
right = analyzeDir(rightFile, progressListener) }
|
||||||
|
|
||||||
|
displayFilter = [ 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 && !opts.q) {
|
||||||
|
displayFilter = [ same: true, content: true, path: true,
|
||||||
|
left: true, right: true] }
|
||||||
|
|
||||||
|
|
||||||
|
if (opts.s) displayFilter.same = true
|
||||||
|
if (opts.S) displayFilter.same = false
|
||||||
|
|
||||||
|
if (opts.c) displayFilter.content = true
|
||||||
|
if (opts.C) displayFilter.content = false
|
||||||
|
|
||||||
|
if (opts.p) displayFilter.path = true
|
||||||
|
if (opts.P) displayFilter.path = false
|
||||||
|
|
||||||
|
if (opts.l) displayFilter.left = true
|
||||||
|
if (opts.L) displayFilter.left = false
|
||||||
|
|
||||||
|
if (opts.r) displayFilter.right = true
|
||||||
|
if (opts.R) displayFilter.right = false
|
||||||
|
|
||||||
|
if (opts.g) displayResultsGui(left, right)
|
||||||
|
else displayResultsCli(left, right)
|
||||||
|
|
||||||
|
if (opts.o) {
|
||||||
|
String rootName = opts.o[0]
|
||||||
|
File leftOut, rightOut
|
||||||
|
|
||||||
|
if (rootName.startsWith('/')) leftOut = new File(rootName + '.left')
|
||||||
|
else leftOut = new File(relativeRoot, rootName + '.left')
|
||||||
|
|
||||||
|
if (rootName.startsWith('/')) rightOut = new File(rootName + '.right')
|
||||||
|
else rightOut = new File(relativeRoot, rootName + '.right')
|
||||||
|
|
||||||
|
objectMapper.writeValue(leftOut, left)
|
||||||
|
objectMapper.writeValue(rightOut, right) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void displayResultsCli(DirAnalysis left, DirAnalysis right) {
|
||||||
|
|
||||||
|
if (displayFilter.same) same(left, right).each {
|
||||||
|
stdout.println "same: ${it.relativePath}" }
|
||||||
|
|
||||||
|
if (displayFilter.content) samePathDifferentContents(left, right).each {
|
||||||
|
stdout.println "contents differ: $it" }
|
||||||
|
|
||||||
|
if (displayFilter.path) sameContentsDifferentPaths(left, right).each {
|
||||||
|
stdout.println "paths differ: ${it.first.relativePath} ${it.second.relativePath}" }
|
||||||
|
|
||||||
|
if (displayFilter.left) firstSideOnly(left, right).each {
|
||||||
|
stdout.println "left only: ${it.relativePath}" }
|
||||||
|
|
||||||
|
if (displayFilter.right) firstSideOnly(right, left).each {
|
||||||
|
stdout.println "right only: ${it.relativePath}" }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static gui(def opts) {
|
||||||
|
frame(title: "TreeDif v${VERSION}", show: true) {
|
||||||
|
boxLayout()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<FileEntry> 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<String> 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<Tuple2<FileEntry, FileEntry> > sameContentsDifferentPaths(DirAnalysis left, DirAnalysis right) {
|
||||||
|
return left.allEntries.inject([]) { acc, l ->
|
||||||
|
List<FileEntry> matches = right.byChecksum[l.checksum]
|
||||||
|
if (matches) {
|
||||||
|
acc.addAll(matches.findAll { l.relativePath != it.relativePath }
|
||||||
|
.collect { r -> new Tuple2<FileEntry, FileEntry>(l, r) }) }
|
||||||
|
return acc }.sort { it.first.checksum }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<FileEntry> firstSideOnly(DirAnalysis first, DirAnalysis second) {
|
||||||
|
return first.allEntries.findAll {
|
||||||
|
!second.byRelativePath.containsKey(it.relativePath) &&
|
||||||
|
!second.byChecksum.containsKey(it.checksum) } }
|
||||||
|
|
||||||
|
public DirAnalysis analyzeDir(File root, ProgressListener progressListener) {
|
||||||
|
DirAnalysis analysis = new DirAnalysis()
|
||||||
|
|
||||||
|
int totalNumFiles = 0
|
||||||
|
int filesProcessed = 0
|
||||||
|
|
||||||
|
root.eachFileRecurse(FileType.FILES) { totalNumFiles++ }
|
||||||
|
|
||||||
|
boolean showProgress = progressListener != null &&
|
||||||
|
(verbose || totalNumFiles > 100);
|
||||||
|
|
||||||
|
if (progressListener) progressListener.init(root, totalNumFiles)
|
||||||
|
|
||||||
|
root.eachFileRecurse(FileType.FILES) { file ->
|
||||||
|
if (showProgress) progressListener.update(file, ++filesProcessed)
|
||||||
|
|
||||||
|
String checksum = ""
|
||||||
|
try { checksum = file.withInputStream { DigestUtils.md5Hex(it) } }
|
||||||
|
catch (Exception e) {
|
||||||
|
if (!quiet) {
|
||||||
|
stderr.println "Unable to process file: ${file.canonicalPath}"
|
||||||
|
stderr.println " details: ${e.getLocalizedMessage()}" } }
|
||||||
|
|
||||||
|
FileEntry entry = new FileEntry(
|
||||||
|
file: file,
|
||||||
|
relativePath: getRelativePath(root, file),
|
||||||
|
checksum: checksum )
|
||||||
|
|
||||||
|
analysis.allEntries << entry
|
||||||
|
analysis.byRelativePath[entry.relativePath] = entry
|
||||||
|
|
||||||
|
if (!analysis.byChecksum.containsKey(entry.checksum)) {
|
||||||
|
analysis.byChecksum[entry.checksum] = [] }
|
||||||
|
analysis.byChecksum[entry.checksum] << entry }
|
||||||
|
|
||||||
|
if (progressListener) progressListener.finish()
|
||||||
|
|
||||||
|
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..<childPath.length]).join('/') }
|
||||||
|
|
||||||
|
public File resolvePath(String path, File rootDir) {
|
||||||
|
File f
|
||||||
|
if (path.startsWith('/')) f = new File(path)
|
||||||
|
else f = new File(rootDir, path)
|
||||||
|
|
||||||
|
if (!f.exists()) {
|
||||||
|
strerr.println "TreeDiff v${VERSION}: '${f.canonicalPath}' cannot be found"
|
||||||
|
System.exit(2) }
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verboseOut(String msg) { if (verbose) stdout.println msg }
|
||||||
|
|
||||||
|
private void verboseErr(String msg) { if (verbose) stderr.println msg }
|
||||||
|
|
||||||
|
public String getUsage() {
|
||||||
|
return """\
|
||||||
|
JDB Labs TreeDiff v${VERSION}
|
||||||
|
|
||||||
|
Gather and display information about the differences between two file trees,
|
||||||
|
including files found in only one side and not the other, files that match on
|
||||||
|
both sides, files which share the same contents but reside in differing paths
|
||||||
|
on each side, and files that reside at the same location on both sides but
|
||||||
|
whose contents differ.
|
||||||
|
|
||||||
|
usage: treediff [options] <left-direction> <right-directory>
|
||||||
|
|
||||||
|
where options are:
|
||||||
|
|
||||||
|
-h, --help Output this usage information.
|
||||||
|
-v, --verbose Enable verbose output.
|
||||||
|
-V, --version Output the version information for the utility.
|
||||||
|
-g, --gui Launch the graphical interface (not yet implemented).
|
||||||
|
|
||||||
|
-s, --same
|
||||||
|
|
||||||
|
Output information about files that are the same on both sides.
|
||||||
|
|
||||||
|
-S, --exclude-same
|
||||||
|
|
||||||
|
Do not output information about files that are the same on both sides.
|
||||||
|
|
||||||
|
-c, --content-mismatch
|
||||||
|
|
||||||
|
Output information about files that have the same relative path on both
|
||||||
|
side but whose contents differ.
|
||||||
|
|
||||||
|
-c, --exclude-content-mismatch
|
||||||
|
|
||||||
|
Do not output information about files that have the same relative path
|
||||||
|
on both side but whose contents differ.
|
||||||
|
|
||||||
|
-p, --path-mismatch
|
||||||
|
|
||||||
|
Output information about files that have the same content but reside at
|
||||||
|
different relative paths on each side.
|
||||||
|
|
||||||
|
-P, --exclude-path-mismatch
|
||||||
|
|
||||||
|
Do not output information about files that have the same content but
|
||||||
|
reside at different relative paths on each side.
|
||||||
|
|
||||||
|
-l, --left-only
|
||||||
|
|
||||||
|
Output information about files found on only the left side (missing
|
||||||
|
from the right entirely).
|
||||||
|
|
||||||
|
-L, --exclude-left-only
|
||||||
|
|
||||||
|
Do not output information about files found on the left side only
|
||||||
|
(missing from the right entirely).
|
||||||
|
|
||||||
|
-r, --right-only
|
||||||
|
|
||||||
|
Output information about files found on only the right side (missing
|
||||||
|
from the left entirely).
|
||||||
|
|
||||||
|
-R, --exclude-right-only
|
||||||
|
|
||||||
|
Do not output information about files found on the right side only
|
||||||
|
(missing from the left entirely).
|
||||||
|
|
||||||
|
-q, --quiet
|
||||||
|
|
||||||
|
Suppress all output and error messages except for the progress
|
||||||
|
indicator.
|
||||||
|
|
||||||
|
-Q, --very-quiet
|
||||||
|
|
||||||
|
Suppress all output and error messages including the progress
|
||||||
|
indicator.
|
||||||
|
|
||||||
|
-rd, --direction <directory-path>
|
||||||
|
|
||||||
|
Use <directory-path> as the root for all relative file paths (input
|
||||||
|
directories to scan for example).
|
||||||
|
|
||||||
|
-i, --analysis-in <left-dir-analysis> <right-dir-analysis>
|
||||||
|
|
||||||
|
Use pre-calculated directory analysis in place of reading local
|
||||||
|
directories. This is useful if you wish to do diffs between two
|
||||||
|
directory trees that are not on the same filesystem, or if you wish to
|
||||||
|
display different output about a diff without re-scanning the
|
||||||
|
filesystem.
|
||||||
|
|
||||||
|
-o, --analysis-out <file-name-root>
|
||||||
|
|
||||||
|
In addition to the requested output on STDOUT, write the analysis for
|
||||||
|
each of the scanned directories to files named <file-name-root>.left
|
||||||
|
and <file-name-root>.right. These analysis files are formatted so that
|
||||||
|
they can be used as inputs to the --analysis-in option.
|
||||||
|
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
63
src/main/nim/cliconstants.nim
Normal file
63
src/main/nim/cliconstants.nim
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
const VERSION* = "2.0.1"
|
||||||
|
|
||||||
|
const USAGE* = """
|
||||||
|
Usage:
|
||||||
|
treediff <left> [<right>] [options]
|
||||||
|
treediff (-h | --help)
|
||||||
|
treediff (-V | --version)
|
||||||
|
|
||||||
|
<left> and <right> represent paths to directory roots to be compared. If one
|
||||||
|
of these paths points to a file instead of a directory, treediff assumes that
|
||||||
|
the file represents a saved directory analysis to be loaded in place of a
|
||||||
|
directory to compare. For example:
|
||||||
|
|
||||||
|
treediff /path/to/dir /path/to/output.json
|
||||||
|
|
||||||
|
will analyze the directory tree at '/path/to/dir' to create the left-side
|
||||||
|
analysis and load a pre-existing analysis from '/path/to/output.json' as the
|
||||||
|
right-side analysis.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h --help Show this usage information.
|
||||||
|
-V --version Show the program version.
|
||||||
|
-v --verbose Enable verbose output.
|
||||||
|
-q --quiet Suppress all output and error messages except for the
|
||||||
|
progress indicator.
|
||||||
|
-Q --very-quiet Suppress all output and error messages includeing the
|
||||||
|
progress indicator.
|
||||||
|
|
||||||
|
-1 --save-left <left_out> Save the left analysis to <left_out> (will be
|
||||||
|
formatted as JSON)
|
||||||
|
-2 --save-right <right_out> Save the right analysis to <right_out> (will be
|
||||||
|
formatted as JSON)
|
||||||
|
|
||||||
|
-s --same
|
||||||
|
-S --exclude-same
|
||||||
|
|
||||||
|
Show or hide information about files which are the same in both trees.
|
||||||
|
|
||||||
|
-c --content-mismatch
|
||||||
|
-C --exclude-content-mismatch
|
||||||
|
|
||||||
|
Show or hide information about files whose relative paths are the same
|
||||||
|
in both trees but whose contents differ.
|
||||||
|
|
||||||
|
-p --path-mismatch
|
||||||
|
-P --exclude-path-mismatch
|
||||||
|
|
||||||
|
Show or hide information about files whose contents are the same in both
|
||||||
|
trees but whose relative paths differ.
|
||||||
|
|
||||||
|
-l --left-only
|
||||||
|
-L --exclude-left-only
|
||||||
|
|
||||||
|
Show or hide information about files which are found only in the left
|
||||||
|
tree.
|
||||||
|
|
||||||
|
-r --right-only
|
||||||
|
-R --exclude-right-only
|
||||||
|
|
||||||
|
Show or hide information about files which are found only in the right
|
||||||
|
tree.
|
||||||
|
|
||||||
|
"""
|
40
src/main/nim/incremental_md5.nim
Normal file
40
src/main/nim/incremental_md5.nim
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import std/streams
|
||||||
|
import checksums/md5
|
||||||
|
|
||||||
|
proc fileToMD5*(filename: string) : string =
|
||||||
|
|
||||||
|
const blockSize: int = 8192 # read files in 8KB chunnks
|
||||||
|
var
|
||||||
|
c: MD5Context
|
||||||
|
d: MD5Digest
|
||||||
|
fs: FileStream
|
||||||
|
buffer: string
|
||||||
|
|
||||||
|
#read chunk of file, calling update until all bytes have been read
|
||||||
|
try:
|
||||||
|
fs = filename.open.newFileStream
|
||||||
|
|
||||||
|
md5Init(c)
|
||||||
|
buffer = fs.readStr(blockSize)
|
||||||
|
|
||||||
|
while buffer.len > 0:
|
||||||
|
md5Update(c, buffer.cstring, buffer.len)
|
||||||
|
buffer = fs.readStr(blockSize)
|
||||||
|
|
||||||
|
md5Final(c, d)
|
||||||
|
|
||||||
|
except IOError: echo("File not found.")
|
||||||
|
finally:
|
||||||
|
if fs != nil:
|
||||||
|
close(fs)
|
||||||
|
|
||||||
|
result = $d
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
|
||||||
|
if paramCount() > 0:
|
||||||
|
let arguments = commandLineParams()
|
||||||
|
echo("MD5: ", fileToMD5(arguments[0]))
|
||||||
|
else:
|
||||||
|
echo("Must pass filename.")
|
||||||
|
quit(-1)
|
284
src/main/nim/treediff.nim
Normal file
284
src/main/nim/treediff.nim
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
## Tree Diff
|
||||||
|
## =========
|
||||||
|
##
|
||||||
|
## Utility to compare the file contents of two directory trees.
|
||||||
|
|
||||||
|
import std/[json, jsonutils, os, tables, sequtils, strutils]
|
||||||
|
import docopt
|
||||||
|
import incremental_md5, console_progress
|
||||||
|
|
||||||
|
import ./cliconstants
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
ProgressWrapper* = tuple[impl: Progress, verbosity: Verbosity]
|
||||||
|
## Wrapper around a console_progress.Progress.
|
||||||
|
|
||||||
|
Verbosity* = enum ## Enum representing the level of output verbosity the tool will emit.
|
||||||
|
very_quiet, ## suppress all output including the progress indicator
|
||||||
|
quiet, ## suppress all output except the progress indicator
|
||||||
|
normal ## emit all output
|
||||||
|
|
||||||
|
proc newProgressWrapper*(outFile = stdout, verbosity = normal): ProgressWrapper =
|
||||||
|
## Create a new ProgressWrapper for the given verbosity.
|
||||||
|
if verbosity > very_quiet:
|
||||||
|
result = (impl: newProgress(0, outFile), verbosity: verbosity)
|
||||||
|
else: result = (impl: nil, verbosity: verbosity)
|
||||||
|
|
||||||
|
proc init(p: ProgressWrapper, root: string, fileCount: int): void =
|
||||||
|
if p.verbosity == normal:
|
||||||
|
echo "-- ", root.expandFilename, "\L ", fileCount, " files"
|
||||||
|
if p.verbosity > very_quiet: p.impl.setMax(fileCount)
|
||||||
|
|
||||||
|
proc update(p: ProgressWrapper, count: int, file: string): void =
|
||||||
|
if p.verbosity > very_quiet:
|
||||||
|
p.impl.updateProgress(count, file[max(file.high - 15, 0)..file.high])
|
||||||
|
|
||||||
|
proc finish(p: ProgressWrapper): void =
|
||||||
|
if p.verbosity > very_quiet:
|
||||||
|
p.impl.erase
|
||||||
|
if p.verbosity == normal: echo " ", p.impl.getMax, " files.\L"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc countFiles(root: string): int =
|
||||||
|
for file in walkDirRec(root): result += 1
|
||||||
|
|
||||||
|
proc getRelPath(ancestor, child: string): string =
|
||||||
|
## Given a ancestor path and a child path, assuming the child path is
|
||||||
|
## contained within the ancestor path, return the relative path from the
|
||||||
|
## ancestor to the child.
|
||||||
|
|
||||||
|
let ancestorPath = ancestor.expandFilename.split({DirSep, AltSep})
|
||||||
|
let childPath = child.expandFilename.split({DirSep, AltSep})
|
||||||
|
|
||||||
|
# If the ancestor path is longer it cannot contain the child path and we
|
||||||
|
# cannot construct a relative path without backtracking.
|
||||||
|
if (ancestorPath.len > childPath.len): return ""
|
||||||
|
|
||||||
|
# Compare the ancestor and child path up until the end of the ancestor path.
|
||||||
|
var idx = 0
|
||||||
|
while idx < ancestorPath.len and ancestorPath[idx] == childPath[idx]: idx += 1
|
||||||
|
|
||||||
|
# If we stopped before reaching the end of the ancestor path it must be that
|
||||||
|
# the paths do not match. The ancestor cannot contain the child and we cannot
|
||||||
|
# build a relative path without backtracking.
|
||||||
|
if idx != ancestorPath.len: return ""
|
||||||
|
return foldl(@["."] & childPath[idx..childPath.high], joinPath(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
FileEntry* = ref tuple[relPath: string, checksum: string]
|
||||||
|
## Data about one file that has been analyzed
|
||||||
|
|
||||||
|
DirAnalysis* = ## Analysis data about one directory tree.
|
||||||
|
tuple[allEntries: seq[FileEntry],
|
||||||
|
byRelPath: TableRef[string, FileEntry],
|
||||||
|
byChecksum: TableRef[string, seq[FileEntry]]]
|
||||||
|
|
||||||
|
|
||||||
|
DisplayOptions = tuple[left, right, same, content, path: bool]
|
||||||
|
## Consolidated description of which types of results to display.
|
||||||
|
|
||||||
|
func `$`(f: FileEntry): string = f.checksum & ": " & f.relPath
|
||||||
|
|
||||||
|
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
|
||||||
|
## convenience method to get a key from a JObject or raise an exception
|
||||||
|
if not n.hasKey(key): raise newException(Exception, objName & " missing key '" & key & "'")
|
||||||
|
return n[key]
|
||||||
|
|
||||||
|
proc getIfExists(n: JsonNode, key: string): JsonNode =
|
||||||
|
## convenience method to get a key from a JObject or return null
|
||||||
|
result = if n.hasKey(key): n[key]
|
||||||
|
else: newJNull()
|
||||||
|
|
||||||
|
func parseFileEntry(n: JsonNode): FileEntry =
|
||||||
|
result = new(FileEntry)
|
||||||
|
result.relPath = n.getOrFail("relPath").getStr
|
||||||
|
result.checksum = n.getOrFail("checksum").getStr
|
||||||
|
|
||||||
|
func initDirAnalysis(): DirAnalysis =
|
||||||
|
(allEntries: @[],
|
||||||
|
byRelPath: newTable[string, FileEntry](),
|
||||||
|
byChecksum: newTable[string, seq[FileEntry]]())
|
||||||
|
|
||||||
|
func indexEntries(da: var DirAnalysis) =
|
||||||
|
for e in da.allEntries:
|
||||||
|
da.byRelPath[e.relPath] = e
|
||||||
|
if not da.byChecksum.hasKey(e.checksum):
|
||||||
|
da.byChecksum[e.checksum] = newSeq[FileEntry]()
|
||||||
|
da.byChecksum[e.checksum].add(e)
|
||||||
|
|
||||||
|
proc analyzeDir*(root: string, progress: ProgressWrapper): DirAnalysis =
|
||||||
|
## Inspect a directory and analyze all files, noting their relative paths and
|
||||||
|
## checksum of their contents.
|
||||||
|
let fileCount = countFiles(root)
|
||||||
|
|
||||||
|
progress.init(root, fileCount + 10)
|
||||||
|
|
||||||
|
result = initDirAnalysis()
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
for file in walkDirRec(root):
|
||||||
|
let md5sum = fileToMd5(file)
|
||||||
|
|
||||||
|
var fileEntry: FileEntry = new(FileEntry)
|
||||||
|
fileEntry[] = (relPath: getRelPath(root, file), checksum: md5sum)
|
||||||
|
result.allEntries.add(fileEntry)
|
||||||
|
|
||||||
|
progress.update(count, file)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
result.indexEntries
|
||||||
|
count += 10
|
||||||
|
progress.finish()
|
||||||
|
|
||||||
|
proc loadAnalysis*(path: string): DirAnalysis =
|
||||||
|
## Load a previously performed directory analysis.
|
||||||
|
let allEntriesJson = parseJson(readFile(path))
|
||||||
|
result = initDirAnalysis()
|
||||||
|
result.allEntries = toSeq(items(allEntriesJson)).map(parseFileEntry)
|
||||||
|
result.indexEntries
|
||||||
|
|
||||||
|
proc saveAnalysis*(path: string, analysis: DirAnalysis): void =
|
||||||
|
## Save a completed analysis.
|
||||||
|
writeFile(path, $(analysis.allEntries.toJson))
|
||||||
|
|
||||||
|
proc intersection*(left, right: DirAnalysis): seq[FileEntry] =
|
||||||
|
## Find all ``FileEntry`` that are the same on both sides: matching contents
|
||||||
|
## and paths.
|
||||||
|
return left.allEntries.filter do (item: FileEntry) -> bool:
|
||||||
|
if not right.byRelPath.hasKey(item.relPath): return false
|
||||||
|
let match = right.byRelPath[item.relPath]
|
||||||
|
if match == nil: return false
|
||||||
|
return item.checksum == match.checksum
|
||||||
|
|
||||||
|
proc difference*(left, right: DirAnalysis): seq[FileEntry] =
|
||||||
|
## Find all ``FileEntry`` that are present in the left but not present in
|
||||||
|
## the right.
|
||||||
|
return left.allEntries.filter do (item: FileEntry) -> bool:
|
||||||
|
return not right.byRelPath.hasKey(item.relPath) and
|
||||||
|
not right.byChecksum.hasKey(item.checksum)
|
||||||
|
|
||||||
|
proc `*`*(left, right: DirAnalysis): seq[FileEntry] {.inline.} =
|
||||||
|
## Alias for `intersection(left, right) <#intersection>`_
|
||||||
|
return intersection(left, right)
|
||||||
|
|
||||||
|
proc `-`*(left, right: DirAnalysis): seq[FileEntry] {.inline.} =
|
||||||
|
## Alias for `difference(left, right) <#difference>`_
|
||||||
|
return difference(left, right)
|
||||||
|
|
||||||
|
proc samePathDifferentContents*(left, right: DirAnalysis): seq[string] =
|
||||||
|
## Find all ``FileEntry`` that have the same paths in both trees but whose
|
||||||
|
## contents differ.
|
||||||
|
let matchingEntries = left.allEntries.filter do (item: FileEntry) -> bool:
|
||||||
|
if not right.byRelPath.hasKey(item.relPath): return false
|
||||||
|
let match = right.byRelPath[item.relPath]
|
||||||
|
return item.checksum != match.checksum
|
||||||
|
return matchingEntries.map(proc(item: FileEntry): string = return item.relPath)
|
||||||
|
|
||||||
|
proc sameContentsDifferentPaths*(left, right: DirAnalysis): seq[tuple[left, right: FileEntry]] =
|
||||||
|
## Find all ``FileEntry`` whose contents are the same in both trees but
|
||||||
|
## which are located at differenc paths.
|
||||||
|
result = @[]
|
||||||
|
for item in left.allEntries:
|
||||||
|
if not right.byChecksum.hasKey(item.checksum): continue
|
||||||
|
for match in right.byChecksum[item.checksum]:
|
||||||
|
if item.relPath != match.relPath: result.add((left: item, right:match))
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
|
||||||
|
let quitWithError = proc (error: string): void =
|
||||||
|
stderr.writeLine("treediff: " & error)
|
||||||
|
quit(QuitFailure)
|
||||||
|
|
||||||
|
let args = docopt(USAGE, version = "treediff " & VERSION)
|
||||||
|
|
||||||
|
var verbosity = normal
|
||||||
|
if args["--quiet"]: verbosity = quiet
|
||||||
|
if args["--very-quiet"]: verbosity = very_quiet
|
||||||
|
let progressWrapper = newProgressWrapper(verbosity = verbosity)
|
||||||
|
|
||||||
|
# Load or perform analysis
|
||||||
|
if not args["<left>"]:
|
||||||
|
quitWithError("Missing <left> parameter.")
|
||||||
|
|
||||||
|
let leftPath: string = $args["<left>"]
|
||||||
|
|
||||||
|
let loadPath = proc (path: string): DirAnalysis =
|
||||||
|
if not path.fileExists and not path.dirExists:
|
||||||
|
quitWithError($path & ": no such file or directory.")
|
||||||
|
|
||||||
|
let fileInfo = path.getFileInfo
|
||||||
|
if fileInfo.kind == pcDir:
|
||||||
|
return analyzeDir(path, progressWrapper)
|
||||||
|
elif fileInfo.kind == pcFile:
|
||||||
|
result = loadAnalysis(path)
|
||||||
|
else:
|
||||||
|
quitWithError($path & ": is not a file or directory")
|
||||||
|
|
||||||
|
var leftAnalysis, rightAnalysis: DirAnalysis
|
||||||
|
|
||||||
|
leftAnalysis = loadPath(leftPath)
|
||||||
|
|
||||||
|
if not args["<right>"]:
|
||||||
|
rightAnalysis = (allEntries: @[],
|
||||||
|
byRelPath: newTable[string, FileEntry](),
|
||||||
|
byChecksum: newTable[string, seq[FileEntry]]())
|
||||||
|
else:
|
||||||
|
var rightPath: string = $args["<right>"]
|
||||||
|
rightAnalysis = loadPath(rightPath)
|
||||||
|
|
||||||
|
# Check for output options
|
||||||
|
if args["--save-left"]:
|
||||||
|
saveAnalysis($args["--save-left"], leftAnalysis)
|
||||||
|
|
||||||
|
if args["--save-right"] and rightAnalysis.allEntries.len > 0:
|
||||||
|
saveAnalysis($args["--save-right"], rightAnalysis)
|
||||||
|
|
||||||
|
# Parse filter options
|
||||||
|
var displayOptions: DisplayOptions = (
|
||||||
|
left: false, right: false, same: false, content: false, path: false)
|
||||||
|
|
||||||
|
# If none of the explicit selectors are given, assume all are expected.
|
||||||
|
if not (args["--left-only"] or args["--right-only"] or
|
||||||
|
args["--same"] or args["--content-mismatch"] or
|
||||||
|
args["--path-mismatch"] ):
|
||||||
|
displayOptions = (left: true, right: true, same: true,
|
||||||
|
content: true, path: true)
|
||||||
|
|
||||||
|
if args["--same"]: displayOptions.same = true
|
||||||
|
if args["--exclude-same"]: displayOptions.same = false
|
||||||
|
if args["--content-mismatch"]: displayOptions.content = true
|
||||||
|
if args["--exclude-content-mismatch"]: displayOptions.content = false
|
||||||
|
if args["--path-mismatch"]: displayOptions.path = true
|
||||||
|
if args["--exclude-path-mismatch"]: displayOptions.path = false
|
||||||
|
if args["--left-only"]: displayOptions.left = true
|
||||||
|
if args["--exclude-left-only"]: displayOptions.left = false
|
||||||
|
if args["--right-only"]: displayOptions.right = true
|
||||||
|
if args["--exclude-right-only"]: displayOptions.right = false
|
||||||
|
|
||||||
|
# Display output results
|
||||||
|
if verbosity == normal:
|
||||||
|
if displayOptions.same:
|
||||||
|
let sameEntries = leftAnalysis * rightAnalysis
|
||||||
|
for fe in sameEntries: echo "same: ", fe.relPath
|
||||||
|
|
||||||
|
if displayOptions.content:
|
||||||
|
let contentsDiffer = samePathDifferentContents(leftAnalysis, rightAnalysis)
|
||||||
|
for path in contentsDiffer: echo "contents differ: ", path
|
||||||
|
|
||||||
|
if displayOptions.path:
|
||||||
|
let pathsDiffer = sameContentsDifferentPaths(leftAnalysis, rightAnalysis)
|
||||||
|
for pair in pathsDiffer:
|
||||||
|
echo "paths differ: ", pair.left.relPath, " ", pair.right.relPath
|
||||||
|
|
||||||
|
if displayOptions.left:
|
||||||
|
let leftOnly = leftAnalysis - rightAnalysis
|
||||||
|
for fe in leftOnly: echo "left only: ", fe.relPath
|
||||||
|
|
||||||
|
if displayOptions.right:
|
||||||
|
let rightOnly = rightAnalysis - leftAnalysis
|
||||||
|
for fe in rightOnly: echo "right only: ", fe.relPath
|
39
treediff.nim
39
treediff.nim
@ -1,39 +0,0 @@
|
|||||||
import os, docopt, tables, md5, iterutils, re
|
|
||||||
|
|
||||||
proc studyDir(root: string, ignore: Iterable[string]): TableRef[string, string] =
|
|
||||||
result = newTable[string, string]()
|
|
||||||
|
|
||||||
for path in walkDirRec(root):
|
|
||||||
var relPath = substr(path, len(root))
|
|
||||||
|
|
||||||
if foldl(ignore, proc (acc: bool, it: string): bool = acc and match(relPath, re(it)), true): continue
|
|
||||||
|
|
||||||
var fileInfo = getFileInfo(path)
|
|
||||||
|
|
||||||
if fileInfo.kind == pcFile:
|
|
||||||
result.add(relPath, $(toMD5(readFile(path))))
|
|
||||||
elif fileInfo.kind == pcDir:
|
|
||||||
result.add(relPath, "directory")
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
|
|
||||||
let doc = """
|
|
||||||
treediff
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
treediff [-i <regex>]... [<path>]...
|
|
||||||
treediff (-h | --help)
|
|
||||||
treediff (-v | --version)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h --help Show this usage information.
|
|
||||||
-v --version Show the program version.
|
|
||||||
"""
|
|
||||||
|
|
||||||
let args = docopt(doc, version = "treediff 0.1")
|
|
||||||
|
|
||||||
for root in @(args["<path>"]):
|
|
||||||
echo "Looking at ", root
|
|
||||||
|
|
||||||
echo studyDir(root, @(args["<regex>"]))
|
|
||||||
echo ""
|
|
@ -1,11 +1,16 @@
|
|||||||
[Package]
|
# Package
|
||||||
name = "treeediff"
|
version = "2.0.1"
|
||||||
version = "0.1.0"
|
|
||||||
author = "Jonathan Bernard (jdb@jdb-labs.com)"
|
author = "Jonathan Bernard (jdb@jdb-labs.com)"
|
||||||
description = "Tree Diff"
|
description = "Utility to generate diffs of full directory trees."
|
||||||
license = "BSD"
|
license = "BSD"
|
||||||
|
bin = @["treediff"]
|
||||||
|
srcDir = "src/main/nim"
|
||||||
|
|
||||||
bin = "treediff"
|
# Dependencies
|
||||||
|
requires: @["nim >= 2.0.0", "docopt == 0.7.1", "checksums"]
|
||||||
|
|
||||||
[Deps]
|
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
||||||
Requires: "nim >= 0.10.0, docopt >= 0.1.0, iterutils >= 0.1.0"
|
requires: @["console_progress >= 1.2.2", "update_nim_package_version"]
|
||||||
|
|
||||||
|
task updateVersion, "Update the version of this package.":
|
||||||
|
exec "update_nim_package_version treediff 'src/main/nim/cliconstants.nim'"
|
||||||
|
6
worklog.md
Normal file
6
worklog.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
To Do
|
||||||
|
========================================
|
||||||
|
|
||||||
|
* Rework the JSON output format so that
|
||||||
|
the Groovy and Nim implementations can
|
||||||
|
read each other's saved analysis.
|
Loading…
x
Reference in New Issue
Block a user