Subversionにcommitしてあるライブラリを更新する

最近は、どんなソフトウェアのプロジェクトであっても、そのプロジェクトから他のオープンソースのライブラリを参照しているなんてことは日常茶飯事だと思います。
たとえば、ベクターグラフィックスのライブラリならば、freetypeなんかは間違いなく利用しているでしょうし、zlibなんかや、libjpegなんて、使っていないプログラムの方が少ないと思います。

そうなってくると、バージョンや整合性の管理の都合から、必然的に、特定のライブラリの特定のバージョンのソースコードを自分のリポジトリにコピーしていると思われるのですが、たとえば、libhogeのバージョン1.XXをすでにリポジトリで管理しているんだけど、これの新しいバージョン、1.YYが出たので、それにアップグレードしたいなんて要求は普通に出てきます。

ところが、このアップグレード作業は意外と面倒くさい。アップグレードして本当に動くかどうかはさておき、何のファイルが追加されて、何のファイルが削除されて、そして何のファイルは更新されているという情報を整理するのは意外と面倒なものです。

まぁ、手元にチェックアウトされたローカルコピーがあるのなら、試しに新しいバージョンで上書きしてみるのが手っ取り早いのですが、この方法だと、修正されたファイル、新たに追加されたファイルはすぐにわかりますが、新しいバージョンで削除されているファイルというのはわかりにくかったりします。

手作業でどうにかこうにか

で、いろいろと考えたあげく、.svnという名前のフォルダ群だけを、ライブラリ1.YYのフォルダにディレクトリ構造を維持したままコピーしてみるという方法を思いつきました。
これは、手でやるには非常に面倒(find ... | xargs ...みたいな感じだけど、Windowsだけだともう泣きそうなぐらい面倒くさい)なのですが、これをやると、svn status(あるいは、Tortoise SVNのCheck for modifications)でファイルの変更・追加・削除が一目瞭然でわかります。

具体的にいうと、変更されたファイルは、Mでマークされており、追加されたファイル(つまり、まだこちらのリポジトリにないファイル)は、?、そして、削除されたファイル(つまり、こちらのリポジトリにのみ存在するファイル)は、!と表示されます。

なので、結局、?のファイルをAddして、!のファイルをRemoveすれば、一応、全体の整合性はとれることになります。だからといって、ここでcommitしちゃだめですけどね。

と、考えてみると、これは確実に自動化できる処理でした。なので、これらの作業を自動化するコードをJavaScript(WSH)で書いてみました。

svncdc.js

たとえば、libcurl-7.10.8がすでに自分のリポジトリhttp://svn.example.com/svn/libcurl/trunk に存在するとしましょう。そして、最新版である、curl-7.19.4.zipを公式サイトから拾ってきたとします。とりあえず、最新版をローカルディレクトリのcurl-7.19.4に展開したとして、

cscript svncdc.js curl-7.19.4 http://svn.example.com/svn/libcurl/trunk

というコマンドを実行すると、このコマンドは、curl-7.19.4というディレクトリをhttp://svn.example.com/svn/libcurl/trunkのローカルコピーに変更が加えられたものに仕立て上げてくれます。

横にtempというディレクトリが置き去りにされますが、バグです。とりあえず、今のところは自分で削除してください。

//------
// Subversion Control Directory Copier
// (C) 2009 Takashi Kawasaki <espresso3389@gmail.com>
//------

var fso, wsh, env, args;
var svncmd = "svn --non-interactive";
var workdir = "temp\\";

try
{
  // initialize global objects
  initializeEnvironment();

  if(args.length != 2)
  {
    showHelp();
    exit(9999);
  }
  
  
  write("Checking out " + args[1]);
  var codir = checkout(args[1]);

  write("Merging...");
  copyDirTree(codir, args[0], function(fn) {
    if(fn.endsWith("/") || fn.indexOf(".svn/") >= 0)
      return true;
    return false;
  });
  WScript.StdOut.Write("\n");
  
  write("Getting status...");
  var list = getFileStatus(args[0]);
  WScript.StdOut.Write("\n");
  
  // organize tasks
  var toRemove = [], toAdd = [];
  for(var file in list)
  {
    var stat = list[file];
    if(stat === "!") toRemove.push(file);
    else if(stat === "?") toAdd.push(file);
  }

  write("Removing files...");
  svnBatch("remove", toRemove);
  
  write("Adding files...");
  svnBatch("add --parents", toAdd);
  
  // remove temporary files...
  try{fso.DeleteFolder(codir);} catch(e) {}
  
  exit(0);
}
catch(e)
{
  showException(e);
  exit(1);
}

//------
function copyDirTree(src, dst, filter)
{
  return copyDirTree_(src, dst, dst, filter)
}

//------
function copyDirTree_(src, dst, root, filter, list)
{
  try
  {
    var f = fso.GetFolder(src);
    var e = new Enumerator(f.files);
    for(; !e.atEnd(); e.moveNext())
    {
      var file = e.item();
      var dstname = dst + "/" + file.Name;
      var relname = dstname.substring(root.length + 1);

      if(filter != undefined && !filter(relname))
        continue;

      WScript.StdOut.Write(".");
      file.Copy(dstname, true);

      if(list != undefined)
        list.push(relname);
    }
    e = new Enumerator(f.subfolders);
    for(; !e.atEnd(); e.moveNext())
    {
      var folder = e.item();
      var dstdir = dst + "/" + folder.Name;
      var relname = dstdir.substring(root.length + 1);

      if(filter != undefined && !filter(relname + "/"))
        continue;

      try
      {
        if(!fso.FolderExists(dstdir))
          fso.CreateFolder(dstdir);
        copyDirTree_(src + "/" + folder.Name, dstdir, root, filter, list);
      }
      catch(e)
      {
        write("Creating " + dstdir + " failed: " + e.description);
      }
    }
  }
  catch(e)
  {
    write("Copying " + src + " failed: " + e.description);
  }
}

//------
function createFolder(folder)
{
  try
  {
    var parent = fso.GetParentFolderName(folder);
    if(parent !== "")
      createFolder(parent);
  
    if(!fso.FolderExists(folder))
      fso.CreateFolder(folder);
  }
  catch(e)
  {
  }
}

//------
function checkout(url, outdir, opts)
{
  if(opts == undefined)
    opts = "";
  else
    opts = " " + opts;
  
  if(outdir === undefined)
  {
    var projname = getProjNameFromUrl(url);
    outdir = workdir + projname;
  }
  
  var cmd = "co " + url;
  execute(svncmd + " " + cmd + opts + " \"" + outdir + "\"");
  
  return outdir;
}

//------
function getFileStatus(dir)
{
  var proc = wsh.Exec(svncmd + " status -u --depth infinity " + dir);
  var svnFiles = proc.StdOut.ReadAll().splitIntoLines();
  var stats = {};
  for(var i = 0; i < svnFiles.length; i++)
  {
    var line = svnFiles[i];
    var stat = line.substr(0, 1);
    var fileName = line.substr(20);
    stats[fileName] = stat;
    WScript.StdOut.Write(".");
  }
  return stats;
}

//------
function svnBatch(cmd, files)
{
  var hdr = svncmd + " " + cmd;
  var line = hdr;
  for(var i = 0; i < files.length; i++)
  {
    var f = files[i];
    if(f.length + line.len > 250)
    {
      execute(line);
      line = hdr;
    }
    line += " " + f;
  }
  if(line !== hdr)
    execute(line);
}

//------
function getProjSuffixIndex(url)
{
  var list = ["/trunk", "/branches", "/tags", "@"];
  for(var i = 0; i < list.length; i++)
  {
    var pos = url.lastIndexOf(list[i]);
    if(pos > 0)
      return pos;
  }
  return -1;
}

//------
function removeSuffixFromUrl(url)
{
  var pos = getProjSuffixIndex(url);
  return pos >= 0 ? url.substr(0, pos) : url;
}

//------
function getProjNameFromUrl(url)
{
  url = removeSuffixFromUrl(url);
  var pos = url.lastIndexOf("/");
  if(pos < 0)
    return url;
  return url.substr(pos + 1);
}

//------
function execute(cmd, chr, nofin)
{
  if(chr == undefined) chr = ".";
  var proc = wsh.Exec(cmd);
  var line = "";
  while(!proc.StdOut.AtEndOfStream)
  {
    line = proc.StdOut.ReadLine();
    WScript.StdOut.Write(chr);
  }
  if(!nofin)
    WScript.StdOut.Write("\n");
  while(!proc.Status) WScript.Sleep(300);
  if(proc.ExitCode !== 0)
    throw "Command failed (" + proc.ExitCode + ") : " + cmd;
  
  return line;
}

//------
function showHelp()
{
  write(
    "Subversion Control Directory Copier\n" +
    "(C) 2009 Takashi Kawasaki <espresso3389@gmail.com>\n" +
    "\n" +
    "Usage: " + getExeName() + " LOCAL_ARCHIVE_DIR REPOSITORY_PATH\n" +
    "\n");
}

//------
function showException(e)
{
  if(typeof e == 'object') // FIXME
    e = e.description;
  write(e);
}

//------
function write(line)
{
  WScript.Echo(line);
}

//------
function exit(ret)
{
  WScript.Quit(ret);
}

//------
function getExeName()
{
  return fso.GetFileName(WScript.ScriptFullName);
}

//------
function initializeEnvironment()
{
  // define several string helpers
  String.prototype.endsWith = function(suffix) {
    if(suffix.length > this.length) return false;
    return suffix === this.substring(this.length - suffix.length);
  }
  String.prototype.splitIntoLines = function() {
    return this.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
  }
  
  // arguments
  args = [];
  for(var i = 0; i < WScript.Arguments.length; i++)
    args.push(WScript.Arguments(i));

  // determine runtime environment
  try
  {
    // assume, Windows here
    fso = WScript.CreateObject("Scripting.FileSystemObject");
    wsh = WScript.CreateObject("WScript.Shell");
    env = wsh.Environment("PROCESS");
    env("LANG") = "C"; // LANG=C
  }
  catch(e)
  {
    // wow, this is not Windows
  }
}