Index: contrib/client-side/mucc.c
===================================================================
--- contrib/client-side/mucc.c	(revision 23417)
+++ contrib/client-side/mucc.c	(working copy)
@@ -117,17 +117,51 @@
     OP_OPEN,
     OP_DELETE,
     OP_ADD,
-    OP_REPLACE
+    OP_REPLACE,
+    OP_SETPROP           /* only for files for which no other operation is
+                            occuring; directories are OP_OPEN with non-empty
+                            props */
   } operation;
-  svn_node_kind_t kind;  /* to copy, mkdir, or put */
+  svn_node_kind_t kind;  /* to copy, mkdir, put or set revprops */
   svn_revnum_t rev;      /* to copy, valid for add and replace */
   const char *url;       /* to copy, valid for add and replace */
   const char *src_file;  /* for put or copy, the source file for contents */
   apr_hash_t *children;  /* const char *path -> struct operation * */
+  apr_table_t *props;    /* const char *prop_name -> const char *prop_value */
   void *baton;           /* as returned by the commit editor */
 };
 
+/* State to be passed to set_props iterator */
+struct driver_state {
+  const svn_delta_editor_t *editor;
+  svn_node_kind_t kind;
+  apr_pool_t *pool;
+  void *baton;
+  svn_error_t* err;
+};
 
+/* An iterator (for use via apr_table_do) which sets file properties. Expects
+ * rec to be a pointer to a struct driver_state. */
+int
+set_props(void *rec, const char *key, const char *value)
+{
+  struct driver_state *d_state = (struct driver_state*)rec;
+  svn_string_t* value_svnstring = value ? svn_string_create(value,
+                                                            d_state->pool) :
+                                          NULL;
+  if(d_state->kind == svn_node_dir)
+    d_state->err = d_state->editor->change_dir_prop(d_state->baton, key,
+                                                    value_svnstring,
+                                                    d_state->pool);
+  else
+    d_state->err = d_state->editor->change_file_prop(d_state->baton, key,
+                                                     value_svnstring,
+                                                     d_state->pool);
+  if(d_state->err)
+    return 0;
+  return 1;
+}
+
 /* Drive EDITOR to affect the change represented by OPERATION.  HEAD
    is the last-known youngest revision in the repository. */
 static svn_error_t *
@@ -138,6 +172,7 @@
 {
   apr_pool_t *subpool = svn_pool_create(pool);
   apr_hash_index_t *hi;
+  struct driver_state state;
   for (hi = apr_hash_first(pool, operation->children);
        hi; hi = apr_hash_next(hi))
     {
@@ -158,7 +193,8 @@
           SVN_ERR(editor->open_directory(key, operation->baton, head, subpool,
                                          &child->baton));
         }
-      if (child->operation == OP_ADD || child->operation == OP_REPLACE)
+      if (child->operation == OP_ADD || child->operation == OP_REPLACE
+          || child->operation == OP_SETPROP)
         {
           if (child->kind == svn_node_dir)
             {
@@ -196,6 +232,16 @@
                                                   handler_baton, NULL, pool));
                   SVN_ERR(svn_io_file_close(f, pool));
                 }
+              /* apply file properties */
+              if((child->kind == svn_node_file) && (!apr_is_empty_table(child->props)))
+                {
+                  state.baton = file_baton;
+                  state.pool = pool;
+                  state.editor = editor;
+                  state.kind = child->kind;
+                  if(!apr_table_do(set_props, &state, child->props, NULL))
+                    SVN_ERR(state.err);
+                }
               SVN_ERR(editor->close_file(file_baton, NULL, subpool));
             }
         }
@@ -206,6 +252,16 @@
           SVN_ERR(drive(child, head, editor, subpool));
           SVN_ERR(editor->close_directory(child->baton, subpool));
         }
+      /* apply directory properties */
+      if((child->kind == svn_node_dir) && (!apr_is_empty_table(child->props)))
+        {
+          state.baton = child->baton;
+          state.pool = pool;
+          state.editor = editor;
+          state.kind = child->kind;
+          if(!apr_table_do(set_props, &state, child->props, NULL))
+            SVN_ERR(state.err);
+        }
     }
   svn_pool_destroy(subpool);
   return SVN_NO_ERROR;
@@ -229,6 +285,7 @@
       child->children = apr_hash_make(pool);
       child->operation = OP_OPEN;
       apr_hash_set(operation->children, path, APR_HASH_KEY_STRING, child);
+      child->props = apr_table_make(pool, 0);
     }
   return child;
 }
@@ -247,12 +304,13 @@
    intermediate nodes that are required.  Here's how the action is
    derived from the inputs:
 
-      URL    REV      SRC-FILE     ACTION
-      -----  -------  --------  =  ------
-      NULL   valid    NULL         delete
-      valid  valid    NULL         copy (add-with-history)
-      valid  invalid  NULL         mkdir
-      valid  valid    valid        put
+      URL    REV      SRC-FILE  propname    ACTION
+      -----  -------  --------  -------- = ------
+      NULL   valid    NULL      NULL       delete
+      valid  valid    NULL      NULL       copy (add-with-history)
+      valid  invalid  NULL      NULL       mkdir
+      valid  valid    valid     NULL       put
+      valid  valid    NULL      valid      propset
 
    Node type information is obtained for any copy source (to determine
    whether to create a file or directory) and for any deleted path (to
@@ -263,6 +321,8 @@
       const char *url,
       const char *src_file,
       svn_revnum_t rev,
+      const char *prop_name,
+      const char *prop_value,
       svn_revnum_t head,
       const char *anchor,
       svn_ra_session_t *session,
@@ -281,11 +341,13 @@
       const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
       path_so_far = svn_path_join(path_so_far, path_bit, pool);
       operation = get_operation(path_so_far, operation, pool);
-      if (! url)
+      if (prop_name || ! url)
         {
-          /* Delete can operate on a copy, track it back to the source */
+          /* Delete or propset can operate on a copy; use the source for
+             determining type. */
           if (operation->operation == OP_REPLACE
-              || operation->operation == OP_ADD)
+              || operation->operation == OP_ADD
+              && operation->url)
             {
               copy_src = subtract_anchor(anchor, operation->url, pool);
               copy_rev = operation->rev;
@@ -294,18 +356,43 @@
             copy_src = svn_path_join(copy_src, path_bit, pool);
         }
     }
+
+  if ( prop_name )
+    {
+      if (operation->operation == OP_DELETE)
+        return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+                                 "cannot set properties on a location being"
+                                 " deleted ('%s')", path);
+      SVN_ERR(svn_ra_check_path(session,
+                                copy_src ? copy_src : path,
+                                copy_src ? copy_rev : head,
+                                &operation->kind, pool));
+      if (operation->kind == svn_node_none)
+        return svn_error_createf(SVN_ERR_BAD_URL, NULL, "propset: '%s' not"
+                                 " found", path);
+      else if ((operation->kind == svn_node_file)
+               && (operation->operation == OP_OPEN))
+        operation->operation = OP_SETPROP;
+      apr_table_set(operation->props, prop_name, prop_value);
+      if (!operation->rev)
+        operation->rev = rev;
+      return SVN_NO_ERROR;
+    }
   
   /* We won't fuss about multiple operations on the same path in the
      following cases:
 
        - the prior operation was, in fact, a no-op (open)
+       - the prior operation was a setprop placeholder
        - the prior operation was a deletion
 
      Note: while the operation structure certainly supports the
      ability to do a copy of a file followed by a put of new contents
      for the file, we don't let that happen (yet).
   */
-  if (! (operation->operation == OP_OPEN || operation->operation == OP_DELETE))
+  if (! (operation->operation == OP_OPEN
+         || operation->operation == OP_SETPROP
+         || operation->operation == OP_DELETE))
     return svn_error_createf(SVN_ERR_BAD_URL, NULL,
                              "unsupported multiple operations on '%s'", path);
 
@@ -375,6 +462,7 @@
     ACTION_MV,
     ACTION_MKDIR,
     ACTION_CP,
+    ACTION_PROP,
     ACTION_PUT,
     ACTION_RM
   } action;
@@ -389,8 +477,9 @@
    * cp      source   target
    * put     target   source
    * rm      target   (null)
+   * setprop target   (null)
    */
-  const char *path[2];
+  const char *path[2], *prop_name, *prop_value;
 };
 
 static svn_error_t *
@@ -425,34 +514,39 @@
         case ACTION_MV:
           path1 = subtract_anchor(anchor, action->path[0], pool);
           path2 = subtract_anchor(anchor, action->path[1], pool);
-          SVN_ERR(build(path2, action->path[0], NULL, head,
+          SVN_ERR(build(path2, action->path[0], NULL, head, NULL, NULL, head,
+                        anchor, session, &root, pool));
+          SVN_ERR(build(path1, NULL, NULL, SVN_INVALID_REVNUM, NULL, NULL,
                         head, anchor, session, &root, pool));
-          SVN_ERR(build(path1, NULL, NULL, SVN_INVALID_REVNUM,
-                        head, anchor, session, &root, pool));
           break;
         case ACTION_CP:
           path1 = subtract_anchor(anchor, action->path[0], pool);
           path2 = subtract_anchor(anchor, action->path[1], pool);
           if (action->rev == SVN_INVALID_REVNUM)
             action->rev = head;
-          SVN_ERR(build(path2, action->path[0], NULL, action->rev,
+          SVN_ERR(build(path2, action->path[0], NULL, action->rev, NULL, NULL,
                         head, anchor, session, &root, pool));
           break;
         case ACTION_RM:
           path1 = subtract_anchor(anchor, action->path[0], pool);
-          SVN_ERR(build(path1, NULL, NULL, SVN_INVALID_REVNUM,
+          SVN_ERR(build(path1, NULL, NULL, SVN_INVALID_REVNUM, NULL, NULL,
                         head, anchor, session, &root, pool));
           break;
         case ACTION_MKDIR:
           path1 = subtract_anchor(anchor, action->path[0], pool);
           SVN_ERR(build(path1, action->path[0], NULL, SVN_INVALID_REVNUM,
-                        head, anchor, session, &root, pool));
+                        NULL, NULL, head, anchor, session, &root, pool));
           break;
         case ACTION_PUT:
           path1 = subtract_anchor(anchor, action->path[0], pool);
           SVN_ERR(build(path1, action->path[0], action->path[1], action->rev,
-                        head, anchor, session, &root, pool));
+                        NULL, NULL, head, anchor, session, &root, pool));
           break;
+        case ACTION_PROP:
+          path1 = subtract_anchor(anchor, action->path[0], pool);
+          SVN_ERR(build(path1, action->path[0], NULL, action->rev,
+                        action->prop_name, action->prop_value, head, anchor,
+                        session, &root, pool));
         }
     }
 
@@ -484,6 +578,10 @@
     "  put REV FILE URL      add or replace file URL with contents copied\n"
     "                        from FILE, and using REV as the base revision\n"
     "                        (for safety purposes)\n"
+    "  setprop REV PROPNAME PROPVALUE URL\n"
+    "                        Set PROPNAME at URL to PROPVALUE\n"
+    "  delprop REV PROPNAME URL\n"
+    "                        Delete PROPNAME from URL\n"
     "\nOptions:\n"
     "  -h, --help            display this text\n"
     "  -m, --message ARG     use ARG as a log message\n"
@@ -649,6 +747,10 @@
         action->action = ACTION_RM;
       else if (! strcmp(action_string, "put"))
         action->action = ACTION_PUT;
+      else if (! strcmp(action_string, "setprop"))
+        action->action = ACTION_PROP;
+      else if (! strcmp(action_string, "delprop"))
+        action->action = ACTION_PROP;
       else
         handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                        "'%s' is not an action\n", 
@@ -656,8 +758,10 @@
       if (++i == action_args->nelts)
         insufficient(pool);
 
-      /* For copies and puts, there should be a revision number next. */
-      if ((action->action == ACTION_CP) || (action->action == ACTION_PUT))
+      /* For copies, puts and property changes there should be a revision
+         number next. */
+      if ((action->action == ACTION_CP) || (action->action == ACTION_PUT)
+          || (action->action == ACTION_PROP))
         {
           const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
           if (strcmp(rev_str, "head") == 0)
@@ -692,10 +796,28 @@
             insufficient(pool);
         }
 
+      /* For setprop, property names and values come next */
+      if (action->action == ACTION_PROP)
+        {
+          action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
+          if (++i == action_args->nelts) insufficient(pool);
+
+          if (action_string[0] == 'd')
+            {
+              action->prop_value = NULL;
+            }
+          else
+            {
+              action->prop_value = APR_ARRAY_IDX(action_args, i, const char *);
+              if (++i == action_args->nelts) insufficient(pool);
+            }
+        }
+
       /* How many URLs does this action expect? */
       if (action->action == ACTION_RM 
           || action->action == ACTION_MKDIR
-          || action->action == ACTION_PUT)
+          || action->action == ACTION_PUT
+          || action->action == ACTION_PROP)
         num_url_args = 1;
       else
         num_url_args = 2;


