GradleでSVNのフォルダをZIP圧縮する

# 概要

「SubversionからタグをエクスポートしてZIP圧縮する」という手動で定期的に行ってる作業を自動化するのが目的。
最近のマイブームがGradleだから、勉強ついでに作ってみた。


# やりたいこと

求める機能は次の通り。

* コマンド一つでエクスポートからZIP圧縮まで行う。
* 実行時にツール名とタグ名を指定する。
* ツールのソースが複数リポジトリに分かれてたら、それぞれのリポジトリからエクスポートして１つのZIPにまとめる。
* 対応するツールが増えた場合は設定を増やすだけで対応したい。

# 作ったもの

ToolAというツールのソースをSVNから取得してZIP圧縮するサンプル。
前作った[メモ](http://qiita.com/canpok1/items/2d87808fe1b8379c544d)の発展版みたいな感じ。

## ディレクトリ構成
ディレクトリ構成は次の通り。

```
build.gradle
settings.gradle
ToolA
  └ build.gradle
```

今回はマルチプロジェクトの構成で作成。
ルートプロジェクトにはエクスポートとかZIP圧縮とかの共通処理をまとめて、サブプロジェクトにはリポジトリのURLとZIP名を設定。
ツールが増える場合は、サブプロジェクトのbuild.gradleにツール用のリポジトリとZIP名を設定すればいいはず。

## 各ファイルの内容

各ファイルの中身は次の通り。

```groovy:build.gradle
import org.tmatesoft.svn.core.*
import org.tmatesoft.svn.core.wc.*

buildscript {
    repositories {
        mavenCentral()
    }
    
    dependencies {
        classpath 'org.tmatesoft.svnkit:svnkit:1.7.8'
    }
}


// 全プロジェクト対象の設定
allprojects {
    task cleanWork << {}
    task cleanArchive << {}
    task cleanAll << {}

}

// サブプロジェクト対象の設定
subprojects {
    // 一時フォルダのパス
    def projectDir = project.projectDir.getPath();
    ext.workDir = "${projectDir}/work"
    // 成果物の出力先
    ext.archiveDir = "${rootProject.projectDir.getPath()}/archive"
    // リポジトリへの認証情報
    ext.userName = ""
    ext.password = ""

    // 作業フォルダを削除
    cleanWork << {
        delete(project.workDir)
        println "delete [${project.workDir}]"
    }

    // アーカイブフォルダを削除
    cleanArchive << {
        delete(project.archiveDir)
        println "delete [${project.archiveDir}]"
    }

    // 一時ファイルをすべて削除
    cleanAll {
        dependsOn cleanWork
        dependsOn cleanArchive
    }

    // 成果物を圧縮
    task zip(type: Zip) {
        destinationDir = file(project.archiveDir)
        doLast {
            println "archive [${project.workDir}]"
            println "     => [${archivePath}]"
        }
    }
}


/**
 * タグ名に対応するタスクを生成します。
 * @param caller 呼び出し元プロジェクト
 */
void setup(Project caller) {
    // SVNからタグ名を取得
    def tagNameSet = new HashSet<String>();
    for(String url : caller.repoUrlList) {
        def tagNameList = getDirNameList(url,
                                         caller.userName,
                                         caller.password);
        tagNameSet.addAll(tagNameList);
    }
    // タスクを定義
    for(String tagName : tagNameSet) {
        createTask(caller, tagName );
    }
}


/**
 * タスクを定義します。
 * @param caller 呼び出し元プロジェクト
 * @param tagName タグ名
 */
void createTask(Project caller,
                String tagName ) {

    caller.tasks.create(name:"${tagName}", dependsOn: caller.cleanAll) << {
        // 各リポジトリのタグからエクスポート
        for(String baseUrl : caller.repoUrlList) {
            def url = "${baseUrl}/${tagName}"
            export(url, caller.workDir, caller.userName, caller.password);
        }

        // zipファイル名を設定
        def archiveName = caller.archiveBaseName + tagName;
        caller.zip.baseName = archiveName
        caller.zip.into(archiveName) {
            from caller.workDir
        }
        caller.zip.execute()
    }
}


/**
 * リポジトリの指定パスに存在するディレクトリ名のリストを取得します。
 * @param repoUrl リポジトリのURL
 * @param userName ユーザー名
 * @param password パスワード
 * @return ディレクトリ名のリスト
 */
List<String> getDirNameList(String repoUrl,
                            String userName,
                            String password ) {
    // SVN操作クラス
    def manager = SVNClientManager.newInstance(
                        SVNWCUtil.createDefaultOptions(true),
                        userName,
                        password)
    def client = manager.getLogClient();

    def dirList = new ArrayList<String>();

    def dirHandler = new ISVNDirEntryHandler() {
        // 取得ディレクトリ名をリストに追加していく無名クラス
        public void handleDirEntry(SVNDirEntry dirEntry) {
            if(!dirEntry.getName().equals("")) {
                dirList.add(dirEntry.getName());
            }
        }
    };

    client.doList(
        SVNURL.parseURIDecoded(repoUrl),
        SVNRevision.HEAD,
        SVNRevision.HEAD,
        false,
        dirHandler );

    return dirList;
}


/**
 * リポジトリからエクスポートします。
 * @param repoUrl リポジトリのURL
 * @param destDir エクスポート先
 * @param userName ユーザー名
 * @param password パスワード
 */
void export(String repoUrl,
            String destDir,
            String userName,
            String password) {

    println "export [${repoUrl}]"
    println "    => [${destDir}]"

    // SVNのエクスポートコマンド
    def command = ["svn", "export", repoUrl, destDir,
                   "--native-eol", "CRLF",
                   "--force" ]
    if(!userName.equals("")) {
        command.addAll(["--username", userName]);
    }
    if(!password.equals("")) {
        command.addAll(["--password", password]);
    }
    println "export completed [${execute(command)}]"
}


/**
 * コマンドを実行します。
 * @param command コマンド
 * @return コマンドの実行結果
 */
int execute(List<String> command) {
    println "command = ${command}"
    ProcessBuilder builder = new ProcessBuilder(command);
    builder.redirectErrorStream(true);
    Process process = builder.start();

    InputStream is = process.getInputStream();
    try {
        while(is.read() >= 0);
    } finally {
        is.close();
    }

    return process.exitValue();
}
```

```groovy:settings.gradle
include 'ToolA'
```

```groovy:ToolA/build.gradle
buildscript {
    // エクスポート元リポジトリ(tagsまで)
    ext.repoUrlList = ["svn://url/tags"]
    // アーカイブのベース名(後ろにタグ名が付加されます)
    ext.archiveBaseName = "ToolA_SRC_"
    // タグ名に対応するタスクを生成
    setup(project)
}
```


## 使い方

ToolAフォルダのbuild.gradleの場所に移動して次のコマンドを実行。

```
gradle タグ名
```

もしくはルートプロジェクトのbuild.gradleの場所で次のコマンドを実行。

```
gradle ToolA:タグ名
```

するとarchiveフォルダ内に「ToolA_SRC_タグ名.zip」が生成される。


## 注意点

エクスポートするとこ、SVNKitではなくSVNコマンドを使ってる。
なんでこんなことしてるかというと、SVNKitの改行コード指定のエクスポートがうまくいかなかったから。
どうなったかというと、指定の改行コードになってくれなかった･･･。
なので、苦肉の策としてSVNコマンドを直接呼び出してる。
だから、SVNコマンドが入ってない環境だとうまくいかないはず。


# まとめ

Gradleを使ってSVNからエクスポートしてZIP圧縮することができた。
ツールが追加になっても設定を少し追加するくらいで対応できるはず。

でも、SVNKitでのエクスポートで改行コード指定がうまくいかないのが気になるところ。
SVNKit使うコード消しちゃったから、時間ある時に検証でもしてみようか。