mirror of
https://github.com/haiwen/seafile.git
synced 2025-01-07 03:17:13 +08:00
bad4c61d96
In order to detect conflict files, we also change the conflict suffix to "(SFConflict username time-of-day)".
638 lines
18 KiB
C
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);
|
|
}
|