http://www.developer.com/

Back to article

A Crash Course in Subversion, Part 2


May 6, 2005

Editor's Note: This piece picks up where the article A Crash Course in Subversion left off.]

Adding, Copying, Renaming, and Removing Files

Well, you've learned the basics, the Subversion commands that fill 90% of your day to day routine, but sooner or later you'll need to hit that last 10%, so let's get on with it. So far we've mainly seen commands associated with making changes to the contents of files and committing them to the repository. But if you recall, Subversion doesn't just version files contents, it versions tree structures. This next section covers the commands that let you make changes to those tree structures, by adding, copying, renaming, and deleting files and directories.

Adding Files and Directories

Sooner or later, you're almost certainly going to want to add some new files or directories to your Subversion repository, so you'll have to become familiar with the svn add command. It works just as you would expect: You check out a working copy, create the file (or directory), and run svn add FILENAME. This schedules the file for addition, and later you can run svn commit to complete the process. The whole procedure looks like this:

$ ls
Makefile bar.c      foo.c      main.c
$ svn status
?      Makefile
$ svn add Makefile
A         Makefile
$ svn status
A     Makefile
$ svn commit -m "Added Makefile"
Adding         Makefile
Transmitting file data .
Committed revision 6.
$ svn status -v
                2      2      rooneg      .
                5      5      rooneg      Makefile
                3      3      rooneg      bar.c
                4      4      colonr      foo.c
                2      2      rooneg      main.c
$

So again, you have some new output to interpret. The first svn status has a ? in the first column of the line about Makefile, which means that Makefile isn't currently under Subversion's control. Next, you run svn add Makefile , and Makefile is scheduled for addition, which you can see from the output of the second svn status, where the first column is an A. You then commit the change, adding Makefile to the repository, and the final svn status -v shows it as any other versioned file.

If you're adding a new directory, there's no need to create and add it in two steps, because you can use Subversion's svn mkdir command, which you used earlier to create directories in the repository, in the working copy as well. Simply run svn mkdir DIRNAME, and it will create the new directory and schedule it for addition when you next commit, all in one step. If you created the directory before you decided to add it, then a regular svn add will schedule it, and all of its contents, for addition. If you don't want to add all of the directory's contents, you can pass the --non-recursive flag to svn add, and only the top-level directory will be scheduled for addition.

Copying and Moving Versioned Items

Adding a file or directory to your version control system isn't all that interesting, but where Subversion really shines is in its ability to copy and rename files and directories, while still retaining their revision history. Many other version control systems, specifically RCS, and the systems built on top of them, such as Perforce and CVS, don't make it easy to do this. In Perforce, you can work around the lack of a copy or move command by integrating from one file to another, and then possibly deleting the original, and in CVS you can fake things by poking around in the repository by hand and copying the RCS files around, but neither gives you a true copy or move command. In Subversion, the operation is as simple as svn copy FROM TO or svn move FROM TO. For simple copying or moving of a file or directory within your project, you generally do this within a working copy, and FROM and TO are the path to the source and destination. For some other operations, it's more convenient to perform the copy or move in the repository directly, but I'll hold off on covering that until I discuss branches and tags later in the chapter. For now, let's take a look at an example of how to use svn copy and svn move.

$ ls
Makefile      bar.c      foo.c      main.c
$ svn status
$ svn copy bar.c baz.c
A         baz.c
$ svn status
A +   baz.c
$ svn commit -m "copied bar.c to baz.c"
Adding         baz.c
Committed revision 7.
$ svn status -v
                2      2      rooneg      .
                5      5      rooneg      Makefile
                3      3      rooneg      bar.c
                6      6      rooneg      baz.c
                4      4      rooneg      foo.c
                2      2      rooneg      main.c
$ svn move bar.c zot.c
A         zot.c
D         bar.c
$ svn status
D      bar.c
A      +zot.c
$ svn commit -m "renamed bar.c to zot.c"
Deleting       bar.c
Adding         zot.c
Committed revision 8.
$ svn status -v
                2      2      rooneg      .
                5      5      rooneg      Makefile
                6      6      rooneg      baz.c
                4      4      rooneg      foo.c
                2      2      rooneg      main.c
                7      7      rooneg      zot.c
$

You've seen most of this output already, but the second svn status has something new. The fourth column of output for baz.c is a +, which means that the file has been added with history. When the change is committed to the repository, the fact that baz.c is a copy of foo.c is recorded, so the revision history of the file remains intact. Similarly, when you moved bar.c to zot.c, the output of svn status shows that foo.c has been deleted (and thus has a D in the first column of its line of status output), and zot.c has been added with history. Again, this fact is recorded in the repository at commit time.

Deleting Versioned Items

Now that you've mastered adding, moving, and copying files, the last of the basic tasks you're likely to perform in the working copy is removing files and directories. As you might expect, the command to do this is svn delete, and it works much as you would expect. svn delete PATH removes the file or directory at PATH and schedules it for removal from the repository. When you next use svn commit, the file or directory is removed from the repository itself. Let's take a look at the process.

$ ls
Makefile baz.c      foo.c      main.c      zot.c
$ svn status
$ svn delete baz.c
D         baz.c
$ svn status
D      baz.c
$ svn commit -m "removing baz.c"
Deleting       baz.c
Committed revision 9.
$ svn status -v
                2      2      rooneg      .
                5      5      rooneg      Makefile
                4      4      rooneg      foo.c
                2      2      rooneg      main.c
                7      7      rooneg      zot.c
$

This output is quite similar to what you saw when you moved a file. When you've deleted something, it shows up in the output of svn status with a D in the first column.

Note that although the file has been removed from the HEAD revision of the repository, it still exists in the previous revisions, and you can easily get the file back either by using svn copy to copy the file from the previous revision into your working copy or by using svn merge to reverse the entire commit that removed it. I discuss the svn merge technique later, but let's now look at how you use svn copy to "undelete" a file.

$ ls
Makefile      foo.c      m1a1in.c      zot.c
$ svn copy --revision 7 file:///path/to/repos/trunk/baz.c baz.c
A      baz.c
$ svn commit -m "resurrect baz.c from the repository"
Adding         baz.c
Committed revision 10.
$

OK, so what happened here? You just copied baz.c as it existed in revision 7 (the last revision before you deleted it) directly from the repository into your working copy. Then you committed the change, and the process of restoring the deleted file completed. No matter what kind of change you make to your project in HEAD, all previous versions always exist in the repository, so you can always revert to the previous state if you make a mistake. You can even do this in a single command, without a working copy at all, by performing the copy within the repository itself, something you'll explore in the next section when you learn about using svn copy to work with branches and tags.

Working with Branches and Tags

Another of Subversion's improvements over CVS is its ability to create branches and tags in constant time and space, as opposed to in time and space proportional to the number of files involved, as in CVS. Subversion accomplishes this by leveraging the design of its versioning repository filesystem. Branches and tags are copies of existing directories, usually placed in specific directories so that users know they're "special."

There are a variety of reasons you might want to create a branch or tag. Generally, you use tags to mark a particular version of your project so you can easily refer back to it later. You might want to make a tag when you release a new version of your project, so that later on you don't have to remember what revision of the repository corresponds to that particular release. Instead of remembering what revision of the trunk corresponds to the release, you can just look at the copy you placed under the tags directory.

Branches are a little more complex. Rather than just making a copy and leaving it there so you can look at it later, as with a tag, you create a branch so that it can be modified. Sometimes you might want to make some changes to your project and check them in to the repository so that other people can see them, but you don't want to make them in the main development version. Perhaps they're experimental and might never be committed to the mainline development version, or maybe the new feature you're implementing is very complex, and it will take some time before it's stable enough for other developers to use it. In cases like these, you might want to create a new development branch by making a separate copy of the trunk, and then commit your changes to the branch. If you later decide that the changes are ready, they can be merged into the version of the project in the trunk and committed, so that everyone can see them.

Creating Branches and Tags

Although you could create a branch or tag by checking out the top level of the repository and doing a standard svn copy in your working copy, that would be quite inefficient. You would use up a huge amount of disk space and possibly network bandwidth in creating the initial working copy, because you would be checking out every branch and tag in the repository simultaneously, and then svn copy would have to do a lot of extra disk I/O copying files around, only to end up wasting a lot of bandwidth again committing your branch or tag to the repository. Fortunately, the Subversion developers anticipated this problem and designed the svn copy command to run against the repository directly as well as within the working copy. Creating a tag is as simple as this:

$ svn copy file:///path/to/repos/trunk file:///path/to/repos/tags/
                   0.8-release \
   -m "tagging 0.8 release"
Committed revision 11.
$ svn diff file:///path/to/repos/trunk file:///path/to/repos/tags/
                   0.8-release
$

As you can see from the output of svn diff, the tag is identical to the HEAD revision of /trunk. Creating a new branch is done in exactly the same way. The only difference is that a tag is intended to never be modified again (this may be enforced by various means; see Chapter 3 for how to do it with hook scripts and Chapter 5 for how to do it with mod_authz_svn), whereas a branch will be used for further development.

Note: mod_authz_svn is an Apache module that works with Subversion's Apache-based server to allow you to control which users have access to various parts of your repository. mod_authz_svn might be used to control who can create new branches or tags, or who can write to a particular branch.

Merging Changes Between Branches

For a branch to be of much use, the version control system needs to provide a way to manage the process of merging changes between related branches. Generally, a branch is created for some kind of development, and at some point in the future either changes from the branch will have to be merged back into the trunk (or wherever the branch originated) or changes from the trunk will have to be merged into the branch (this is common in the case of long-lived branches to support released versions of software). In Subversion, the process of merging changes is handled by the svn merge command. The merge command has two modes of operation. In the first, you give the command two source URLs (with optional revisions specified with @REVNUM at the end of the URL), and it will merge the differences between the two sources into your current working directory (or another directory if it's specified on the command line). The process looks like this:

$ svn copy file:///path/to/repos/tags/0.9-release \
           file://path/to/repos/branches/my-development-branch \
           -m "create development branch from version 0.9 tag"
Committed revision 12.
$ svn checkout file:///path/to/repos/branches/my-development-branch wc
A   wc/Makefile
A   wc/foo.c
A   wc/zot.c
A   wc/main.c
Checked out revision 12.
$ cd wc
[ ... do some work, make some changes, commit them to the branch ... ]
$
[ ... time passes,changes are made in the trunk,version 1.0 is released,
and now you want to merge those changes into your branch ... ]
$svn merge file:///path/to/repos/branches/my-development-branch \
           file:///path/to/repos/tags/1.0-release \
.
A   bar.c
A   README
A   baz.c
D   foo.c
$ svn status
A   +     README
A   +     bar.c
A   +     baz.c
D         foo.c
$ svn commit -m "merge changes from trunk up to 1.0 release into
                 dev branch"
Adding         README
Adding         bar.c
Adding         baz.c
Deleting       foo.c
Transmitting file data ...
Committed revision 20.
$

First, you make a development branch by copying the 0.9-release tag. Then you use the branch for some private development, committing changes into it as desired. In the meantime, more changes are made to the version in trunk and the 1.0 version is tagged as tags/1.0-release. At this point, you merge the changes that occurred between the point you created your branch and the time 1.0 was tagged into your local working copy (which was checked out from the branch) and commit them. Now your branch is up to date with regard to all the changes that were made to the trunk. Note that just like when you run svn update, when you merge changes into your working copy there's always the chance of a conflict, and any that occur will have to be resolved before you can continue. Likewise, as with any other change to a working copy, you'll have to svn commit the results of the merge for them to be propagated back into the repository.

More often, you'll need to merge a particular change or set of changes from one branch into another. For example, say you have a branch that's being used to manage the "stable" release of your software, separate from the main development trunk, where more complex, destabilizing changes are being made. When a bug fix is made to the main version of the code in the trunk, you might want to merge that particular change into the stable branch, so it will be in the next release. In this case, when the change you're merging is a particular revision, you can use a slightly simpler version of the svn merge, where there's a single source (which can be a URL or a working copy path), the revisions of which are specified via the standard -r START:END syntax. The changes to that source path over that range of revisions will be merged into your current working directory (or another working copy directory if one is given on the command line). The process looks like this:

$ svn merge -r 12:13 file:///path/to/repos/branches/
                             my-development-branch
U main.c
U zot.c
$ svn status
M      main.c
M      zot.c
$ svn commit -m "merged changes from development branch into trunk"
Sending        main.c
Sending        zot.c
Transmitting file data ..
Committed revision 30.
$

Here the change to /branches/my-development-branch from revision 13 is has been merged into a checked-out version of the trunk. Once the user verifies that the change is correct, it can be committed and the process will be complete.

Using svn merge to Revert a Change
A less common use of svn merge is to revert the change committed in a particular revision. To do this, you simply reverse the order of the revision arguments. For example, to revert the change to main.c that occurred in revision 5, you run svn merge -r 5:4 main.c. This means, literally, apply the change needed to go from revision 5 of main.c to revision 4 of main.c to the version of main.c in your current working directory. In practice, you can use this to revert a particular change that shouldn't have been committed.

Switching to Another Branch

Because creating branches in Subversion is cheap (in terms of time and disk space at least), some thought has gone into how to make them easier to work with. The majority of this work will be done after the 1.0 release of Subversion is complete (specifically, the process of merging changes between branches will be made easier), but there is one feature available now that makes working with branches considerably simpler, the svn switch command. svn switch lets you move your checked-out working copy (and any changes you made to it, assuming they don't conflict) to another branch in the repository. The standard use case is either to migrate an entire working copy from one branch to another or, more commonly, to move specific parts of your working copy to another branch.

The best way to understand this concept is probably through an example. Say that you're adding a new feature to Subversion itself, but the work is going to take some time to complete. Subversion uses an unstable trunk policy, which means that most development is done right in the main trunk branch. However, for changes that are likely to be either disruptive to other developers (by breaking existing functionality for a time, for example) or that are highly experimental and might never be destined to be merged into the trunk, a branch will be created.

The Subversion code base is split into several libraries, each of which lives in its own subdirectory. Because most changes of this sort will be limited to one of those libraries, you might make the branch via an svn copy command targeting the repository directly, and then switch the affected subdirectory over to that branch. As you continue to make modifications to that subdirectory, you can safely commit them to the repository, and none of the other developers will have to see them unless they specifically check out (or switch to) that branch. You can safely svn update all the other directories in your working copy to track the latest changes other developers are making to the trunk.

This ensures that your modifications to one library will continue to work with the latest version of the software (making the eventual merging of my changes into the trunk easier), while avoiding the need to inconvenience other developers by making disruptive changes in the trunk. The only thing you need to remember is that other developers could be making changes to the same library you're working on in the trunk, so as those changes occur, you need to merge them into your development branch to ensure that your final merge doesn't obliterate the other developers' changes. Let's see a quick example of how this can work:

$ svn list file:///path/to/repos/trunk
library_one/
library_two/
library_three/
main.c
$ svn copy -m "create branch for development of library one"\
      file:///path/to/repos/trunk \
      file:///path/to/repos/branches/library-one-dev
Committed revision 10.
$ svn checkout file:///path/to/repos/trunk wc
[ ... lots of output ... ]
$ cd wc
$ svn info library_one | grep ^URL
URL:file:///path/to/repos/trunk/main.c
$ svn switch file:///path/to/repos/branches/library-one-dev/library_one \
             library_one
At revision 10.
$ svn info library_one |grep ^URL
URL:file:///path/to/repos/branches/library-one-dev/library_one
$ svn status
    S library_one
$

In this example, you start out by making a new branch by copying the trunk, which contains a single C file, main.c, and several subdirectories, each of which contains an independent library of code that's used by main.c.

You then check out the trunk, so your copy of the library_one directory has a URL of file:///path/to/repos/trunk/library_one. Next, you use svn switch to switch your copy of library_one over to the development branch. Its URL now points to the version of library_one on the development branch you created, and when you run svn status you can see that it's switched because of the S in the fifth column of the status output.

Going forward, each time you run any command that involves library_one or any of its contents, it will refer to the versions of those files that live on the branch. Updates will pull down changes that occur on the branch, commits of changes you make to it will be committed to the branch, and so forth. This allows you to track the changes that are being made on the branch in the context of the changes to the rest of the project that are going on in the trunk, which can be quite useful for a couple of reasons. First off, it allows the developers not working on changes to the library_one directory to avoid having to deal with your changes, which could be experimental or large enough that they introduce temporary breakage. Second, it allows you to make sure your new changes work with what others are doing in the trunk, without having to explicitly merge those changes over to the trunk, which simplifies the process of working on your branch.

Changing a Repository URL with svn switch --relocate

There's one other time you might need to use svn switch , and that's when the URL you use to access your Subversion repository has changed. This could happen if the URL has moved to another machine, the path it's located at changes, or if you switch from one repository access layer to another (say you want to stop accessing the repository directly via a file:// URL, and start accessing over a network with a http://or svn:// URL). In these situations, you use the --relocate flag with svn switch, and give it the portions of the URL you're changing. For example, if the repository was accessed via ra_local on your local machine at the path /home/repos, and you need to change to accessing it over ra_svn, because this working copy has been moved to a different machine, you do something like this:

$ svn info | grep ^URL
URL:file:///home/repos/trunk
$ svn switch --relocate file:///svn://servername/
$ svn info | grep ^URL
URL:svn://servername/home/repos/trunk
$

Note that svn switch --relocate will work only if the new URL points to the exact same repository. Each Subversion repository has its own UUID, and the client will compare the UUID for the new repository to the one from the old repository and ensure that they're the same before allowing the switch to complete.

Removing Old Branches

One more advantage of Subversion's "everything is a copy" design is that there's no need to keep old development branches around in the HEAD version of the repository. Once a branch's useful life has ended, a simple svn delete will remove it from HEAD, and users will no longer be bothered by its presence in the output of svn list commands. Of course, the branch will still exist in the history of the repository, so you can easily retrieve any of its contents later by just specifying the correct revision of the repository.

Properties

Subversion allows you to associate arbitrary metadata with files, directories, and even revisions in the repository. Each bit of metadata, or property, consists of a name, which can be any Unicode string, and an arbitrary value, which can be anything at all. Internally, this functionality is used to implement a variety of different features of Subversion (log entries are properties associated with each revision, for example), but it's also exposed to users for their own use. You could, for example, choose to mark files with a "maintainer" property to indicate which developer is responsible for it. In addition to your own properties, Subversion reserves all properties starting with the prefix svn:for itself, and you can manipulate several properties in that namespace to control Subversion's behavior in useful ways. Before diving into those, though, let's look at the commands you need to use to work with properties.

First, you'll need a way to tell what, if any, properties have been set on a particular versioned resource. The svn proplist command, as you might expect, does that for you. Let's take a look at how it works:

$ ls -a
.svn
index.html
button.png
$ svn proplist index.html
Properties on 'index.html':
   svn:mime-type
$ svn proplist button.png
Properties on 'button.png':
   svn:mime-type
$

Here you can see a working copy that holds the contents of a web site. The files index.html and button.png both have the svn:mime-type property, which is a special property Subversion uses internally to track the mime type of a file.

Well, now you've seen what properties have been set on a particular file, but that's not very useful unless you can determine what value they've been set to. The svn propget command prints out the value of a given property for you. The syntax for the command is simple. It takes as its first argument the name of the property you're printing out, and subsequent arguments are the files it should use as targets.

$ svn propget svn:mime-type index.html
text/html
$ svn propget svn:mime-type button.png
image/png
$

As you might have guessed, index.html has the svn:mime-type of text/html, and button.png has image/png.

To make use of properties, you'll need to be able to set and change them. To do that, you'll need to use the svn propset and svn propedit commands. svn propset sets the value of a specific property on a versioned entity. You specify the property as its first argument, and then the second argument can be the value or, if you're dealing with a value that would be inconvenient to deal with on the command line, you can specify a file that holds the contents of the property via the -F filename argument. Subsequent arguments are the targets on which the property will be set.

$ svn add logo.png
A  (bin)  logo.png
$ svn commit -m "added logo.png"
Adding (bin)web/logo.png
Transmitting file data .
Committed revision 35.
$ svn propget svn:mime-type logo.png
application/octet-stream
$ svn propset svn:mime-type image/png logo.png
property 'svn:mime-type'set on 'logo.png'
$ svn status
 M     logo.png
$svn propget svn:mime-type logo.png
image/png
$ svn commit -m "set mime-type on logo.png to image/png"
Sending        web/logo.png
Committed revision 36.
$

Note that the mime type of logo.png was set to application/octet-stream when you first added it. This is the default behavior when Subversion determines that the file isn't plain text, and it will prevent Subversion from doing things like end-of-line conversion or keyword substitution, which would be destructive when applied to a binary file. It also keeps Subversion from attempting to display textual diffs in the output of svn diff or from automatically merging in changes during svn update, both of which aren't possible with binary files. Later, when you set the svn:mime-type to image/png, you'll see yet another new kind of output from svn status. The M in the second column of the output indicates that the file's properties have been modified. Also note that you had to commit the change back to the repository, because like most other things in Subversion, properties that are set on files and directories are versioned.

Similar to svn propset is svn propedit, which allows you to edit the contents of a property in your default text editor. Let's take a look at how this works.

$ svn proplist .
Properties on '':
   svn:ignore
$ svn propget svn:ignore .
*~
.*~
$ svn propedit svn:ignore .
[
   editor pops up with contents of svn:ignore in it.
   we edit it,save,and exit
]
$ svn status
 M .
$svn diff
Property changes on:
___________________________________________________________________
Name:svn:ignore
   -*~
.*~
   +*~
.*~
.DS_Store
$ svn commit -m "added .DS_store to svn:ignore"
Sending .
Committed revision 50.
$

This example shows how you can use svn propedit to edit the svn:ignore property. Also note that the svn diff command shows you the changes you make to properties as well as to the contents of files. Don't focus too much on the specifics of the value of the svn:ignore property (I'll discuss that in a minute)—just remember that the svn propedit command allows you to modify a property with an editor and that svn diff will show you the difference once you do.

As you can see by the long help message for svn propset, Subversion has several special properties, some of which you've already seen. You can set these special properties on your files and directories to control Subversion's behavior in certain ways, as shown in Table 2-3.

Table 2-3. Special Properties

Name Purpose
svn:ignore Indicates which files in a directory should be ignored by svn status
svn:keywords Indicates which RCS style keywords should be expanded in a given file
svn:executable Indicates that a given file should be made executable, if such a thing is possible on the operating system in question
svn:eol-style Indicates what style line endings should be used for a given textual file
svn:mime-type Indicates what mime type should be used when a given file is served from a mod_dav_svn enabled Apache server
svn:externals Lists directories within a given directory that should be retrieved from alternate URLs

svn:mime-type

The simplest of Subversion's special properties is svn:mime-type, which tells Subversion what mime type a particular file is. This is used in two ways. First, if a file has a mime type that doesn't start with text (or some other prefix that Subversion knows refers to a textual file), it won't try to automatically merge changes into the file, or do end-of-line translation or keyword expansion, all of which rely on a file being textual in nature. This keeps Subversion from accidentally mangling binary files, something that can be a constant problem in other version control systems (specifically CVS, and to a lesser degree Perforce). When you run svn add on a file, Subversion will attempt to determine if the file you're adding is textual, and if it isn't, it will set the mime type to application/octet-stream, which just means that the file is some kind of binary format. If you want a more specific mime type, you'll have to set it manually with svn propset or svn propedit. The second reason for setting a svn:mime-type property is so that Subversion's mod_dav_svn Apache module can serve the file as the correct mime type. This allows you to browse the repository in a web browser and have things generally work as you expect (e.g., HTML pages are rendered as HTML instead of plain text, images can be viewed as the appropriate image type, and so on). You'll explore mod_dav_svn further in the next chapter.

svn:ignore

The other special property you've already seen is svn:ignore, which is Subversion's equivalent of CVS's .cvsignore feature. Often, files will show up in your working copy that don't want to be checked in to Subversion—for example, object files that are generated as part of a program's build process or backup files that are generated by text editors. To keep these files from constantly showing up in the output of svn status and generally being annoying, you can simply add an entry to the directory's svn:ignore property that matches their filenames, and svn status will ignore them, unless you pass it the --no-ignore option. This can probably be best shown with an example:

$ ls -a
./          Makefile        main.c    foo.c
../         .svn/main.c    ~foo.c     ~
$svn status
?     main.c~
?     foo.c~
$ svn propset svn:ignore "*~"
property 'svn:ignore' set on ''
$ svn status
 M     .
$ svn commit -m "added *~to svn:ignore"
Sending        .
Committed revision 70.
$ svn status
$ svn status --no-ignore
I      main.c~
I      foo.c~
$

As usual, you can find some more information about the output of svn status in this example. At first, the main.c~ and foo.c~ file shows up as an unversioned file.3 This is somewhat irritating and clutters up the output of svn status, because you normally don't care about the backup files generated by your editor. To get rid of that output, you set the svn:ignore property on the directory to "*~", and you can see that the next svn status shows you the property modification on the directory, but doesn't say anything about the files that end in a tilde (~). You then commit, and the third svn status shows you nothing at all. Finally, you see that it's possible to run svn status --no-ignore, and you're informed by the I in the first column that the main.c~ and foo.c~ files are indeed being ignored. To add more patterns to the list of things to be ignored, just place them on separate lines in the property.

svn:keywords

One feature of the RCS-based version control systems that you haven't yet seen in Subversion is the ability to embed special keywords in files and have Subversion expand them into something when the file is checked out. You'll generally use this to embed the revision in which the file was last changed or the URL of the file in the repository (or something like that) in the file so that the information will be easy to access without having to use Subversion. This can be important if you're rolling a release of some software, and you want people to be able to accurately report what version of a particular file they have when sending in bug reports. In some other version control systems, keywords turn out to be a mixed blessing, because it's quite possible for there to be a file that contains the keyword but it would be a bad idea for Subversion to expand it (binary files come to mind). For this reason, Subversion defaults to not expanding any keywords. If you want Subversion to do so, you need only set the svn:keywords property on the file to a string containing the names of the keywords you would like expanded, separated by spaces, and it will expand them for you. Subversion supports the following keywords: HeadURL (abbreviated as URL), which expands to the URL of the file in the repository; LastChangedBy (abbreviated as Author), which expands to the username of the last person who modified the file; LastChangedDate (abbreviated as Date), which stores the date on which the file was last modified; LastChangedRevision (abbreviated as Rev), which stores the revision in which the file last changed; and Id, which stores a compressed version of the other four keywords. Let's take a look at how these keywords work in an example.

$ cat main.c
#include <stdio.h>
int
main (int argc, char *argv [])
{
   printf ("hello world \n");
   return 0;
}
$ vi main.c
[ ... add keywords ... ]
$ cat main.c
/*$URL$
 *$Rev$
 *$Author$
 *$Date$
 *$Id$
 */
#include <stdio.h>
int
main (int argc, char *argv [])
{
   printf ("hello world \n");
   return 0;
}
$ svn propset svn:keywords "Url Rev Author Date Id" main.c
property 'svn:keywords'set on 'main.c'
$ svn commit -m "Set svn:keywords on main.c"
Sending        main.c
Committed revision 75.
$ cat main.c
/*$URL:file:///path/to/repos/main.c $
 *$ Rev:3 $
 *$ Author:rooneg $
 *$ Date:2003-06-30 18:37:06 -0400 (Mon,30 Jun 2003)$
 *$ Id:main.c 3 2003-06-30 22:37:06Z rooneg $
 */
#include <stdio.h>
int
main (int argc, char *argv [])
{
   printf ("hello world \n");
   return 0;
}
$

In this example, a comment is added to the top of main.c, which contains the $URL$, $Rev, $Author$, $Date$, and $Id$ keywords. Then, the svn:keywords property is set to Url Rev Author Date Id, and the change is committed. After the commit, the keywords in main.c expand to hold information about the current version of main.c.

svn:eol-style

A constant issue for developers of cross-platform software is the fact that different operating systems use different characters to indicate the end of a line in a text file. Worse yet, some tools are extremely picky about the line endings of the files they consume. svn:eol-style makes it a bit easier to deal with such tools. By setting it to native, you can ensure that the file in question is always in the native end-of-line style, making it easy to interoperate with tools on the system. Unix operating systems (including Mac OS X) use LF line endings when native is specified, Windows-based systems use CRLF and, hypothetically, Classic Mac OS systems would use CR, but that's academic because Subversion doesn't run on Classic Mac OS. For cases in which a specific line-ending style is always required (for example, the .dsp files Microsoft Visual Studio uses must use a carriage return and linefeed at the end of each line), you can set it to either LF to indicate only linefeeds should be used, CR to indicate only carriage returns should be used, or CRLF to indicate carriage return/linefeed-style endings. As mentioned in the svn:mime-type discussion, these line-ending conversions can be applied only to files with a textual mime type.

svn:executable

Another common need for software development is to specify that certain files are executable, meaning that the operating system can run them as full-fledged programs. This is a somewhat platform-specific thing to do, as some operating systems, such as Microsoft Windows, don't have a way to indicate that a particular file is executable. In any case, if the svn:executable property is set to on, Subversion will use whatever platform-specific means it can to make the file executable when it's checked out into a working copy.

svn:externals

To provide something roughly analogous to CVS's modules, Subversion offers the svn:externals property. This property is set on a directory and contains a newline-separated list of module specifications. Each module specification contains a relative path to the directory the module should be checked out into, an optional revision (which is specified as -r REVNUM), and a URL pointing to a location in a Subversion repository. When the directory with the svn:externals property is checked out, the modules will also be checked out into the same working copy. This allows you to pull directories from other parts of the repository, or even completely different repositories, into your working copy, which can be useful if you're sharing code between multiple projects, or if you need to modify the directory layout of your working copy in certain situations.

$ svn list file:///path/to/repos
application/
libedit/
libnetwork/
$ svn propget file:///path/to/repos/application/trunk/libs svn:externals
libedit file:///path/to/repos/libedit/trunk
libnetwork file:///path/to/repos/libnetwork/trunk
$ svn checkout file:///path/to/repos/application/trunk application
A application/README
A application/main.c
A application/libs
Checked out revision 47.
Fetching external item into libedit
A application/libs/libedit/Makefile
A application/libs/libedit/README
A application/libs/libedit/edit.c
A application/libs/libedit/history.c
A application/libs/libedit/prompt.c
Checked out revision 47.
Fetching external item into libnetwork
A application/libs/libnetwork/Makefile
A application/libs/libnetwork/README
A application/libs/libnetwork/socket.c
A application/libs/libnetwork/marshal.c
Checked out revision 47.
$ svn info application/libs | grep ^URL
URL:file:///path/to/repos/application/trunk/libs
$ svn info application/libs/libedit | grep ^URL
URl:file:///path/to/repos/libedit/trunk
$ svn info application/libs/libnetwork | grep ^URL
URL:file:///path/to/repos/libnetwork/trunk
$

Here you can see an example of using svn:externals to allow everyone who checks out the source for an application, which lives in file:///path/to/repos/application/trunk, to also get the source for several libraries that the application depends on. In this case, the libraries are stored in the same repository as the application, but there's nothing that says they have to. The URLs in the svn:externals property could just as easily have pointed to a completely different repository, and it would work just the same.

Revision Properties

In addition to the standard versioned properties Subversion can store for each file or directory in your repository, there are properties stored for each revision in the repository. These "revprops" are used to record information such as the author of each revision (author), the date and time the revision was created (date), and the log entry for the revision (log). Most of the time, you don't need to care about these properties. They're created automatically, and Subversion is perfectly capable of using them for the commands that require the information they store without any intervention from you.

The exception to this rule, of course, is when you need to go back and edit them. Most of the time, you find people doing this when they want to adjust a log entry on an old revision. Because these properties are stored on a per-revision basis, they aren't themselves versioned. That means that if you change them, the previous version is lost for good (unless you resort to looking in your repository backups or something like that). Because of this, Subversion won't allow you to change revision properties unless you explicitly turn this capability on. To do this, you need to enable the pre-revprop-change hook script. The procedure for doing so is documented in the next chapter, so for now, all you need to know is that you should look inside the repository, in the hooks directory. There, you'll see a file named pre-revprop-change.tmpl. This is the sample pre-revprop-change hook script. Assuming you're on a Unix machine, you should be able to just copy pre-revprop-change.tmpl to pre-revprop-change, make it executable with a quick chmod +x pre-revprop-change, and you're all set. The default script in pre-revprop-change.tmpl will let you change only the svn:log revprop, so let's take a look at how you can do that.

$ ls /path/to/repos/hooks
post-commit.tmpl        pre-commit.tmpl        start-commit.tmpl
post-revprop-change.tmpl pre-revprop-change.tmpl
$ cp /path/to/repos/hooks/pre-revprop-change.tmpl \
     /path/to/repos/hooks/pre-revprop-change
$ chmod +x /path/to/repos/hooks/pre-revprop-change
$ svn log --revision 10 file:///path/to/repos
-------------------------------------------------------------------
r10 |  rooneg |  2003-06-30 18:12:07 -0400 (Mon,30 Jun 2003) | 1 line
This is revision 10's log entry
-------------------------------------------------------------------
$ svn propget --revprop svn:log --revision 10 file:///path/to/repos
This is revision 10's log entry.
$ svn propset --revprop svn:log --revision 10 \
      "This is revision 10's new log entry"file:///path/to/repos
property 'svn:log'set on repository revision '10'
$ svn propget --revprop svn:log --revision 10 file:///path/to/repos
This is revision 10's new log entry.
$ svn log --revision 10 file:///path/to/repos
-------------------------------------------------------------------
r10 |  rooneg | 2003-06-30 18:12:07 -0400 (Mon,30 Jun 2003) | 1 line
This is revision 10's new log entry
-------------------------------------------------------------------
$

There you have it—changing an incorrect log entry is that easy. All you have to do is enable the pre-revprop-change script, use the standard commands you normally use to access Subversion properties, add a --revprop argument and a --revision, and you're done.

Miscellaneous Commands

And now you're down to it, those last few client-side commands that just don't fit into any other specific section of the chapter. Not that svn blame and svn cleanup aren't important, it's just that they both don't fit in with any of the groups of commands we've already talked about. They're still useful though, and you'll most likely find yourself needing them at least every now and then.

svn blame

When you're working on a file that has changed many times over the course of its history, it's often nice to be able to ask the question "What revision introduced this particular line?" Although it's possible to manually go back through the revision history diffing files until you find the revision responsible, it's much more convenient to use the svn blame command.4 svn blame takes a working copy path or URL and optionally a range of revisions, and outputs a formatted version of the file in which each line is prefixed with the revision that line originates from and the username of the author of that revision. The output looks like this:

$ svn blame svn://svn.example.org/repos/project/trunk/hello.c
12          steve #include <stdio.h>
10           greg
10           greg int
10           greg main (int argc, char *argv)
10           greg {
11         robert printf ("hello world \n");
10           greg return 0;
10           greg }
$

From this output, you can see that most of the file originated in revision 10, which was committed by greg, with line 6 being changed by robert in revision 11, and line 1 being changed by steve in revision 12.

It's worth noting that although the output from svn blame can be exceptionally useful, Subversion's repository design doesn't make it particularly efficient to implement. Because Subversion stores differences using a binary diffing algorithm that doesn't track changes on a line-by-line basis, the current implementation involves downloading each individual revision of the file in question and manually diffing them on the client machine to determine the origin of each line. Thus the command may take quite a while to execute when the file in question has many revisions or when it's quite large. Also keep in mind that svn blame is meaningless when applied to a directory or to a binary file.

svn cleanup

The Subversion working copy library is designed to operate like a journaled filesystem, in that as you run commands and your client modifies the working copy, it first locks that portion of the working copy and writes out the changes to disk in the form of a log file. This means that in the event of a problem such as a power outage, an operating system crash, or the client process being interrupted somehow, the working copy will always be returned to a useful state. Should one of these problems occur, you can use the svn cleanup command to return everything to working order. svn cleanup just takes the path to your working copy as an argument (or uses the current working directory if you don't give it one) and runs through the working copy, finishing up all unfinished work and removing stray locks. Note that you should run svn cleanup only on a working copy that isn't being accessed by another Subversion client, as the cleanup code will assume that it's perfectly fine to break locks that the running client has made, which will almost certainly result in significant problems, because the locks are there precisely to keep two processes from changing that part of the working copy at the same time.

$ svn update
svn:Working copy '.' locked
svn:run 'svn cleanup' to remove locks (type 'svn help cleanup' for details)
$ svn status
   L   docs
   M    docs/design.xml
$ svn cleanup
$ svn status
M        docs/design.xml
$ svn update
At revision 100.

Here you can see that something has happened to the docs directory of this working copy to leave it in a locked state, as the third column of its line in the svn status output is an L. Running an svn cleanup completed whatever unfinished work needed to be done for that directory, and the second svn status shows only the modified file in that subdirectory, as it should.

svn export

You've already see svn import, but there's also a command for the reverse procedure, svn export. The export command is useful when you need to release a version of your software and you don't want to include all the administrative directories from a working copy. svn export will write out a complete directory tree containing all versioned files from either a URL in the repository (with an optional revision) or a working copy path. You can think of it as an svn checkout that doesn't write out the administrative directories.

$s vn export file:///path/to/repository/tags/release-1.0 release-1.0
A  release-1.0
A  release-1.0/foo.c
A  release-1.0/main.c
A  release-1.0/Makefile
A  release-1.0/zot.c
$ ls -al release-1.0
total 24
-rw-r--r--   1 rooneg    staff      0   Jul 26 20:53   .
-rw-r--r--   1 rooneg    staff      0   Jul 26 20:53   ..
-rw-r--r--   1 rooneg    staff      0   Jul 26 20:53   Makefile
-rw-r--r--   1 rooneg    staff     76   Jul 26 20:53   foo.c
-rw-r--r--   1 rooneg    staff     99   Jul 26 20:53   main.c
-rw-r--r--   1 rooneg    staff     76   Jul 26 20:53   zot.c
$

There isn't a whole lot to say about the output of svn export, as it's substantially similar to that of svn checkout. Just note that the directory you export to isn't a working copy—it has no administrative .svn directory, so you can't use any Subversion commands that require a working copy to function on its contents.

Summary

In this chapter you've encountered the majority of the commands you're likely to use in your day-to-day work with Subversion. You've created a repository, imported your data, checked out working copies, committed changes, merged conflicts, added and removed files and directories, worked with branches, and covered the lesser-known but still useful Subversion features such as properties. In the process, you've gained a general understanding of how to interact with Subversion and what it can do for you.

End Notes

3. OK, technically, if you run this command in a working copy directory that's full of files ending in a tilde (~), you won't see anything, because the default list of global-ignores used by Subversion includes *~. To modify this list, you can edit the ~/.subversion/config file.

4. There was a fair amount of controversy when this command was named. Most Subversion developers felt that the name accurately represented the common use case, determining exactly who wrote the line of code that caused the bug they've been tracking down; thus, the canonical name is svn blame. However, a significant number felt that retaining compatibility with the name of the CVS equivalent, annotate, was a more important consideration, and a small but vocal minority felt that "blame" had a negative connotation. Thus, svn blame can also be spelled svn annotate by those whose fingers are accustomed to the CVS command, and svn praise by those who feel that "blame" is too negative.

About the Author

Garrett Rooney is a software engineer at FactSet Research Systems, in Stamford, Connecticut, where he works on real-time news feeds. Rooney attended Rensselaer Polytechnic Institute, where he managed to complete 3 years of a mechanical engineering degree before coming to his senses and realizing he wanted to get a job where someone would pay him to play with computers. Since then, Rooney completed a computer science degree at RPI and has spent far too much time working on a wide variety of open source projects, most notably Subversion.

About the Book

Practical Subversion By Garrett Rooney

Published: November 2004, Softbound, 336 pages
Published by Apress
ISBN 1-59059-290-5
Retail price: Price: $34.99
This material is from Chapter 2 of the book.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date