#include "seafile-session.h" #include "merge-new.h" #include "vc-common.h" #define DEBUG_FLAG SEAFILE_DEBUG_MERGE #include "log.h" static int merge_trees_recursive (const char *store_id, int version, int n, SeafDir *trees[], const char *basedir, MergeOptions *opt); static char * merge_conflict_filename (const char *store_id, int version, MergeOptions *opt, const char *basedir, const char *filename) { char *path = NULL, *modifier = NULL, *conflict_name = NULL; gint64 mtime; SeafCommit *commit; path = g_strconcat (basedir, filename, NULL); int rc = get_file_modifier_mtime (opt->remote_repo_id, store_id, version, opt->remote_head, path, &modifier, &mtime); if (rc < 0) { commit = seaf_commit_manager_get_commit (seaf->commit_mgr, opt->remote_repo_id, version, opt->remote_head); if (!commit) { seaf_warning ("Failed to find remote head %s:%s.\n", opt->remote_repo_id, opt->remote_head); goto out; } modifier = g_strdup(commit->creator_name); mtime = (gint64)time(NULL); seaf_commit_unref (commit); } conflict_name = gen_conflict_path (filename, modifier, mtime); out: g_free (path); g_free (modifier); return conflict_name; } static char * merge_conflict_dirname (const char *store_id, int version, MergeOptions *opt, const char *basedir, const char *dirname) { char *modifier = NULL, *conflict_name = NULL; SeafCommit *commit; commit = seaf_commit_manager_get_commit (seaf->commit_mgr, opt->remote_repo_id, version, opt->remote_head); if (!commit) { seaf_warning ("Failed to find remote head %s:%s.\n", opt->remote_repo_id, opt->remote_head); goto out; } modifier = g_strdup(commit->creator_name); seaf_commit_unref (commit); conflict_name = gen_conflict_path (dirname, modifier, (gint64)time(NULL)); out: g_free (modifier); return conflict_name; } static int merge_entries (const char *store_id, int version, int n, SeafDirent *dents[], const char *basedir, GList **dents_out, MergeOptions *opt) { SeafDirent *files[3]; int i; memset (files, 0, sizeof(files[0])*n); for (i = 0; i < n; ++i) { if (dents[i] && S_ISREG(dents[i]->mode)) files[i] = dents[i]; } /* If we're running 2-way merge, or the caller requires not to * actually merge contents, just call the callback function. */ if (n == 2 || !opt->do_merge) return opt->callback (basedir, files, opt); /* Otherwise, we're doing a real 3-way merge of the trees. * It means merge files and handle any conflicts. */ SeafDirent *base, *head, *remote; char *conflict_name; base = files[0]; head = files[1]; remote = files[2]; if (head && remote) { if (strcmp (head->id, remote->id) == 0) { seaf_debug ("%s%s: files match\n", basedir, head->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); } else if (base && strcmp (base->id, head->id) == 0) { seaf_debug ("%s%s: unchanged in head, changed in remote\n", basedir, head->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); } else if (base && strcmp (base->id, remote->id) == 0) { seaf_debug ("%s%s: unchanged in remote, changed in head\n", basedir, head->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); } else { /* File content conflict. */ seaf_debug ("%s%s: files conflict\n", basedir, head->name); conflict_name = merge_conflict_filename(store_id, version, opt, basedir, head->name); if (!conflict_name) return -1; /* Change remote entry name in place. So opt->callback * will see the conflict name, not the original name. */ g_free (remote->name); remote->name = conflict_name; remote->name_len = strlen (remote->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); opt->conflict = TRUE; } } else if (base && !head && remote) { if (strcmp (base->id, remote->id) != 0) { if (dents[1] != NULL) { /* D/F conflict: * Head replaces file with dir, while remote change the file. */ seaf_debug ("%s%s: DFC, file -> dir, file\n", basedir, remote->name); conflict_name = merge_conflict_filename(store_id, version, opt, basedir, remote->name); if (!conflict_name) return -1; /* Change the name of remote, keep dir name in head unchanged. */ g_free (remote->name); remote->name = conflict_name; remote->name_len = strlen (remote->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); opt->conflict = TRUE; } else { /* Deleted in head and changed in remote. */ seaf_debug ("%s%s: deleted in head and changed in remote\n", basedir, remote->name); /* Keep version of remote. */ *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); } } else { /* If base and remote match, the file should not be added to * the merge result. */ seaf_debug ("%s%s: file deleted in head, unchanged in remote\n", basedir, remote->name); } } else if (base && head && !remote) { if (strcmp (base->id, head->id) != 0) { if (dents[2] != NULL) { /* D/F conflict: * Remote replaces file with dir, while head change the file. */ seaf_debug ("%s%s: DFC, file -> file, dir\n", basedir, head->name); /* We use remote head commit author name as conflict * suffix of a dir. */ conflict_name = merge_conflict_dirname (store_id, version, opt, basedir, dents[2]->name); if (!conflict_name) return -1; /* Change remote dir name to conflict name in place. */ g_free (dents[2]->name); dents[2]->name = conflict_name; dents[2]->name_len = strlen (dents[2]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); opt->conflict = TRUE; } else { /* Deleted in remote and changed in head. */ seaf_debug ("%s%s: deleted in remote and changed in head\n", basedir, head->name); /* Keep version of remote. */ *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); } } else { /* If base and head match, the file should not be added to * the merge result. */ seaf_debug ("%s%s: file deleted in remote, unchanged in head\n", basedir, head->name); } } else if (!base && !head && remote) { if (!dents[1]) { /* Added in remote. */ seaf_debug ("%s%s: added in remote\n", basedir, remote->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); } else if (dents[0] != NULL && strcmp(dents[0]->id, dents[1]->id) == 0) { /* Contents in the dir is not changed. * The dir will be deleted in merge_directories(). */ seaf_debug ("%s%s: dir in head will be replaced by file in remote\n", basedir, remote->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); } else { /* D/F conflict: * Contents of the dir is changed in head, while * remote replace the dir with a file. * * Or, head adds a new dir, while remote adds a new file, * with the same name. */ seaf_debug ("%s%s: DFC, dir -> dir, file\n", basedir, remote->name); conflict_name = merge_conflict_filename(store_id, version, opt, basedir, remote->name); if (!conflict_name) return -1; g_free (remote->name); remote->name = conflict_name; remote->name_len = strlen (remote->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(remote)); opt->conflict = TRUE; } } else if (!base && head && !remote) { if (!dents[2]) { /* Added in remote. */ seaf_debug ("%s%s: added in head\n", basedir, head->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); } else if (dents[0] != NULL && strcmp(dents[0]->id, dents[2]->id) == 0) { /* Contents in the dir is not changed. * The dir will be deleted in merge_directories(). */ seaf_debug ("%s%s: dir in remote will be replaced by file in head\n", basedir, head->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); } else { /* D/F conflict: * Contents of the dir is changed in remote, while * head replace the dir with a file. * * Or, remote adds a new dir, while head adds a new file, * with the same name. */ seaf_debug ("%s%s: DFC, dir -> file, dir\n", basedir, head->name); conflict_name = merge_conflict_dirname (store_id, version, opt, basedir, dents[2]->name); if (!conflict_name) return -1; g_free (dents[2]->name); dents[2]->name = conflict_name; dents[2]->name_len = strlen (dents[2]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(head)); opt->conflict = TRUE; } } else if (base && !head && !remote) { /* Don't need to add anything to dents_out. */ seaf_debug ("%s%s: deleted in head and remote\n", basedir, base->name); } return 0; } static int merge_directories (const char *store_id, int version, int n, SeafDirent *dents[], const char *basedir, GList **dents_out, MergeOptions *opt) { SeafDir *dir; SeafDir *sub_dirs[3]; char *dirname = NULL; char *new_basedir; int ret = 0; int dir_mask = 0, i; SeafDirent *merged_dent; for (i = 0; i < n; ++i) { if (dents[i] && S_ISDIR(dents[i]->mode)) dir_mask |= 1 << i; } seaf_debug ("dir_mask = %d\n", dir_mask); if (n == 3 && opt->do_merge) { switch (dir_mask) { case 0: g_return_val_if_reached (-1); case 1: /* head and remote are not dirs, nothing to merge. */ seaf_debug ("%s%s: no dir, no need to merge\n", basedir, dents[0]->name); return 0; case 2: /* only head is dir, add to result directly, no need to merge. */ seaf_debug ("%s%s: only head is dir\n", basedir, dents[1]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(dents[1])); return 0; case 3: if (strcmp (dents[0]->id, dents[1]->id) == 0) { /* Base and head are the same, but deleted in remote. */ seaf_debug ("%s%s: dir deleted in remote\n", basedir, dents[0]->name); return 0; } seaf_debug ("%s%s: dir changed in head but deleted in remote\n", basedir, dents[1]->name); break; case 4: /* only remote is dir, add to result directly, no need to merge. */ seaf_debug ("%s%s: only remote is dir\n", basedir, dents[2]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(dents[2])); return 0; case 5: if (strcmp (dents[0]->id, dents[2]->id) == 0) { /* Base and remote are the same, but deleted in head. */ seaf_debug ("%s%s: dir deleted in head\n", basedir, dents[0]->name); return 0; } seaf_debug ("%s%s: dir changed in remote but deleted in head\n", basedir, dents[2]->name); break; case 6: case 7: if (strcmp (dents[1]->id, dents[2]->id) == 0) { /* Head and remote match. */ seaf_debug ("%s%s: dir is the same in head and remote\n", basedir, dents[1]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(dents[1])); return 0; } else if (dents[0] && strcmp(dents[0]->id, dents[1]->id) == 0) { seaf_debug ("%s%s: dir changed in remote but unchanged in head\n", basedir, dents[1]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(dents[2])); return 0; } else if (dents[0] && strcmp(dents[0]->id, dents[2]->id) == 0) { seaf_debug ("%s%s: dir changed in head but unchanged in remote\n", basedir, dents[1]->name); *dents_out = g_list_prepend (*dents_out, seaf_dirent_dup(dents[1])); return 0; } seaf_debug ("%s%s: dir is changed in both head and remote, " "merge recursively\n", basedir, dents[1]->name); break; default: g_return_val_if_reached (-1); } } memset (sub_dirs, 0, sizeof(sub_dirs[0])*n); for (i = 0; i < n; ++i) { if (dents[i] != NULL && S_ISDIR(dents[i]->mode)) { dir = seaf_fs_manager_get_seafdir (seaf->fs_mgr, store_id, version, dents[i]->id); if (!dir) { seaf_warning ("Failed to find dir %s:%s.\n", store_id, dents[i]->id); ret = -1; goto free_sub_dirs; } opt->visit_dirs++; sub_dirs[i] = dir; dirname = dents[i]->name; } } new_basedir = g_strconcat (basedir, dirname, "/", NULL); ret = merge_trees_recursive (store_id, version, n, sub_dirs, new_basedir, opt); g_free (new_basedir); if (n == 3 && opt->do_merge) { if (dir_mask == 3 || dir_mask == 6 || dir_mask == 7) { merged_dent = seaf_dirent_dup (dents[1]); memcpy (merged_dent->id, opt->merged_tree_root, 40); *dents_out = g_list_prepend (*dents_out, merged_dent); } else if (dir_mask == 5) { merged_dent = seaf_dirent_dup (dents[2]); memcpy (merged_dent->id, opt->merged_tree_root, 40); *dents_out = g_list_prepend (*dents_out, merged_dent); } } free_sub_dirs: for (i = 0; i < n; ++i) seaf_dir_free (sub_dirs[i]); return ret; } static gint compare_dirents (gconstpointer a, gconstpointer b) { const SeafDirent *denta = a, *dentb = b; return strcmp (dentb->name, denta->name); } static int merge_trees_recursive (const char *store_id, int version, int n, SeafDir *trees[], const char *basedir, MergeOptions *opt) { GList *ptrs[3]; SeafDirent *dents[3]; int i; SeafDirent *dent; char *first_name; gboolean done; int ret = 0; SeafDir *merged_tree; GList *merged_dents = NULL; for (i = 0; i < n; ++i) { if (trees[i]) ptrs[i] = trees[i]->entries; else ptrs[i] = NULL; } while (1) { first_name = NULL; memset (dents, 0, sizeof(dents[0])*n); done = TRUE; /* Find the "largest" name, assuming dirents are sorted. */ for (i = 0; i < n; ++i) { if (ptrs[i] != NULL) { done = FALSE; dent = ptrs[i]->data; if (!first_name) first_name = dent->name; else if (strcmp(dent->name, first_name) > 0) first_name = dent->name; } } if (done) break; /* * Setup dir entries for all names that equal to first_name */ int n_files = 0, n_dirs = 0; for (i = 0; i < n; ++i) { if (ptrs[i] != NULL) { dent = ptrs[i]->data; if (strcmp(first_name, dent->name) == 0) { if (S_ISREG(dent->mode)) ++n_files; else if (S_ISDIR(dent->mode)) ++n_dirs; dents[i] = dent; ptrs[i] = ptrs[i]->next; } } } /* Merge entries of this level. */ if (n_files > 0) { ret = merge_entries (store_id, version, n, dents, basedir, &merged_dents, opt); if (ret < 0) return ret; } /* Recurse into sub level. */ if (n_dirs > 0) { ret = merge_directories (store_id, version, n, dents, basedir, &merged_dents, opt); if (ret < 0) return ret; } } if (n == 3 && opt->do_merge) { merged_dents = g_list_sort (merged_dents, compare_dirents); merged_tree = seaf_dir_new (NULL, merged_dents, dir_version_from_repo_version(version)); memcpy (opt->merged_tree_root, merged_tree->dir_id, 40); if ((trees[1] && strcmp (trees[1]->dir_id, merged_tree->dir_id) == 0) || (trees[2] && strcmp (trees[2]->dir_id, merged_tree->dir_id) == 0)) { seaf_dir_free (merged_tree); } else { ret = seaf_dir_save (seaf->fs_mgr, store_id, version, merged_tree); seaf_dir_free (merged_tree); if (ret < 0) { seaf_warning ("Failed to save merged tree %s:%s.\n", store_id, basedir); } } } return ret; } int seaf_merge_trees (const char *store_id, int version, int n, const char *roots[], MergeOptions *opt) { SeafDir **trees, *root; int i, ret; g_return_val_if_fail (n == 2 || n == 3, -1); trees = g_new0 (SeafDir *, n); for (i = 0; i < n; ++i) { root = seaf_fs_manager_get_seafdir (seaf->fs_mgr, store_id, version, roots[i]); if (!root) { seaf_warning ("Failed to find dir %s:%s.\n", store_id, roots[i]); g_free (trees); return -1; } trees[i] = root; } ret = merge_trees_recursive (store_id, version, n, trees, "", opt); for (i = 0; i < n; ++i) seaf_dir_free (trees[i]); g_free (trees); return ret; }