add directory-examining recursive compare derp(1)
This commit is contained in:
parent
5cb6be9ce3
commit
c0c2660f74
2 changed files with 606 additions and 0 deletions
132
sys/man/1/derp
Normal file
132
sys/man/1/derp
Normal file
|
@ -0,0 +1,132 @@
|
|||
.TH DERP 1
|
||||
.SH NAME
|
||||
derp \- directory-examining recursive compare
|
||||
.SH SYNOPSIS
|
||||
.B derp
|
||||
[
|
||||
.B -qcutDL
|
||||
] [
|
||||
.B -p
|
||||
.I perms
|
||||
]
|
||||
.I myfile
|
||||
.I oldfile
|
||||
.I yourfile
|
||||
.SH DESCRIPTION
|
||||
.I Derp
|
||||
recursively compares the two directories
|
||||
.I myfile
|
||||
and
|
||||
.I yourfile
|
||||
using a third common backup directory
|
||||
.I oldfile
|
||||
as reference. The changes found are printed
|
||||
to standard output, one per line, with the file
|
||||
status describing either side followed by tabulator
|
||||
and the relative file path.
|
||||
.LP
|
||||
The possible status codes:
|
||||
.TP
|
||||
.B an
|
||||
File added in
|
||||
.I myfile
|
||||
.TP
|
||||
.B na
|
||||
File added in
|
||||
.I yourfile
|
||||
.TP
|
||||
.B aa!
|
||||
Both sides added different files with the
|
||||
same name
|
||||
.TP
|
||||
.B mn
|
||||
File was modified in
|
||||
.I myfile
|
||||
.TP
|
||||
.B nm
|
||||
File was modified in
|
||||
.I yourfile
|
||||
.TP
|
||||
.B mm!
|
||||
File was changed differently in
|
||||
.I myfile
|
||||
and
|
||||
.I yourfile
|
||||
.TP
|
||||
.B dn
|
||||
File was deleted in
|
||||
.I myfile
|
||||
.TP
|
||||
.B nd
|
||||
File was deleted in
|
||||
.I yourfile
|
||||
.TP
|
||||
.B md!
|
||||
File was modified in
|
||||
.I myfile
|
||||
but deleted in
|
||||
.I yourfile
|
||||
.TP
|
||||
.B dm!
|
||||
File was modified in
|
||||
.I yourfile
|
||||
but deleted in
|
||||
.I myfile
|
||||
.LP
|
||||
Errors are printed to standard error unless
|
||||
.B -q
|
||||
option is specified. The program is terminated
|
||||
when errors are encountered unless the
|
||||
.B -c
|
||||
option is given. This can be usefull if files
|
||||
are not accessible due to file permission or
|
||||
media corruption.
|
||||
.PP
|
||||
The
|
||||
.B -u
|
||||
option will consider changes of file owner and group.
|
||||
When omited, file ownership is ignored.
|
||||
.PP
|
||||
The
|
||||
.B -p
|
||||
option sets the octal mask
|
||||
.I perms
|
||||
of bits to check in the file permissions. The default
|
||||
ignores file permissions.
|
||||
.PP
|
||||
When modification times are comparable then the
|
||||
.B -t
|
||||
option can be used to quickly find changes. If specified,
|
||||
files are considered unchanged if the name, file size and
|
||||
the modification time matches. This is usefull when
|
||||
comparing /n/dump archives on the same fileserver.
|
||||
.PP
|
||||
Files are considered the same if they are from the
|
||||
same mount and ther
|
||||
.B qid
|
||||
(see
|
||||
.IR stat (5))
|
||||
matches. For directories, the access time is also
|
||||
compared. If the access time was disabled on the
|
||||
fileserver, then all directories need to be compared
|
||||
using the
|
||||
.B -D
|
||||
option.
|
||||
.PP
|
||||
Some filesystems like
|
||||
.IR hgfs (4)
|
||||
do not always return exact file size in stat, so
|
||||
the length check can be disabled with the
|
||||
.B -L
|
||||
option.
|
||||
.SH SOURCE
|
||||
.B /sys/src/cmd/derp.c
|
||||
.SH "SEE ALSO"
|
||||
.IR cmp (1),
|
||||
.IR diff (1),
|
||||
.IR history (1),
|
||||
.IR fs (4),
|
||||
.IR hgfs (4)
|
||||
.SH DIAGNOSTICS
|
||||
The exit status is set to 'errors' when
|
||||
errors where encountered.
|
474
sys/src/cmd/derp.c
Normal file
474
sys/src/cmd/derp.c
Normal file
|
@ -0,0 +1,474 @@
|
|||
#include <u.h>
|
||||
#include <libc.h>
|
||||
|
||||
int permcheck = 0;
|
||||
int usercheck = 0;
|
||||
int dumpcheck = 1;
|
||||
int sizecheck = 1;
|
||||
int timecheck = 0;
|
||||
|
||||
int quiet = 0;
|
||||
int errors = 0;
|
||||
int noerror = 0;
|
||||
|
||||
void
|
||||
error(char *fmt, ...)
|
||||
{
|
||||
char buf[ERRMAX];
|
||||
va_list a;
|
||||
|
||||
errors++;
|
||||
if(!quiet){
|
||||
va_start(a, fmt);
|
||||
vsnprint(buf, sizeof(buf), fmt, a);
|
||||
va_end(a);
|
||||
|
||||
fprint(2, "%s: %s\n", argv0, buf);
|
||||
}
|
||||
if(!noerror)
|
||||
exits("errors");
|
||||
}
|
||||
|
||||
#pragma varargck argpos error 1
|
||||
|
||||
enum {
|
||||
BUFSIZE = 8*1024,
|
||||
};
|
||||
|
||||
int
|
||||
cmpfile(char *a, char *b)
|
||||
{
|
||||
static uchar buf1[BUFSIZE], buf2[BUFSIZE];
|
||||
int r, n, m, fd1, fd2;
|
||||
|
||||
if((fd1 = open(a, OREAD)) < 0)
|
||||
error("can't open %s: %r", a);
|
||||
if((fd2 = open(b, OREAD)) < 0)
|
||||
error("can't open %s: %r", b);
|
||||
|
||||
r = fd1 != fd2;
|
||||
if(fd1 >= 0 && fd2 >= 0)
|
||||
do{
|
||||
if((n = readn(fd1, buf1, sizeof(buf1))) < 0){
|
||||
error("can't read %s: %r", a);
|
||||
break;
|
||||
}
|
||||
m = n;
|
||||
if(m == 0)
|
||||
m++; /* read past eof to verify size */
|
||||
if((m = readn(fd2, buf2, m)) < 0){
|
||||
error("can't read %s: %r", b);
|
||||
break;
|
||||
}
|
||||
if(m != n)
|
||||
break;
|
||||
if(n == 0){
|
||||
r = 0;
|
||||
break;
|
||||
}
|
||||
} while(memcmp(buf1, buf2, n) == 0);
|
||||
|
||||
if(fd1 >= 0)
|
||||
close(fd1);
|
||||
if(fd2 >= 0)
|
||||
close(fd2);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
int
|
||||
samefile(Dir *a, Dir *b)
|
||||
{
|
||||
if(a == b)
|
||||
return 1;
|
||||
|
||||
if(a->type == b->type && a->dev == b->dev &&
|
||||
a->qid.type == b->qid.type &&
|
||||
a->qid.path == b->qid.path &&
|
||||
a->qid.vers == b->qid.vers){
|
||||
|
||||
if((a->qid.type & QTDIR) == 0)
|
||||
return 1;
|
||||
|
||||
/*
|
||||
* directories in /n/dump have the same qid, but
|
||||
* atime can be used to skip potentially
|
||||
* untouched subtrees.
|
||||
*/
|
||||
if(dumpcheck && a->atime == b->atime)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
dcmp(Dir *a, Dir *b)
|
||||
{
|
||||
if(a == nil || b == nil)
|
||||
return a != b;
|
||||
|
||||
if(samefile(a, b))
|
||||
return 0;
|
||||
|
||||
if((a->qid.type | b->qid.type) & QTDIR)
|
||||
return 1;
|
||||
|
||||
if((a->mode ^ b->mode) & permcheck)
|
||||
return 1;
|
||||
|
||||
if(usercheck){
|
||||
if(strcmp(a->uid, b->uid) != 0)
|
||||
return 1;
|
||||
if(strcmp(a->gid, b->gid) != 0)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(sizecheck)
|
||||
if(a->length != b->length)
|
||||
return 1;
|
||||
|
||||
if(timecheck)
|
||||
if(a->mtime != b->mtime)
|
||||
return 1;
|
||||
|
||||
if(sizecheck && timecheck)
|
||||
return 0;
|
||||
|
||||
return cmpfile(a->name, b->name);
|
||||
}
|
||||
|
||||
Dir*
|
||||
statdir(char *path)
|
||||
{
|
||||
Dir *d;
|
||||
|
||||
d = dirstat(path);
|
||||
if(d == nil)
|
||||
error("can't stat %s: %r", path);
|
||||
else
|
||||
d->name = strdup(path);
|
||||
return d;
|
||||
}
|
||||
|
||||
Dir*
|
||||
absdir(Dir *d, char *path)
|
||||
{
|
||||
if(d != nil)
|
||||
d->name = smprint("%s/%s", path, d->name);
|
||||
return d;
|
||||
}
|
||||
|
||||
void
|
||||
cleardir(Dir *d)
|
||||
{
|
||||
if(d != nil){
|
||||
free(d->name);
|
||||
d->name = "";
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
freedir(Dir *d)
|
||||
{
|
||||
cleardir(d);
|
||||
free(d);
|
||||
}
|
||||
|
||||
int
|
||||
dnamecmp(void *a, void *b)
|
||||
{
|
||||
return strcmp(((Dir*)a)->name, ((Dir*)b)->name);
|
||||
}
|
||||
|
||||
int
|
||||
readifdir(Dir **dp)
|
||||
{
|
||||
int n, fd;
|
||||
Dir *d;
|
||||
|
||||
d = *dp;
|
||||
*dp = nil;
|
||||
if(d == nil || (d->qid.type & QTDIR) == 0)
|
||||
return 0;
|
||||
fd = open(d->name, OREAD);
|
||||
if(fd < 0){
|
||||
error("can't open %s: %r", d->name);
|
||||
return -1;
|
||||
}
|
||||
if((n = dirreadall(fd, dp)) < 0)
|
||||
error("can't read %s: %r", d->name);
|
||||
close(fd);
|
||||
if(n > 1)
|
||||
qsort(*dp, n, sizeof(Dir), dnamecmp);
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
diffgen(Dir *ld, Dir *rd, Dir *ad, char *path);
|
||||
|
||||
void
|
||||
diffdir(Dir *ld, Dir *rd, Dir *ad, char *path)
|
||||
{
|
||||
int n, m, o, i, j, k, t, v;
|
||||
char *sp, *lp, *rp, *ap;
|
||||
Dir *od;
|
||||
|
||||
lp = rp = ap = nil;
|
||||
if(ld != nil)
|
||||
lp = ld->name;
|
||||
if(rd != nil)
|
||||
rp = rd->name;
|
||||
if(ad != nil){
|
||||
/* check if ad is the same as ld or rd */
|
||||
if(ld != nil && samefile(ad, ld)){
|
||||
ap = ld->name;
|
||||
ad = nil; /* don't read directory twice */
|
||||
}
|
||||
else if(rd != nil && samefile(ad, rd)){
|
||||
ap = rd->name;
|
||||
ad = nil; /* don't read directory twice */
|
||||
}
|
||||
else
|
||||
ap = ad->name;
|
||||
}
|
||||
|
||||
n = readifdir(&ld);
|
||||
m = readifdir(&rd);
|
||||
if(n <= 0 && m <= 0)
|
||||
return;
|
||||
|
||||
/* at least one side is directory */
|
||||
o = readifdir(&ad);
|
||||
|
||||
i = j = k = 0;
|
||||
for(;;){
|
||||
if(i < n)
|
||||
t = (j < m) ? strcmp(ld[i].name, rd[j].name) : -1;
|
||||
else if(j < m)
|
||||
t = 1;
|
||||
else
|
||||
break;
|
||||
|
||||
od = nil;
|
||||
if(t < 0){
|
||||
sp = smprint("%s/%s", path, ld[i].name);
|
||||
if(ap == lp)
|
||||
od = &ld[i];
|
||||
else while(k < o){
|
||||
v = strcmp(ad[k].name, ld[i].name);
|
||||
if(v == 0){
|
||||
od = absdir(&ad[k++], ap);
|
||||
break;
|
||||
} else if(v > 0)
|
||||
break;
|
||||
k++;
|
||||
}
|
||||
diffgen(absdir(&ld[i], lp), nil, od, sp);
|
||||
cleardir(&ld[i]);
|
||||
if(&ld[i] == od)
|
||||
od = nil;
|
||||
i++;
|
||||
} else {
|
||||
sp = smprint("%s/%s", path, rd[j].name);
|
||||
if(ap == rp)
|
||||
od = &rd[j];
|
||||
else while(k < o){
|
||||
v = strcmp(ad[k].name, rd[j].name);
|
||||
if(v == 0){
|
||||
od = absdir(&ad[k++], ap);
|
||||
break;
|
||||
} else if(v > 0)
|
||||
break;
|
||||
k++;
|
||||
}
|
||||
if(t > 0)
|
||||
diffgen(nil, absdir(&rd[j], rp), od, sp);
|
||||
else {
|
||||
if(ap == lp)
|
||||
od = &ld[i];
|
||||
diffgen(absdir(&ld[i], lp), absdir(&rd[j], rp), od, sp);
|
||||
cleardir(&ld[i]);
|
||||
if(&ld[i] == od)
|
||||
od = nil;
|
||||
i++;
|
||||
}
|
||||
cleardir(&rd[j]);
|
||||
if(&rd[j] == od)
|
||||
od = nil;
|
||||
j++;
|
||||
}
|
||||
cleardir(od);
|
||||
free(sp);
|
||||
}
|
||||
|
||||
free(ld);
|
||||
free(rd);
|
||||
free(ad);
|
||||
}
|
||||
|
||||
void
|
||||
diffgen(Dir *ld, Dir *rd, Dir *ad, char *path)
|
||||
{
|
||||
char *p;
|
||||
|
||||
if(dcmp(ld, rd) == 0)
|
||||
return;
|
||||
|
||||
p = nil;
|
||||
if(ld == nil || rd == nil){
|
||||
/* one side doesnt exit anymore */
|
||||
if(ad != nil){
|
||||
/* existed before, is deletion */
|
||||
if(ld != nil && (ad->qid.type & QTDIR) && (ld->qid.type & QTDIR)){
|
||||
/* remote deleted direcotry, remote newer */
|
||||
p = smprint("nd\t%s\n", path);
|
||||
} else if(rd != nil && (ad->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
|
||||
/* local deleted direcotry, local newer */
|
||||
p = smprint("dn\t%s\n", path);
|
||||
} else if(dcmp(rd, ad) == 0){
|
||||
/* local deleted file, local newer */
|
||||
print("dn\t%s\n", path);
|
||||
} else if(dcmp(ld, ad) == 0){
|
||||
/* remote deleted file, remote newer */
|
||||
print("nd\t%s\n", path);
|
||||
} else if(ld != nil){
|
||||
/* local modified, remote deleted, conflict */
|
||||
print("md!\t%s\n", path);
|
||||
} else {
|
||||
/* remote modified, local deleted, conflict */
|
||||
print("dm!\t%s\n", path);
|
||||
}
|
||||
} else {
|
||||
/* didnt exist before, is addition */
|
||||
if(ld != nil){
|
||||
/* local added file, local newer */
|
||||
print("an\t%s\n", path);
|
||||
} else {
|
||||
/* remote added file, remote newer */
|
||||
print("na\t%s\n", path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(ad != nil){
|
||||
/* existed before, is modification */
|
||||
if((ad->qid.type & QTDIR) && (ld->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
|
||||
/* all still directories, no problem */
|
||||
} else if(dcmp(rd, ad) == 0){
|
||||
/* local modified file, local newer */
|
||||
print("mn\t%s\n", path);
|
||||
} else if(dcmp(ld, ad) == 0){
|
||||
/* remote modified file, remote newer */
|
||||
print("nm\t%s\n", path);
|
||||
} else {
|
||||
/* local and remote modified, conflict */
|
||||
print("mm!\t%s\n", path);
|
||||
}
|
||||
} else {
|
||||
/* didnt exist before, is addition from both */
|
||||
if((ld->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
|
||||
/* local and remote added directories, no problem */
|
||||
} else {
|
||||
/* local and remote added files, conflict */
|
||||
print("aa!\t%s\n", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diffdir(ld, rd, ad, path);
|
||||
|
||||
if(p != nil){
|
||||
print("%s", p);
|
||||
free(p);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
diff3(char *lp, char *ap, char *rp)
|
||||
{
|
||||
Dir *ld, *rd, *ad;
|
||||
char *name;
|
||||
|
||||
rd = ad = nil;
|
||||
if((ld = statdir(lp)) == nil)
|
||||
goto Out;
|
||||
if((rd = statdir(rp)) == nil)
|
||||
goto Out;
|
||||
|
||||
if(strcmp(ap, lp) == 0)
|
||||
ad = ld;
|
||||
else if(strcmp(ap, rp) == 0)
|
||||
ad = rd;
|
||||
else if((ad = statdir(ap)) == nil)
|
||||
goto Out;
|
||||
else if(samefile(ad, ld)){
|
||||
freedir(ad);
|
||||
ad = ld;
|
||||
}
|
||||
else if(samefile(ad, rd)){
|
||||
freedir(ad);
|
||||
ad = rd;
|
||||
}
|
||||
|
||||
if(ld->qid.type & QTDIR)
|
||||
name = ".";
|
||||
else {
|
||||
if(name = strrchr(lp, '/'))
|
||||
name++;
|
||||
else
|
||||
name = lp;
|
||||
}
|
||||
diffgen(ld, rd, ad, name);
|
||||
Out:
|
||||
freedir(ld);
|
||||
freedir(rd);
|
||||
if(ad != nil && ad != ld && ad != rd)
|
||||
freedir(ad);
|
||||
}
|
||||
|
||||
void
|
||||
usage(void)
|
||||
{
|
||||
fprint(2, "usage: %s [ -qcutDL ] [ -p perms ] myfile oldfile yourfile\n", argv0);
|
||||
exits("usage");
|
||||
}
|
||||
|
||||
void
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
ARGBEGIN {
|
||||
case 'q':
|
||||
quiet = 1;
|
||||
break;
|
||||
case 'c':
|
||||
noerror = 1;
|
||||
break;
|
||||
case 'u':
|
||||
usercheck = 1;
|
||||
break;
|
||||
case 't':
|
||||
timecheck = 1;
|
||||
break;
|
||||
case 'D':
|
||||
dumpcheck = 0;
|
||||
break;
|
||||
case 'L':
|
||||
sizecheck = 0;
|
||||
break;
|
||||
case 'p':
|
||||
permcheck = strtol(EARGF(usage()), nil, 8) & 0777;
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
} ARGEND;
|
||||
|
||||
if(argc != 3)
|
||||
usage();
|
||||
|
||||
diff3(argv[0], argv[1], argv[2]);
|
||||
|
||||
if(errors)
|
||||
exits("errors");
|
||||
|
||||
exits(0);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue