Index: subversion/tests/cmdline/diff_tests.py =================================================================== --- subversion/tests/cmdline/diff_tests.py (revision 27293) +++ subversion/tests/cmdline/diff_tests.py (working copy) @@ -3334,6 +3334,102 @@ 'diff', '-x', '--ignore-eol-style', file_path) +def diff_summarize_xml(sbox): + "xml diff summarize" + + sbox.build() + wc_dir = sbox.wc_dir + + # A content modification. + svntest.main.file_append(os.path.join(wc_dir, "A", "mu"), "New mu content") + + # A prop modification. + svntest.main.run_svn(None, + '--username', svntest.main.wc_author, + '--password', svntest.main.wc_passwd, + "propset", "prop", "val", + os.path.join(wc_dir, 'iota')) + + # Both content and prop mods. + tau_path = os.path.join(wc_dir, "A", "D", "G", "tau") + svntest.main.file_append(tau_path, "tautau") + svntest.main.run_svn(None, + '--username', svntest.main.wc_author, + '--password', svntest.main.wc_passwd, + "propset", "prop", "val", tau_path) + + # A file addition. + newfile_path = os.path.join(wc_dir, 'newfile') + svntest.main.file_append(newfile_path, 'newfile') + svntest.main.run_svn(None, 'add', newfile_path) + + # A file deletion. + svntest.main.run_svn(None, "delete", os.path.join(wc_dir, 'A', 'B', + 'lambda')) + + # A directory addition + svntest.main.run_svn(None, "mkdir", os.path.join(wc_dir, 'newdir')) + + expected_output = svntest.wc.State(wc_dir, { + 'A/mu': Item(verb='Sending'), + 'iota': Item(verb='Sending'), + 'newfile': Item(verb='Adding'), + 'A/D/G/tau': Item(verb='Sending'), + 'A/B/lambda': Item(verb='Deleting'), + 'newdir': Item(verb='Adding'), + }) + expected_status = svntest.actions.get_virginal_state(wc_dir, 1) + expected_status.add({ + 'newfile': Item(status=' ', wc_rev=2), + 'newdir': Item(status=' ', wc_rev=2), + }) + expected_status.tweak("A/mu", "iota", "A/D/G/tau", "newfile", "newdir", + wc_rev=2) + expected_status.remove("A/B/lambda") + + + + svntest.actions.run_and_verify_commit(wc_dir, expected_output, + expected_status, None, + None, None, None, None, + wc_dir) + + # 1) Test --xml without --summarize + svntest.actions.run_and_verify_svn(None, None, + ".*--xml' option only valid with '--summarize' option", + 'diff', wc_dir, '--xml') + + # 2) Test --xml on invalid revision + svntest.actions.run_and_verify_diff_summarize_xml(".*No such revision 5555555", + wc_dir, None, None, None, + None, '-r0:5555555') + + # 3) Test working copy summarize + svntest.actions.run_and_verify_diff_summarize_xml(".*Summarizing diff can only compare repository to repository", + wc_dir, None, None, None, + None) + + # 4) Test --summarize --xml on -c2 + paths = ['iota',] + items = ['none',] + kinds = ['file',] + props = ['modified',] + + svntest.actions.run_and_verify_diff_summarize_xml([], + os.path.join(wc_dir, 'iota'), + paths, items, props, kinds, + '-c2') + + # 5) Test --summarize --xml on -r1:2 + paths = ['A/mu', 'iota', 'A/D/G/tau', 'newfile', 'A/B/lambda', + 'newdir',] + items = ['modified', 'none', 'modified', 'added', 'deleted', 'added',] + kinds = ['file','file','file','file','file', 'dir',] + props = ['none', 'modified', 'modified', 'none', 'none', 'none',] + + svntest.actions.run_and_verify_diff_summarize_xml([], wc_dir, paths, items, + props, kinds, '-r1:2') + ######################################################################## #Run the tests @@ -3383,6 +3479,7 @@ diff_in_renamed_folder, diff_with_depth, diff_ignore_eolstyle_empty_lines, + diff_summarize_xml, ] if __name__ == '__main__': Index: subversion/tests/cmdline/svntest/actions.py =================================================================== --- subversion/tests/cmdline/svntest/actions.py (revision 27293) +++ subversion/tests/cmdline/svntest/actions.py (working copy) @@ -18,6 +18,7 @@ import os, shutil, re, sys, errno import difflib, pprint import xml.parsers.expat +from xml.dom.minidom import parseString import main, verify, tree, wc # general svntest routines in this module. from svntest import Failure @@ -895,6 +896,67 @@ else: tree.compare_trees (actual, output_tree) +def run_and_verify_diff_summarize_xml(error_re_string = [], + object = '', + expected_paths = [], + expected_items = [], + expected_props = [], + expected_kinds = [], + *args): + """Run 'diff --summarize --xml' with the arguments *ARGS. + If ERROR_RE_STRING, the ocmmand must exit with error, and the error + message must match regular expression ERROR_RE_STRING. + + Else if ERROR_RE_STRING is None, the subcommand output will be parsed + into an XML document and will then be verified by comparing the parsed + output to the contents in the EXPECTED_PATHS, EXPECTED_ITEMS, + EXPECTED_PROPS and EXPECTED_KINDS. Returns on success, raises + on failure.""" + + output, errput = run_and_verify_svn(None, None, error_re_string, 'diff', + object, '--summarize', '--xml', *args) + + # Return if errors are present since they were expected + if len(errput) > 0: + return + + doc = parseString(''.join(output)) + paths = doc.getElementsByTagName("path") + modified_paths = expected_paths + items = expected_items + kinds = expected_kinds + modified_props = expected_props + + for path in paths: + modified_path = path.childNodes[0].data.replace(object, '')[1:].strip() + + # Workaround single-object diff + if len(modified_path) == 0: + modified_path = path.childNodes[0].data.split("/")[-1] + + if modified_path not in modified_paths: + print "ERROR: %s not expected in the changed paths." % modified_path + raise Failure + + index = modified_paths.index(modified_path) + expected_item = items[index] + expected_kind = kinds[index] + expected_props = modified_props[index] + actual_item = path.getAttribute('item') + actual_kind = path.getAttribute('kind') + actual_props = path.getAttribute('props') + + if expected_item != actual_item: + print "ERROR: expected:", expected_item, "actual:", actual_item + raise Failure + + if expected_kind != actual_kind: + print "ERROR: expected:", expected_kind, "actual:", actual_kind + raise Failure + + if expected_props != actual_props: + print "ERROR: expected:", expected_props, "actual:", actual_props + raise Failure def run_and_verify_diff_summarize(output_tree, error_re_string = None, singleton_handler_a = None, Index: subversion/svn/diff-cmd.c =================================================================== --- subversion/svn/diff-cmd.c (revision 27293) +++ subversion/svn/diff-cmd.c (working copy) @@ -30,6 +30,7 @@ #include "svn_error.h" #include "svn_types.h" #include "svn_cmdline.h" +#include "svn_xml.h" #include "cl.h" #include "svn_private_config.h" @@ -39,7 +40,7 @@ /* Convert KIND into a single character for display to the user. */ static char -text_mod_char(svn_client_diff_summarize_kind_t kind) +kind_to_char(svn_client_diff_summarize_kind_t kind) { switch (kind) { @@ -57,6 +58,52 @@ } } +/* Convert KIND into a word describing the kind to the user. */ +static const char * +kind_to_word(svn_client_diff_summarize_kind_t kind) +{ + switch (kind) + { + case svn_client_diff_summarize_kind_modified: return "modified"; + case svn_client_diff_summarize_kind_added: return "added"; + case svn_client_diff_summarize_kind_deleted: return "deleted"; + default: return "none"; + } +} + +/* Print summary information about a given change as XML, implements the + * svn_client_diff_summarize_func_t interface. */ +static svn_error_t * +summarize_func_xml(const svn_client_diff_summarize_t *summary, + void *baton, + apr_pool_t *pool) +{ + const char *path = baton; + + /* Tack on the target path, so we can differentiate between different parts + * of the output when we're given multiple targets. */ + path = svn_path_join(path, summary->path, pool); + + /* Convert non-urls to local style, so that things like "" show up as "." */ + if (! svn_path_is_url(path)) + path = svn_path_local_style(path, pool); + + svn_stringbuf_t *sb = svn_stringbuf_create("", pool); + + svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path", + "kind", svn_cl__node_kind_str(summary->node_kind), + "item", kind_to_word(summary->summarize_kind), + "props", summary->prop_changed ? "modified" : "none", + NULL); + + svn_xml_escape_cdata_cstring(&sb, path, pool); + svn_xml_make_close_tag(&sb, pool, "path"); + + SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); + + return SVN_NO_ERROR; +} + /* Print summary information about a given change, implements the * svn_client_diff_summarize_func_t interface. */ static svn_error_t * @@ -80,7 +127,7 @@ SVN_ERR(svn_cmdline_printf(pool, "%c%c %s\n", - text_mod_char(summary->summarize_kind), + kind_to_char(summary->summarize_kind), summary->prop_changed ? 'M' : ' ', path)); @@ -119,6 +166,21 @@ return svn_error_wrap_apr(status, _("Can't open stdout")); if ((status = apr_file_open_stderr(&errfile, pool))) return svn_error_wrap_apr(status, _("Can't open stderr")); + + if (opt_state-> xml) + { + /* Check that the --summarize is passed as well. */ + if (!opt_state->summarize) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("'--xml' option only valid with " + "'--summarize' option")); + + SVN_ERR(svn_cl__xml_print_header("diff", pool)); + + svn_stringbuf_t *sb = svn_stringbuf_create("", pool); + svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths", NULL); + SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); + } /* Before allowing svn_opt_args_to_target_array2() to canonicalize all the targets, we need to build a list of targets made of both @@ -261,6 +323,8 @@ iterpool = svn_pool_create(pool); for (i = 0; i < targets->nelts; ++i) { + const svn_client_diff_summarize_func_t summarize_callback = + (opt_state->xml ? summarize_func_xml : summarize_func); const char *path = APR_ARRAY_IDX(targets, i, const char *); const char *target1, *target2; @@ -278,7 +342,7 @@ &opt_state->end_revision, opt_state->depth, opt_state->notice_ancestry ? FALSE : TRUE, - summarize_func, + summarize_callback, (void *) target1, ((svn_cl__cmd_baton_t *)baton)->ctx, iterpool)); @@ -321,7 +385,7 @@ &opt_state->end_revision, opt_state->depth, opt_state->notice_ancestry ? FALSE : TRUE, - summarize_func, + summarize_callback, (void *) truepath, ((svn_cl__cmd_baton_t *)baton)->ctx, iterpool)); @@ -343,6 +407,15 @@ iterpool)); } } + + if (opt_state-> xml) + { + svn_stringbuf_t *sb = svn_stringbuf_create("", pool); + svn_xml_make_close_tag(&sb, pool, "paths"); + SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); + SVN_ERR(svn_cl__xml_print_footer("diff", pool)); + } + svn_pool_destroy(iterpool); return SVN_NO_ERROR; Index: subversion/svn/schema/diff.rnc =================================================================== --- subversion/svn/schema/diff.rnc (revision 0) +++ subversion/svn/schema/diff.rnc (revision 0) @@ -0,0 +1,18 @@ +# XML RELAX NG schema for Subversion command-line client output +# For "svn diff --summarize --xml" + +include "common.rnc" + +start = diff + +paths = element paths { path* } + +## A path entry +path = element path { attlist.path, text } +attlist.path &= + ## The props of the entry. + attribute props { "none" | "modified" }, + ## The kind of the entry. + attribute kind { "dir" | "file" }, + ## The item of the entry. + attribute item { "none" | "added" | "modified" | "deleted" } Index: subversion/svn/main.c =================================================================== --- subversion/svn/main.c (revision 27293) +++ subversion/svn/main.c (working copy) @@ -410,7 +410,7 @@ {'r', 'c', svn_cl__old_cmd_opt, svn_cl__new_cmd_opt, 'N', svn_cl__depth_opt, svn_cl__diff_cmd_opt, 'x', svn_cl__no_diff_deleted, svn_cl__notice_ancestry_opt, svn_cl__summarize, svn_cl__changelist_opt, - svn_cl__force_opt, SVN_CL__AUTH_OPTIONS, + svn_cl__force_opt, svn_cl__xml_opt, SVN_CL__AUTH_OPTIONS, svn_cl__config_dir_opt} }, { "export", svn_cl__export, {0}, N_