Index: subversion/include/svn_subst.h
===================================================================
--- subversion/include/svn_subst.h	(revision 14802)
+++ subversion/include/svn_subst.h	(working copy)
@@ -75,7 +75,10 @@
                                 const char *value);
 
 
-/** Values used in keyword expansion. */
+/** Values used in keyword expansion.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ */
 typedef struct svn_subst_keywords_t
 {
   /** @{ */
@@ -90,16 +93,36 @@
 } svn_subst_keywords_t;
 
 
-/** Fill in an <tt>svn_subst_keywords_t *</tt> @a kw with the appropriate 
- * contents given a @a keywords_string (the contents of the svn:keywords 
+/**
+ * Set @a *kw to a new keywords hash filled with the appropriate contents
+ * given a @a keywords_string (the contents of the svn:keywords
  * property for the file in question), the revision @a rev, the @a url, 
  * the @a date the file was committed on, and the @a author of the last 
- * commit.  Any of these can be @c NULL to indicate that the information is 
+ * commit.  Any of these can be @c NULL to indicate that the information is
  * not present, or @c 0 for @a date.
+ *
+ * Hash keys are of type <tt>const char *</tt>.
+ * Hash values are of type <tt>svn_string_t *</tt>.
  * 
  * All memory is allocated out of @a pool.
+ *
+ * @since New in 1.3.
  */
 svn_error_t *
+svn_subst_build_keywords2 (apr_hash_t **kw,
+                           const char *keywords_string,
+                           const char *rev,
+                           const char *url,
+                           apr_time_t date,
+                           const char *author,
+                           apr_pool_t *pool);
+
+/** Similar to svn_subst_build_keywords2() except that it populates
+ * an existing structure @a *kw instead of creating a keywords hash.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ */
+svn_error_t *
 svn_subst_build_keywords (svn_subst_keywords_t *kw,
                           const char *keywords_string,
                           const char *rev,
@@ -111,6 +134,11 @@
 
 /** Return @c TRUE if @a a and @a b do not hold the same keywords.
  *
+ * @a a and @a b are hashes of the form produced by
+ * svn_subst_build_keywords2().
+ *
+ * @since New in 1.3.
+ *
  * If @a compare_values is @c TRUE, "same" means that the @a a and @a b 
  * contain exactly the same set of keywords, and the values of corresponding
  * keywords match as well.  Else if @a compare_values is @c FALSE, then
@@ -121,6 +149,17 @@
  * equivalent to holding no keywords.
  */
 svn_boolean_t 
+svn_subst_keywords_differ2 (apr_hash_t *a,
+                            apr_hash_t *b,
+                            svn_boolean_t compare_values,
+                            apr_pool_t *pool);
+
+/** Similar to svn_subst_keywords_differ2() except that it compares
+ * two @c svn_subst_keywords_t structs instead of keyword hashes.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ */
+svn_boolean_t 
 svn_subst_keywords_differ (const svn_subst_keywords_t *a,
                            const svn_subst_keywords_t *b,
                            svn_boolean_t compare_values);
@@ -130,7 +169,7 @@
  * Copy and translate the data in stream @a src into stream @a dst.  It is
  * assumed that @a src is a readable stream and @a dst is a writable stream.
  *
- * @since New in 1.2.
+ * @since New in 1.3.
  *
  * If @a eol_str is non-@c NULL, replace whatever bytestring @a src uses to
  * denote line endings with @a eol_str in the output.  If @a src has an
@@ -144,7 +183,7 @@
  * re-expand expanded keywords.  If @a expand is @c FALSE, contract expanded
  * keywords and ignore contracted ones.  @c NULL for any of the keyword
  * values (@a keywords->revision, e.g.) indicates that keyword should be
- * ignored (not contracted or expanded).  If the @a keywords structure
+ * ignored (not contracted or expanded).  If the @a keywords hash
  * itself is @c NULL, keyword substitution will be altogether ignored.
  *
  * Detect only keywords that are no longer than @c SVN_IO_MAX_KEYWORD_LEN
@@ -154,7 +193,8 @@
  * @a keywords must be non-@c NULL.
  *
  * Recommendation: if @a expand is false, then you don't care about the
- * keyword values, so pass empty strings as non-null signifiers.
+ * keyword values, so use empty strings as non-null signifiers when you
+ * build the keywords hash.
  *
  * Notes: 
  *
@@ -162,6 +202,20 @@
  * convenient way to get @a eol_str and @a keywords if in libsvn_wc.
  */
 svn_error_t *
+svn_subst_translate_stream3 (svn_stream_t *src,
+                             svn_stream_t *dst,
+                             const char *eol_str,
+                             svn_boolean_t repair,
+                             apr_hash_t *keywords,
+                             svn_boolean_t expand,
+                             apr_pool_t *pool);
+
+/** Similar to svn_subst_translate_stream3() except relies upon a
+ * @c svn_subst_keywords_t struct instead of a hash for the keywords.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ */
+svn_error_t *
 svn_subst_translate_stream2 (svn_stream_t *src,
                              svn_stream_t *dst,
                              const char *eol_str,
@@ -188,13 +242,10 @@
 
 
 /**
- * @since New in 1.1.
+ * Convenience routine: a variant of svn_subst_translate_stream3()
+ * which operates on files.  In addition, it will create/detranslate a special
+ * file if @a special is @c TRUE.
  *
- * Convenience routine: a variant of svn_subst_translate_stream2()
- * which operates on files.  (See previous docstring for details.)  In
- * addition, it will create/detranslate a special file if @a special
- * is @c TRUE.
- *
  * Copy the contents of file-path @a src to file-path @a dst atomically,
  * either creating @a dst (or overwriting @a dst if it exists), possibly
  * performing line ending and keyword translations.
@@ -204,8 +255,27 @@
  *
  * If @a eol_str and @a keywords are @c NULL, behavior is just a byte-for-byte
  * copy.
+ *
+ * @since New in 1.3.
  */
 svn_error_t *
+svn_subst_copy_and_translate3 (const char *src,
+                               const char *dst,
+                               const char *eol_str,
+                               svn_boolean_t repair,
+                               apr_hash_t *keywords,
+                               svn_boolean_t expand,
+                               svn_boolean_t special,
+                               apr_pool_t *pool);
+
+/**
+ * Similar to svn_subst_copy_and_translate3() except that @a keywords is a
+ * @c svn_subst_keywords_t struct instead of a keywords hash.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ * @since New in 1.1.
+ */
+svn_error_t *
 svn_subst_copy_and_translate2 (const char *src,
                                const char *dst,
                                const char *eol_str,
@@ -216,10 +286,10 @@
                                apr_pool_t *pool);
 
 /**
- * @deprecated Provided for backward compatibility with the 1.0 API.
- *
  * Similar to svn_subst_copy_and_translate2() except that @a special is
  * always set to @c FALSE.
+ *
+ * @deprecated Provided for backward compatibility with the 1.0 API.
  */
 svn_error_t *
 svn_subst_copy_and_translate (const char *src,
@@ -231,9 +301,12 @@
                               apr_pool_t *pool);
 
 
-/** Convenience routine: a variant of svn_subst_translate_stream2() which
- * operates on cstrings.  (See previous docstring for details.)
+/**
+ * Convenience routine: a variant of svn_subst_translate_stream3() which
+ * operates on cstrings.
  *
+ * @since New in 1.3.
+ *
  * Return a new string in @a *dst, allocated in @a pool, by copying the
  * contents of string @a src, possibly performing line ending and keyword
  * translations.
@@ -242,6 +315,21 @@
  * copy.
  */
 svn_error_t *
+svn_subst_translate_cstring2 (const char *src,
+                              const char **dst,
+                              const char *eol_str,
+                              svn_boolean_t repair,
+                              apr_hash_t *keywords,
+                              svn_boolean_t expand,
+                              apr_pool_t *pool);
+
+/**
+ * Similar to svn_subst_translate_cstring2() except that @a keywords is a
+ * @c svn_subst_keywords_t struct instead of a keywords hash.
+ *
+ * @deprecated Provided for backward compatibility with the 1.2 API.
+ */
+svn_error_t *
 svn_subst_translate_cstring (const char *src,
                              const char **dst,
                              const char *eol_str,
Index: subversion/libsvn_subr/subst.c
===================================================================
--- subversion/libsvn_subr/subst.c	(revision 14802)
+++ subversion/libsvn_subr/subst.c	(working copy)
@@ -91,33 +91,172 @@
     }
 }
 
-/* A helper function to convert the date property to something suitable for 
-   printing out.  If LONG_P is TRUE, use the long format, otherwise use a 
-   shorter one.  Returns a UTF8 encoded cstring. */
-static svn_error_t *
-date_prop_to_human (const char **human, svn_boolean_t long_p, apr_time_t when,
-                    apr_pool_t *pool)
+
+/* Helper function for svn_subst_build_keywords */
+
+/* Given a printf-like format string, return a string with proper
+ * information filled in.
+ *
+ * Important API note: This function is the core of the implementation of
+ * svn_subst_build_keywords (all versions), and as such must implement the
+ * tolerance of NULL and zero inputs that that function's documention
+ * stipulates.
+ *
+ * The codes of format:
+ *
+ * %a author of this revision
+ * %b basename of the URL of this file
+ * %d short format of date of this revision
+ * %D long format of date of this revision
+ * %r number of this revision
+ * %u URL of this file
+ *
+ * All memory is allocated out of @a pool.
+ */
+static svn_string_t *
+keyword_printf (const char *fmt,
+                const char *rev,
+                const char *url,
+                apr_time_t date,
+                const char *author,
+                apr_pool_t *pool)
 {
-  if (long_p)
-    *human = svn_time_to_human_cstring (when, pool);
-  else
+  svn_stringbuf_t *value = svn_stringbuf_ncreate ("", 0, pool);
+  const char *cur;
+  int n;
+
+  for (;;)
     {
-      apr_time_exp_t exploded_time;
+      cur = fmt;
 
-      apr_time_exp_gmt (&exploded_time, when);
+      while (*cur != '\0' && *cur != '%')
+        cur++;
 
-      *human = apr_psprintf (pool, "%04d-%02d-%02d %02d:%02d:%02dZ",
-                             exploded_time.tm_year + 1900,
-                             exploded_time.tm_mon + 1,
-                             exploded_time.tm_mday,
-                             exploded_time.tm_hour,
-                             exploded_time.tm_min,
-                             exploded_time.tm_sec);
+      if ((n = cur - fmt) > 0) /* Do we have an as-is string? */
+        svn_stringbuf_appendbytes (value, fmt, n);
+
+      if (*cur == '\0')
+        break;
+
+      switch (cur[1])
+        {
+        case 'a': /* author of this revision */
+          if (author)
+            svn_stringbuf_appendcstr (value, author);
+          break;
+        case 'b': /* basename of this file */
+          if (url)
+            {
+              const char *base_name = svn_path_basename (url, pool);
+              svn_stringbuf_appendcstr (value, base_name);
+            }
+          break;
+        case 'd': /* short format of date of this revision */
+          if (date)
+            {
+              apr_time_exp_t exploded_time;
+              const char *human;
+
+              apr_time_exp_gmt (&exploded_time, date);
+
+              human = apr_psprintf (pool, "%04d-%02d-%02d %02d:%02d:%02dZ",
+                                    exploded_time.tm_year + 1900,
+                                    exploded_time.tm_mon + 1,
+                                    exploded_time.tm_mday,
+                                    exploded_time.tm_hour,
+                                    exploded_time.tm_min,
+                                    exploded_time.tm_sec);
+
+              svn_stringbuf_appendcstr (value, human);
+            }
+          break;
+        case 'D': /* long format of date of this revision */
+          if (date)
+            svn_stringbuf_appendcstr (value,
+                                      svn_time_to_human_cstring (date, pool));
+          break;
+        case 'r': /* number of this revision */
+          if (rev)
+            svn_stringbuf_appendcstr (value, rev);
+          break;
+        case 'u': /* URL of this file */
+          if (url)
+            svn_stringbuf_appendcstr (value, url);
+          break;
+        case '\0': /* '%' as the last character of the string. */
+          svn_stringbuf_appendbytes (value, cur, 1);
+          /* Now go back one character, since this was just a one character
+           * sequence, whereas all others are two characters, and we do not
+           * want to skip the null terminator entirely and carry on
+           * formatting random memory contents. */
+          cur--;
+          break;
+        default: /* Unrecognized code, just print it literally. */
+          svn_stringbuf_appendbytes (value, cur, 2);
+          break;
+        }
+
+      /* Format code is processed - skip it, and get ready for next chunk. */
+      fmt = cur + 2;
     }
 
-  return SVN_NO_ERROR;
+  return svn_string_create_from_buf (value, pool);
 }
 
+/* Convert an old-style svn_subst_keywords_t struct * into a new-style
+ * keywords hash.  Keyword values are shallow copies, so the produced
+ * hash must not be assumed to have lifetime longer than the struct it
+ * is based on.  A NULL input causes a NULL output. */
+static apr_hash_t *
+kwstruct_to_kwhash (const svn_subst_keywords_t *kwstruct,
+                    apr_pool_t *pool)
+{
+  apr_hash_t *kwhash;
+
+  if (kwstruct == NULL)
+    return NULL;
+
+  kwhash = apr_hash_make(pool);
+
+  if (kwstruct->revision)
+    {
+      apr_hash_set (kwhash, SVN_KEYWORD_REVISION_LONG,
+                    APR_HASH_KEY_STRING, kwstruct->revision);
+      apr_hash_set (kwhash, SVN_KEYWORD_REVISION_MEDIUM,
+                    APR_HASH_KEY_STRING, kwstruct->revision);
+      apr_hash_set (kwhash, SVN_KEYWORD_REVISION_SHORT,
+                    APR_HASH_KEY_STRING, kwstruct->revision);
+    }
+  if (kwstruct->date)
+    {
+      apr_hash_set (kwhash, SVN_KEYWORD_DATE_LONG,
+                    APR_HASH_KEY_STRING, kwstruct->date);
+      apr_hash_set (kwhash, SVN_KEYWORD_DATE_SHORT,
+                    APR_HASH_KEY_STRING, kwstruct->date);
+    }
+  if (kwstruct->author)
+    {
+      apr_hash_set (kwhash, SVN_KEYWORD_AUTHOR_LONG,
+                    APR_HASH_KEY_STRING, kwstruct->author);
+      apr_hash_set (kwhash, SVN_KEYWORD_AUTHOR_SHORT,
+                    APR_HASH_KEY_STRING, kwstruct->author);
+    }
+  if (kwstruct->url)
+    {
+      apr_hash_set (kwhash, SVN_KEYWORD_URL_LONG,
+                    APR_HASH_KEY_STRING, kwstruct->url);
+      apr_hash_set (kwhash, SVN_KEYWORD_URL_SHORT,
+                    APR_HASH_KEY_STRING, kwstruct->url);
+    }
+  if (kwstruct->id)
+    {
+      apr_hash_set (kwhash, SVN_KEYWORD_ID,
+                    APR_HASH_KEY_STRING, kwstruct->id);
+    }
+
+  return kwhash;
+}
+
 svn_error_t *
 svn_subst_build_keywords (svn_subst_keywords_t *kw,
                           const char *keywords_val,
@@ -127,8 +266,52 @@
                           const char *author,
                           apr_pool_t *pool)
 {
+  apr_hash_t *kwhash;
+  const svn_string_t *val;
+
+  SVN_ERR (svn_subst_build_keywords2 (&kwhash, keywords_val, rev,
+                                      url, date, author, pool));
+
+  /* The behaviour of pre-1.3 svn_subst_build_keywords, which we are
+   * replicating here, is to write to a slot in the svn_subst_keywords_t
+   * only if the relevant keyword was present in keywords_val, otherwise
+   * leaving that slot untouched. */
+
+  val = apr_hash_get(kwhash, SVN_KEYWORD_REVISION_LONG, APR_HASH_KEY_STRING);
+  if (val)
+    kw->revision = val;
+  
+  val = apr_hash_get(kwhash, SVN_KEYWORD_DATE_LONG, APR_HASH_KEY_STRING);
+  if (val)
+    kw->date = val;
+
+  val = apr_hash_get(kwhash, SVN_KEYWORD_AUTHOR_LONG, APR_HASH_KEY_STRING);
+  if (val)
+    kw->author = val;
+
+  val = apr_hash_get(kwhash, SVN_KEYWORD_URL_LONG, APR_HASH_KEY_STRING);
+  if (val)
+    kw->url = val;
+
+  val = apr_hash_get(kwhash, SVN_KEYWORD_ID, APR_HASH_KEY_STRING);
+  if (val)
+    kw->id = val;
+
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_subst_build_keywords2 (apr_hash_t **kw,
+                           const char *keywords_val,
+                           const char *rev,
+                           const char *url,
+                           apr_time_t date,
+                           const char *author,
+                           apr_pool_t *pool)
+{
   apr_array_header_t *keyword_tokens;
   int i;
+  *kw = apr_hash_make (pool);
 
   keyword_tokens = svn_cstring_split (keywords_val, " \t\v\n\b\r\f",
                                       TRUE /* chop */, pool);
@@ -141,45 +324,57 @@
           || (! strcmp (keyword, SVN_KEYWORD_REVISION_MEDIUM))
           || (! strcasecmp (keyword, SVN_KEYWORD_REVISION_SHORT)))
         {
-          kw->revision = svn_string_create (rev, pool);
-        }      
+          svn_string_t *revision_val;
+
+          revision_val = keyword_printf ("%r", rev, url, date, author, pool);
+          apr_hash_set (*kw, SVN_KEYWORD_REVISION_LONG,
+                        APR_HASH_KEY_STRING, revision_val);
+          apr_hash_set (*kw, SVN_KEYWORD_REVISION_MEDIUM,
+                        APR_HASH_KEY_STRING, revision_val);
+          apr_hash_set (*kw, SVN_KEYWORD_REVISION_SHORT,
+                        APR_HASH_KEY_STRING, revision_val);
+        }
       else if ((! strcmp (keyword, SVN_KEYWORD_DATE_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_DATE_SHORT)))
         {
-          if (date)
-            {
-              const char *human_date;
+          svn_string_t *date_val;
 
-              SVN_ERR (date_prop_to_human (&human_date, TRUE, date, pool));
-
-              kw->date = svn_string_create (human_date, pool);
-            }
-          else
-            kw->date = svn_string_create ("", pool);
+          date_val = keyword_printf ("%D", rev, url, date, author, pool);
+          apr_hash_set (*kw, SVN_KEYWORD_DATE_LONG,
+                        APR_HASH_KEY_STRING, date_val);
+          apr_hash_set (*kw, SVN_KEYWORD_DATE_SHORT,
+                        APR_HASH_KEY_STRING, date_val);
         }
       else if ((! strcmp (keyword, SVN_KEYWORD_AUTHOR_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_AUTHOR_SHORT)))
         {
-          kw->author = svn_string_create (author ? author : "", pool);
+          svn_string_t *author_val;
+
+          author_val = keyword_printf ("%a", rev, url, date, author, pool);
+          apr_hash_set (*kw, SVN_KEYWORD_AUTHOR_LONG,
+                        APR_HASH_KEY_STRING, author_val);
+          apr_hash_set (*kw, SVN_KEYWORD_AUTHOR_SHORT,
+                        APR_HASH_KEY_STRING, author_val);
         }
       else if ((! strcmp (keyword, SVN_KEYWORD_URL_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_URL_SHORT)))
         {
-          kw->url = svn_string_create (url ? url : "", pool);
+          svn_string_t *url_val;
+
+          url_val = keyword_printf ("%u", rev, url, date, author, pool);
+          apr_hash_set (*kw, SVN_KEYWORD_URL_LONG,
+                        APR_HASH_KEY_STRING, url_val);
+          apr_hash_set (*kw, SVN_KEYWORD_URL_SHORT,
+                        APR_HASH_KEY_STRING, url_val);
         }
       else if ((! strcasecmp (keyword, SVN_KEYWORD_ID)))
         {
-          const char *base_name = url ? svn_path_basename (url, pool) : "";
-          const char *human_date = NULL;
+          svn_string_t *id_val;
 
-          if (date)
-            SVN_ERR (date_prop_to_human (&human_date, FALSE, date, pool));
-
-          kw->id = svn_string_createf (pool, "%s %s %s %s",
-                                       base_name,
-                                       rev,
-                                       human_date ? human_date : "",
-                                       author ? author : "");
+          id_val = keyword_printf ("%b %d %a %r", rev, url, date, author,
+                                   pool);
+          apr_hash_set (*kw, SVN_KEYWORD_ID,
+                        APR_HASH_KEY_STRING, id_val);
         }
     }
 
@@ -377,8 +572,12 @@
 translate_keyword (char *buf,
                    apr_size_t *len,
                    svn_boolean_t expand,
-                   const svn_subst_keywords_t *keywords)
+                   apr_hash_t *keywords)
 {
+  const svn_string_t *value;
+  char keyword_name[SVN_KEYWORD_MAX_LEN + 1];
+  int i;
+
   /* Make sure we gotz good stuffs. */
   assert (*len <= SVN_KEYWORD_MAX_LEN);
   assert ((buf[0] == '$') && (buf[*len - 1] == '$'));
@@ -387,87 +586,20 @@
   if (! keywords)
     return FALSE;
 
-  /* Revision */
-  if (keywords->revision)
-    {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_REVISION_LONG,
-                                   (sizeof (SVN_KEYWORD_REVISION_LONG)) - 1,
-                                   expand ? keywords->revision : NULL))
-        return TRUE;
+  /* Extract the name of the keyword */
+  for (i = 0; i < *len - 2 && buf[i + 1] != ':'; i++)
+    keyword_name[i] = *(buf + i + 1);
+  keyword_name[i] = 0;
 
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_REVISION_MEDIUM,
-                                   (sizeof (SVN_KEYWORD_REVISION_MEDIUM)) - 1,
-                                   expand ? keywords->revision : NULL))
-        return TRUE;
+  value = apr_hash_get (keywords, keyword_name, APR_HASH_KEY_STRING);
 
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_REVISION_SHORT,
-                                   (sizeof (SVN_KEYWORD_REVISION_SHORT)) - 1,
-                                   expand ? keywords->revision : NULL))
-        return TRUE;
-    }
-
-  /* Date */
-  if (keywords->date)
+  if (value)
     {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_DATE_LONG,
-                                   (sizeof (SVN_KEYWORD_DATE_LONG)) - 1,
-                                   expand ? keywords->date : NULL))
-        return TRUE;
-
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_DATE_SHORT,
-                                   (sizeof (SVN_KEYWORD_DATE_SHORT)) - 1,
-                                   expand ? keywords->date : NULL))
-        return TRUE;
+      return translate_keyword_subst (buf, len,
+                                      keyword_name, strlen (keyword_name),
+                                      expand ? value : NULL);
     }
 
-  /* Author */
-  if (keywords->author)
-    {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_AUTHOR_LONG,
-                                   (sizeof (SVN_KEYWORD_AUTHOR_LONG)) - 1,
-                                   expand ? keywords->author : NULL))
-        return TRUE;
-
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_AUTHOR_SHORT,
-                                   (sizeof (SVN_KEYWORD_AUTHOR_SHORT)) - 1,
-                                   expand ? keywords->author : NULL))
-        return TRUE;
-    }
-
-  /* URL */
-  if (keywords->url)
-    {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_URL_LONG,
-                                   (sizeof (SVN_KEYWORD_URL_LONG)) - 1,
-                                   expand ? keywords->url : NULL))
-        return TRUE;
-
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_URL_SHORT,
-                                   (sizeof (SVN_KEYWORD_URL_SHORT)) - 1,
-                                   expand ? keywords->url : NULL))
-        return TRUE;
-    }
-
-  /* Id */
-  if (keywords->id)
-    {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_ID,
-                                   (sizeof (SVN_KEYWORD_ID)) - 1,
-                                   expand ? keywords->id : NULL))
-        return TRUE;
-    }
-
-  /* No translations were successful.  Return FALSE. */
   return FALSE;
 }
 
@@ -584,7 +716,39 @@
   return FALSE;
 }
 
+svn_boolean_t
+svn_subst_keywords_differ2 (apr_hash_t *a,
+                            apr_hash_t *b,
+                            svn_boolean_t compare_values,
+                            apr_pool_t *pool)
+{
+  apr_hash_index_t *hi;
 
+  if ((a == NULL) && (b == NULL))
+    return FALSE;
+
+  if (((a == NULL) && (b != NULL)) ||
+      ((a != NULL) && (b == NULL)) ||
+      (apr_hash_count (a) != apr_hash_count (b)))
+    return TRUE;
+
+  /* The hashes are both non-NULL, and have the same number of items.
+   * We must check that every item of A is present in B. */
+  for (hi = apr_hash_first(pool, a); hi; hi = apr_hash_next(hi))
+    {
+      const char *key;
+      svn_string_t *a_val, *b_val;
+
+      apr_hash_this (hi, (const void **)&key, NULL, (void **)&a_val);
+      b_val = apr_hash_get (b, key, APR_HASH_KEY_STRING);
+
+      if (!b_val || (compare_values && svn_string_compare (a_val, b_val)))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
 svn_error_t *
 svn_subst_translate_stream2 (svn_stream_t *s, /* src stream */
                              svn_stream_t *d, /* dst stream */
@@ -594,6 +758,20 @@
                              svn_boolean_t expand,
                              apr_pool_t *pool)
 {
+  apr_hash_t *kh = kwstruct_to_kwhash (keywords, pool);
+
+  return svn_subst_translate_stream3 (s, d, eol_str, repair, kh, expand, pool);
+}
+
+svn_error_t *
+svn_subst_translate_stream3 (svn_stream_t *s, /* src stream */
+                             svn_stream_t *d, /* dst stream */
+                             const char *eol_str,
+                             svn_boolean_t repair,
+                             apr_hash_t *keywords,
+                             svn_boolean_t expand,
+                             apr_pool_t *pool)
+{
   char *buf;
   const char *p, *interesting;
   apr_size_t len, readlen;
@@ -740,6 +918,21 @@
                              svn_boolean_t expand,
                              apr_pool_t *pool)
 {
+  apr_hash_t *kh = kwstruct_to_kwhash (keywords, pool);
+
+  return svn_subst_translate_cstring2 (src, dst, eol_str, repair,
+                                       kh, expand, pool);
+}
+
+svn_error_t *
+svn_subst_translate_cstring2 (const char *src,
+                              const char **dst,
+                              const char *eol_str,
+                              svn_boolean_t repair,
+                              apr_hash_t *keywords,
+                              svn_boolean_t expand,
+                              apr_pool_t *pool)
+{
   svn_stringbuf_t *src_stringbuf, *dst_stringbuf;
   svn_stream_t *src_stream, *dst_stream;
   svn_error_t *err;
@@ -759,7 +952,7 @@
   dst_stream = svn_stream_from_stringbuf (dst_stringbuf, pool);
 
   /* Translate src stream into dst stream. */
-  err = svn_subst_translate_stream2 (src_stream, dst_stream,
+  err = svn_subst_translate_stream3 (src_stream, dst_stream,
                                      eol_str, repair, keywords, expand, pool);
   if (err)
     {
@@ -949,6 +1142,23 @@
                                svn_boolean_t special,
                                apr_pool_t *pool)
 {
+  apr_hash_t *kh = kwstruct_to_kwhash (keywords, pool);
+
+  return svn_subst_copy_and_translate3 (src, dst, eol_str,
+                                        repair, kh, expand, special,
+                                        pool);
+}
+
+svn_error_t *
+svn_subst_copy_and_translate3 (const char *src,
+                               const char *dst,
+                               const char *eol_str,
+                               svn_boolean_t repair,
+                               apr_hash_t *keywords,
+                               svn_boolean_t expand,
+                               svn_boolean_t special,
+                               apr_pool_t *pool)
+{
   const char *dst_tmp = NULL;
   svn_stream_t *src_stream, *dst_stream;
   apr_file_t *s = NULL, *d = NULL;  /* init to null important for APR */
@@ -1001,7 +1211,7 @@
   dst_stream = svn_stream_from_aprfile (d, subpool);
 
   /* Translate src stream into dst stream. */
-  err = svn_subst_translate_stream2 (src_stream, dst_stream, eol_str,
+  err = svn_subst_translate_stream3 (src_stream, dst_stream, eol_str,
                                      repair, keywords, expand, subpool);
   if (err)
     {
@@ -1072,13 +1282,13 @@
       SVN_ERR (svn_utf_cstring_to_utf8 (&val_utf8, value->data, pool));
     }
 
-  SVN_ERR (svn_subst_translate_cstring (val_utf8,
-                                        &val_utf8_lf,
-                                        "\n",  /* translate to LF */
-                                        FALSE, /* no repair */
-                                        NULL,  /* no keywords */
-                                        FALSE, /* no expansion */
-                                        pool));
+  SVN_ERR (svn_subst_translate_cstring2 (val_utf8,
+                                         &val_utf8_lf,
+                                         "\n",  /* translate to LF */
+                                         FALSE, /* no repair */
+                                         NULL,  /* no keywords */
+                                         FALSE, /* no expansion */
+                                         pool));
   
   *new_value = svn_string_create (val_utf8_lf, pool);
 
@@ -1102,13 +1312,13 @@
       return SVN_NO_ERROR;
     }
 
-  SVN_ERR (svn_subst_translate_cstring (value->data,
-                                        &val_neol,
-                                        APR_EOL_STR,  /* 'native' eol */
-                                        FALSE, /* no repair */
-                                        NULL,  /* no keywords */
-                                        FALSE, /* no expansion */
-                                        pool));
+  SVN_ERR (svn_subst_translate_cstring2 (value->data,
+                                         &val_neol,
+                                         APR_EOL_STR,  /* 'native' eol */
+                                         FALSE, /* no repair */
+                                         NULL,  /* no keywords */
+                                         FALSE, /* no expansion */
+                                         pool));
 
   if (for_output)
     {


