seafile/common/vc-common.c
Jiaqiang Xu bad4c61d96 [client] Popup notification when file conflicts.
In order to detect conflict files, we also change the conflict suffix
to "(SFConflict username time-of-day)".
2015-09-10 15:42:35 +08:00

638 lines
18 KiB
C

#include "common.h"
#include "seafile-session.h"
#include "vc-common.h"
#include "log.h"
#include "seafile-error.h"
static GList *
merge_bases_many (SeafCommit *one, int n, SeafCommit **twos);
static gint
compare_commit_by_time (gconstpointer a, gconstpointer b, gpointer unused)
{
const SeafCommit *commit_a = a;
const SeafCommit *commit_b = b;
/* Latest commit comes first in the list. */
return (commit_b->ctime - commit_a->ctime);
}
static gint
compare_commit (gconstpointer a, gconstpointer b)
{
const SeafCommit *commit_a = a;
const SeafCommit *commit_b = b;
return strcmp (commit_a->commit_id, commit_b->commit_id);
}
static gboolean
add_to_commit_hash (SeafCommit *commit, void *vhash, gboolean *stop)
{
GHashTable *hash = vhash;
char *key = g_strdup (commit->commit_id);
g_hash_table_replace (hash, key, key);
return TRUE;
}
static GHashTable *
commit_tree_to_hash (SeafCommit *head)
{
GHashTable *hash;
gboolean res;
hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
res = seaf_commit_manager_traverse_commit_tree (seaf->commit_mgr,
head->repo_id,
head->version,
head->commit_id,
add_to_commit_hash,
hash, FALSE);
if (!res)
goto fail;
return hash;
fail:
g_hash_table_destroy (hash);
return NULL;
}
static GList *
get_independent_commits (GList *commits)
{
SeafCommit **rslt;
GList *list, *result;
int cnt, i, j;
SeafCommit *c;
g_debug ("Get independent commits.\n");
cnt = g_list_length (commits);
rslt = calloc(cnt, sizeof(*rslt));
for (list = commits, i = 0; list; list = list->next)
rslt[i++] = list->data;
g_list_free (commits);
for (i = 0; i < cnt - 1; i++) {
for (j = i+1; j < cnt; j++) {
if (!rslt[i] || !rslt[j])
continue;
result = merge_bases_many(rslt[i], 1, &rslt[j]);
for (list = result; list; list = list->next) {
c = list->data;
/* If two commits have fast-forward relationship,
* drop the older one.
*/
if (strcmp (rslt[i]->commit_id, c->commit_id) == 0) {
seaf_commit_unref (rslt[i]);
rslt[i] = NULL;
}
if (strcmp (rslt[j]->commit_id, c->commit_id) == 0) {
seaf_commit_unref (rslt[j]);
rslt[j] = NULL;
}
seaf_commit_unref (c);
}
}
}
/* Surviving ones in rslt[] are the independent results */
result = NULL;
for (i = 0; i < cnt; i++) {
if (rslt[i])
result = g_list_insert_sorted_with_data (result, rslt[i],
compare_commit_by_time,
NULL);
}
free(rslt);
return result;
}
typedef struct {
GList *result;
GHashTable *commit_hash;
} MergeTraverseData;
static gboolean
get_merge_bases (SeafCommit *commit, void *vdata, gboolean *stop)
{
MergeTraverseData *data = vdata;
/* Found a common ancestor.
* Dont traverse its parenets.
*/
if (g_hash_table_lookup (data->commit_hash, commit->commit_id)) {
if (!g_list_find_custom (data->result, commit, compare_commit)) {
data->result = g_list_insert_sorted_with_data (data->result, commit,
compare_commit_by_time,
NULL);
seaf_commit_ref (commit);
}
*stop = TRUE;
}
return TRUE;
}
/*
* Merge "one" with commits in "twos".
* The ancestors returned may not be ancestors for all the input commits.
* They are common ancestors for one and some commits in twos array.
*/
static GList *
merge_bases_many (SeafCommit *one, int n, SeafCommit **twos)
{
GHashTable *commit_hash;
GList *result = NULL;
SeafCommit *commit;
int i;
MergeTraverseData data;
gboolean res;
for (i = 0; i < n; i++) {
if (one == twos[i])
return g_list_append (result, one);
}
/* First construct a hash table of all commit ids rooted at one. */
commit_hash = commit_tree_to_hash (one);
if (!commit_hash) {
g_warning ("Failed to load commit hash.\n");
return NULL;
}
data.commit_hash = commit_hash;
data.result = NULL;
for (i = 0; i < n; i++) {
res = seaf_commit_manager_traverse_commit_tree (seaf->commit_mgr,
twos[i]->repo_id,
twos[i]->version,
twos[i]->commit_id,
get_merge_bases,
&data, FALSE);
if (!res)
goto fail;
}
g_hash_table_destroy (commit_hash);
result = data.result;
if (!result || !result->next)
return result;
/* There are more than one. Try to find out independent ones. */
result = get_independent_commits (result);
return result;
fail:
result = data.result;
while (result) {
commit = result->data;
seaf_commit_unref (commit);
result = g_list_delete_link (result, result);
}
g_hash_table_destroy (commit_hash);
return NULL;
}
/*
* Returns common ancesstor for two branches.
* Any two commits should have a common ancestor.
* So returning NULL indicates an error, for e.g. corupt commit.
*/
SeafCommit *
get_merge_base (SeafCommit *head, SeafCommit *remote)
{
GList *result, *iter;
SeafCommit *one, **twos;
int n, i;
SeafCommit *ret = NULL;
one = head;
twos = (SeafCommit **) calloc (1, sizeof(SeafCommit *));
twos[0] = remote;
n = 1;
result = merge_bases_many (one, n, twos);
free (twos);
if (!result || !result->next)
goto done;
/*
* More than one common ancestors.
* Loop until the oldest common ancestor is found.
*/
while (1) {
n = g_list_length (result) - 1;
one = result->data;
twos = calloc (n, sizeof(SeafCommit *));
for (iter = result->next, i = 0; i < n; iter = iter->next, i++) {
twos[i] = iter->data;
}
g_list_free (result);
result = merge_bases_many (one, n, twos);
free (twos);
if (!result || !result->next)
break;
}
done:
if (result)
ret = result->data;
g_list_free (result);
return ret;
}
/*
* Returns true if src_head is ahead of dst_head.
*/
gboolean
is_fast_forward (const char *repo_id, int version,
const char *src_head, const char *dst_head)
{
VCCompareResult res;
res = vc_compare_commits (repo_id, version, src_head, dst_head);
return (res == VC_FAST_FORWARD);
}
VCCompareResult
vc_compare_commits (const char *repo_id, int version,
const char *c1, const char *c2)
{
SeafCommit *commit1, *commit2, *ca;
VCCompareResult ret;
/* Treat the same as up-to-date. */
if (strcmp (c1, c2) == 0)
return VC_UP_TO_DATE;
commit1 = seaf_commit_manager_get_commit (seaf->commit_mgr, repo_id, version, c1);
if (!commit1)
return VC_INDEPENDENT;
commit2 = seaf_commit_manager_get_commit (seaf->commit_mgr, repo_id, version, c2);
if (!commit2) {
seaf_commit_unref (commit1);
return VC_INDEPENDENT;
}
ca = get_merge_base (commit1, commit2);
if (!ca)
ret = VC_INDEPENDENT;
else if (strcmp(ca->commit_id, commit1->commit_id) == 0)
ret = VC_UP_TO_DATE;
else if (strcmp(ca->commit_id, commit2->commit_id) == 0)
ret = VC_FAST_FORWARD;
else
ret = VC_INDEPENDENT;
if (ca) seaf_commit_unref (ca);
seaf_commit_unref (commit1);
seaf_commit_unref (commit2);
return ret;
}
/**
* Diff a specific file with parent(s).
* If @commit is a merge, both parents will be compared.
* @commit must have this file and it's id is given in @file_id.
*
* Returns 0 if there is no difference; 1 otherwise.
* If returns 0, @parent will point to the next commit to traverse.
* If I/O error occurs, @error will be set.
*/
static int
diff_parents_with_path (SeafCommit *commit,
const char *repo_id,
const char *store_id,
int version,
const char *path,
const char *file_id,
char *parent,
GError **error)
{
SeafCommit *p1 = NULL, *p2 = NULL;
char *file_id_p1 = NULL, *file_id_p2 = NULL;
int ret = 0;
p1 = seaf_commit_manager_get_commit (seaf->commit_mgr,
commit->repo_id,
commit->version,
commit->parent_id);
if (!p1) {
g_warning ("Failed to find commit %s.\n", commit->parent_id);
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, " ");
return 0;
}
if (strcmp (p1->root_id, EMPTY_SHA1) == 0) {
seaf_commit_unref (p1);
return 1;
}
if (commit->second_parent_id) {
p2 = seaf_commit_manager_get_commit (seaf->commit_mgr,
commit->repo_id,
commit->version,
commit->second_parent_id);
if (!p2) {
g_warning ("Failed to find commit %s.\n", commit->second_parent_id);
seaf_commit_unref (p1);
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, " ");
return 0;
}
}
if (!p2) {
file_id_p1 = seaf_fs_manager_path_to_obj_id (seaf->fs_mgr,
store_id,
version,
p1->root_id, path,
NULL,
error);
if (*error)
goto out;
if (!file_id_p1 || strcmp (file_id, file_id_p1) != 0)
ret = 1;
else
memcpy (parent, p1->commit_id, 41);
} else {
file_id_p1 = seaf_fs_manager_path_to_obj_id (seaf->fs_mgr,
store_id,
version,
p1->root_id, path,
NULL, error);
if (*error)
goto out;
file_id_p2 = seaf_fs_manager_path_to_obj_id (seaf->fs_mgr,
store_id,
version,
p2->root_id, path,
NULL, error);
if (*error)
goto out;
if (file_id_p1 && file_id_p2) {
if (strcmp(file_id, file_id_p1) != 0 &&
strcmp(file_id, file_id_p2) != 0)
ret = 1;
else if (strcmp(file_id, file_id_p1) == 0)
memcpy (parent, p1->commit_id, 41);
else
memcpy (parent, p2->commit_id, 41);
} else if (file_id_p1 && !file_id_p2) {
if (strcmp(file_id, file_id_p1) != 0)
ret = 1;
else
memcpy (parent, p1->commit_id, 41);
} else if (!file_id_p1 && file_id_p2) {
if (strcmp(file_id, file_id_p2) != 0)
ret = 1;
else
memcpy (parent, p2->commit_id, 41);
} else {
ret = 1;
}
}
out:
g_free (file_id_p1);
g_free (file_id_p2);
if (p1)
seaf_commit_unref (p1);
if (p2)
seaf_commit_unref (p2);
return ret;
}
static int
get_file_modifier_mtime_v0 (const char *repo_id, const char *store_id, int version,
const char *head, const char *path,
char **modifier, gint64 *mtime)
{
char commit_id[41];
SeafCommit *commit = NULL;
char *file_id = NULL;
int changed;
int ret = 0;
GError *error = NULL;
*modifier = NULL;
*mtime = 0;
memcpy (commit_id, head, 41);
while (1) {
commit = seaf_commit_manager_get_commit (seaf->commit_mgr,
repo_id, version,
commit_id);
if (!commit) {
ret = -1;
break;
}
/* We hit the initial commit. */
if (!commit->parent_id)
break;
file_id = seaf_fs_manager_path_to_obj_id (seaf->fs_mgr,
store_id, version,
commit->root_id,
path,
NULL,
&error);
if (error) {
g_clear_error (&error);
ret = -1;
break;
}
/* We expect commit to have this file. */
if (!file_id) {
ret = -1;
break;
}
changed = diff_parents_with_path (commit,
repo_id, store_id, version,
path, file_id,
commit_id, &error);
if (error) {
g_clear_error (&error);
ret = -1;
break;
}
if (changed) {
*modifier = g_strdup (commit->creator_name);
*mtime = commit->ctime;
break;
} else {
/* If this commit doesn't change the file, commit_id will be set
* to the parent commit to traverse.
*/
g_free (file_id);
seaf_commit_unref (commit);
}
}
g_free (file_id);
if (commit)
seaf_commit_unref (commit);
return ret;
}
static int
get_file_modifier_mtime_v1 (const char *repo_id, const char *store_id, int version,
const char *head, const char *path,
char **modifier, gint64 *mtime)
{
SeafCommit *commit = NULL;
SeafDir *dir = NULL;
SeafDirent *dent = NULL;
int ret = 0;
commit = seaf_commit_manager_get_commit (seaf->commit_mgr,
repo_id, version,
head);
if (!commit) {
seaf_warning ("Failed to get commit %s.\n", head);
return -1;
}
char *parent = g_path_get_dirname (path);
if (strcmp(parent, ".") == 0) {
g_free (parent);
parent = g_strdup("");
}
char *filename = g_path_get_basename (path);
dir = seaf_fs_manager_get_seafdir_by_path (seaf->fs_mgr,
store_id, version,
commit->root_id,
parent, NULL);
if (!dir) {
seaf_warning ("dir %s doesn't exist in repo %s.\n", parent, repo_id);
ret = -1;
goto out;
}
GList *p;
for (p = dir->entries; p; p = p->next) {
SeafDirent *d = p->data;
if (strcmp (d->name, filename) == 0) {
dent = d;
break;
}
}
if (!dent) {
goto out;
}
*modifier = g_strdup(dent->modifier);
*mtime = dent->mtime;
out:
g_free (parent);
g_free (filename);
seaf_commit_unref (commit);
seaf_dir_free (dir);
return ret;
}
/**
* Get the user who last changed a file and the mtime.
* @head: head commit to start the search.
* @path: path of the file.
*/
int
get_file_modifier_mtime (const char *repo_id,
const char *store_id,
int version,
const char *head,
const char *path,
char **modifier,
gint64 *mtime)
{
if (version > 0)
return get_file_modifier_mtime_v1 (repo_id, store_id, version,
head, path,
modifier, mtime);
else
return get_file_modifier_mtime_v0 (repo_id, store_id, version,
head, path,
modifier, mtime);
}
char *
gen_conflict_path (const char *origin_path,
const char *modifier,
gint64 mtime)
{
char time_buf[64];
time_t t = (time_t)mtime;
char *copy = g_strdup (origin_path);
GString *conflict_path = g_string_new (NULL);
char *dot, *ext;
strftime(time_buf, 64, "%Y-%m-%d-%H-%M-%S", localtime(&t));
dot = strrchr (copy, '.');
if (dot != NULL) {
*dot = '\0';
ext = dot + 1;
if (modifier)
g_string_printf (conflict_path, "%s (SFConflict %s %s).%s",
copy, modifier, time_buf, ext);
else
g_string_printf (conflict_path, "%s (SFConflict %s).%s",
copy, time_buf, ext);
} else {
if (modifier)
g_string_printf (conflict_path, "%s (SFConflict %s %s)",
copy, modifier, time_buf);
else
g_string_printf (conflict_path, "%s (SFConflict %s)",
copy, time_buf);
}
g_free (copy);
return g_string_free (conflict_path, FALSE);
}
char *
gen_conflict_path_wrapper (const char *repo_id, int version,
const char *head, const char *in_repo_path,
const char *original_path)
{
char *modifier;
gint64 mtime;
/* XXX: this function is only used in client, so store_id is always
* the same as repo_id. This can be changed if it's also called in
* server.
*/
if (get_file_modifier_mtime (repo_id, repo_id, version, head, in_repo_path,
&modifier, &mtime) < 0)
return NULL;
return gen_conflict_path (original_path, modifier, mtime);
}