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.
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);
}
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);
}
|
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);
}
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);
}
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);
}
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);
}
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);
}
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.