Linux DevCenter    
 Published on Linux DevCenter (http://www.linuxdevcenter.com/)
 See this if you're having trouble printing code examples


Using the Subversion Client API, Part 2

by Garrett Rooney
05/15/2003

In the first article of this series, we learned how to make basic use of the Subversion client libraries. We covered why one would want to use the Subversion client libraries directly instead of simply calling the svn binary directly, the basic use of the Apache Portable Runtime, which Subversion uses as its portability library, and a few low-level Subversion constructs, including svn_error_t and svn_client_ctx_t. Finally, we went over the first few functions necessary for making a minimal Subversion client.

Let's continue with the example begun in the first article, expanding our basic client. The examples in this article will be written for version 0.20.0 of the Subversion API. If you are using an earlier version, you should upgrade. If you are using a later version, you should be aware that there may be some differences in the API, but the concepts should still apply.

Now that you've provided your company's web developers with a minimalistic Subversion client application, which allows them to deploy sites directly from your repository onto the web server, they've become a bit more used to the idea of version control. Some of them are even using the regular svn command line client. The rest, though, are a bit stuck. They still need a way to interact with the repository to make changes to the sites that are stored there, but they uncertain about using command line tools. Fortunately, your newfound knowledge of the Subversion client APIs will save the day. All you have to do is extend your client to support a few more features. You've already got checkout, status, and update. There are just a few more you'll need to provide to give them enough functionality to make changes to the sites they're working on and commit them back into the repository.

First, a note about function targets. Many subversion commands (and the underlying libsvn_client functions which implement them) can target either a working copy or a repository. For example, you can use svn copy to copy a file in your local working copy and then commit that change back to the repository later, or you can use it to copy a file or directory in the repository directly. You would do this if you were tagging a particular release of your software, for example. Generally, the effect you get from a command that targets the repository directly can be achieved within the working copy, as long as you follow it up with a commit.

The reason the functions can work on both the repository and the working copy is efficiency. If you tag a new release of your software by performing the copy in your checked out working copy, you have to check out the directory that holds your tags as well as the one that holds the version you are tagging, which could take up a lot of disk space. Then when you do the actual copy, the client will need to write many files out to disk as part of maintaining the working copy (the contents of all the .svn directories in the new directory, as well as the actual files you copy). When you finally commit, all the changes need to be communicated to the server. Doing the copy directly in the server saves all this trouble, so it's the usual way of working with large copies like tags and branches.

For the examples in this article, we use the libsvn_client functions on the working copy, but using them on the repository is essentially the same. It only requires the addition of a new parameter which Subversion uses to hold the results of the commit and a callback function inside the client context that Subversion uses to get the log message for the commit. We'll cover both when we talk about svn_client_commit.

So What Did I Change Anyway?

To get your web developers started, all you need to do is give them the ability to edit a file, verify that the change is what they want, and commit it back to the repository. The editing part is easy. Subversion doesn't actually require you to do anything before making a change, so users can make changes in their favorite editor. Once they've made a change, use svn_client_diff to show them exactly what they are going to be committing to the repository.

svn_client_diff takes a number of arguments, but it's really not that complicated. Here's the function prototype.

svn_error_t *svn_client_diff (
                              const apr_array_header_t *diff_options,
                              const char *path1,
                              const svn_opt_revision_t *revision1,
                              const char *path2,
                              const svn_opt_revision_t *revision2,
                              svn_boolean_t recurse,
                              svn_boolean_t no_diff_deleted,
                              apr_file_t *outfile,
                              apr_file_t *errfile,
                              svn_client_ctx_t *ctx,
                              apr_pool_t *pool);

The diff_options argument is an apr_array_header_t * of const char * command line arguments to be passed to an external diff command such as GNU diff. We can just pass an empty array, since we'll be using Subversion's internal diff library to produce our diffs. It doesn't understand any options yet. path1 is the path (or URL in the repository) for the source file and revision1 determines which revision of that file to read. path2 and revision2 determine the destination file. recurse determines if the diff should recurse into the target (if the target is a directory) and no_diff_deleted indicates that there should not be any diff for deleted files. outfile and errfile are apr_file_t *s that will hold the output of the diff and any errors that occur. The client context is used for authentication when diffing against a repository.

Here's an example of how to use svn_client_diff to find the difference between the version you have in your working copy and the version you started with:

void
diff_wc_to_working(const char *filename,
                   svn_client_ctx_t *ctx,
                   apr_pool_t *pool)
{
  apr_array_header_t *diff_opts = apr_array_make(pool, 0, sizeof (char *));
  svn_opt_revision_t rev1 = { 0 }, rev2 = { 0 };
  apr_file_t *outfile, *errfile;
  svn_error_t *err;

  /* the revision we started with. */
  rev1.kind = svn_opt_revision_head;

  /* to the revision we've got here. */
  rev2.kind = svn_opt_revision_working;

  /* for your client, you'd probably want to open temp files for this, but for 
   * our purposes we'll just use stdout and stderr. */
  apr_file_open_stdout (&outfile, pool);
  apr_file_open_stderr (&errfile, pool);

  err = svn_client_diff (diff_opts,
                         filename, &rev1,
                         filename, &rev2,
                         TRUE,
                         FALSE,
                         outfile,
                         errfile,
                         ctx,
                         pool);
  if (err)
    handle_error (err);
}

Oops, I Didn't Mean To Do That

Once the developer can see what changes have been made to the working copy, a reversion back to unmodified files may be required. Subversion provides svn_client_revert to do just that. svn_client_revert is pretty simple: you give it the path to the file or directory in your working copy that you want to revert and a flag to determine if the revert should recurse into subdirectories. Then it will revert the current changes. As with svn_client_status, you can include a notification callback in the client context structure to be called for each file that is reverted. Here's an example:

void
revert_notification_callback (void *baton,
                              const char *path,
                              svn_wc_notify_action_t action,
                              svn_node_kind_t kind,
                              const char *mime_type,
                              svn_wc_notify_state_t content_state,
                              svn_wc_notify_state_t prop_state,
                              svn_revnum_t revision)
{
  printf ("reverting %s\n", path);
}

void
revert_wc_file (const char *path,
                svn_client_ctx_t *ctx,
                apr_pool_t *pool)
{
  ctx->notify_func = revert_notification_callback;

  svn_error_t *err = svn_client_revert (path, FALSE, ctx, pool);
  if (err)
    handle_error (err);
}

So And So Did What?

Now that we've moved into the realm of editing files in the working copy, we'll have to account for how that will interact with svn_client_update. If you have uncommitted changes in the tree and you update, conflicts can occur. When this happens, Subversion will leave three extra versions of the file in your working copy: the base version from which you started, your modified version, and the new version from the repository. The file you had edited will also have conflict markers inserted into it showing where the conflict occurred. Once you have resolved the conflict manually--by removing the conflict markers and leaving the file in its final state--call svn_client_resolve to tell Subversion that the conflict has been resolved. This will remove the other three versions of the file and Subversion will then allow you to commit your changes. svn_client_resolve is quite simple, so let's look at an example.

void
resolve_notification_callback (void *baton,
                               const char *path,
                               svn_wc_notify_action_t action,
                               svn_node_kind_t kind,
                               const char *mime_type,
                               svn_wc_notify_state_t content_state,
                               svn_wc_notify_state_t prop_state,
                               svn_revnum_t revision)
{
  printf ("resolving %s\n", path);
}

void
resolve_conflict (const char *path,
                  svn_client_ctx_t *ctx,
                  apr_pool_t *pool)
{
  ctx->notify_func = resolve_notification_callback;

  svn_error_t *err = svn_client_resolve (path, FALSE, ctx, pool);
  if (err)
    handle_error (err);
}

And You Thought You Could Never Commit To Anything

Now that you can edit files in your working copy, view diffs, and revert unwanted changes, you'll need to commit the changes to the repository for safekeeping. To do this, call svn_client_commit. As you'd expect, svn_client_commit uses some callback functions and batons from the client context. In addition to the standard notification callback, it uses a log message callback which fetches a log message for the commit from the client application. In the svn command line client, this function starts your $EDITOR and returns what you write there.

The next example assumes you have the log entry before you call svn_client_commit. Pass in your log entry as log_msg_baton and have the callback just return it. To make things fancier, use the tmp_file or commit_items parameters. tmp_file holds the name of a file that contains the log message. This file will be deleted when the commit completes but will remain if the commit fails. The user will not lose the log message. commit_items parameter holds information about each item that is being committed. It's useful for composing a default form for your log message.

svn_error_t *
commit_log_callback (const char **log_msg,
                     const char **tmp_file, 
                     apr_array_header_t *commit_items,
                     void *baton,
                     apr_pool_t *pool)
{
  *tmp_file = NULL;
  *log_msg = baton;

  return SVN_NO_ERROR;
}

With that callback you can now commit a change to the repository.

svn_client_commit introduces a few new concepts. Besides returning a svn_error_t to indicate an error, it also takes a svn_client_commit_info_t ** which it will fill in with the results of the commit. Since this function can take multiple different targets, we pass in an apr_array_header_t * that holds an array of const char * paths to items to commit. The rest of the arguments are typical of libsvn_client: a boolean that controls recursing into directories, a client context, and a pool for memory allocation. Here's an example of how this all works.

void
commit_item (const char *item,
             const char *log_entry,
             svn_client_ctx_t *ctx,
             apr_pool_t *pool)
{
  apr_array_header_t *targets = apr_array_make (pool, 1, sizeof (char *));
  svn_client_commit_info_t *commit_info;
  svn_error_t *err;

  /* yeah, i think this looks kind of nasty too... */
  (*((const char **) apr_array_push (targets))) = item;

  ctx->log_msg_func = commit_log_callback;

  /* this cast is just because log_entry is const and the baton isn't. */
  ctx->log_msg_baton = (void *) log_entry;

  err = svn_client_commit (&commit_info,
                           targets,
                           TRUE,
                           ctx,
                           pool);
  if (err)
    handle_error (err);

  printf ("revision %" SVN_REVNUM_T_FMT " committed at %s by %s\n",
          commit_info->revision,
          commit_info->date,
          commit_info->author);
}

Hey, New Stuff!

This gives your client the ability to make changes to an existing file and commit them to the repository. Eventually they'll want to add new items, so you'll need to use svn_client_add. This is another stereotypical libsvn_client function. It takes a path to an item (file or directory) in the working copy, a flag to indicate if it should recurse, a client context, and a pool. When it succeeds, the item is scheduled for addition during the next commit. Showing an example for this function is pointless, so just look at the one for svn_client_revert and replace revert with add. It's really that simple.

svn_client_add does have a quirk, though. Subversion tries to guess at the MIME type of the file as you add it. While it does a pretty good job of figuring out when something is a text file and when it isn't, it doesn't yet try to determine anything else. This means that if you add a PNG image, the svn:mime-type property will be set to application/octet-stream, which is all well and good for Subversion, but probably isn't what you need. With this MIME type, mod_dav_svn won't know enough to serve the file, so you won't be able to easily view it in a web browser. To make that work, you need to use svn_client_propset to set the svn:mime-type to something more appropriate (image/png in this case). Here's some example code that shows how to do that:

void
set_mime_type_to_png (const char *target,
                      apr_pool_t *pool)
{
  static const svn_string_t propval = { "image/png", 10 };

  svn_error_t *err = svn_client_propset ("svn:mime-type",
                                         &propval,
                                         target,
                                         FALSE,
                                         pool);
  if (err)
    handle_error (err);
}

Just Get Rid Of It!

Eventually your user is going to want to remove a file from the repository, so you'll need to use svn_client_delete. Again, this function can work either on a local working copy or a (possibly remote) repository. Its signature resembles that of svn_client_commit. If you're using it on a repository directly, pass it a svn_client_commit_info_t **, to get back information about the commit it performs and the url of the item in the repository. The log message callback and baton in the client context will be used to get the log message for the commit, and the context's authentication baton will be used to authenticate.

If you're deleting an item from a working copy, pass the path to the item on disk. You can also pass an svn_wc_adm_access_t *, in which case Subversion will use its existing directory lock, or NULL to open a new lock. force is a flag to indicate that Subversion should delete the item even if it is locally modified or unversioned, which normally results in an error. Let's take a look at how you would use svn_client_delete to schedule a file for deletion.

void
delete_item (const char *target,
             svn_client_ctx_t *ctx,
             apr_pool_t *pool)
{
  svn_error_t *err = svn_client_delete (NULL,   /* this isn't a commit */
                                        target,
                                        NULL,   /* let svn open a new lock */
                                        FALSE,  /* don't force it */
                                        ctx,
                                        pool);
  if (err)
    handle_error (err);
}

No, Put It Over There!

The remaining, relevant libsvn_client functions are svn_client_copy and svn_client_move. Since the ability to rename a file while keeping its revision history intact is one of Subversion's selling points over CVS, a good client needs this feature. Both functions have the same signature. They take as arguments a svn_client_commit_info_t ** (used to get information about the commit that is performed if you use them on the repository directly, just like in svn_client_commit); a src_path and src_revision, which identify the path (or url) for the source file and its revision; a dst_path, which indicates the destination path or url; an svn_wc_adm_access_t * (which can be NULL, like in svn_client_delete); and, finally, a client context and a pool. Let's see one last example, which copies a file within a working copy. This could just as easily move a file using svn_client_move, since they are the same from the point of view of the calling code.

void
copy_file (const char *source,
           const char *dest,
           svn_client_ctx_t *ctx,
           apr_pool_t *pool)
{
  svn_opt_revision_t source_rev = { 0 };
  svn_error_t *err;

  err = svn_client_copy (NULL,
                         source,
                         &source_rev,
                         dest,
                         NULL,
                         ctx,
                         pool);
  if (err)
    handle_error (err);
}

Conclusion

You're probably starting to notice that all the libsvn_client functions feel pretty similar. That's intentional. They reuse the same patterns. Once you've mastered one function that commits a change to the repository, you'll be able to use the rest with little trouble.

Even though we've gone over most of the functions in the libsvn_client API, your client isn't perfectly complete. There are still a number of interesting functions left, and your users will eventually clamor for them. The source code to the existing clients (especially the command line client distributed with the Subversion source tree) is your the best guide, along with the svn_client.h header file. Once you figure out what else you'd like your client to do, dig in and get to hacking.

Garrett Rooney is a software developer at FactSet Research Systems, where he works on real-time market data.


Return to ONLamp.com.

Copyright © 2009 O'Reilly Media, Inc.