My Keynote/Subversion Solution
Having moved over to Macland earlier this year, I have been slowly adopting Mac applications. One app that I have become found of is Keynote. However, my one show stopper for moving all my presentations over to Keynote is the fact that it does not work well with Subversion. Specifically, because Keynote stores it’s files as a Mac bundle (a directory of files rather than one file) any .svn directories that exist within a bundle’s directory get blown away when you save a Keynote file.
This problem is not unique to me. I have seem numerous work arounds for this issue, none of which have satisfied me. The most common solution is to create an archive of the directory and check that into Subversion. I don’t like this for a few reasons:
- I prefer to commit many small files rather than one big file.
- Checking in one big binary reduces the chances that Subversion can commit a delta rather than an entire file.
- It just feels wrong!
So, like any good developer… I reinvented the wheel and wrote my own script. My approach was to create two directories - one working directory that I edit with Keynote and a versioned directory that I do not touch but commit to Subversion. My script (written in Groovy) syncs the changes from the working directory to the versioned directory, including Subversion adds (for new files) and Subversion deletes (for obsolete files).
I have only been using this script for a week or so, but I have been pleased. I am not sure that this approach is best, but I do like the fact that I can commit incrementally. If you also like this approach, feel free to use this code. And if you see any improvements (or fixes), please let me know.
workDir = new File(args[0])
svnDir = new File(args[1])
println "Syncing [$workDir.absolutePath] with [$svnDir.absolutePath]."
syncDeletions(workDir, svnDir)
syncAddsAndUpdates(workDir, svnDir)
void syncDeletions(workDir, svnDir)
{
svnDir.eachFile { svnFile ->
// skip Subversion meta directories
if (svnFile.name == ".svn")
return
workFile = new File(workDir, svnFile.name)
if (!workFile.exists())
svn("delete $svnFile.path")
else if (svnFile.isDirectory())
syncDeletions(workFile, svnFile)
}
}
void syncAddsAndUpdates(File workDir, File svnDir)
{
workDir.eachFile { workFile ->
if (!workFile.directory)
addOrUpdateFile(workFile, svnDir)
else {
svnSubDir = new File(svnDir, workFile.name)
if (svnSubDir.exists())
syncAddsAndUpdates(workFile, svnSubDir)
else {
copyDir(workFile, svnDir)
svn("add $svnDir.path")
}
}
}
}
void addOrUpdateFile(File workFile, File svnDir)
{
svnFile = new File(svnDir, workFile.name)
boolean add = !svnFile.exists()
copyFile(workFile, svnFile)
if (add)
svn("add $svnFile.path")
}
void copyDir(File workDir, File svnDir)
{
svnDir.mkdir()
}
void copyFile(File workFile, File svnFile)
{
reader = workFile.newReader()
svnFile.withWriter { writer ->
writer << reader
}
reader.close()
}
void svn(String command)
{
command = "svn $command"
p = "$command".execute()
p.waitFor()
if (p.exitValue())
println "Failed to execute $command successfully."
}