Index: subversion/include/svn_types.h =================================================================== --- subversion/include/svn_types.h (revision 25291) +++ subversion/include/svn_types.h (working copy) @@ -199,6 +199,34 @@ svn_recursive }; +/** The concept of automatic conflict resolution. + * + * @since New in 1.5 + */ +typedef enum +{ + /* Resolve the conflict as usual */ + svn_accept_default, + + /* Resolve the conflict with the pre-update base file */ + svn_accept_orig, + + /* Resolve the conflict with the pre-update working copy file */ + svn_accept_mine, + + /* Resolve the conflict with the during-update repository file, new base */ + svn_accept_base, + +} 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 25291) +++ subversion/include/svn_wc.h (working copy) @@ -2474,6 +2474,23 @@ void *cancel_baton, apr_pool_t *pool); +/** + * Just like "" but with the ability to pass an svn_accept_t argument + * for automatical conflict resolution handling. + * + * @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); /** * Assuming @a path is under version control and in a state of conflict, @@ -2504,6 +2521,8 @@ * success gets reported. * * @since New in 1.2. + * + * @deprecated since 1.5. */ svn_error_t *svn_wc_resolved_conflict2(const char *path, svn_wc_adm_access_t *adm_access, Index: subversion/include/svn_client.h =================================================================== --- subversion/include/svn_client.h (revision 25291) +++ subversion/include/svn_client.h (working copy) @@ -2585,6 +2585,8 @@ * 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. + * + * @deprecated since 1.5 */ svn_error_t * svn_client_resolved(const char *path, @@ -2592,6 +2594,16 @@ svn_client_ctx_t *ctx, apr_pool_t *pool); +/** Similar to svn_client_resolved() but automatically updates the + * content of the 'conflicted' file with the contents of one of + * the conflict's temporary files. + */ +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); /** @} */ Index: subversion/libsvn_wc/adm_ops.c =================================================================== --- subversion/libsvn_wc/adm_ops.c (revision 25291) +++ 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,25 @@ 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); + + /* Handle automatic conflict resolution before the temporary files are + * deleted. */ + if (accept != svn_accept_default) + { + const char *base_name; + + if (accept == svn_accept_orig) { + base_name = entry->conflict_old; + } else if (accept == svn_accept_mine) { + base_name = entry->conflict_wrk; + } else if (accept == svn_accept_base) { + base_name = entry->conflict_new; + } /* Should never be anything else thanks to the svn_accept_from_word() */ + + SVN_ERR(svn_io_copy_file( + svn_path_join(svn_wc_adm_access_path(conflict_dir), base_name, 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 +2618,8 @@ svn_boolean_t resolve_text; /* TRUE if property conflicts are to be resolved. */ svn_boolean_t resolve_props; + /* Placeholder for 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 +2654,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 +2700,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 +2726,7 @@ baton->adm_access = adm_access; baton->notify_func = notify_func; baton->notify_baton = notify_baton; + baton->accept = accept; if (! recurse) { @@ -2694,7 +2740,6 @@ SVN_ERR(svn_wc_walk_entries2(path, adm_access, &resolve_walk_callbacks, baton, FALSE, cancel_func, cancel_baton, pool)); - } return SVN_NO_ERROR; Index: subversion/libsvn_subr/kitchensink.c =================================================================== --- subversion/libsvn_subr/kitchensink.c (revision 25291) +++ subversion/libsvn_subr/kitchensink.c (working copy) @@ -32,6 +32,18 @@ return uuid_str; } +svn_accept_t +svn_accept_from_word(const char *word) +{ + if (strcmp(word, "orig") == 0) + return svn_accept_orig; + if (strcmp(word, "mine") == 0) + return svn_accept_mine; + if (strcmp(word, "base") == 0) + return svn_accept_base; + /* Return default, which means no automatic conflict resolution */ + return svn_accept_default; +} const char * svn_depth_to_word(svn_depth_t depth) @@ -55,7 +67,6 @@ } } - svn_depth_t svn_depth_from_word(const char *word) { Index: subversion/libsvn_client/resolved.c =================================================================== --- subversion/libsvn_client/resolved.c (revision 25291) +++ 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 25291) +++ subversion/tests/cmdline/basic_tests.py (working copy) @@ -1936,6 +1936,8 @@ expected_output, expected_disk, expected_status) + +#---------------------------------------------------------------------- def basic_rm_urls_multi_repos(sbox): "remotely remove directories from two repositories" @@ -1993,6 +1995,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 'orig', 'mine', or 'base'\n", + ], + 'resolved', + '--accept=NOTVALID', + omega_path_backup) + + # Run 'svn resolved --accept=orig. Using lambda for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=orig', + lambda_path_backup) + + # Run 'svn resolved --accept=mine. Using mu for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=mine', + mu_path_backup) + + # Run 'svn resolved --accept=base. Using rho for the test. + svntest.actions.run_and_verify_svn("Resolved command", None, [], + 'resolved', + '--accept=base', + 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 @@ -2036,6 +2243,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 25291) +++ 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 25291) +++ 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 25291) +++ 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" + " " + "('orig', 'mine', or 'base')")}, + {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" @@ -1381,6 +1385,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 three expected. Since default 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_default) + { + return svn_cmdline_handle_exit_error + (svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("'%s' is not a valid accept value; try " + "'orig', 'mine', or 'base'"), + 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. */