Index: subversion/include/svn_types.h =================================================================== --- subversion/include/svn_types.h (revision 25367) +++ subversion/include/svn_types.h (working copy) @@ -199,6 +199,37 @@ svn_recursive }; +/** The concept of automatic conflict resolution. + * + * @since New in 1.5. + */ +typedef enum +{ + /* Invalid accept flag */ + svn_accept_invalid = -1, + + /* Resolve the conflict as usual */ + svn_accept_default, + + /* Resolve the conflict with the pre-conflict base file */ + svn_accept_left, + + /* Resolve the conflict with the pre-conflict working copy file */ + svn_accept_working, + + /* Resolve the conflict with the post-conflict base file */ + svn_accept_right, + +} svn_accept_t; + +/** Return the appropriate accept for @a accept_str. @a word is as + * returned from svn_accept_to_word(). + * + * @since New in 1.5. + */ +svn_accept_t +svn_accept_from_word(const char *word); + /** The concept of depth for directories. * * @note This is similar to, but not exactly the same as, the WebDAV Index: subversion/include/svn_wc.h =================================================================== --- subversion/include/svn_wc.h (revision 25367) +++ subversion/include/svn_wc.h (working copy) @@ -2481,6 +2481,16 @@ * any text conflict is resolved, if @a resolve_props is true then any * property conflicts are resolved. If @a recurse is true, then search * recursively for conflicts to resolve. + * + * @a accept is the argument used to facilitate automatic conflict + * resolution. If @a accept is svn_accept_left, the contents of the conflicted + * file will be replaced with the prestine contents of the pre-modification + * base file contents. If @a accept is svn_accept_right, the contents of the + * conflicted file will be replaced with the post-conflict base file contents. + * If @a accept is svn_accept_working, the contents of the conflicted file + * will be the content of the pre-conflict working copy file. If @a accept + * is svn_accept_default, conflict resolution will be handled just like + * before automatic conflict resolution was availble. * * @a adm_access is an access baton, with a write lock, for @a path. * @@ -2503,8 +2513,28 @@ * property conflict resolution was requested, and it was successful, then * success gets reported. * - * @since New in 1.2. + * @since New in 1.5. + * */ +svn_error_t *svn_wc_resolved_conflict3(const char *path, + svn_wc_adm_access_t *adm_access, + svn_boolean_t resolve_text, + svn_boolean_t resolve_props, + svn_boolean_t recurse, + svn_accept_t accept, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool); + + +/** + * Similar to svn_wc_resolved_conflict3(), but without automatic conflict + * resolution support. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ svn_error_t *svn_wc_resolved_conflict2(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t resolve_text, Index: subversion/include/svn_client.h =================================================================== --- subversion/include/svn_client.h (revision 25367) +++ subversion/include/svn_client.h (working copy) @@ -2573,11 +2573,33 @@ * @{ */ +/** + * Similar to svn_client_resolved2(), but without automatic conflict + * resolution support. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ +svn_error_t * +svn_client_resolved(const char *path, + svn_boolean_t recursive, + svn_client_ctx_t *ctx, + apr_pool_t *pool); + /** Remove the 'conflicted' state on a working copy @a path. This will * not semantically resolve conflicts; it just allows @a path to be * committed in the future. The implementation details are opaque. * If @a recursive is set, recurse below @a path, looking for conflicts * to resolve. + * + * @a accept is the argument used to facilitate automatic conflict + * resolution. If @a accept is svn_accept_left, the contents of the conflicted + * file will be replaced with the prestine contents of the pre-modification + * base file contents. If @a accept is svn_accept_right, the contents of the + * conflicted file will be replaced with the post-conflict base file contents. + * If @a accept is svn_accept_working, the contents of the conflicted file + * will be the content of the pre-conflict working copy file. If @a accept + * is svn_accept_default, conflict resolution will be handled just like + * before automatic conflict resolution was availble. * * ### TODO(sd): I don't see any reason to change this recurse parameter * ### to a depth, but making a note to re-check this logic later. @@ -2585,14 +2607,16 @@ * If @a path is not in a state of conflict to begin with, do nothing. * If @a path's conflict state is removed and @a ctx->notify_func2 is non-null, * call @a ctx->notify_func2 with @a ctx->notify_baton2 and @a path. + * + * @since New in 1.5. */ svn_error_t * -svn_client_resolved(const char *path, - svn_boolean_t recursive, - svn_client_ctx_t *ctx, - apr_pool_t *pool); +svn_client_resolved2(const char *path, + svn_boolean_t recursive, + svn_accept_t accept, + svn_client_ctx_t *ctx, + apr_pool_t *pool); - /** @} */ /** Index: subversion/libsvn_wc/adm_ops.c =================================================================== --- subversion/libsvn_wc/adm_ops.c (revision 25367) +++ subversion/libsvn_wc/adm_ops.c (working copy) @@ -2499,7 +2499,11 @@ /* Conflict resolution involves removing the conflict files, if they exist, and clearing the conflict filenames from the entry. The latter needs to - be done whether or not the conflict files exist. + be done whether or not the conflict files exist. If @a accept is anything + but svn_accept_default, automatically resolve the + conflict with the respective temporary file contents. + + @since 1.5 Automatic Conflict Resolution (Issue 2784) PATH is the path to the item to be resolved, BASE_NAME is the basename of PATH, and CONFLICT_DIR is the access baton for PATH. ORIG_ENTRY is @@ -2512,6 +2516,7 @@ const char *base_name, svn_boolean_t resolve_text, svn_boolean_t resolve_props, + svn_accept_t accept, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) @@ -2519,6 +2524,33 @@ svn_boolean_t was_present, need_feedback = FALSE; apr_uint64_t modify_flags = 0; svn_wc_entry_t *entry = svn_wc_entry_dup(orig_entry, pool); + const char *auto_resolve_src; + + /* Handle automatic conflict resolution before the temporary files are + * deleted, if necessary. */ + switch (accept) + { + case svn_accept_left: + auto_resolve_src = entry->conflict_old; + break; + case svn_accept_working: + auto_resolve_src = entry->conflict_wrk; + break; + case svn_accept_right: + auto_resolve_src = entry->conflict_new; + break; + case svn_accept_default: + auto_resolve_src = NULL; + break; + case svn_accept_invalid: + return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Invalid 'accept' argument")); + } + + if (auto_resolve_src) + SVN_ERR(svn_io_copy_file( + svn_path_join(svn_wc_adm_access_path(conflict_dir), auto_resolve_src, pool), + path, TRUE, pool)); /* Yes indeed, being able to map a function over a list would be nice. */ if (resolve_text && entry->conflict_old) @@ -2594,6 +2626,8 @@ svn_boolean_t resolve_text; /* TRUE if property conflicts are to be resolved. */ svn_boolean_t resolve_props; + /* The type of automatic conflict resolution to perform */ + svn_accept_t accept; /* An access baton for the tree, with write access */ svn_wc_adm_access_t *adm_access; /* Notification function and baton */ @@ -2628,8 +2662,8 @@ return resolve_conflict_on_entry(path, entry, adm_access, base_name, baton->resolve_text, baton->resolve_props, - baton->notify_func, baton->notify_baton, - pool); + baton->accept, baton->notify_func, + baton->notify_baton, pool); } static const svn_wc_entry_callbacks_t @@ -2674,6 +2708,25 @@ void *cancel_baton, apr_pool_t *pool) { + return svn_wc_resolved_conflict3(path, adm_access, resolve_text, + resolve_props, recurse, svn_accept_default, + notify_func, notify_baton, cancel_func, + cancel_baton, pool); +} + +svn_error_t * +svn_wc_resolved_conflict3(const char *path, + svn_wc_adm_access_t *adm_access, + svn_boolean_t resolve_text, + svn_boolean_t resolve_props, + svn_boolean_t recurse, + svn_accept_t accept, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ struct resolve_callback_baton *baton = apr_pcalloc(pool, sizeof(*baton)); baton->resolve_text = resolve_text; @@ -2681,6 +2734,7 @@ baton->adm_access = adm_access; baton->notify_func = notify_func; baton->notify_baton = notify_baton; + baton->accept = accept; if (! recurse) { Index: subversion/libsvn_subr/kitchensink.c =================================================================== --- subversion/libsvn_subr/kitchensink.c (revision 25367) +++ subversion/libsvn_subr/kitchensink.c (working copy) @@ -32,6 +32,19 @@ return uuid_str; } +svn_accept_t +svn_accept_from_word(const char *word) +{ + if (strcmp(word, "left") == 0) + return svn_accept_left; + if (strcmp(word, "working") == 0) + return svn_accept_working; + if (strcmp(word, "right") == 0) + return svn_accept_right; + /* Return svn_accept_invalid which means that the passed string is not + * a recognized accept option. */ + return svn_accept_invalid; +} const char * svn_depth_to_word(svn_depth_t depth) Index: subversion/libsvn_client/resolved.c =================================================================== --- subversion/libsvn_client/resolved.c (revision 25367) +++ subversion/libsvn_client/resolved.c (working copy) @@ -37,6 +37,16 @@ svn_client_ctx_t *ctx, apr_pool_t *pool) { + return svn_client_resolved2(path, recursive, svn_accept_default, ctx, pool); +} + +svn_error_t * +svn_client_resolved2(const char *path, + svn_boolean_t recursive, + svn_accept_t accept, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ svn_wc_adm_access_t *adm_access; SVN_ERR(svn_wc_adm_probe_open3(&adm_access, NULL, path, TRUE, @@ -44,7 +54,8 @@ ctx->cancel_func, ctx->cancel_baton, pool)); - SVN_ERR(svn_wc_resolved_conflict2(path, adm_access, TRUE, TRUE, recursive, + SVN_ERR(svn_wc_resolved_conflict3(path, adm_access, TRUE, TRUE, recursive, + accept, ctx->notify_func2, ctx->notify_baton2, ctx->cancel_func, ctx->cancel_baton, pool)); Index: subversion/tests/cmdline/basic_tests.py =================================================================== --- subversion/tests/cmdline/basic_tests.py (revision 25367) +++ subversion/tests/cmdline/basic_tests.py (working copy) @@ -1925,6 +1925,8 @@ expected_output, expected_disk, expected_status) + +#---------------------------------------------------------------------- def basic_rm_urls_multi_repos(sbox): "remotely remove directories from two repositories" @@ -1982,6 +1984,211 @@ expected_disk, expected_status) +#----------------------------------------------------------------------- +def automatic_conflict_resolution(sbox): + "automatic conflict resolution" + + sbox.build() + wc_dir = sbox.wc_dir + + # Make a backup copy of the working copy + wc_backup = sbox.add_wc_path('backup') + svntest.actions.duplicate_dir(wc_dir, wc_backup) + + # Make a couple of local mods to files which will be committed + mu_path = os.path.join(wc_dir, 'A', 'mu') + lambda_path = os.path.join(wc_dir, 'A', 'B', 'lambda') + rho_path = os.path.join(wc_dir, 'A', 'D', 'G', 'rho') + omega_path = os.path.join(wc_dir, 'A', 'D', 'H', 'omega') + svntest.main.file_append(mu_path, 'Original appended text for mu\n') + svntest.main.file_append(lambda_path, 'Original appended text for lambda\n') + svntest.main.file_append(rho_path, 'Original appended text for rho\n') + svntest.main.file_append(omega_path, 'Original appended text for omega\n') + + # Make a couple of local mods to files which will be conflicted + mu_path_backup = os.path.join(wc_backup, 'A', 'mu') + lambda_path_backup = os.path.join(wc_backup, 'A', 'B', 'lambda') + rho_path_backup = os.path.join(wc_backup, 'A', 'D', 'G', 'rho') + omega_path_backup = os.path.join(wc_backup, 'A', 'D', 'H', 'omega') + svntest.main.file_append(mu_path_backup, + 'Conflicting appended text for mu\n') + svntest.main.file_append(lambda_path_backup, + 'Conflicting appended text for lambda\n') + svntest.main.file_append(rho_path_backup, + 'Conflicting appended text for rho\n') + svntest.main.file_append(omega_path_backup, + 'Conflicting appended text for omega\n') + + # Created expected output tree for 'svn ci' + expected_output = wc.State(wc_dir, { + 'A/mu' : Item(verb='Sending'), + 'A/B/lambda' : Item(verb='Sending'), + 'A/D/G/rho' : Item(verb='Sending'), + 'A/D/H/omega' : Item(verb='Sending'), + }) + + # Create expected status tree; all local revisions should be at 1, + # but lambda, mu and rho should be at revision 2. + expected_status = svntest.actions.get_virginal_state(wc_dir, 1) + expected_status.tweak('A/mu', 'A/B/lambda', 'A/D/G/rho', 'A/D/H/omega', + wc_rev=2) + + # Commit. + svntest.actions.run_and_verify_commit(wc_dir, expected_output, + expected_status, None, + None, None, None, None, wc_dir) + + # Create expected output tree for an update of the wc_backup. + expected_output = wc.State(wc_backup, { + 'A/mu' : Item(status='C '), + 'A/B/lambda' : Item(status='C '), + 'A/D/G/rho' : Item(status='C '), + 'A/D/H/omega' : Item(status='C '), + }) + + # Create expected disk tree for the update. + expected_disk = svntest.main.greek_state.copy() + expected_disk.tweak('A/B/lambda', + contents="\n".join(["This is the file 'lambda'.", + "<<<<<<< .mine", + "Conflicting appended text for lambda", + "=======", + "Original appended text for lambda", + ">>>>>>> .r2", + ""])) + expected_disk.tweak('A/mu', + contents="\n".join(["This is the file 'mu'.", + "<<<<<<< .mine", + "Conflicting appended text for mu", + "=======", + "Original appended text for mu", + ">>>>>>> .r2", + ""])) + expected_disk.tweak('A/D/G/rho', + contents="\n".join(["This is the file 'rho'.", + "<<<<<<< .mine", + "Conflicting appended text for rho", + "=======", + "Original appended text for rho", + ">>>>>>> .r2", + ""])) + expected_disk.tweak('A/D/H/omega', + contents="\n".join(["This is the file 'omega'.", + "<<<<<<< .mine", + "Conflicting appended text for omega", + "=======", + "Original appended text for omega", + ">>>>>>> .r2", + ""])) + + # Create expected status tree for the update. + expected_status = svntest.actions.get_virginal_state(wc_backup, '2') + expected_status.tweak('A/mu', 'A/B/lambda', 'A/D/G/rho', 'A/D/H/omega', + status='C ') + + # "Extra" files that we expect to result from the conflicts. + # These are expressed as list of regexps. What a cool system! :-) + extra_files = ['mu.*\.r1', 'mu.*\.r2', 'mu.*\.mine', + 'lambda.*\.r1', 'lambda.*\.r2', 'lambda.*\.mine', + 'omega.*\.r1', 'omega.*\.r2', 'omega.*\.mine', + 'rho.*\.r1', 'rho.*\.r2', 'rho.*\.mine',] + + # Do the update and check the results in three ways. + # All "extra" files are passed to detect_conflict_files(). + svntest.actions.run_and_verify_update(wc_backup, + expected_output, + expected_disk, + expected_status, + None, + svntest.tree.detect_conflict_files, + extra_files) + + # verify that the extra_files list is now empty. + if len(extra_files) != 0: + # Because we want to be a well-behaved test, we silently raise if + # the test fails. However, these two print statements would + # probably reveal the cause for the failure, if they were + # uncommented: + # + # print "Not all extra reject files have been accounted for:" + # print extra_files + ### we should raise a less generic error here. which? + raise svntest.Failure + + # So now lambda, mu and rho are all in a "conflicted" state. Run 'svn + # resolved' with the respective "--accept[mine|orig|repo]" flag. + + # Run 'svn resolved --accept=NOTVALID. Using omega for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [ + "svn: 'NOTVALID' is not a valid accept value; " + "try 'left', 'right', or 'working'\n", + ], + 'resolved', + '--accept=NOTVALID', + omega_path_backup) + + # Run 'svn resolved --accept=left. Using lambda for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=left', + lambda_path_backup) + + # Run 'svn resolved --accept=working. Using mu for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=working', + mu_path_backup) + + # Run 'svn resolved --accept=right. Using rho for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=right', + rho_path_backup) + + # Set the expected disk contents for the test + expected_disk = svntest.main.greek_state.copy() + + expected_disk.tweak('A/B/lambda', contents="This is the file 'lambda'.\n") + expected_disk.tweak('A/mu', contents="This is the file 'mu'.\n" + "Conflicting appended text for mu\n") + expected_disk.tweak('A/D/G/rho', contents="This is the file 'rho'.\n" + "Original appended text for rho\n") + expected_disk.tweak('A/D/H/omega', + contents="\n".join(["This is the file 'omega'.", + "<<<<<<< .mine", + "Conflicting appended text for omega", + "=======", + "Original appended text for omega", + ">>>>>>> .r2", + ""])) + + # Set the expected extra files for the test + extra_files = ['omega.*\.r1', 'omega.*\.r2', 'omega.*\.mine',] + + # Set the expected status for the test + expected_status = svntest.actions.get_virginal_state(wc_backup, 2) + expected_status.tweak('A/mu', 'A/B/lambda', 'A/D/G/rho', 'A/D/H/omega', + wc_rev=2) + + expected_status.tweak('A/mu', status='M ') + expected_status.tweak('A/B/lambda', status='M ') + expected_status.tweak('A/D/G/rho', status=' ') + expected_status.tweak('A/D/H/omega', status='C ') + + # Set the expected output for the test + expected_output = wc.State(wc_backup, {}) + + # Do the update and check the results in three ways. + svntest.actions.run_and_verify_update(wc_backup, + expected_output, + expected_disk, + expected_status, + None, + svntest.tree.detect_conflict_files, + extra_files) + +#---------------------------------------------------------------------- + ######################################################################## # Run the tests @@ -2025,6 +2232,7 @@ windows_paths_in_repos, basic_rm_urls_one_repo, XFail(basic_rm_urls_multi_repos), + automatic_conflict_resolution, ] if __name__ == '__main__': Index: subversion/svn/cl.h =================================================================== --- subversion/svn/cl.h (revision 25367) +++ subversion/svn/cl.h (working copy) @@ -85,7 +85,8 @@ svn_cl__xml_opt, svn_cl__keep_local_opt, svn_cl__with_revprop_opt, - svn_cl__parents_opt + svn_cl__parents_opt, + svn_cl__accept_opt, } svn_cl__longopt_t; @@ -159,6 +160,7 @@ apr_hash_t *revprop_table; /* table with revision properties to set */ svn_boolean_t parents; /* create intermediate directories */ svn_boolean_t use_merge_history; /* use/display extra merge information */ + svn_accept_t accept; /* automatically resolve conflict with original, wc or repository file */ } svn_cl__opt_state_t; Index: subversion/svn/resolved-cmd.c =================================================================== --- subversion/svn/resolved-cmd.c (revision 25367) +++ subversion/svn/resolved-cmd.c (working copy) @@ -64,8 +64,9 @@ const char *target = APR_ARRAY_IDX(targets, i, const char *); svn_pool_clear(subpool); SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton)); - err = svn_client_resolved(target, + err = svn_client_resolved2(target, SVN_DEPTH_TO_RECURSE(opt_state->depth), + opt_state->accept, ctx, subpool); if (err) Index: subversion/svn/main.c =================================================================== --- subversion/svn/main.c (revision 25367) +++ subversion/svn/main.c (working copy) @@ -207,7 +207,11 @@ {"use-merge-history", 'g', 0, N_("use/display additional information from merge " "history")}, - {0, 0, 0, 0} + {"accept", svn_cl__accept_opt, 1, + N_("specify automatic conflict resolution source\n" + " " + "('left', 'right', or 'working')")}, + {0, 0, 0, 0}, }; @@ -696,7 +700,7 @@ " remove conflict markers; it merely removes the conflict-related\n" " artifact files and allows PATH to be committed again.\n"), {svn_cl__targets_opt, 'R', svn_cl__depth_opt, 'q', - svn_cl__config_dir_opt} }, + svn_cl__config_dir_opt, svn_cl__accept_opt} }, { "revert", svn_cl__revert, {0}, N_ ("Restore pristine working copy file (undo most local edits).\n" @@ -1005,6 +1009,7 @@ opt_state.start_revision.kind = svn_opt_revision_unspecified; opt_state.end_revision.kind = svn_opt_revision_unspecified; opt_state.depth = svn_depth_unknown; + opt_state.accept = svn_accept_default; /* No args? Show usage. */ if (argc <= 1) @@ -1381,6 +1386,21 @@ case 'g': opt_state.use_merge_history = TRUE; break; + case svn_cl__accept_opt: + opt_state.accept = svn_accept_from_word(opt_arg); + + /* We need to make sure that the value passed to the accept flag + * was one of the available options. Since svn_accept_invalid is what + * gets set when one of the three expected are not passed, checking + * for this as part of the command line parsing makes sense. */ + if (opt_state.accept == svn_accept_invalid) + { + return svn_cmdline_handle_exit_error + (svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("'%s' is not a valid accept value; try " + "'left', 'right', or 'working'"), + opt_arg), pool, "svn: "); + } default: /* Hmmm. Perhaps this would be a good place to squirrel away opts that commands like svn diff might need. Hmmm indeed. */