タグ「Subversion」が付けられているもの

なぜpythonかと言うと、rubyは入っていないしperlは5.6でNet::SMTP使えないしsendmail行方不明だし・・・。pythonはcvs2svnで必要だったのでインストールしてもらっていたのでした。PERLも5.10をインストールして貰えばいいんですが、依頼をして待つ時間を考えるとpythonで書いた方が早いだろうと判断したからです。

動けばいいやーレベルで書いたので穴があるかもしれないです。ちなみにバージョンは2.6で確認しました。

●post-commit

#!/bin/bash

REPOSITORY="$1"
REVISION="$2"
MAILER="/data/svn/proj1/hooks/mail.py"

export LANG=ja_JP.UTF-8
export LC_ALL=ja_JP.UTF-8

$MAILER $REPOSITORY $REVISION

●mail.py

#!/usr/local/python/bin/python
# -*- coding: utf-8 -*-

import re
import sys
import os
import subprocess
import smtplib
from email.MIMEText import MIMEText
from email.Header import Header
from email.Utils import formatdate

SMTP_HOST = 'xxx.xxx.xxx.xxx'
SMTP_PORT = 25
SVNLOOK = '/usr/local/svn/bin/svnlook'
subject_prefix = "[svn][proj1]"
from_addr = 'no-reply@example.com'
to_addr = ["user1@example.com", "user2@example.com"]


def send(from_addr, to_addr, msg):
    s = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    s.sendmail(from_addr, to_addr, msg.as_string())
    s.close()

def create_message(from_addr, to_addr, subject, body, encoding):
    msg = MIMEText(body, 'plain', encoding)
    msg['Subject'] = Header(subject, encoding)
    msg['From'] = from_addr
    msg['To'] = ', '.join(to_addr)
    msg['Date'] = formatdate()
    return msg

if __name__ == '__main__':

    REPOS = sys.argv[1]
    REV = sys.argv[2]

    # exec svnlook
    p = subprocess.Popen(["%(SVNLOOK)s info %(REPOS)s -r %(REV)s" % locals()], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    lines1 = p.stdout.read().splitlines()
    AUTHOR = lines1.pop(0)
    DATE = lines1.pop(0)
    lines1.pop(0)
    LOG = "\n".join(lines1)

    # exec dirs-changes
    p = subprocess.Popen(["%(SVNLOOK)s dirs-changed %(REPOS)s -r %(REV)s" % locals()], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    dirchanged = ""
    for dir in sorted(p.stdout.read().splitlines(True)):
        dirchanged += "    " + dir

    # exec changed
    p = subprocess.Popen(["%(SVNLOOK)s changed %(REPOS)s -r %(REV)s" % locals()], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    r = re.compile('^(.).  (.*)$')
    adds = []
    dels = []
    mods = []
    for file in (p.stdout.read().splitlines(True)):
        (tmp, code, path, tmp) = r.split(file)
        if tmp == 'A':
            adds.append("    " + path)
        elif tmp == 'D':
            dels.append("    " + path)
        else:
            mods.append("    " + path)

    body  = "Author: " + AUTHOR + "\n"
    body += "Date: " + DATE + "\n"
    body += "New Revision: " + REV + "\n"
    body += "\n"
    body += "Log:\n"
    body += LOG
    body += "\n\n"
    body += "Direcoties:\n"
    body +=  dirchanged
    body += "\n"
    if len(adds) != 0:
        body += "Added:\n"
        body += "\n".join(sorted(adds))
    if len(dels) != 0:
        body += "Removed:\n"
        body += "\n".join(sorted(dels))
    if len(mods) != 0:
        body += "Modified:\n"
        body += "\n".join(sorted(mods))

    subject = subject_prefix + " r" + REV + " - " + AUTHOR + " -"
    msg = create_message(from_addr, to_addr, subject, body, 'UTF-8')
    send(from_addr, to_addr, msg)

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化すれば良いです。

Subversionの設定メモ

普通にSubversionを設定するだけなら簡単ですが、Rootを持っていないサーバに対して設置して貰う場合は依頼内容を明確にしないといけないのでそのメモ。

○前提条件

  • Apacheと連携してhttp://でアクセスできるようにする
  • 1個の設定で複数のレポジトリを管理できるように
  • Apacheのユーザーはnobodyで動いている
  • SVNのデータ置き場は/data/svn
  • BASIC認証をする
  • 特定のディレクトリproj1/document は自動管理をする。 

○Apacheの設定

<Location /svn>
    DAV svn
    SVNParentPath /data/svn
    SVNListParentPath on

    AuthType Basic
    AuthName "tamtam private repository"
    AuthUserFile /etc/svn-auth-file
    Require valid-user
</Location>
<Location /svn/proj1/document>
    SVNAutoversioning on
</Location>

HTTPSのみ受け付けるようにするには、以下の記述を追加する。

    Require SSL connection for password protection.
    SSLRequireSSL

○レポジトリを作成する

svnadmin create /data/svn/proj1
chmod -R nobody:nobody /data/svn/proj1

○コミットメールの設定

コミットメールはhookディレクトリにpost-commitというファイルを作成します。
検索するとRubyを使ってメールを送信しているものが多いですが、会社のサーバには標準インストールされていないのでPERLで書いてみます。

これは別の記事に書こう。

以下のコマンドでリビジョンを取得できます。

svnversion -n ディレクトリ

指定するディレクトリは.svnディレクトリが存在しなければいけません。
-nオプションをつけると末尾の改行を出力しません。

この機能を使って今回はビルドバージョンを埋め込むようにしました。具体的なコードは面倒なので書きませんが、Antで以下の手順で処理します。

・SVNからソースを取得
・リビジョン番号を取得する
・コンパイルする→DESTディレクトリへ
・必要なファイルをDESTへコピーする
・DESTにあるXMLファイルへリビジョンを埋め込む
・WARファイルを作成する
・サーバへデプロイする

こんな感じかな。正確にはXMLの設定ファイルに「svn.revesion」みたいな定数を定義して、その値を置換しています。(このXMLファイルの設定は実行時にDIで設定保持クラスにInjectしています)

バイナリに埋め込む場合はコンパイルする前に埋め込まないといけないですね。

 

とりあえずは、これで共有環境のアプリがどのバージョンでコンパイルされたものなのか分かるようになりました。

Linuxの設定ファイルをSubversionで管理したいと思います。
しかし、以下のようなケースで困った事が起こったのでその解決策としてのメモを残しておきます。

■ケース
 保存はファイル単位(Subversionはディレクトリ単位)
 普通のSubversionの使い方はimport → co → 変更 → commit
 でもファイル単位なので、例えばtomcatのserver.xmlだけを対象としたい場合に困る。
 サーバ側の設定ファイル置き場に.svnが出来てしまう。
 単純に保存するだけならimportで十分だが、上書きができない。

■理想
 importで上書きしてほしい
 →そんなん無理です。。

■解決策
 WebDAVと自動コミットを使って解決する

そうでした・・・。WebDAVを使えば自動コミットが出来るのでした。
つまりWebDAVクライアントからファイルをPUTすれば勝手にSVNにコピーされます。
というわけで、早速設定してみました。

まずwebdavのクライアントが必要です。Windowsでは標準でしょぼいクライアントがついてきますが、サーバはLinuxなのでLinuxのWebDAVクライアントを探しました。cadaverというのが有名みたいです。Fedora8ではyumからインストールできます。うむ。便利!
次にApache(WebDAV)の設定をします。

SVNAutoversioning on

を追加します。
設定全部をさらすと私の場合はこんな感じです。 (必要に応じて自分の設定に変更してください。)

<Location /svn>
    DAV svn
    SVNParentPath /data/svn
    SVNAutoversioning on

    Require SSL connection for password protection.
    SSLRequireSSL

    AuthType Basic
    AuthName "tamtam private repository"
    AuthUserFile /etc/svn-auth-file
    Require valid-user
</Location>

これで設定は完了です。Apacheの設定をreloadしてLinuxのcadaverからアクセスしてみたいと思います。

cadaver https://tamsvr01/svn/repos
WARNING: Untrusted server certificate presented for `tamsvr01':
Issued to: Admin, tamsvr01, Shinagawa, Tokyo, JP
Issued by: Admin, TripleKiss, Tokyo, JP
Certificate is valid from Sun, 15 Oct 2006 04:20:01 GMT to Mon, 15 Oct 2007 04:20:01 GMT
Do you wish to accept the certificate? (y/n) y
Authentication required for tamtam private repository on server `tamsvr01':
Username: tamtam
Password:
dav:/svn/repos/> help
Available commands:
 ls         cd         pwd        put        get        mget       mput
 edit       less       mkcol      cat        delete     rmcol      copy
 move       lock       unlock     discover   steal      showlocks  version
 checkin    checkout   uncheckout history    label      propnames  chexec
 propget    propdel    propset    search     set        open       close
 echo       quit       unset      lcd        lls        lpwd       logout
 help       describe   about
Aliases: rm=delete, mkdir=mkcol, mv=move, cp=copy, more=less, quit=exit=bye

SubversionのレポジトリがWebDAVとして公開してあるので、cadaverでURLを指定して実行します。
証明書がオレオレ証明書かつ有効期限が切れているので警告が出ています。
(会社からアクセスする時に中身を閲覧されたくないのでSSL暗号を使うためだけにHTTPSにしてます)
YESを選択して、ユーザー名とパスワードを入力します。認証が成功したらFTPのようにWebDAVプロンプトが出てきます。
とりあえず、どんなコマンドが使えるのか確認するためにhelpを実行してみました。
ふむふむ。コマンドがFTPに似ていますね。
ローカルファイルをリモートにアップロードするコマンドはputなので、putを実行してみます。

と、その前に、自動コミットで正しくリビジョンがあがるのか確認するために、一度ファイルをインポートしてみます。

svn import -m "tomcat default server.xml" server.xml https://tamsvr01/svn/repos/settings/tamsvr02/server.xml

次にWebDAVクライアントからputしてみます。

dav:/svn/repos/> cd settings
dav:/svn/repos/settings/> cd tamsvr02
dav:/svn/repos/settings/tamsvr02/> put server.xml
Uploading server.xml to `/svn/repos/settings/tamsvr02/server.xml':
Progress: [=============================>] 100.0% of 5623 bytes succeeded.
dav:/svn/repos/settings/tamsvr02/> ls
Listing collection `/svn/repos/settings/tamsvr02/': succeeded.
      > server.xml                          5623   2月 10 07:06
      > smb.conf                            7786   2月 10 05:57
      > smbusers                             114   2月 10 05:56

ふむ。成功したようです。ちなみにApacheの設定がきちんと出来ていないと、以下のようにエラーになります。

Progress: [=============================>] 100.0% of 1197 bytes failed:
409 Conflict

ログをWindowsのTortoiseSVNから見てみました。

問題なくコミット出来ていました。さて、ここのコメントはどうやって編集するのでしょう・・。
あと、やっぱりコメントをput時に指定できると嬉しかったりしますが、はてさて・・。