#include "common.hpp"
#include "dir.hpp"
#include <fcntl.h>
#include <sys/stat.h>
typedef unsigned op_t;
#define OP_NUM_UPDATE_A 1u
#define OP_NUM_UPDATE_B 2u
#define OP_NUM_REPLACE_A 3u
#define OP_NUM_REPLACE_B 4u
#define OP_NUM_CREATE_A 5u
#define OP_NUM_CREATE_B 6u
#define OP_NUM_DELETE_A 7u
#define OP_NUM_DELETE_B 8u
#define OP_NUM_DELETE_ALL 9u
#define OP_UPDATE_A (1u<<(OP_NUM_UPDATE_A-1))
#define OP_UPDATE_B (1u<<(OP_NUM_UPDATE_B-1))
#define OP_REPLACE_A (1u<<(OP_NUM_REPLACE_A-1))
#define OP_REPLACE_B (1u<<(OP_NUM_REPLACE_B-1))
#define OP_CREATE_A (1u<<(OP_NUM_CREATE_A-1))
#define OP_CREATE_B (1u<<(OP_NUM_CREATE_B-1))
#define OP_DELETE_A (1u<<(OP_NUM_DELETE_A-1))
#define OP_DELETE_B (1u<<(OP_NUM_DELETE_B-1))
#define OP_DELETE_ALL (1u<<(OP_NUM_DELETE_ALL-1))
#define OP_UNKNOWN (1u<<OP_NUM_DELETE_ALL)
static struct {
bool dry_run;
bool yes_to_all;
const char* backup_dir[2];
int backup[2]; // dirfds
char delete_excluded;
char keep;
} config = {};
static const char* exclude[] = {
"*.o",
".svn",
".bzr",
".git",
"build",
"Thumbs.db",
"a.out",
"tmp",
"*.tmp",
"* - tmp",
"old",
"bup",
"backup",
"*.bup",
"*.old",
"*~",
"~*",
"*.pyc",
"__pycache__",
".mypy_cache",
"venv",
"libraries",
"Debug",
"Release",
"workspace",
"*.workspace",
".project",
".idea",
".vscode",
NULL
};
static char get_char() {
static char rv[16];
if (!fgets(rv, sizeof(rv), stdin)) *rv = 0;
return *rv;
}
static unsigned numbits(unsigned n) {
unsigned rv = 0;
while (n) {
rv += n & 1;
n >>= 1;
}
return rv;
}
static op_t confirm(op_t op, const char* desc_a, const char* desc_b, bool choose) {
assert(op);
const char* s; // strip common path prefix for legibility
while ((s = strchr(desc_a, '/')) != NULL) {
if (strncmp(desc_a, desc_b, s-desc_a+1) != 0) break;
desc_b += s-desc_a+1;
desc_a = s+1;
}
if (choose && numbits(op) == 1) {
choose = false;
}
if (choose) printf(" 0) skip\n");
if (op & OP_UPDATE_A) printf(" %d) " COLOR(YELLOW, "update") " in %s\n", OP_NUM_UPDATE_A, desc_a);
if (op & OP_UPDATE_B) printf(" %d) " COLOR(YELLOW, "update") " in %s\n", OP_NUM_UPDATE_B, desc_b);
if (op & OP_REPLACE_A) printf(" %d) " COLOR(RED, "replace") " in %s\n", OP_NUM_REPLACE_A, desc_a);
if (op & OP_REPLACE_B) printf(" %d) " COLOR(RED, "replace") " in %s\n", OP_NUM_REPLACE_B, desc_b);
if (op & OP_CREATE_A) printf(" %d) " COLOR(GREEN, "create") " in %s\n", OP_NUM_CREATE_A, desc_a);
if (op & OP_CREATE_B) printf(" %d) " COLOR(GREEN, "create") " in %s\n", OP_NUM_CREATE_B, desc_b);
if (op & OP_DELETE_A) printf(" %d) " COLOR(RED, "delete") " in %s\n", OP_NUM_DELETE_A, desc_a);
if (op & OP_DELETE_B) printf(" %d) " COLOR(RED, "delete") " in %s\n", OP_NUM_DELETE_B, desc_b);
if (op & OP_DELETE_ALL) printf(" %d) " COLOR(RED, "delete") " in %s " COLOR(RED, "and") " %s\n", OP_NUM_DELETE_ALL, desc_a, desc_b);
if (config.dry_run) {
printf("[skipped %s]\n", choose? "selection": "operation");
return 0;
} else if (config.yes_to_all) {
if (choose) {
if (!(op & (op - 1))) { // is a power of 2; only one set
printf(COLOR(YELLOW, "[yes]") "\n");
return op;
} else {
printf(COLOR(RED, "[skipped]") "\n");
return 0;
}
} else {
printf(COLOR(GREEN, "[yes]") "\n");
return op;
}
}
while (true) {
if (choose) {
printf("[0-9] which one? ");
} else {
printf("[y/n/?] all of those? ");
}
fflush(stdout);
char rv = get_char();
if (choose) {
if (rv < '0' || rv > '9') continue;
if (rv == '0') return 0;
op_t chosen = 1u << ((rv - '0')-1);
if ((op & chosen) != 0) return chosen;
} else {
switch(rv) {
case 'y':
return op;
case 'n':
return 0;
case '?':
return OP_UNKNOWN; // only case for this
}
if (rv != 'y' && rv != 'n' && rv != 's') continue;
return (rv == 'y')? op: 0;
}
}
}
static void log_transfer(const Ent* a, const Ent* b, bool success) {
Ent::print(a, " ", "\n");
Ent::print(b, success? COLOR(GREEN, " -> "): COLOR(RED, " X> "), "\n");
}
static void log_info(const Ent* a, const char* desc_a, const Ent* b, const char* desc_b) {
Ent::print(a, "\n >> ", " @ ");
puts(desc_a);
Ent::print(b, " << ", " @ ");
puts(desc_b);
}
op_t proposal(const Dir* dir_a, const char* desc_a, const Ent* a, const Dir* dir_b, const char* desc_b, const Ent* b) {
assert(a || b);
op_t proposal = 0;
op_t possible = 0;
bool dir_a_newer = dir_a->attr->mtime() > dir_b->attr->mtime();
bool dir_b_newer = dir_b->attr->mtime() > dir_a->attr->mtime();
if ((a && a->type == Ent::TYPE_NONE) || (b && b->type == Ent::TYPE_NONE)) {
// cannot even delete -> skip
} else if (matches(exclude, a? a->name: b->name)) { // skip silently or propose a delete if configured
if (a && (config.delete_excluded == 'a' || config.delete_excluded == '*')) proposal |= OP_DELETE_A;
if (b && (config.delete_excluded == 'b' || config.delete_excluded == '*')) proposal |= OP_DELETE_B;
if (proposal) { // chose to possibly delete instead of silently skip, so propose other side's deletion, too
if (a) possible |= OP_DELETE_A;
if (b) possible |= OP_DELETE_B;
}
} else if (!a || !b) { // create/delete
if (dir_a_newer) {
proposal |= a? OP_CREATE_B: OP_DELETE_B;
} else if (dir_b_newer) {
proposal |= b? OP_CREATE_A: OP_DELETE_A;
}
possible |= a? OP_DELETE_A|OP_CREATE_B: OP_CREATE_A|OP_DELETE_B;
} else { // both exist: update?
switch (a->cmp(b)) {
case Attr::FAIL:
break;
case Attr::SAME:
break;
case Attr::ATTR_THIS:
proposal |= OP_UPDATE_B;
possible |= OP_UPDATE_A;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::ATTR_THAT:
proposal |= OP_UPDATE_A;
possible |= OP_UPDATE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::ATTR_CONFLICT:
possible |= OP_UPDATE_A;
possible |= OP_UPDATE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::FULL_THIS:
proposal |= OP_REPLACE_B;
possible |= OP_REPLACE_A;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::FULL_THAT:
proposal |= OP_REPLACE_A;
possible |= OP_REPLACE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::FULL_OR_ATTR_THIS:
proposal |= OP_REPLACE_B;
possible |= OP_REPLACE_A;
possible |= OP_UPDATE_A|OP_UPDATE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::FULL_OR_ATTR_THAT:
proposal |= OP_REPLACE_A;
possible |= OP_REPLACE_B;
possible |= OP_UPDATE_A|OP_UPDATE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
case Attr::FULL_CONFLICT:
possible |= OP_REPLACE_A;
possible |= OP_REPLACE_B;
possible |= OP_DELETE_A|OP_DELETE_B;
break;
}
}
if (config.keep == 'a') {
proposal &= ~(OP_UPDATE_A|OP_REPLACE_A|OP_CREATE_A|OP_DELETE_A);
possible &= ~(OP_UPDATE_A|OP_REPLACE_A|OP_CREATE_A|OP_DELETE_A);
} else if (config.keep == 'b') {
proposal &= ~(OP_UPDATE_B|OP_REPLACE_B|OP_CREATE_B|OP_DELETE_B);
possible &= ~(OP_UPDATE_B|OP_REPLACE_B|OP_CREATE_B|OP_DELETE_B);
}
possible |= proposal;
if ((possible & OP_DELETE_A) && (possible & OP_DELETE_B)) {
possible |= OP_DELETE_ALL;
}
if (!possible) {
return 0; // silently
}
log_info(a, desc_a, b, desc_b);
if (proposal) {
op_t chosen = confirm(proposal, desc_a, desc_b, false);
if (chosen != OP_UNKNOWN) {
return chosen;
}
}
return confirm(possible, desc_a, desc_b, true);
}
static bool run(DirStack* a, const char* desc_a, DirStack* b, const char* desc_b) {
Dir* dir_a = a->get();
Dir* dir_b = b->get();
assert(dir_a && dir_b);
DirEntries* ls_a = dir_a->entries;
DirEntries* ls_b = dir_b->entries;
while (true) {
Ent* ent_a = ls_a->pop();
Ent* ent_b = ls_b->pop();
if (!ent_a && !ent_b) {
break;
}
if (ent_a && ent_b && strcmp(ent_a->name, ent_b->name) != 0) {
if (DirEntries::before(ent_a, ent_b)) { // b does not contain a's name
ls_b->unpop(ent_b);
ent_b = NULL;
} else { // a does not contain b's name
ls_a->unpop(ent_a);
ent_a = NULL;
}
}
op_t op = proposal(dir_a, desc_a, ent_a, dir_b, desc_b, ent_b);
if (config.dry_run) {
assert(!op);
}
if (op & OP_DELETE_ALL) {
op &= ~OP_DELETE_ALL;
op |= OP_DELETE_A|OP_DELETE_B;
}
if (op & OP_UPDATE_A) {
if (!ent_a->apply(ent_b)) {
log_transfer(ent_b, ent_a, false);
goto err;
} else {
log_transfer(ent_b, ent_a, true);
}
}
if (op & OP_UPDATE_B) {
if (!ent_b->apply(ent_a)) {
log_transfer(ent_a, ent_b, false);
goto err;
} else {
log_transfer(ent_a, ent_b, true);
}
}
if (op & OP_DELETE_A) {
if (!ent_a->delInst(config.backup[0])) {
log_transfer(ent_a, NULL, false);
goto err;
} else {
log_transfer(ent_a, NULL, true);
}
delete ent_a;
ent_a = NULL;
}
if (op & OP_DELETE_B) {
if (!ent_b->delInst(config.backup[1])) {
log_transfer(ent_b, NULL, false);
goto err;
} else {
log_transfer(ent_b, NULL, true);
}
delete ent_b;
ent_b = NULL;
}
if (op & OP_CREATE_A) {
ent_a = Ent::createInst(dir_a->fd, ent_b->name, ent_b->type == Ent::TYPE_DIR);
if (!ent_a || (ent_b->type == Ent::TYPE_REG && !ent_a->read_from(ent_b)) || !ent_a->apply(ent_b)) {
log_transfer(ent_b, ent_a, false);
goto err;
}
log_transfer(ent_b, ent_a, true);
}
if (op & OP_CREATE_B) {
ent_b = Ent::createInst(dir_b->fd, ent_a->name, ent_a->type == Ent::TYPE_DIR);
if (!ent_b || (ent_b->type == Ent::TYPE_REG && !ent_b->read_from(ent_a)) || !ent_b->apply(ent_a)) {
log_transfer(ent_a, ent_b, false);
goto err;
}
log_transfer(ent_a, ent_b, true);
}
if (op & OP_REPLACE_A) {
if (!ent_a->read_from(ent_b, config.backup[0]) || !ent_a->apply(ent_b)) {
log_transfer(ent_b, ent_a, false);
goto err;
}
log_transfer(ent_b, ent_a, true);
}
if (op & OP_REPLACE_B) {
if (!ent_b->read_from(ent_a, config.backup[1]) || !ent_b->apply(ent_a)) {
log_transfer(ent_a, ent_b, false);
goto err;
}
log_transfer(ent_a, ent_b, true);
}
if (ent_a && ent_b && ent_a->type == Ent::TYPE_DIR && !matches(exclude, ent_a->name)) {
assert(ent_b->type == Ent::TYPE_DIR);
if (!a->push(ent_a->name)) {
goto err;
}
if (!b->push(ent_b->name)) {
a->pop();
goto err;
}
char* next_desc_a = NULL;
UNUSED(asprintf(&next_desc_a, "%s/%s", desc_a, ent_a->name));
char* next_desc_b = NULL;
UNUSED(asprintf(&next_desc_b, "%s/%s", desc_b, ent_b->name));
if (!run(a, next_desc_a, b, next_desc_b)) {
free(next_desc_a);
free(next_desc_b);
goto err;
}
free(next_desc_a);
free(next_desc_b);
}
if (ent_a) delete ent_a;
if (ent_b) delete ent_b;
continue;
err: // TODO: ask for retry or skip
if (ent_a) delete ent_a;
if (ent_b) delete ent_b;
a->pop();
b->pop();
return false;
}
a->pop();
b->pop();
return true;
}
int main(int argc, char **argv) {
if (argc < 3) {
LOG("usage: %s [--delete-excluded[-a|-b]] [--keep-(a|b)] [--dry-run] [--backup[-a|-b]=dirname] a/ b/", argv[0]);
return 1;
}
const char* const arga = argv[argc-2];
const char* const argb = argv[argc-1];
for (int i=1; i<argc-2; ++i) {
bool override = false;
if (!strcmp(argv[i], "--delete-excluded-a")) {
if (config.delete_excluded != 0) override = true;
config.delete_excluded = 'a';
} else if (!strcmp(argv[i], "--delete-excluded-b")) {
if (config.delete_excluded != 0) override = true;
config.delete_excluded = 'b';
} else if (!strcmp(argv[i], "--delete-excluded")) {
if (config.delete_excluded != 0) override = true;
config.delete_excluded = '*';
} else if (!strcmp(argv[i], "--dry-run")) {
if (config.dry_run) override = true;
config.dry_run = true;
} else if (!strcmp(argv[i], "--yes-to-all")) {
if (config.yes_to_all) override = true;
config.yes_to_all = true;
} else if (!strcmp(argv[i], "--keep-a")) {
if (config.keep != 0) override = true;
config.keep = 'a';
} else if (!strcmp(argv[i], "--keep-b")) {
if (config.keep != 0) override = true;
config.keep = 'b';
} else if (!strncmp(argv[i], "--backup=", 9)) {
if (config.backup_dir[0] || config.backup_dir[1]) override = true;
config.backup_dir[0] = config.backup_dir[1] = argv[i] + 9;
} else if (!strncmp(argv[i], "--backup-a=", 11)) {
if (config.backup_dir[0]) override = true;
config.backup_dir[0] = argv[i] + 11;
} else if (!strncmp(argv[i], "--backup-b=", 11)) {
if (config.backup_dir[1]) override = true;
config.backup_dir[1] = argv[i] + 11;
} else {
LOG("unknown arg '%s'", argv[i]);
return 1;
}
if (override) {
LOG("value for arg '%s' has already been set", argv[i]);
return 1;
}
}
LOG("starting up%s...", config.dry_run? " (DRY-RUN)": "");
DirStack a(arga);
if (!a.get()) return 1;
DirStack b(argb);
if (!b.get()) return 1;
if (config.backup_dir[0]) {
if (mkdirat(a.get()->fd, config.backup_dir[0], S_IRUSR|S_IWUSR|S_IXUSR) == -1) { // XXX: race
LOG_ERRNO("mkdirat(%s, %s)", arga, config.backup_dir[0]);
return 1;
}
config.backup[0] = openat(a.get()->fd, config.backup_dir[0], O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
if (config.backup[0] == -1) {
LOG_ERRNO("openat(%s, %s)", arga, config.backup_dir[0]);
return 1;
}
} else {
config.backup[0] = -1;
}
if (config.backup_dir[1]) {
if (mkdirat(b.get()->fd, config.backup_dir[1], S_IRUSR|S_IWUSR|S_IXUSR) == -1) {
LOG_ERRNO("mkdirat(%s, %s)", argb, config.backup_dir[1]);
return 1;
}
config.backup[1] = openat(b.get()->fd, config.backup_dir[1], O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
if (config.backup[1] == -1) {
LOG_ERRNO("openat(%s, %s)", argb, config.backup_dir[1]);
return 1;
}
} else {
config.backup[1] = -1;
}
bool rv = run(&a, arga, &b, argb);
LOG("Done: %s", rv? "success": "fail");
#if 0
if (config.dry_run && rv) {
printf("re-run with '--yes-to-all'? [y/n] ");
fflush(stdout);
char c = get_char();
if (c == 'y') {
config.dry_run = false;
config.yes_to_all = true;
assert(!a.get() && !b.get());
a.push(arga);
a.push(argb);
if (!a.get() || !b.get()) return 1;
rv = run(&a, arga, &b, argb);
LOG("Done: %s", rv? "success": "fail");
}
}
#endif
return rv? 0: 1;
}