svnkitを使ってSubversionを操作する

例えばツールで、何かをバージョン管理したい時はバックエンドにSVNを使っていたりするのですが、Davによる自動コミットがONになっていない環境だとファイルの更新が面倒です。ファイルの追加はSVNコマンドでIMPORTを使えば可能ですが、既に存在するファイルの更新は事前にローカルのワークファイルが存在しないとコミットできません。

というわけで、既に登録されているファイルを、チェックアウトしなくても更新できるプログラムを書いてみました。
本当はきちんとクラス化してあるのですが、長くなるので主要な部分を切り貼りしていきます。

// SVNKitの初期化
DAVRepositoryFactory.setup();
// 引数にはSVNのURLを指定します
// 認証とかの設定をしてレポジトリオブジェクトを返します
private SVNRepository getRepos(String baseUrl) throws SVNException {
	SVNURL url = SVNURL.parseURIDecoded(baseUrl);
	SVNRepository repos = SVNRepositoryFactory.create(url);
	if (userName != null) {
		ISVNAuthenticationManager authManager =
			SVNWCUtil.createDefaultAuthenticationManager(
					this.userName,
					this.password == null ? "" : this.password
			);
		repos.setAuthenticationManager(authManager);
	}
	return repos;
}

まずはファイルをSVNに追加するコードです。

/**
 * ファイルをSVNに追加する。
 * 実際に追加される場所は、baseUrl + filePath
 * @param baseUrl SVNのベースのURL
 * @param filePath ファイル名
 * @param fileData 追加するデータの内容
 * @param logMessage コミットログの内容
 * @return コミットログ
 */
public SVNCommitInfo addFile(String baseUrl, String filePath, byte[] fileData, String logMessage) {

	SVNCommitInfo cInfo = null;
	try {

		SVNRepository repos = getRepos(baseUrl);

		ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
		try {
			editor.openRoot(-1);
			editor.addFile(filePath, null, -1);
			editor.applyTextDelta(filePath, null);
			SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
			String checksum = deltaGenerator.sendDelta(filePath, new ByteArrayInputStream(fileData), editor, true);
			editor.closeFile(filePath, checksum);
			editor.closeDir();

			cInfo = editor.closeEdit();
		} catch (SVNException e) {
			editor.abortEdit();
			throw e;
		}

	} catch (SVNException e) {
		throw new RuntimeException(e);
	}

	return cInfo;
}

次はSVNのファイルの内容を更新するコードです。 上の登録のコードもそうですが、基本は差分(DELTA)を計算してそれを送信しているだけです。

/**
 * SVNのファイルの内容を更新する(変更する)。
 * 実際に変更されるファイルの場所は、baseUrl + filePath
 * @param baseUrl SVNのベースのURL
 * @param filePath ファイル名
 * @param fileData 変更後のデータの内容
 * @param logMessage コミットログの内容
 * @return コミットログ
 */
public SVNCommitInfo updateFile(String baseUrl, String filePath, byte[] fileData, String logMessage) {

	SVNCommitInfo cInfo = null;
	try {

		SVNRepository repos = getRepos(baseUrl);

		// 古いコンテンツを取得する
		ByteArrayOutputStream oldOut = new ByteArrayOutputStream();
		repos.getFile(filePath, -1, SVNProperties.wrap(Collections.EMPTY_MAP), oldOut);
		byte[] oldData = oldOut.toByteArray();

		ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
		try {
			editor.openRoot(-1);
			editor.openFile(filePath, -1);
			editor.applyTextDelta(filePath, null);

			SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
			String checksum = deltaGenerator.sendDelta(
					filePath,
					new ByteArrayInputStream(oldData),
					0,
					new ByteArrayInputStream(fileData),
					editor,
					true
					);
			editor.closeFile(filePath, checksum);
			editor.closeDir();
			cInfo = editor.closeEdit();
		} catch (SVNException e) {
			editor.abortEdit();
			throw e;
		}

	} catch (SVNException e) {
		throw new RuntimeException(e);
	}

	return cInfo;

}

複数ファイルを変更する場合、上記のメソッドを複数回呼び出すと別々のコミットになってしまいます。 まぁeditor.closeEdit()を複数回呼んでいるので、当然と言えば当然ですが。

というわけで、複数のファイルを1回で追加・更新するコードを書いてみます。
その前にファイルが存在するかを確認するメソッドを追加します。

public boolean exists(String baseUrl, String filePath) {
	try {

		SVNRepository repos = getRepos(baseUrl);
		SVNNodeKind nodeKind = repos.checkPath(filePath, -1);
		if (nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.DIR) {
			return false;
		}

		return true;

	} catch (SVNException e) {
		throw new RuntimeException(e);
	}
}

複数のファイルを良い感じに更新するメソッドです。
BLOGにコードを書き写した時に手動で一部のメソッドを書き換えているのでコピペしても動かないかもしれません。

事前にexists等を求めているのはlockメソッドでエラーが出るからです。SVNKitのバグかどうかは知りません。

public static class SvnRequestEntry {
	public String filePath;
	public byte[] fileData;
	boolean exists;
	byte[] oldData;
}

/**
 * 複数のファイルをSVNに追加する。既にファイルが存在する場合は更新される。
 * また、コミットは1回で行われる。
 * @param baseUrl SVNのベースURL
 * @param entries 追加するデータのリスト
 * @param logMessage コミットログの内容
 * @return コミットログ
 */
public SVNCommitInfo putFiles(String baseUrl, List<SvnRequestEntry> entries, String logMessage) {

	SVNCommitInfo cInfo = null;
	try {

		SVNRepository repos = getRepos(baseUrl);

		// 事前にチェックを行う
		for (SvnRequestEntry entry: entries) {

			SVNNodeKind nodeKind = repos.checkPath(entry.filePath, -1);
			entry.exists = !(nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.DIR);

			// 古いコンテンツを取得する
			if (entry.exists) {
				ByteArrayOutputStream oldOut = new ByteArrayOutputStream();
				repos.getFile(entry.filePath, -1, null, oldOut);
				entry.oldData = oldOut.toByteArray();
			}

		}

		ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
		try {

			editor.openRoot(-1);
			for (SvnRequestEntry entry: entries) {

				String filePath = entry.filePath;
				byte[] fileData = entry.fileData;
				boolean exists = entry.exists;
				byte[] oldData = entry.oldData;

				// 存在チェック
				if (!exists) {

					// 追加する
					editor.addFile(filePath, null, -1);
					editor.applyTextDelta(filePath, null);
					SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
					String checksum = deltaGenerator.sendDelta(filePath, new ByteArrayInputStream(fileData), editor, true);
					editor.textDeltaEnd(filePath);
					editor.closeFile(filePath, checksum);

				} else {

					// 更新する
					editor.openFile(filePath, -1);
					editor.applyTextDelta(filePath, null);

					SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
					String checksum = deltaGenerator.sendDelta(
							filePath,
							new ByteArrayInputStream(oldData),
							0,
							new ByteArrayInputStream(fileData),
							editor,
							true
							);
					editor.textDeltaEnd(filePath);
					editor.closeFile(filePath, checksum);

				}
			}
			editor.closeDir();
			cInfo = editor.closeEdit();

		} catch (SVNException e) {
			editor.abortEdit();
			throw e;
		}

	} catch (SVNException e) {
		throw new RuntimeException(e);
	}

	return cInfo;

}

使い方はこんな感じです。他のメソッドも同じ感じです。

SVNCommitInfo info1 = addFile(
	baseUrl,
	"test01.txt",
	"test01_init_data".getBytes(),
	"プログラムからからファイルを追加してみる\nテスト。"
	);

putFilesを呼び出すときは、SvnRequestEntryのfilePathとfileDataに値を入れて、 更新したいファイルの数だけList化すれば良いです。

新しいサイトもよろしくお願いします!