Index: subversion/include/svn_types.h =================================================================== --- subversion/include/svn_types.h (revision 13365) +++ subversion/include/svn_types.h (working copy) @@ -247,6 +247,11 @@ * @{ */ +/** The SVN_KEYWORD_... defines are used at the below places + * subversion/libsvn_wc/props.c + * subversion/libsvn_subr/subst.c + */ + /** The maximum size of an expanded or un-expanded keyword. */ #define SVN_KEYWORD_MAX_LEN 255 Index: subversion/libsvn_wc/props.c =================================================================== --- subversion/libsvn_wc/props.c (revision 13365) +++ subversion/libsvn_wc/props.c (working copy) @@ -987,6 +987,82 @@ } +/** Return in a svn_stringbuf_t *, the + * canonicalized equivalent of @a value. + * Repeated instances of the same keyword are ignored. + * + * All memory is allocated out of @a pool. + */ +static svn_stringbuf_t * +canonicalize_keywords (const svn_string_t *value, + apr_pool_t *pool) +{ + /* Structure to be used for canonicalization of svn:keywords */ + typedef struct canon_table_s + { + const char *prop_value; /* the property value */ + const int index; /* the index to the corresponding canonical form */ + const int flag; /* bit pattern to flag that this canonical form + ** is already set */ + } canon_table_t; + static canon_table_t canon_kw_table[] + = { + { SVN_KEYWORD_REVISION_LONG, 2, 0x0001 }, + { SVN_KEYWORD_REVISION_SHORT, 2, 0x0001 }, + { SVN_KEYWORD_REVISION_MEDIUM, 2, 0x0001 }, + { SVN_KEYWORD_DATE_LONG, 4, 0x0002 }, + { SVN_KEYWORD_DATE_SHORT, 4, 0x0002 }, + { SVN_KEYWORD_AUTHOR_LONG, 6, 0x0004 }, + { SVN_KEYWORD_AUTHOR_SHORT, 6, 0x0004 }, + { SVN_KEYWORD_URL_SHORT, 7, 0x0008 }, + { SVN_KEYWORD_URL_LONG, 7, 0x0008 }, + { SVN_KEYWORD_ID, 9, 0x0010 } + }; + const int sizeof_canon_kw_table + = sizeof (canon_kw_table) / sizeof (canon_table_t); + + apr_array_header_t *keyword_tokens; + svn_stringbuf_t *canonicalized_value = NULL; + int flags = 0 ; + int i, j; + + /* tokenize the input */ + keyword_tokens = svn_cstring_split (value->data, " \t\v\n\b\r\f", + TRUE /* chop */, pool); + + /* for all the tokens */ + for (i = 0; i < keyword_tokens->nelts; ++i) + { + const char *keyword = APR_ARRAY_IDX (keyword_tokens, i, const char *); + + for (j = 0; j < sizeof_canon_kw_table; j++) + { + /* see if an equivalent standard form exists */ + if ((! strcasecmp (canon_kw_table[j].prop_value, keyword)) + && (! (flags & canon_kw_table[j].flag ))) + { + /* If so, canonicalize and prepare output */ + if (!canonicalized_value) + canonicalized_value = svn_stringbuf_create + (canon_kw_table[canon_kw_table[j].index].prop_value, + pool); + else + { + svn_stringbuf_appendcstr (canonicalized_value, " "); + svn_stringbuf_appendcstr ( + canonicalized_value, + canon_kw_table[canon_kw_table[j].index].prop_value); + } + flags |= canon_kw_table[j].flag; + break; /* goto the next token from the input */ + } + } + } + + return canonicalized_value; +} + + svn_error_t * svn_wc_prop_set2 (const char *name, const svn_string_t *value, @@ -1061,7 +1137,7 @@ } else if (strcmp (name, SVN_PROP_KEYWORDS) == 0) { - new_value = svn_stringbuf_create_from_string (value, pool); + new_value = canonicalize_keywords (value, pool); svn_stringbuf_strip_whitespace (new_value); } } Index: subversion/tests/clients/cmdline/prop_tests.py =================================================================== --- subversion/tests/clients/cmdline/prop_tests.py (revision 13365) +++ subversion/tests/clients/cmdline/prop_tests.py (working copy) @@ -857,8 +857,8 @@ ['foo http://foo.com/repos'+os.linesep]) # Check svn:keywords - check_prop('svn:keywords', iota_path, ['Rev Date']) - check_prop('svn:keywords', mu_path, ['Rev Date']) + check_prop('svn:keywords', iota_path, ['Revision Date']) + check_prop('svn:keywords', mu_path, ['Revision Date']) # Check svn:executable check_prop('svn:executable', iota_path, ['*']) Index: subversion/tests/clients/cmdline/trans_tests.py =================================================================== --- subversion/tests/clients/cmdline/trans_tests.py (revision 13367) +++ subversion/tests/clients/cmdline/trans_tests.py (working copy) @@ -714,7 +714,111 @@ expected_status = svntest.actions.get_virginal_state(wc_dir, 1) svntest.actions.run_and_verify_status(wc_dir, expected_status) - + +#---------------------------------------------------------------------- +# Testing for canonicalized input to svn:keywords +# Propset a case-different keyword and check if +# expansion works +def canonicalize_keywords_prop(sbox): + "test canonicalization of the svn:keywords input" + + tests = [ + ("lastchangedrevision", "Revision\n"), + ("lastcHANgedREviSIoN", "Revision\n"), + ("revision", "Revision\n"), + ("rev", "Revision\n"), + ("lastchangedby", "Author\n"), + ("author", "Author\n"), + ("aUtHoR", "Author\n"), + ("headurl", "URL\n"), + ("url", "URL\n"), + ("lasTChangEDdate", "Date\n"), + ("date", "Date\n"), + ("DaTe", "Date\n"), + ("id", "Id\n"), + ("lastchangedrevision author LastChangedBy", "Revision Author\n"), + ] + + sbox.build() + wc_dir = sbox.wc_dir + + iota_path = os.path.join(wc_dir, 'iota') + + for given_input, expected_output in tests: + svntest.actions.run_and_verify_svn(None, None, [], 'propset', + "svn:keywords", + given_input, + iota_path) + svntest.actions.run_and_verify_svn(None, [expected_output], [], 'propget', + "svn:keywords", + iota_path) + + +#---------------------------------------------------------------------- +# Testing for expansion of keywords in the file +# in addition to canonicalizable input to svn:keywords +# Propset a case-different keyword and check if +# expansion works +def canonicalized_and_keywords_expanded(sbox): + "test keyword expansion after canonicalization" + + keywords = [ + 'LastChangedRevision', + 'Revision', + 'Rev', + 'LastChangedDate', + 'Date', + 'LastChangedBy', + 'Author', + 'HeadURL', + 'URL', + ] + + sbox.build() + wc_dir = sbox.wc_dir + + Z_path = os.path.join(wc_dir, 'Z') + svntest.actions.run_and_verify_svn(None, None, [], 'mkdir', Z_path) + + # Add the file that has the keyword to be expanded + url_path = os.path.join(Z_path, 'url') + for kwd in keywords: + svntest.main.file_append(url_path, + kwd + ":$" + kwd + "$\n" + + kwd.lower() + ":$" + kwd.lower() + "$\n") + + svntest.actions.run_and_verify_svn(None, None, [], 'add', url_path) + svntest.actions.run_and_verify_svn(None, None, [], 'propset', + "svn:keywords", + "lastchangedrevision lastchangeddate lastchangedby headurl", + url_path) + + svntest.actions.run_and_verify_svn(None, None, [], + 'ci', '-m', 'log msg', wc_dir) + + # Check that the properly capitalised keyword "kwd" has been expanded on + # the first line of "lines", and a lower-case version of it has not been + # expanded on the next line of "lines". + def kwd_test(kwd, lines): + if not (lines[0].startswith(kwd + ":$" + kwd + ": ")): + print kwd + " expansion failed for", url_path + print "Line is: " + lines[0] + raise svntest.Failure + kwd = kwd.lower() + if not (lines[1].startswith(kwd + ":$" + kwd + "$")): + print kwd + " expansion failed for", url_path + print "Line is: " + lines[1] + raise svntest.Failure + + # Check keyword got expanded (and thus the mkdir, add, ps, commit + # etc. worked) + fp = open(url_path, 'r') + lines = fp.readlines() + for kwd in keywords: + kwd_test(kwd, lines) + lines = lines[2:] + fp.close() + ######################################################################## # Run the tests @@ -732,6 +836,8 @@ copy_propset_commit, propset_commit_checkout_nocrash, propset_revert_noerror, + canonicalize_keywords_prop, + canonicalized_and_keywords_expanded, ] if __name__ == '__main__':