VSS管理されているソース取得を自動化(JScript)

コマンドラインから、SS.exeを利用して

SS Get /$ -R -GWS

とかすると、一括でサブプロジェクトまで取得できるのだが、
新しくフォルダ(プロジェクト)が作成されていたりするとコケる。
NAntを使って取得してもいいのだけど、vssgetタスクはあまり情報を返してくれない。
どのファイルが更新されたのか とか チェックアウトせずに
一時的にローカルで変更しているファイル とか は知りたい。


しょうがないのでJScriptで処理を書いてみた。
意外と面倒。

/**
 * SS.EXE のGetコマンドを発行する。
 * 存在しないローカルディレクトリは作成する。
 */
var SS_PATH   = "C:/Progra~1/VSS2005/ss.exe";   // ss.exeのパス
var SS_DIR    = "";                             // sourcesafe.iniのあるディレクトリ
var SS_USER   = "";                             // VSSユーザ名
var SS_PWD    = "";                             // VSSパスワード
var SS_ROOT   = "";                             // 処理を開始するルート
var SS_WORK   = "";                             // ソースを取得するローカルディレクトリのルート
var BIN_ONLY  = false;                          // 実行ファイルのみ取得する場合はtrue

// ss.exeで連続して操作系のコマンドを発行すると挙動不審になる。
// コマンド発行後に待機する時間をここで設定する。
var SS_INT    = 300; // (msec)
var _axo = new ActiveXObject("WScript.Shell");

WScript.Quit(main());

function main(){
  // 引数の処理
  if (WScript.Arguments.Named.Exists("SSPATH")) {
    // /SSPATH:[VALUE]
    SS_PATH = WScript.Arguments.Named.Item("SSPATH");
  }
  if (WScript.Arguments.Named.Exists("SSDIR")) {
    // /SSDIR:[VALUE]
    SS_DIR = WScript.Arguments.Named.Item("SSDIR");
  }
  if (WScript.Arguments.Named.Exists("SSUSER")) {
    // /SSUSER:[VALUE]
    SS_USER = WScript.Arguments.Named.Item("SSUSER");
  }
  if (WScript.Arguments.Named.Exists("SSPWD")) {
    // /SSPWD:[VALUE]
    SS_PWD = WScript.Arguments.Named.Item("SSPWD");
  }
  if (WScript.Arguments.Named.Exists("SSROOT")) {
    // /SSROOT:[VALUE]
    SS_ROOT = WScript.Arguments.Named.Item("SSROOT");
  }
  if (WScript.Arguments.Named.Exists("SSWORK")) {
    // /SSWORK:[VALUE]
    SS_WORK = WScript.Arguments.Named.Item("SSWORK");
  }
  if (WScript.Arguments.Named.Exists("BIN")) {
    // /BIN
    BIN_ONLY = true;
  }

  // 環境変数の設定
  var env = _axo.Environment("Process");
  env.Item("ssdir")    = SS_DIR;
  env.Item("ssuser")   = SS_USER;
  env.Item("sspwd")    = SS_PWD;

  // 日付・オプションの表示
  var t = new Date();
  WScript.Echo(t.toLocaleString());
  WScript.Echo("");
  WScript.Echo("SSDIR  = " + SS_DIR);
  WScript.Echo("SSUSER = " + SS_USER);
  WScript.Echo("ROOT   = " + SS_ROOT);
  WScript.Echo("WORK   = " + SS_WORK);
  if (BIN_ONLY == true) {
    WScript.Echo("BINARY ONLY");
  }
  WScript.Echo("");

  // 使用している Visual SourceSafe についての情報を表示
  ss_About();

  // ルート以下全てのファイルを作業フォルダに取得
  WScript.Echo("SS Get");
  ss_GetAll(SS_ROOT, SS_WORK);
  WScript.Echo("");

  // チェックアウトしているファイルを確認
  WScript.Echo("SS Status");
  ss_StatusAll(SS_ROOT, SS_USER);
  WScript.Echo("");

  // 終了時刻の表示
  t = new Date();
  WScript.Echo(t.toLocaleString());

  env = null;
  t = null;
}
/******************************************************************************/

/**
 * 親フォルダが無い場合でも、再帰的に親フォルダを作成していきます。
 * (パクリました)
 * @param Path 作成するフォルダのフルパス
 * @see http://winscript.s41.xrea.com/wiki/index.php?%5B%5B%A5%C6%A5%AF%A5%CB%A5%C3%A5%AF%5D%5D#content_1_16
 */
function CreateFolderEx(Path) {
  var oFSO = WScript.CreateObject('Scripting.FileSystemObject');
  var sParent = oFSO.GetParentFolderName(Path);
  if (oFSO.FolderExists(sParent) && !oFSO.FolderExists(Path)){
    WScript.Echo(Path + " が作成されました。");
    oFSO.CreateFolder(Path);
  }
  else if (oFSO.FolderExists(Path)) {
  }
  else{
    CreateFolderEx(sParent);
    oFSO.CreateFolder(Path);
  }
  
  oFSO = null;
  sParent = null;
}

/**
 * テキストストリームから1行ずつ読み込んでエコー
 * @param stream TextStreamオブジェクト
 */
function EchoTextStream(stream) {
  while (!stream.AtEndOfStream) {
    WScript.Echo(stream.ReadLine());
  }
}

/*
 * SS.EXE コマンド ラインの終了コード
 * http://msdn.microsoft.com/ja-jp/library/39czz3he(VS.80).aspx
 * 100 : 問題が発生したことを示しています。
 *       たとえば、Visual SourceSafe がデータ ファイルを見つけることができない、
 *       チェックアウトしようとしたファイルが既にチェックアウトされている、などの問題があります。
 *   1 : 現在のコマンドが正常に実行されたけれども、次のコマンドで軽度の障害が発生する可能性があることを示しています。
 *       この終了コードは、次の状況のいずれかで発生します。
 *       ・Dir コマンドを実行したときに、項目が見つからない場合。
 *       ・Status コマンドを実行したときに、少なくとも 1 つの項目がチェックアウトされている場合。
 *       ・Diff コマンドを実行したときに、少なくとも 1 つのファイルに相違点が含まれていることがわかった場合。
 *   0 : Visual SourceSafe は正常に実行されました。
 */

/**
 * 使用している Visual SourceSafe についての情報を表示します。
 * @see http://msdn.microsoft.com/ja-jp/library/y5e47585(VS.80).aspx
 */
function ss_About() {
  var result = _axo.exec(SS_PATH + " About");
  WScript.sleep(SS_INT);
  EchoTextStream(result.StdOut);

  if (result.Status != 0) {
    EchoTextStream(result.StdErr);
  }

  return result.Status;
}

/**
 * 指定したユーザーがチェックアウトしている、システム内のすべてのファイルを表示します。
 * ユーザーを指定しない場合は現在のユーザーについて表示します。
 * @param root 検索の起点となるプロジェクト
 * @param user VSSユーザー
 * @see http://msdn.microsoft.com/ja-jp/library/d6kac9fd(VS.80).aspx
 */
function ss_StatusAll(root, user){
  var result = _axo.exec(SS_PATH + " Status " + root + " -R -U" + user);
  WScript.sleep(SS_INT);
  EchoTextStream(result.StdOut);

  if (result.Status != 0) {
    EchoTextStream(result.StdErr);
  }

  return result.Status;
}

/**
 * 指定したプロジェクトにあるすべてのサブプロジェクトを一覧表示します。
 * ※全てのサブプロジェクトを再帰的に表示します。ファイルは表示しません。
 * @param project 一覧の起点となるプロジェクト
 * @see http://msdn.microsoft.com/ja-jp/library/z05zat9e(VS.80).aspx
 */
function ss_DirPrj(project) {
  var result = _axo.exec(SS_PATH + " Dir \"" + project + "\" -R -F-");

  if (result.Status != 0) {
    EchoTextStream(result.StdErr);
  }

  return result.StdOut;
}

/**
 * 指定したプロジェクト以下全てのファイルについて読み取り専用コピーを取得して、作業フォルダに配置します。
 * 作業フォルダは、指定したフォルダ以下に自動的に作成されます。
 * ※書込み可能ファイルはスキップします。ファイルの日付はチェックイン日時となります。
 * @param project 取得の起点となるプロジェクト
 * @param folder 取得の起点となる作業フォルダ
 * @see http://msdn.microsoft.com/ja-jp/library/661w6e3d(VS.80).aspx
 */
function ss_GetAll(root, work) {
  // 指定されたルート以下のプロジェクト情報を取得して
  // 全てのプロジェクトを取得する。
  // SS Get -R で再帰的に取得すると、ローカルディレクトリがない時に落ちるので
  // フォルダの存在を確認してなければ作成しながら処理を進める。
  var ssdir = ss_DirPrj(root);
  var line, project, folder;
  while (!ssdir.AtEndOfStream) {
    line = ssdir.ReadLine();
    if (line.length > 0 && line.lastIndexOf(":") == line.length - 1){
      // 文字数が0より大きく、セミコロンで終了している行はプロジェクトを示す
      project = line.slice(0, line.length - 1);
      folder = work + project.slice(root.length);
      ss_GetPrj(project, folder);
    }
  }
  ssdir.Close();
  ssdir = null;
  line = null;
  project = null;
  folder = null;
}

/**
 * 指定したプロジェクトの読み取り専用コピーを取得して、作業フォルダに配置します。
 * ※書込み可能ファイルはスキップします。ファイルの日付はチェックイン日時となります。
 * @param project 取得するプロジェクト
 * @param folder 作業フォルダ
 * @see http://msdn.microsoft.com/ja-jp/library/661w6e3d(VS.80).aspx
 */
function ss_GetPrj(project, folder) {
  // 指定されたプロジェクト配下の一覧を取得
  var files = _axo.exec(SS_PATH + " Dir \"" + project + "\" -F");

  // プロジェクト配下のファイルについてループしながらGetコマンドを発行する。
  // プロジェクトを丸ごと取得するとファイル個別のステータスが得られないので。。
  while (!files.StdOut.AtEndOfStream) {
    var fname = files.StdOut.ReadLine();
    if (fname.indexOf("$") >= 0) {
      // "$"で始まる行はプロジェクトであるので無視
      continue;
    }
    else if (fname.length == 0) {
      // 空白行は "xx file(s)" の直前の行であるので
      // 空白行を読み込んだら終了
      break;
    }
    else if (BIN_ONLY == true &&
             fname.toUpperCase().indexOf(".DLL") < 0 &&
             fname.toUpperCase().indexOf(".EXE") < 0 &&
             fname.toUpperCase().indexOf(".CONFIG") < 0 &&
             fname.toUpperCase().indexOf(".INI") < 0 &&
             fname.toUpperCase().indexOf(".CRF") < 0 &&
             fname.toUpperCase().indexOf(".MANIFEST") < 0
    ) {
      // BINオプションが指定されている場合は実行可能ファイルのみ取得
      continue;
    }
    else {
      CreateFolderEx(folder); // 作業フォルダが存在しなければ作成
      fname = project + "/" + fname;
      var result = _axo.exec(SS_PATH + " Get \"" + fname + "\" -GTU -GWS -GL\"" + folder + "\"");

      // SS Getの標準出力を全て読み込む
      var response = result.StdOut.ReadAll();

      // ss.exeの出力は80文字(改行文字含む)で区切られるようだ。(StdOutの仕様かもしれない。詳細は調査してない。)
      // 取得するファイル名と正しく比較できるように、SS Getの出力から改行を除去
      response = response.replace(/[\r\n]*/gi, "");

      // SS Getの標準出力は、特に何もしない場合はファイル名が出力される。
      // これを常に出力すると鬱陶しいので、更新等があったときのみ出力。
      // ※*.vspsccはなぜか常に更新される場合がある。これもやかましいので無視。
      if (fname != response &&
          fname.search(new RegExp("\.vspscc", "igm")) < 0) {
        WScript.Echo(fname);    // TODO : デバッグ用 期待しないメッセージが出力されることがあるので調査
        WScript.Echo(response);
      }

      // StdErrは常に出力
      EchoTextStream(result.StdErr);

      result = null;
      count = null;
      request = null;
      response = null;
    }
    fname = null;
  }
  files = null;
}

で、こんな感じで使うようにしてみた。

CScript /NoLogo SSGet.js /SSDIR:"\\VSS_Server" /SSROOT:"$/" /SSWORK:"C:\work" /SSUSER:scott /SSPWD:tiger > SSGET.log
start notepad SSGET.log

一通り取得してログに吐いてからメモ帳で表示する。
これをバッチファイルにしておいてタスクスケジューラに登録してやれば
昼休みに事が終わっているはず。