Index: subversion/include/svn_subst.h
===================================================================
--- subversion/include/svn_subst.h	(revision 9955)
+++ subversion/include/svn_subst.h	(working copy)
@@ -75,28 +75,27 @@
                                 const char *value);
 
 
-/** Values used in keyword expansion. */
-typedef struct svn_subst_keywords_t
+/** One keyword expansion to perform. */
+typedef struct svn_subst_keyword_t
 {
-  const svn_string_t *revision;
-  const svn_string_t *date;
-  const svn_string_t *author;
-  const svn_string_t *url;
-  const svn_string_t *id;
-} svn_subst_keywords_t;
+  const svn_string_t *keyword;
+  const svn_string_t *string;
+} svn_subst_keyword_t;
 
 
-/** Fill in an <tt>svn_subst_keywords_t *</tt> @a kw with the appropriate 
+/** Build an <tt>svn_subst_keyword_t</tt> array @a kw with the appropriate 
  * contents given an @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 
  * not present, or @c 0 for @a date.
  * 
- * All memory is allocated out of @a pool.
+ * All memory is allocated out of @a pool. Upon successful return,
+ * <tt>*kw</tt> will be a newly created array containing the desired
+ * keyword substitutions, or @c NULL if there are no substitutions.
  */
 svn_error_t *
-svn_subst_build_keywords (svn_subst_keywords_t *kw,
+svn_subst_build_keywords (apr_array_header_t **kw,
                           const char *keywords_string,
                           const char *rev,
                           const char *url,
@@ -117,8 +116,8 @@
  * equivalent to holding no keywords.
  */
 svn_boolean_t 
-svn_subst_keywords_differ (const svn_subst_keywords_t *a,
-                           const svn_subst_keywords_t *b,
+svn_subst_keywords_differ (const apr_array_header_t *a,
+                           const apr_array_header_t *b,
                            svn_boolean_t compare_values);
 
 
@@ -135,9 +134,7 @@
  * Expand and contract keywords using the contents of @a keywords as the
  * new values.  If @a expand is @c TRUE, expand contracted keywords and
  * 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
+ * keywords and ignore contracted ones.  If the @a keywords array pointer
  * itself is @c NULL, keyword substitution will be altogether ignored.
  *
  * Detect only keywords that are no longer than @c SVN_IO_MAX_KEYWORD_LEN
@@ -159,7 +156,7 @@
                             svn_stream_t *dst,
                             const char *eol_str,
                             svn_boolean_t repair,
-                            const svn_subst_keywords_t *keywords,
+                            const apr_array_header_t *keywords,
                             svn_boolean_t expand);
 
 
@@ -181,7 +178,7 @@
                               const char *dst,
                               const char *eol_str,
                               svn_boolean_t repair,
-                              const svn_subst_keywords_t *keywords,
+                              const apr_array_header_t *keywords,
                               svn_boolean_t expand,
                               apr_pool_t *pool);
 
@@ -200,7 +197,7 @@
                              const char **dst,
                              const char *eol_str,
                              svn_boolean_t repair,
-                             const svn_subst_keywords_t *keywords,
+                             const apr_array_header_t *keywords,
                              svn_boolean_t expand,
                              apr_pool_t *pool);
 
Index: subversion/libsvn_wc/merge.c
===================================================================
--- subversion/libsvn_wc/merge.c	(revision 9955)
+++ subversion/libsvn_wc/merge.c	(working copy)
@@ -49,7 +49,7 @@
   const char *mt_pt, *mt_bn;
   apr_file_t *tmp_f, *result_f;
   svn_boolean_t is_binary;
-  svn_subst_keywords_t *keywords;
+  apr_array_header_t *keywords;
   const char *eol;
   const svn_wc_entry_t *entry;
   svn_boolean_t contains_conflicts;
Index: subversion/libsvn_wc/translate.h
===================================================================
--- subversion/libsvn_wc/translate.h	(revision 9955)
+++ subversion/libsvn_wc/translate.h	(working copy)
@@ -81,7 +81,7 @@
    corresponding value is available, set that element of *KEYWORDS to
    the empty string ("").
 */
-svn_error_t *svn_wc__get_keywords (svn_subst_keywords_t **keywords,
+svn_error_t *svn_wc__get_keywords (apr_array_header_t **keywords,
                                    const char *path,
                                    svn_wc_adm_access_t *adm_access,
                                    const char *force_list,
Index: subversion/libsvn_wc/props.c
===================================================================
--- subversion/libsvn_wc/props.c	(revision 9955)
+++ subversion/libsvn_wc/props.c	(working copy)
@@ -1000,7 +1000,7 @@
   svn_error_t *err;
   apr_hash_t *prophash;
   apr_file_t *fp = NULL;
-  svn_subst_keywords_t *old_keywords;
+  apr_array_header_t *old_keywords;
   svn_stringbuf_t *new_value = NULL;
   svn_node_kind_t kind;
   enum svn_prop_kind prop_kind = svn_property_kind (NULL, name);
@@ -1130,7 +1130,7 @@
 
   if (kind == svn_node_file && strcmp (name, SVN_PROP_KEYWORDS) == 0)
     {
-      svn_subst_keywords_t *new_keywords;
+      apr_array_header_t *new_keywords;
       SVN_ERR (svn_wc__get_keywords (&new_keywords, path, adm_access, NULL,
                                      pool));
 
Index: subversion/libsvn_wc/adm_crawler.c
===================================================================
--- subversion/libsvn_wc/adm_crawler.c	(revision 9955)
+++ subversion/libsvn_wc/adm_crawler.c	(working copy)
@@ -62,7 +62,7 @@
               apr_pool_t *pool)
 {
   const char *text_base_path, *tmp_text_base_path;
-  svn_subst_keywords_t *keywords;
+  apr_array_header_t *keywords;
   const char *eol;
   const svn_wc_entry_t *entry;
   svn_wc_entry_t newentry;
Index: subversion/libsvn_wc/log.c
===================================================================
--- subversion/libsvn_wc/log.c	(revision 9955)
+++ subversion/libsvn_wc/log.c	(working copy)
@@ -113,7 +113,7 @@
 
     case svn_wc__xfer_cp_and_translate:
       {
-        svn_subst_keywords_t *keywords;
+        apr_array_header_t *keywords;
         const char *eol_str;
 
         /* Note that this action takes properties from dest, not source. */
@@ -137,7 +137,7 @@
 
     case svn_wc__xfer_cp_and_detranslate:
       {
-        svn_subst_keywords_t *keywords;
+        apr_array_header_t *keywords;
         const char *eol_str;
 
         /* Note that this action takes properties from source, not dest. */
@@ -207,7 +207,7 @@
   const char *filepath;
   const char *tmp_text_base;
   svn_node_kind_t kind;
-  svn_subst_keywords_t *keywords;
+  apr_array_header_t *keywords;
   apr_file_t *ignored;
   svn_boolean_t same, did_set;
   const char *tmp_wfile, *pdir, *bname;
Index: subversion/libsvn_wc/adm_ops.c
===================================================================
--- subversion/libsvn_wc/adm_ops.c	(revision 9955)
+++ subversion/libsvn_wc/adm_ops.c	(working copy)
@@ -1243,7 +1243,7 @@
              missing altogether), copy the text-base out into
              the working copy, and update the timestamp in the entries
              file. */
-          svn_subst_keywords_t *keywords;
+          apr_array_header_t *keywords;
           const char *eol;
           
           SVN_ERR (svn_wc__get_eol_style (NULL, &eol, fullpath, adm_access,
Index: subversion/libsvn_wc/translate.c
===================================================================
--- subversion/libsvn_wc/translate.c	(revision 9955)
+++ subversion/libsvn_wc/translate.c	(working copy)
@@ -56,7 +56,7 @@
 {
   svn_subst_eol_style_t style;
   const char *eol;
-  svn_subst_keywords_t *keywords;
+  apr_array_header_t *keywords;
   
   SVN_ERR (svn_wc__get_eol_style (&style, &eol, vfile, adm_access, pool));
   SVN_ERR (svn_wc__get_keywords (&keywords, vfile, adm_access, NULL, pool));
@@ -171,14 +171,14 @@
 
 
 svn_error_t *
-svn_wc__get_keywords (svn_subst_keywords_t **keywords,
+svn_wc__get_keywords (apr_array_header_t **keywords,
                       const char *path,
                       svn_wc_adm_access_t *adm_access,
                       const char *force_list,
                       apr_pool_t *pool)
 {
   const char *list;
-  svn_subst_keywords_t tmp_keywords = { 0 };
+  apr_array_header_t *tmp_keywords = NULL;
   const svn_wc_entry_t *entry = NULL;
 
   /* Start by assuming no keywords. */
@@ -213,7 +213,7 @@
                                      entry->cmt_author,
                                      pool));
 
-  *keywords = apr_pmemdup (pool, &tmp_keywords, sizeof (tmp_keywords));
+  *keywords = tmp_keywords;
       
   return SVN_NO_ERROR;
 }
Index: subversion/libsvn_subr/subst.c
===================================================================
--- subversion/libsvn_subr/subst.c	(revision 9955)
+++ subversion/libsvn_subr/subst.c	(working copy)
@@ -115,7 +115,7 @@
 }
 
 svn_error_t *
-svn_subst_build_keywords (svn_subst_keywords_t *kw,
+svn_subst_build_keywords (apr_array_header_t **kw,
                           const char *keywords_val,
                           const char *rev,
                           const char *url,
@@ -123,6 +123,7 @@
                           const char *author,
                           apr_pool_t *pool)
 {
+  apr_array_header_t *tmp_kw = NULL;
   apr_array_header_t *keyword_tokens;
   int i;
 
@@ -132,11 +133,28 @@
   for (i = 0; i < keyword_tokens->nelts; ++i)
     {
       const char *keyword = APR_ARRAY_IDX (keyword_tokens, i, const char *);
+      svn_subst_keyword_t *sub;
+      const svn_string_t *replacement;
+      const char *customkey;
+      char *s;
 
+      /* Check for a custom keyword substitution (ignore empty strings) */
+      if ((s = strchr(keyword, '=')) != NULL && s > keyword)
+        {
+          customkey = keyword;
+          *s++ = '\0';
+          keyword = s;
+        }
+      else
+        {
+          customkey = keyword;
+        }
+
+      /* Check for keywords we recognize */
       if ((! strcmp (keyword, SVN_KEYWORD_REVISION_LONG))
           || (! strcasecmp (keyword, SVN_KEYWORD_REVISION_SHORT)))
         {
-          kw->revision = svn_string_create (rev, pool);
+          replacement = svn_string_create (rev, pool);
         }      
       else if ((! strcmp (keyword, SVN_KEYWORD_DATE_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_DATE_SHORT)))
@@ -147,20 +165,20 @@
 
               SVN_ERR (date_prop_to_human (&human_date, TRUE, date, pool));
 
-              kw->date = svn_string_create (human_date, pool);
+              replacement = svn_string_create (human_date, pool);
             }
           else
-            kw->date = svn_string_create ("", pool);
+            replacement = svn_string_create ("", pool);
         }
       else if ((! strcmp (keyword, SVN_KEYWORD_AUTHOR_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_AUTHOR_SHORT)))
         {
-          kw->author = svn_string_create (author ? author : "", pool);
+          replacement = svn_string_create (author ? author : "", pool);
         }
       else if ((! strcmp (keyword, SVN_KEYWORD_URL_LONG))
                || (! strcasecmp (keyword, SVN_KEYWORD_URL_SHORT)))
         {
-          kw->url = svn_string_create (url ? url : "", pool);
+          replacement = svn_string_create (url ? url : "", pool);
         }
       else if ((! strcasecmp (keyword, SVN_KEYWORD_ID)))
         {
@@ -170,14 +188,31 @@
           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 : "");
+          replacement = svn_string_createf (pool, "%s %s %s %s",
+                                            base_name,
+                                            rev,
+                                            human_date ? human_date : "",
+                                            author ? author : "");
         }
+      else
+        {
+          continue;
+        }
+
+      /* Create array if not created yet */
+      if (tmp_kw == NULL)
+        {
+          tmp_kw = apr_array_make (pool, keyword_tokens->nelts,
+                                   sizeof (svn_subst_keyword_t));
+        }
+
+      /* Add new keyword substitution to the array */
+      sub = apr_array_push (tmp_kw);
+      sub->keyword = svn_string_create (customkey, pool);
+      sub->string = replacement;
     }
 
+  *kw = tmp_kw;
   return SVN_NO_ERROR;
 }
 
@@ -329,8 +364,10 @@
 translate_keyword (char *buf,
                    apr_size_t *len,
                    svn_boolean_t expand,
-                   const svn_subst_keywords_t *keywords)
+                   const apr_array_header_t *keywords)
 {
+  int i;
+
   /* Make sure we gotz good stuffs. */
   assert (*len <= SVN_KEYWORD_MAX_LEN);
   assert ((buf[0] == '$') && (buf[*len - 1] == '$'));
@@ -339,80 +376,18 @@
   if (! keywords)
     return FALSE;
 
-  /* Revision */
-  if (keywords->revision)
+  /* Try each substitution in the array. */
+  for (i = 0; i < keywords->nelts; i++)
     {
-      if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_REVISION_LONG,
-                                   (sizeof (SVN_KEYWORD_REVISION_LONG)) - 1,
-                                   expand ? keywords->revision : NULL))
-        return TRUE;
+      const svn_subst_keyword_t *sub = &APR_ARRAY_IDX (keywords, i,
+                                                       svn_subst_keyword_t);
 
       if (translate_keyword_subst (buf, len,
-                                   SVN_KEYWORD_REVISION_SHORT,
-                                   (sizeof (SVN_KEYWORD_REVISION_SHORT)) - 1,
-                                   expand ? keywords->revision : NULL))
+                                   sub->keyword->data, sub->keyword->len,
+                                   expand ? sub->string : NULL))
         return TRUE;
     }
 
-  /* Date */
-  if (keywords->date)
-    {
-      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;
-    }
-
-  /* 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;
 }
@@ -464,71 +439,65 @@
   return translate_write (dst, eol_str, eol_str_len);
 }
 
+/*
+ * Compare two @c svn_subst_keyword_t structures for sorting.
+ */
+static int
+compare_keywords(const void *a, const void *b)
+{
+  const svn_subst_keyword_t *const key1 = a;
+  const svn_subst_keyword_t *const key2 = b;
+  int diff;
 
+  if ((diff = strcmp (key1->keyword->data, key2->keyword->data)) != 0)
+      return diff;
+  if ((diff = strcmp (key1->string->data, key2->string->data)) != 0)
+      return diff;
+  return 0;
+}
+
 
 /*** Public interfaces. ***/
 
 svn_boolean_t
-svn_subst_keywords_differ (const svn_subst_keywords_t *a,
-                           const svn_subst_keywords_t *b,
+svn_subst_keywords_differ (const apr_array_header_t *a,
+                           const apr_array_header_t *b,
                            svn_boolean_t compare_values)
 {
-  if (((a == NULL) && (b == NULL)) /* no A or B */
-      /* no A, and B has no contents */
-      || ((a == NULL) 
-          && (b->revision == NULL)
-          && (b->date == NULL)
-          && (b->author == NULL)
-          && (b->url == NULL))
-      /* no B, and A has no contents */
-      || ((b == NULL)           && (a->revision == NULL)
-          && (a->date == NULL)
-          && (a->author == NULL)
-          && (a->url == NULL))
-      /* neither A nor B has any contents */
-      || ((a != NULL) && (b != NULL) 
-          && (b->revision == NULL)
-          && (b->date == NULL)
-          && (b->author == NULL)
-          && (b->url == NULL)
-          && (a->revision == NULL)
-          && (a->date == NULL)
-          && (a->author == NULL)
-          && (a->url == NULL)))
-    {
+  const int a_empty = (a == NULL || a->nelts == 0);
+  const int b_empty = (b == NULL || b->nelts == 0);
+  int i;
+
+  /* Check the empty/NULL cases. */
+  if (a_empty && b_empty)
       return FALSE;
+  if (a_empty || b_empty)
+      return TRUE;
+
+  /* Check for different array lengths. */
+  if (a->nelts != b->nelts)
+      return TRUE;
+
+  /*
+   * Otherwise both A and B have the same number of substitutions.
+   * We sort both arrays and then compare them element by element.
+   */
+  qsort(a->elts, a->nelts, a->elt_size, compare_keywords);
+  qsort(b->elts, b->nelts, b->elt_size, compare_keywords);
+  for (i = 0; i < a->nelts; i++)
+    {
+      const svn_subst_keyword_t *key1 = &APR_ARRAY_IDX (a, i,
+                                                        svn_subst_keyword_t);
+      const svn_subst_keyword_t *key2 = &APR_ARRAY_IDX (b, i,
+                                                        svn_subst_keyword_t);
+
+      if (!svn_string_compare (key1->keyword, key2->keyword))
+          return TRUE;
+      if (compare_values && !svn_string_compare (key1->string, key2->string))
+          return TRUE;
     }
-  else if ((a == NULL) || (b == NULL))
-    return TRUE;
-  
-  /* Else both A and B have some keywords. */
-  
-  if ((! a->revision) != (! b->revision))
-    return TRUE;
-  else if ((compare_values && (a->revision != NULL))
-           && (strcmp (a->revision->data, b->revision->data) != 0))
-    return TRUE;
-    
-  if ((! a->date) != (! b->date))
-    return TRUE;
-  else if ((compare_values && (a->date != NULL))
-           && (strcmp (a->date->data, b->date->data) != 0))
-    return TRUE;
-    
-  if ((! a->author) != (! b->author))
-    return TRUE;
-  else if ((compare_values && (a->author != NULL))
-           && (strcmp (a->author->data, b->author->data) != 0))
-    return TRUE;
-  
-  if ((! a->url) != (! b->url))
-    return TRUE;
-  else if ((compare_values && (a->url != NULL))
-           && (strcmp (a->url->data, b->url->data) != 0))
-    return TRUE;
-  
-  /* Else we never found a difference, so they must be the same. */  
-  
+
+  /* No differences */
   return FALSE;
 }
 
@@ -538,7 +507,7 @@
                             svn_stream_t *d, /* dst stream */
                             const char *eol_str,
                             svn_boolean_t repair,
-                            const svn_subst_keywords_t *keywords,
+                            const apr_array_header_t *keywords,
                             svn_boolean_t expand)
 {
   char buf[SVN_STREAM_CHUNK_SIZE + 1];
@@ -655,7 +624,7 @@
                              const char **dst,
                              const char *eol_str,
                              svn_boolean_t repair,
-                             const svn_subst_keywords_t *keywords,
+                             const apr_array_header_t *keywords,
                              svn_boolean_t expand,
                              apr_pool_t *pool)
 {
@@ -702,7 +671,7 @@
                               const char *dst,
                               const char *eol_str,
                               svn_boolean_t repair,
-                              const svn_subst_keywords_t *keywords,
+                              const apr_array_header_t *keywords,
                               svn_boolean_t expand,
                               apr_pool_t *pool)
 {
Index: subversion/libsvn_client/export.c
===================================================================
--- subversion/libsvn_client/export.c	(revision 9955)
+++ subversion/libsvn_client/export.c	(working copy)
@@ -183,7 +183,7 @@
         {
           const char *copy_from = svn_path_join (from, item, iterpool);
           const char *copy_to = svn_path_join (to, item, iterpool);
-          svn_subst_keywords_t kw = { 0 };
+          apr_array_header_t *kw = NULL;
           svn_subst_eol_style_t style;
           apr_hash_t *props;
           const char *base;
@@ -274,7 +274,7 @@
             }
           
           SVN_ERR (svn_subst_copy_and_translate (base, copy_to, eol, FALSE,
-                                                 &kw, TRUE, iterpool));
+                                                 kw, TRUE, iterpool));
           if (executable)
             SVN_ERR (svn_io_set_file_executable (copy_to, TRUE, 
                                                  FALSE, iterpool));
@@ -640,7 +640,7 @@
     {
       svn_subst_eol_style_t style;
       const char *eol;
-      svn_subst_keywords_t final_kw = {0};
+      apr_array_header_t *final_kw = NULL;
 
       if (fb->eol_style_val)
         SVN_ERR (get_eol_style (&style, &eol, fb->eol_style_val->data, 
@@ -655,7 +655,7 @@
                (fb->tmppath, fb->path,
                 fb->eol_style_val ? eol : NULL,
                 fb->eol_style_val ? TRUE : FALSE, /* repair */
-                fb->keywords_val ? &final_kw : NULL,
+                fb->keywords_val ? final_kw : NULL,
                 fb->keywords_val ? TRUE : FALSE, /* expand */
                 pool));
 
Index: subversion/libsvn_client/cat.c
===================================================================
--- subversion/libsvn_client/cat.c	(revision 9955)
+++ subversion/libsvn_client/cat.c	(working copy)
@@ -76,7 +76,7 @@
     }
   else
     {
-      svn_subst_keywords_t kw = { 0 };
+      apr_array_header_t *kw = NULL;
       svn_subst_eol_style_t style;
       const char *temp_dir;
       const char *tmp_filename;
@@ -129,7 +129,7 @@
                     pool));
         }
 
-      SVN_ERR (svn_subst_translate_stream (tmp_stream, out, eol, FALSE, &kw,
+      SVN_ERR (svn_subst_translate_stream (tmp_stream, out, eol, FALSE, kw,
                                            TRUE));
 
       SVN_ERR (svn_stream_close (tmp_stream));
Index: subversion/libsvn_client/commit.c
===================================================================
--- subversion/libsvn_client/commit.c	(revision 9955)
+++ subversion/libsvn_client/commit.c	(working copy)
@@ -84,7 +84,7 @@
      Keywords get unexpanded.  */
   if (eol_style_val || keywords_val)
     {
-      svn_subst_keywords_t keywords = {0};
+      apr_array_header_t *keywords = NULL;
       const char *temp_dir;
       svn_stream_t *tmp_stream;
       apr_file_t *tmp_f;
@@ -112,7 +112,7 @@
       if ((err = svn_subst_translate_stream (contents, tmp_stream, 
                                              eol_style_val ? "\n" : NULL,
                                              FALSE,
-                                             keywords_val ? &keywords : NULL,
+                                             keywords_val ? keywords : NULL,
                                              FALSE)))
         goto cleanup;
 
Index: subversion/clients/cmdline/main.c
===================================================================
--- subversion/clients/cmdline/main.c	(revision 9955)
+++ subversion/clients/cmdline/main.c	(working copy)
@@ -520,6 +520,10 @@
        "      Id                       - A compressed summary of the "
        "previous\n"
        "                                   4 keywords.\n"
+       "      Key1=Key2                - Where 'Key2' is one of the above,\n"
+       "                                   do the same substitution as is\n"
+       "                                   done by 'Key2', but using the\n"
+       "                                   keyword 'Key1' instead of 'Key2'.\n"
        "    svn:executable - If present, make the file executable. This\n"
        "      property cannot be set on a directory.  A non-recursive "
        "attempt\n"
Index: subversion/tests/libsvn_wc/translate-test.c
===================================================================
--- subversion/tests/libsvn_wc/translate-test.c	(revision 9955)
+++ subversion/tests/libsvn_wc/translate-test.c	(working copy)
@@ -264,7 +264,8 @@
 {
   svn_error_t *err;
   svn_stringbuf_t *contents;
-  svn_subst_keywords_t keywords;
+  apr_array_header_t *keywords;
+  svn_subst_keyword_t *sub;
   apr_size_t idx = 0;
   apr_size_t i;
   const char *expect[(sizeof (lines) / sizeof (*lines))];
@@ -276,14 +277,46 @@
   SVN_ERR (remove_file (dst_fname, pool));
   SVN_ERR (create_file (src_fname, src_eol, pool));
 
-  keywords.revision = rev    ? svn_string_create (rev, pool)    : NULL;
-  keywords.date     = date   ? svn_string_create (date, pool)   : NULL;
-  keywords.author   = author ? svn_string_create (author, pool) : NULL;
-  keywords.url      = url    ? svn_string_create (url, pool)    : NULL;
-  keywords.id       = NULL;
+  keywords = apr_array_make (pool, 8, sizeof (svn_subst_keyword_t));
+  if (rev)
+    {
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_REVISION_LONG, pool);
+      sub->string = svn_string_create (rev, pool);
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_REVISION_SHORT, pool);
+      sub->string = svn_string_create (rev, pool);
+    }
+  if (date)
+    {
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_DATE_LONG, pool);
+      sub->string = svn_string_create (date, pool);
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_DATE_SHORT, pool);
+      sub->string = svn_string_create (date, pool);
+    }
+  if (author)
+    {
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_AUTHOR_LONG, pool);
+      sub->string = svn_string_create (author, pool);
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_AUTHOR_SHORT, pool);
+      sub->string = svn_string_create (author, pool);
+    }
+  if (url)
+    {
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_URL_LONG, pool);
+      sub->string = svn_string_create (url, pool);
+      sub = apr_array_push (keywords);
+      sub->keyword = svn_string_create (SVN_KEYWORD_URL_SHORT, pool);
+      sub->string = svn_string_create (url, pool);
+    }
 
   err = svn_subst_copy_and_translate (src_fname, dst_fname, dst_eol, repair,
-                                      &keywords, expand, pool);
+                                      keywords, expand, pool);
 
 
   /* Conversion should have failed, if src has mixed eol, and the
Index: doc/book/book/ch07.xml
===================================================================
--- doc/book/book/ch07.xml	(revision 9955)
+++ doc/book/book/ch07.xml	(working copy)
@@ -1386,6 +1386,17 @@
                 <literal>sally</literal>.</para>
             </listitem>
           </varlistentry>
+          <varlistentry>
+            <term><literal>Key1=Key2</literal></term>
+            <listitem>
+              <para>This causes the keyword <literal>Key1</literal>
+              to be treated as an alias for <literal>Key2</literal>,
+              which must be one of the above recognized keywords.
+              This allows a repository to have its own custom keywords
+              that won't conflict with other repositories' keywords
+              when files are imported.
+            </listitem>
+          </varlistentry>
         </variablelist>
 
         <para>Simply adding keyword anchor text to your file does


