Hack 92 Apply, Understand, and Create Patches

figs/moderate.gif figs/hack92.gif

Sometimes only the little differences matter.

Despite all your best efforts, eventually you'll end up with multiple versions of a file. Perhaps you forgot to keep your .vimrc in sync between two machines [Hack #10] . Alternatively, you may want to see the changes between an old configuration file and the new version. You may even want to distribute a bugfix to a manpage or program.

Sending the entire changed file won't always work: it takes up too much space and it's hard to find exactly what changed. It's often easier and usually faster to see only the changes (see [Hack #80] for a practical example). That's where diff comes in: it shows the differences between two files.

As you'd expect, applying changes manually is tedious. Enter patch, which applies the changes from a diff file.

9.5.1 Finding Differences

Suppose you've shared a useful script with a friend and both of you have added new features. Instead of printing out both copies and marking differences by hand or, worse, trying to reconcile things by copying and pasting from one program to another, use diff to see only the differences between the two programs.

For example, I've customized an earlier version of the copydotfiles.pl script from [Hack #9] to run on Linux instead of FreeBSD. When it came time to unify the programs, I wanted to see the changes as a whole. diff requires two arguments, the source file and the destination. Here's the cryptic (at first) result:

$ diff -u copydotfiles.pl copydotfiles_linux.pl --- copydotfiles.pl        2004-02-23 16:09:49.000000000 -0800 +++ copydotfiles_linux.pl        2004-02-23 16:09:32.000000000 -0800 @@ -5,8 +5,8 @@  #    - change ownership of those files  # You may wish to change these two constants for your system: -use constant HOMEDIR => '/usr/home'; -use constant SKELDIR => '/usr/share/skel'; +use constant HOMEDIR => '/home'; +use constant SKELDIR => '/etc/skel';  use strict; @@ -19,8 +19,8 @@  {      for my $dotfile (@ARGV)      { -        my $source = catfile( SKELDIR( ),        'dot' . $dotfile ); -        my $dest   = catfile( $user->{homedir},         $dotfile ); +        my $source = catfile( SKELDIR( ),        $dotfile ); +        my $dest   = catfile( $user->{homedir}, $dotfile );          if (-e $dest)          {

This output reveals only three changes. Linux and FreeBSD keep user home directories and skeleton configuration files in different directories. Fortunately, this only involved changing two constants at the top of the file.

The -u flag produces unified output, mingling the source and destination lines. It's not the default, but it's the easiest to read and to explain. Count yourself lucky if you never run across the alternatives.

As you may have guessed from the name, only the differences appear. Each of the two files has a separate marker at the leftmost column. Let's look at that header again:

--- copydotfiles.pl            2004-02-23 16:09:49.000000000 -0800 +++ copydotfiles_linux.pl      2004-02-23 16:09:32.000000000 -0800

The first line marks the source file, the FreeBSD version. We're marking changes against that file. diff will mark lines that have changed from that file with a leading minus (-) character. The second line marks the destination file, the Linux version. Lines that have changed in this file appear with a leading plus (+) character.

diff produces output that you can apply to the first file to produce the second file. That is, you should remove (or subtract) all of the lines with the leading minus character and add all of the lines with the leading plus character to produce the destination file.

The rest of the output consists of hunks. Each hunk also has a header:

@@ -5,8 +5,8 @@

This indicates that the hunk starts on line 5 of the source file and affects eight lines. It also starts on the fifth line of the destination file and affects eight lines a simple substitution. In general, you can ignore this unless you're working on something really detailed.

The actual lines of the file are more important. Pay close attention to the leading characters.

#    - change ownership of those files # You may wish to change these two constants for your system: -use constant HOMEDIR => '/usr/home'; -use constant SKELDIR => '/usr/share/skel'; +use constant HOMEDIR => '/home'; +use constant SKELDIR => '/etc/skel'; use strict;

Again, this is a simple substitution. Since diff only works on lines, it has no way of indicating that only the value of the constants has changed.

9.5.2 Applying Patches

By redirecting this output to a file, I can produce a patch file. Though anyone who can read diff output could apply those changes manually, it's much easier to use the patch program, especially if the file I'm patching has had other changes in the meantime. As long as those changes do not overlap, patch will work magically well.

Suppose I'd written:

$ diff -u copydotfiles.pl copydotfiles_linux.pl > dotfiles.patch

Now anyone who wants to apply the changes from the latter file to the former file can apply the patch. Copy the dotfiles.patch file into the same directory as copydotfiles.pl and use the command:

$ patch < dotfiles.patch patching file copydotfiles.pl

If you're lucky, the patch will apply with little fanfare. If you're unlucky, things may have moved around in your file since the creation of the patch. In that case, patch may warn about some fuzz. If I rearrange a couple of lines in the first hunk that aren't actually changed in the patch, I might see a message such as:

$ patch < dot.patch patching file copydotfiles.pl Hunk #1 succeeded at 7 with fuzz 2 (offset 2 lines).

If I were really unlucky, I'd have had changes in the lines the patch also changed. patch tries as hard as it can to massage patches, but sometimes it just can't resolve things. You'll see output like this in those cases:

$ patch < dot.patch patching file copydotfiles.pl Hunk #1 succeeded at 7 with fuzz 2 (offset 2 lines). Hunk #2 FAILED at 21. 1 out of 2 hunks FAILED -- saving rejects to file copydotfiles.pl.rej

In this case, it's up to you, the user, to resolve any changes. patch has actually created two new files, copydotfiles.pl.orig and copydotfiles.pl.rej. The first contains the file before any patching attempt; the second contains the hunks patch could not apply.

Fortunately, the original file does contain the hunks that could apply without conflicts. In this case, it's easier to open the copydotfiles.pl.rej file to apply the changes manually.

*************** *** 21,28 ****   {       for my $dotfile (@ARGV)       { -         my $source = catfile( SKELDIR( ),        'dot' . $dotfile ); -         my $dest   = catfile( $user->{homedir},           $dotfile );           if (-e $dest)           { --- 21,28 ----   {       for my $dotfile (@ARGV)       { +         my $source = catfile( SKELDIR( ),        $dotfile ); +         my $dest   = catfile( $user->{homedir},   $dotfile );           if (-e $dest)           {

This format is a little harder to read than the unified format, but it's reasonably straightforward. The top half comes from the source file in the patch and represents lines 21 through 28 of the original file. Again, the leading minus character represents lines to remove. The bottom half comes from the destination file in the patch, also lines 21 through 28. This contains two lines to add.

Looking in copydotfiles.pl around those lines, it turns out that the first line containing SKELDIR( ) has changed subtly, thus causing the conflict:

{     for my $dotfile (@ARGV)     {         my $source = catfile( SKELDIR( ),        "dot$dotfile" );         my $dest   = catfile( $user->{homedir},        $dotfile );         if (-e $dest)         {

I have two options: I could edit the file directly, making the modifications as seen in either the source file or the destination file of the patch, or I could ignore this hunk if the local modifications are better than those of the patch.

In this case, the patch is clearly an improvement. Since it's only two lines, I'll just make the changes directly. Otherwise, I could revert the changes in my local file and try to reapply the rejected hunks.

9.5.3 Creating Patches

It's often handy to create patches from normal files, as in the previous example, when sharing code or text with another user. It's also useful to see the differences between configuration files when upgrading an application. Knowing how to read a diff between your version of httpd.conf and httpd.conf.default can save you hours of debugging time.

What if you want to find differences between entire directories, though? Suppose you want to see the changes between two versions of a program. If you can't upgrade to the new version right away but want to see if there's a patch available that you can apply, use diff on the directories themselves. Be sure to pass the recursive flag (-r) if you want to compare files in subdirectories:

$ diff -ur sdl/trunk SDL_Perl-2.1.0 > sdl_trunk.patch

If that's not appropriate and you want to patch only a couple of files at a time, run diff multiple times. Append the output to a combined patch. patch is smart enough to recognize the start of file markers:

$ diff -u sdl/trunk/CHANGELOG SDL_Perl-2.1.0/CHANGELOG >> \     sdl_textfiles.patch $ diff -u sdl/trunk/README SDL_Perl-2.1.0/README >> \     sdl_textfiles.patch $ diff -u sdl/trunk/INSTALL SDL_Perl-2.1.0/INSTALL >> \     sdl_textfiles.patch

Finally, if you need to create a patch for a file that doesn't exist, use the null file flag (-n) with /dev/null as the source:

$ diff -un /dev/null SDL_Perl-2.1.0/LICENSE >> \     sdl_textfiles.patch

This will create the file when someone applies the patch. You could also touch the file in the source directory.

9.5.4 Revision Control

Life's much easier when you're working with revision control. Someday, you may find yourself patching source code or text files in core BSD. Modify the code in your tree, make sure it works, and then use cvs diff -u to generate patches to mail to the appropriate development list.

Subversion, the likely successor to CVS, generates the right kind of patches without the -u flag simply use svn diff. There is a FreeBSD port and a NetBSD package for Subversion. You can also download binary packages and source for most operating systems from http://subversion.tigris.org/.

Once you're used to using patches to keep track of file differences, you may find yourself tempted to keep all important files under version control. Hey, why not?

9.5.5 See Also

  • man diff

  • man patch

  • "CVS homedir," Joey Hess's Linux Journal article on keeping his home directory in CVS (http://www.linuxjournal.com/article.php?sid=5976)

BSD Hacks
BSD Hacks
ISBN: 0596006799
EAN: 2147483647
Year: 2006
Pages: 160
Authors: Lavigne

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net