blob: a661d57eed879dded2730e7f743bb262fb5ab4e8 [file] [log] [blame]
/*
* tree_conflicts.c: Handling of known problematic tree conflict use cases.
*
* ====================================================================
* Copyright (c) 2007 CollabNet. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://subversion.tigris.org/license-1.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
*
* This software consists of voluntary contributions made by many
* individuals. For exact contribution history, see the revision
* history and logs, available at http://subversion.tigris.org/.
* ====================================================================
*/
#include "tree_conflicts.h"
#include "log.h"
#include "entries.h"
#include "svn_path.h"
#include "svn_types.h"
#include "svn_private_config.h"
static const char field_separator = SVN_WC__TREE_CONFLICT_DESC_FIELD_SEPARATOR;
static const char desc_separator = SVN_WC__TREE_CONFLICT_DESC_SEPARATOR;
static const char escape_char = SVN_WC__TREE_CONFLICT_ESCAPE_CHAR;
/* If **INPUT starts with *TOKEN, advance *INPUT by the length of *TOKEN
* and return TRUE. Else, return FALSE and leave *INPUT alone. */
static svn_boolean_t
advance_on_match(const char **input, const char *token)
{
int len = strlen(token);
if ((strncmp(*input, token, len)) == 0)
{
*input += len;
return TRUE;
}
else
return FALSE;
}
/* Parse the 'victim path' field pointed to by *START. Modify the 'path'
* field of *CONFLICT by appending the victim name to its existing value.
* Stop reading at a field delimiter and never read past END.
* After reading, make *START point to the character after
* the field delimiter.
* Do all allocations in POOL.
*/
static svn_error_t *
read_victim_path(svn_wc_conflict_description_t *conflict,
const char **start,
const char *end,
apr_pool_t *pool)
{
svn_stringbuf_t *victim_path;
svn_boolean_t escape = FALSE;
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
victim_path = svn_stringbuf_create("", pool);
while (*start < end)
{
/* The field or description separators may occur inside the
* victim_path if they are escaped. */
if (! escape && **start == escape_char)
{
escape = TRUE;
(*start)++;
if (! (*start < end))
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Unexpected end of tree conflict description, within escape "
"sequence in 'victim_path'"));
if (**start != desc_separator
&& **start != field_separator
&& **start != escape_char)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Illegal escaped character in 'victim_path' of tree "
"conflict description"));
}
if (! escape)
{
if (**start == field_separator)
break;
else if (**start == desc_separator)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Unescaped description delimiter inside 'victim_path' "
"in tree conflict description"));
}
svn_stringbuf_appendbytes(victim_path, *start, 1);
escape = FALSE;
(*start)++;
}
if (victim_path->len == 0)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Empty 'victim_path' in tree conflict description"));
if (**start == field_separator)
(*start)++;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("No delimiter after 'victim_path' in tree conflict description"));
conflict->path = svn_path_join(conflict->path, victim_path->data, pool);
return SVN_NO_ERROR;
}
/* Parse the 'node_kind' field pointed to by *START into the
* the tree conflict descriptor pointed to by DESC.
* Don't read further than END.
* After reading, make *START point to the character after
* the field delimiter.
*/
static svn_error_t *
read_node_kind(svn_wc_conflict_description_t *conflict,
const char **start,
const char *end)
{
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
if (advance_on_match(start, SVN_WC__NODE_FILE))
conflict->node_kind = svn_node_file;
else if (advance_on_match(start, SVN_WC__NODE_DIR))
conflict->node_kind = svn_node_dir;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Invalid 'node_kind' field in tree conflict description"));
if (*start >= end || **start != field_separator)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("No delimiter after 'node_kind' in tree conflict description"));
(*start)++;
return SVN_NO_ERROR;
}
/* Parse the 'operation' field pointed to by *START into the
* the tree conflict descriptor pointed to by DESC.
* Don't read further than END.
* After reading, make *START point to the character after
* the field delimiter.
*/
static svn_error_t *
read_operation(svn_wc_conflict_description_t *conflict,
const char **start,
const char *end)
{
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
if (advance_on_match(start, SVN_WC__OPERATION_UPDATE))
conflict->operation = svn_wc_operation_update;
else if (advance_on_match(start, SVN_WC__OPERATION_SWITCH))
conflict->operation = svn_wc_operation_switch;
else if (advance_on_match(start, SVN_WC__OPERATION_MERGE))
conflict->operation = svn_wc_operation_merge;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Invalid 'operation' field in tree conflict description"));
if (*start >= end || **start != field_separator)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("No delimiter after 'operation' in tree conflict description"));
(*start)++;
return SVN_NO_ERROR;
}
/* Parse the 'action' field pointed to by *START into the
* the tree conflict descriptor pointed to by DESC.
* Don't read further than END.
* After reading, make *START point to the character after
* the field delimiter.
*/
static svn_error_t *
read_action(svn_wc_conflict_description_t *conflict,
const char **start,
const char *end)
{
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
if (advance_on_match(start, SVN_WC__CONFLICT_ACTION_EDITED))
conflict->action = svn_wc_conflict_action_edit;
else if (advance_on_match(start, SVN_WC__CONFLICT_ACTION_DELETED))
conflict->action = svn_wc_conflict_action_delete;
else if (advance_on_match(start, SVN_WC__CONFLICT_ACTION_ADDED))
conflict->action = svn_wc_conflict_action_add;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Invalid 'action' field in tree conflict description"));
if (*start >= end || **start != field_separator)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("No delimiter after 'action' in tree conflict description"));
(*start)++;
return SVN_NO_ERROR;
}
/* Parse the 'reason' field pointed to by *START into the
* the tree conflict descriptor pointed to by DESC.
* Don't read further than END.
*/
static svn_error_t *
read_reason(svn_wc_conflict_description_t *conflict,
const char **start,
const char *end)
{
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
if (advance_on_match(start, SVN_WC__CONFLICT_REASON_EDITED))
conflict->reason = svn_wc_conflict_reason_edited;
else if (advance_on_match(start, SVN_WC__CONFLICT_REASON_DELETED))
conflict->reason = svn_wc_conflict_reason_deleted;
else if (advance_on_match(start, SVN_WC__CONFLICT_REASON_MISSING))
conflict->reason = svn_wc_conflict_reason_missing;
else if (advance_on_match(start, SVN_WC__CONFLICT_REASON_OBSTRUCTED))
conflict->reason = svn_wc_conflict_reason_obstructed;
else if (advance_on_match(start, SVN_WC__CONFLICT_REASON_ADDED))
conflict->reason = svn_wc_conflict_reason_added;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Invalid 'reason' field in tree conflict description"));
/* This was the last field to parse, leave *START alone.
* It should already point to a description separator.
*/
return SVN_NO_ERROR;
}
/* Parse a newly allocated svn_wc_conflict_description_t object from the
* character string pointed to by *START. Return the result in *CONFLICT.
* Don't read further than END. If a description separator is found in
* the string after the conflict description that was read, set *START
* to the first character of the next conflict description.
* Otherwise, set *START to NULL.
* DIR_PATH is the path to the WC directory whose conflicts are being read.
* Do all allocations in pool.
*/
static svn_error_t *
read_one_tree_conflict(svn_wc_conflict_description_t **conflict,
const char **start,
const char *end,
const char *dir_path,
apr_pool_t *pool)
{
if (*start >= end)
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Expected tree conflict data but got none"));
*conflict = svn_wc_conflict_description_create_tree(
dir_path, NULL, svn_node_none, 0, pool);
/* Each of these modifies *START ! */
SVN_ERR(read_victim_path(*conflict, start, end, pool));
SVN_ERR(read_node_kind(*conflict, start, end));
SVN_ERR(read_operation(*conflict, start, end));
SVN_ERR(read_action(*conflict, start, end));
SVN_ERR(read_reason(*conflict, start, end));
/* *START should now point to a description separator
* if there are any descriptions left. */
if (**start == desc_separator)
(*start)++;
else
{
if (*start >= end)
*start = NULL;
else
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("No delimiter at end of tree conflict description, "
"even though there is still data left to read"));
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_wc__read_tree_conflicts_from_entry(apr_array_header_t *conflicts,
const svn_wc_entry_t *dir_entry,
const char *dir_path,
apr_pool_t *pool)
{
const char *start, *end;
svn_wc_conflict_description_t *conflict = NULL;
if (dir_entry->tree_conflict_data == NULL)
{
conflicts = NULL;
return SVN_NO_ERROR;
}
SVN_ERR_ASSERT(conflicts);
start = dir_entry->tree_conflict_data;
end = start + strlen(start);
while (start != NULL && start <= end) /* Yes, '<=', because 'start == end'
is a special case that is dealt
with further down the call chain. */
{
SVN_ERR(read_one_tree_conflict(&conflict, &start, end, dir_path, pool));
if (conflict != NULL)
APR_ARRAY_PUSH(conflicts, svn_wc_conflict_description_t *) = conflict;
}
if (start != NULL)
/* Not all conflicts have been read from the entry, but no error
* has been thrown yet. We should not even be here! */
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Invalid tree conflict data in 'entries' file, "
"but no idea what went wrong"));
return SVN_NO_ERROR;
}
/*
* This function could be static, but we need to link to it
* in a unit test in tests/libsvn_wc/, so it isn't.
*/
svn_error_t *
svn_wc__write_tree_conflicts_to_entry(apr_array_header_t *conflicts,
svn_wc_entry_t *dir_entry,
apr_pool_t *pool)
{
svn_stringbuf_t *buf = svn_stringbuf_create("", pool);
const char *path;
int i, j, len;
for (i = 0; i < conflicts->nelts; i++)
{
const svn_wc_conflict_description_t *conflict =
APR_ARRAY_IDX(conflicts, i, svn_wc_conflict_description_t *);
path = svn_path_basename(conflict->path, pool);
len = strlen(path);
SVN_ERR_ASSERT(len > 0);
/* Escape separator chars while writing victim path. */
for (j = 0; j < len; j++)
{
if ((path[j] == field_separator) ||
(path[j] == desc_separator) ||
(path[j] == escape_char))
{
svn_stringbuf_appendbytes(buf, &escape_char, 1);
}
svn_stringbuf_appendbytes(buf, &(path[j]), 1);
}
svn_stringbuf_appendbytes(buf, &field_separator, 1);
switch (conflict->node_kind)
{
case svn_node_dir:
svn_stringbuf_appendcstr(buf, SVN_WC__NODE_DIR);
break;
case svn_node_file:
svn_stringbuf_appendcstr(buf, SVN_WC__NODE_FILE);
break;
default:
SVN_ERR_MALFUNCTION();
}
svn_stringbuf_appendbytes(buf, &field_separator, 1);
switch (conflict->operation)
{
case svn_wc_operation_update:
svn_stringbuf_appendcstr(buf, SVN_WC__OPERATION_UPDATE);
break;
case svn_wc_operation_switch:
svn_stringbuf_appendcstr(buf, SVN_WC__OPERATION_SWITCH);
break;
case svn_wc_operation_merge:
svn_stringbuf_appendcstr(buf, SVN_WC__OPERATION_MERGE);
break;
default:
SVN_ERR_MALFUNCTION();
}
svn_stringbuf_appendbytes(buf, &field_separator, 1);
switch (conflict->action)
{
case svn_wc_conflict_action_edit:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_ACTION_EDITED);
break;
case svn_wc_conflict_action_delete:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_ACTION_DELETED);
break;
case svn_wc_conflict_action_add:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_ACTION_ADDED);
break;
default:
SVN_ERR_MALFUNCTION();
}
svn_stringbuf_appendbytes(buf, &field_separator, 1);
switch (conflict->reason)
{
case svn_wc_conflict_reason_edited:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_REASON_EDITED);
break;
case svn_wc_conflict_reason_deleted:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_REASON_DELETED);
break;
case svn_wc_conflict_reason_added:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_REASON_ADDED);
break;
case svn_wc_conflict_reason_missing:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_REASON_MISSING);
break;
case svn_wc_conflict_reason_obstructed:
svn_stringbuf_appendcstr(buf, SVN_WC__CONFLICT_REASON_OBSTRUCTED);
break;
default:
SVN_ERR_MALFUNCTION();
}
if (i < (conflicts->nelts - 1))
svn_stringbuf_appendbytes(buf, &desc_separator, 1);
}
dir_entry->tree_conflict_data = apr_pstrdup(pool, buf->data);
return SVN_NO_ERROR;
}
/*
* This function could be static, but we need to link to it
* in a unit test in tests/libsvn_wc/, so it isn't.
*/
svn_boolean_t
svn_wc__tree_conflict_exists(apr_array_header_t *conflicts,
const char *victim_path,
apr_pool_t *pool)
{
const svn_wc_conflict_description_t *conflict;
int i;
for (i = 0; i < conflicts->nelts; i++)
{
conflict = APR_ARRAY_IDX(conflicts, i,
svn_wc_conflict_description_t *);
if (strcmp(svn_path_basename(conflict->path, pool), victim_path) == 0)
return TRUE;
}
return FALSE;
}
svn_error_t *
svn_wc_add_tree_conflict_data(const svn_wc_conflict_description_t *conflict,
svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
svn_stringbuf_t *log_accum = svn_stringbuf_create("", pool);
SVN_ERR(svn_wc__loggy_add_tree_conflict_data(log_accum,
conflict,
adm_access,
pool));
SVN_ERR(svn_wc__write_log(adm_access, 0, log_accum, pool));
SVN_ERR(svn_wc__run_log(adm_access, NULL, pool));
return SVN_NO_ERROR;
}
svn_error_t *
svn_wc__loggy_add_tree_conflict_data(
svn_stringbuf_t *log_accum,
const svn_wc_conflict_description_t *conflict,
svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
const char *dir_path;
const svn_wc_entry_t *entry;
apr_array_header_t *conflicts;
svn_wc_entry_t tmp_entry;
/* Make sure the node is a directory.
* Otherwise we should not have been called. */
dir_path = svn_wc_adm_access_path(adm_access);
SVN_ERR(svn_wc_entry(&entry, dir_path, adm_access, TRUE, pool));
SVN_ERR_ASSERT(entry->kind == svn_node_dir);
conflicts = apr_array_make(pool, 0,
sizeof(svn_wc_conflict_description_t *));
SVN_ERR(svn_wc__read_tree_conflicts_from_entry(conflicts, entry, dir_path,
pool));
/* If CONFLICTS has a tree conflict with the same victim path as the
* new conflict, then the working copy has been corrupted. */
if (svn_wc__tree_conflict_exists(conflicts,
svn_path_basename(conflict->path, pool),
pool))
return svn_error_create(SVN_ERR_WC_CORRUPT, NULL,
_("Attempt to add tree conflict that already exists"));
APR_ARRAY_PUSH(conflicts, const svn_wc_conflict_description_t *) = conflict;
SVN_ERR(svn_wc__write_tree_conflicts_to_entry(conflicts, &tmp_entry, pool));
SVN_ERR(svn_wc__loggy_entry_modify(&log_accum,
adm_access,
dir_path,
&tmp_entry,
SVN_WC__ENTRY_MODIFY_TREE_CONFLICT_DATA,
pool));
return SVN_NO_ERROR;
}
svn_error_t *
svn_wc_get_tree_conflict(svn_wc_conflict_description_t **tree_conflict,
const char *victim_path,
svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
const char *parent_path = svn_path_dirname(victim_path, pool);
svn_wc_adm_access_t *parent_adm_access;
svn_boolean_t parent_adm_access_is_temporary = FALSE;
svn_error_t *err;
apr_array_header_t *conflicts;
const svn_wc_entry_t *entry;
int i;
/* Try to get the parent's admin access baton from the baton set. */
err = svn_wc_adm_retrieve(&parent_adm_access, adm_access, parent_path,
pool);
if (err && (err->apr_err == SVN_ERR_WC_NOT_LOCKED))
{
svn_error_clear(err);
/* Try to access the parent dir independently. We can't add
a parent's access baton to the existing access baton set
of its child, because the lifetimes would be wrong
according to doc string of svn_wc_adm_open3(), so we get
open it temporarily and close it after use. */
err = svn_wc_adm_open3(&parent_adm_access, NULL, parent_path,
FALSE, 0, NULL, NULL, pool);
parent_adm_access_is_temporary = TRUE;
/* If the parent isn't a WC dir, the child can't be
tree-conflicted. */
if (err && (err->apr_err == SVN_ERR_WC_NOT_DIRECTORY))
{
svn_error_clear(err);
*tree_conflict = NULL;
return SVN_NO_ERROR;
}
}
SVN_ERR(err);
conflicts = apr_array_make(pool, 0,
sizeof(svn_wc_conflict_description_t *));
SVN_ERR(svn_wc_entry(&entry, parent_path, parent_adm_access, TRUE, pool));
SVN_ERR(svn_wc__read_tree_conflicts_from_entry(conflicts, entry, parent_path,
pool));
*tree_conflict = NULL;
for (i = 0; i < conflicts->nelts; i++)
{
svn_wc_conflict_description_t *conflict;
conflict = APR_ARRAY_IDX(conflicts, i,
svn_wc_conflict_description_t *);
if (strcmp(svn_path_basename(conflict->path, pool),
svn_path_basename(victim_path, pool)) == 0)
{
*tree_conflict = conflict;
break;
}
}
/* If we opened a temporary admin access baton, close it. */
if (parent_adm_access_is_temporary)
SVN_ERR(svn_wc_adm_close2(parent_adm_access, pool));
return SVN_NO_ERROR;
}