昨日、Subversionのアップデートをしていて、リポジトリのファイルシステムのバージョン周りで
どのように互換性が保たれているのか、気になったので調べてみた。
ソースコードは以下のURLのものを使用した。
http://subversion.tigris.org/downloads/subversion-1.5.3.tar.bz2
まず、ファイルシステムにどのようにアクセスしているのか、という事を見てみる。
ファイルシステムへのアクセスは、FSFSとBDBがサポートされているため、抽象化されている。
抽象化する際には、関数ポインタの構造体を用いて抽象化を行っているようである。
(SubversionがC++を使っていれば、クラスを使って抽象化していただろう)
fs-loader.h 60行付近
/*** Top-level library vtable type ***/
typedef struct fslibraryvtablet
{
/* This field should always remain first in the vtable.
Apart from that, it can be changed however you like, since exact
version equality is required between loader and module. This policy
was weaker during 1.1.x, but only in ways which do not conflict with
this statement, now that the minor version has increased. */
const svnversiont (getversion)(void);
/* The openfs/create/openfsforrecovery/upgradefs functions are
serialized so that they may use the commonpool parameter to
allocate fs-global objects such as the bdb env cache. /
svnerrort *(create)(svnfst fs, const char *path, aprpoolt *pool,
aprpoolt *commonpool);
svnerror_t *(openfs)(svnfst *fs, const char *path, aprpoolt *pool,
aprpoolt *commonpool);
/* openforrecovery() is like open(), but used to fill in an fs pointer
that will be passed to recover(). We assume that the open() method
might not be immediately appropriate for recovery. /
svnerrort *(openfsforrecovery)(svnfst *fs, const char *path,
aprpoolt *pool,
aprpoolt *commonpool);
svnerrort (upgradefs)(svnfst *fs, const char *path, aprpoolt *pool,
aprpoolt *commonpool);
svnerrort (deletefs)(const char *path, aprpoolt *pool);
svnerrort (hotcopy)(const char *srcpath, const char destpath,
svnbooleant clean, aprpool_t *pool);
const char *(getdescription)(void);
svnerrort (recover)(svnfst *fs,
svncancelfunct cancelfunc, void *cancelbaton,
aprpoolt *pool);
/* Provider-specific functions should go here, even if they could go
in an object vtable, so that they are all kept together. /
svnerrort *(bdblogfiles)(aprarrayheadert **logfiles,
const char *path, svnbooleant onlyunused,
aprpool_t *pool);
/* This is to let the base provider implement the deprecated
svnfsparseid, which we've decided doesn't belong in the FS
API. If we change our minds and decide to add a real
svnfsparseid variant which takes an FS object, it should go
into the FS vtable. /
svnfsid_t *(parseid)(const char *data, aprsizet len,
aprpoolt *pool);
} fslibraryvtablet;
ファイルシステムごとの関数ポインタの構造体を取得するためには、
getlibraryvtable関数(fs-loader.cの200行目)を使って関数ポインタの構造体を取得する。
取得する際のキーには、svnfstype関数(fs-loader.cの217行目)からのファイルシステムタイプの情報を
使って、関数ポインタの構造体を取得している。
svnfstype関数では、リポジトリのdb/fs-typeファイル(内部的にはFSTYPEFILENAME定数でファイル名が
指定されている)の内容をそのまま返している。
次に、実際にリポジトリを開いて、詳細なバージョン情報を取得するところまでを見てみる。
先の説明でfslibraryvtablet構造体のopenfs関数からファイルシステムがオープンされる。
さらに、そのopen_fs関数は関数ポインタを保持しているだけなので、実体を探す必要がある。
ここでは、FSFSの中身を見ることとして、FSFSの関数ポインタのテーブルを以下に示す。
libsvnfsfsfs.c 314行目付近
static fslibraryvtablet libraryvtable = {
fsversion,
fscreate,
fsopen,
fsopenforrecovery,
fsupgrade,
fsdeletefs,
fshotcopy,
fsgetdescription,
svnfsfs_recover,
fslogfiles
};
このテーブルからFSFSでファイルシステムをオープンするにはfsopen関数を用いていることがわかる。
fsopen関数からは、svnfsfsopen関数が呼び出されている。
ここで、svnfsfsopen関数の中身を見てみることにする。
libsvnfsfsfs_fs.c 1000行目付近
svnerrort *
svnfsfs_open(svnfst *fs, const char *path, aprpoolt *pool)
{
fsfsdatat *ffd = fs->fsapdata;
aprfilet *uuidfile;
int format, maxfilesperdir;
char buf[APRUUIDFORMATTEDLENGTH + 2];
aprsizet limit;
fs->path = apr_pstrdup(fs->pool, path);
/* Read the FS format number. */
SVNERR(readformat(&format, &maxfilesperdir,
pathformat(fs, pool), pool));
/* Now we've got a format number no matter what. */
ffd->format = format;
ffd->maxfilesperdir = maxfilesperdir;
SVNERR(checkformat(format));
/* Read in and cache the repository uuid. */
SVNERR(svniofileopen(&uuidfile, pathuuid(fs, pool),
APRREAD | APRBUFFERED, APROSDEFAULT, pool));
limit = sizeof(buf);
SVNERR(svnioreadlengthline(uuidfile, buf, &limit, pool));
ffd->uuid = apr_pstrdup(fs->pool, buf);
SVNERR(svniofileclose(uuid_file, pool));
SVNERR(getyoungest(&(ffd->youngestrevcache), path, pool));
return SVNNOERROR;
}
この中で、readformat関数を呼び出している。
この関数がFSFSの詳細なフォーマットを取得するために用いられている関数である。
readformat関数の中では、いくつかの関数を呼び出して、PATH_FORMAT定数で与えられる
ファイル(dbformat)の中身を読み込んで、戻り値を返している。
これで、リポジトリを開いて、詳細なバージョン情報を取得するまでがわかった。
それでは、次に、最初に出ていたfslibraryvtablet構造体のopenfs関数が
どうやって呼び出されているのかを見てみる。
open_fs関数を呼び出している関数を探していくと次のようなコールグラフとなる。
openfs()
← svnfsopen(libsvnfsfs-loader.c 385行目付近)
← getrepos(libsvnrepos
epos.c 1306行目付近)
← svnreposopen(libsvn_repos
epos.c 1368行目付近)
最後のsvnreposopen関数のソースコードは次のようになっている。
svnerrort *
svnreposopen(svnrepost *reposp,
const char *path,
aprpool_t *pool)
{
/ Fetch a repository object initialized with a shared read/write
lock on the database. */
SVNERR(getrepos(repos_p, path, FALSE, FALSE, TRUE, pool));
return SVNNOERROR;
}
この中のsvnrepost変数について調べてみると、以下のような記述がある。
/* The path to the repository's top-level directory. */
char *path;
/* The path to the repository's conf directory. */
char *conf_path;
/* The path to the repository's hooks directory. */
char *hook_path;
/* The path to the repository's locks directory. */
char *lock_path;
/* The path to the Berkeley DB filesystem environment. */
char *db_path;
/* The format number of this repository. */
int format;
/* The FS backend in use within this repository. */
const char *fs_type;
/* If non-null, a list of all the capabilities the client (on the
current connection) has self-reported. Each element is a
'const char ', one of SVNRACAPABILITY_.
aprarrayheadert *clientcapabilities;
/* Maps SVNREPOSCAPABILITYfoo keys to "yes" or "no" values.
If a capability is not yet discovered, it is absent from the table.
Most likely the keys and values are constants anyway (and
sufficiently well-informed internal code may just compare against
those constants' addresses, therefore). */
aprhasht *repositorycapabilities;
};
最初の方に、repositoryオブジェクトを作成するには、svnreposopen関数か
svnreposcreate関数を呼び出す必要があるとなっている。
従って、リポジトリを開くには、svnreposopen関数を呼び出せばいいことがわかる。
以上のことをまとめると、svnreposopen関数を呼び出すと、関数の中でバージョン情報を取得したり
して、リポジトリを開くことがわかった。また、BDBとFSFSとの切り替えは、
関数テーブルを用意して、formatファイルの情報を元に使用する関数テーブルを切り替えている。
FSFSのバージョンごとの仕様の違いについては、libsvnfsfsstructureファイルに書いてある。
コード上では、以下のようにしてバージョンごとに振り分けて互換性を保つようになっている。
libsvnfsfsfs_fs.c 4803行目付近
この中のSVNFSFS_MINNOGLOBALIDSFORMAT定数は、libsvnfs_fsfs.hの中で定義されており、
バージョンの閾値を定数で保持している。
ここまでで、FSFSの互換性が保たれていることがわかる。
ここに来て、バージョンがどのように決定されるか気になってくる。次にそれを見てみる。
FSFSのバージョンが決定されるのは、もちろんファイルシステムが作成されたときであるので、
その部分を探してみるとsvnfsfs__create関数が見つかる。
libsvnfsfsfs_fs.c 5370行目付近
svnerrort *
svnfsfscreate(svnfst *fs,
const char *path,
aprpoolt *pool)
{
int format = SVNFSFSFORMATNUMBER;
fsfsdatat *ffd = fs->fsap_data;
fs->path = aprpstrdup(pool, path);
/* See if we had an explicitly requested pre-1.4- or pre-1.5-compatible. */
if (fs->config)
{
if (aprhashget(fs->config, SVNFSCONFIGPRE14COMPATIBLE,
APRHASHKEYSTRING))
format = 1;
else if (aprhashget(fs->config, SVNFSCONFIGPRE15COMPATIBLE,
APRHASHKEY_STRING))
format = 2;
}
ffd->format = format;
定数の定義は以下の通りとなっている。
つまり、「pre-1.4-compatible」と、リポジトリを作成するときに引数を
つけると、指定されたバージョンで作成されることが何となくわかる。
実際、「pre-1.4-compatible」と検索してみると、Format1の形式で作成されることが書いてある。
aprhashget関数は、APR = Apache Portable Runtimeの意味であり、そのうちのハッシュ機能が
この関数である。
この関数の使い方については、以下のページが大変参考になる。
ハッシュテーブルを使ってみる - kurukuru-papaの日記
http://d.hatena.ne.jp/kurukuru-papa/20080114/1200289172
fs->configがハッシュテーブルであり、これがどこから来るのかをたどってみる。
svnfsfs__create関数のコールグラフをたどると次のようになる。
svnfsfs_create
← fscreate(libsvnfsfsfs.c 180行目付近)
fslibraryvtablet構造体の関数テーブルをたどる
← svnfscreate(libsvnfsfs-loader.c 352行目付近)
ここまでたどってきて、fs->configに値を代入している場所がわかる。
svnfscreate関数の中でsvnfsnew関数(libsvn_fsfs-loader.c 329行目付近)を呼び出しており、
その中で以下のように代入している。
svnfst *
svnfsnew(aprhasht fsconfig, aprpoolt *pool)
{
svnfst *fs = aprpalloc(pool, sizeof(fs));
fs->pool = pool;
fs->path = NULL;
fs->warning = defaultwarningfunc;
fs->warningbaton = NULL;
fs->config = fsconfig;
fs->accessctx = NULL;
fs->vtable = NULL;
fs->fsapdata = NULL;
return fs;
}
svnfsnew関数では、単純に値を代入しているだけで、fs->configの実体を探すにはさらにたどる必要がある。
さらに、svnfscreate関数をたどると、svnreposcreate関数(libsvn_repos
epos.c)にたどり着く。
この関数の内容を以下に示す。
svnerrort *
svnreposcreate(svnrepost **reposp,
const char *path,
const char *unused1,
const char *unused2,
aprhasht *config,
aprhasht *fsconfig,
aprpoolt *pool)
{
svnrepost *repos;
svnerrort *err;
const char *root_path;
/* Allocate a repository object, filling in the format we will create. */
repos = createsvnrepost(path, pool);
repos->format = SVNREPOS_FORMATNUMBER;
/* Discover the type of the filesystem we are about to create. */
if (fsconfig)
{
repos->fstype = aprhashget(fsconfig, SVNFSCONFIGFSTYPE,
APRHASHKEYSTRING);
if (aprhashget(fsconfig, SVNFSCONFIGPRE14COMPATIBLE,
APRHASHKEYSTRING))
repos->format = SVNREPOSFORMATNUMBER_LEGACY;
}
if (! repos->fstype)
repos->fstype = DEFAULTFSTYPE;
/* Don't create a repository inside another repository. */
rootpath = svnreposfindrootpath(path, pool);
if (rootpath != NULL)
return svnerrorcreatef(SVNERRREPOSBADARGS, NULL, ("'%s' is a "
"subdirectory of an existing repository rooted "
"at '%s'"), path, rootpath);
/* Create the various files and subdirectories for the repository. */
SVNERRW(createreposstructure(repos, path, fs_config, pool),
_("Repository creation failed"));
/* Lock if needed. */
SVNERR(lockrepos(repos, FALSE, FALSE, pool));
/* Create an environment for the filesystem. /
if ((err = svnfscreate(&repos->fs, repos->dbpath, fsconfig, pool)))
{
/ If there was an error making the filesytem, e.g. unknown/supported
* filesystem type. Clean up after ourselves. Yes this is safe because
* createreposstructure will fail if the path existed before we started
* so we can't accidentally remove a directory that previously existed. */
svnerrorclear(svnioremove_dir2(path, FALSE, NULL, NULL, pool));
return err;
}
/* This repository is ready. Stamp it with a format number. */
SVNERR(svniowriteversionfile
(svnpathjoin(path, SVNREPOS__FORMAT, pool),
repos->format, pool));
*reposp = repos;
return SVNNO_ERROR;
}
よくよく見てみると、svnfsfs_create関数と同じような形をしていることに気づく。
この段階になってようやく気づいたのだが、もしかしたら、Subversionのリポジトリは、
リポジトリとファイルシステムで分離されているのかもしれない。これについては、
おそらく、設計を見てみればわかるはずだが、今回は特に気にしないことにした。
あと、SVNFSCONFIGPRE15_COMPATIBLEに関する記述がないけど、これって大丈夫なのかな・・・?
svnreposcreate関数からさらにたどって、subcommand_create関数(svnadminmain.c 520行目付近)から
呼び出されており、さらにこの関数は、以下の変数の関数ポインタに設定されていることがわかる。
(svnadminmain.c 309行目付近)
/* Array of available subcommands.
* The entire list must be terminated with an entry of nulls.
*/
static const svnoptsubcommanddesct cmdtable[] =
{
{"crashtest", subcommandcrashtest, {0}, N_
("usage: svnadmin crashtest REPOS_PATH
"
"Open the repository at REPOS_PATH, then abort, thus simulating
"
"a process that crashes while holding an open repository handle.
"),
{0} },
{"create", subcommandcreate, {0}, N
("usage: svnadmin create REPOS_PATH
"
"Create a new, empty repository at REPOSPATH.
"),
{svnadminbdbtxnnosync, svnadminbdblogkeep,
svnadminconfigdir, svnadminfs_type, svnadminpre14compatible,
svnadminpre15compatible } },
おそらく、main関数を見てみるとコマンド引数とこの変数の値を使って、
いろいろと設定しているはずである。
これ以上追うと、また複雑な部分に入ってしまうので、ここら辺でconfigの値を追うのはやめておく。
最後に、コミットをしたときにリポジトリとファイルシステムが開かれるまでを見てみる。
svnコマンドのソースコードを見ると次のような記述が見つかる。
svnmain.c 380行目付近
{ "commit", svncl_commit, {"ci"},
おそらく、svncl_commit関数がコミットのコマンドラインでの処理を行うだろう、と仮定して
これを探ってみる。
ここからソースコードをたどってコールグラフを書くと次のようなコールの手順が見つかる。
svnclcommit(svncommit-cmd.c 40行目)
→ svnclientcommit4(libsvnclientcommit.c 1348行目付近)
→ getraeditor(libsvnclientcommit.c 597行目付近)
→ svnclient_openrasessioninternal(libsvnclient
a.c 281行目付近)
→ svnraopen3(libsvnra
a_loader.c 381行目付近)
RAとはRepository Accessの意味である。
svnraopen3関数では、RAの方法をどれにするか決定する。
RAはライブラリとして格納されており、以下のように定義されている。
libsvnra
aloader.c 76行目付近
static const struct ralibdefn {
/* the name of this RA library (e.g. "neon" or "local") */
const char *ra_name;
const char * const schemes;
/ the initialization function if linked in; otherwise, NULL */
svnrainitfunct initfunc;
svnrainitfunct compatinitfunc;
} ralibraries[] = {
{
"neon",
davschemes,
},
{
"svn",
svn_schemes,
},
{
"local",
local_schemes,
},
{
"serf",
dav_schemes,
},
/* ADD NEW RA IMPLEMENTATIONS HERE (as they're written) */
/* sentinel */
{ NULL }
};
このライブラリを用いて、各スキームの値とユーザが指定したURLの形式を比較して、
どのRAを使用するか決定する。RAのライブラリには初期化関数のポインタも入っているので
それを利用して初期化処理を行う。
ここでは、ローカルアクセスのRAを使用してアクセスすると仮定して話を進めていく。
RAの方法としては、localの他にDavを使った方法などがあり、これからは抽象化されて
アクセスすることができる。このために、ファイルシステムの抽象化のときと同じように
関数テーブルが用意されていて、それを用いてアクセスするようになっている。
関数テーブルの定義は以下のようになっている。
libsvnra
aloader.h 34行目付近
/* The RA layer vtable. /
typedef struct svnravtablet {
/ This field should always remain first in the vtable. /
const svnversiont *(get_version)(void);
/* Return a short description of the RA implementation, as a localized
* string. /
const char *(get_description)(void);
/* Return a list of actual URI schemes supported by this implementation.
* The returned array is NULL-terminated. /
const char * const *(getschemes)(aprpool_t *pool);
/* Implementations of the public API functions. */
/* All fields in SESSION, except priv, have been initialized by the
time this is called. SESSION->priv may be set by this function. /
svnerrort *(opensession)(svnrasessiont session,
const char *reposURL,
const svnracallbacks2t *callbacks,
void *callbackbaton,
aprhasht *config,
aprpool_t *pool);
/ URL is guaranteed to have what getreposroot() returns as a prefix. /
svnerrort *(reparent)(svnrasession_t *session,
const char *url,
例えば、localであれば以下の関数テーブルが使用される。
libsvnra
aplugin.c 1389行目付近
static const svnravtablet ralocalvtable =
{
ralocalversion,
svnralocalgetdescription,
svnra_localgetschemes,
svnralocalopen,
svnralocalreparent,
svnralocalgetsessionurl,
svnralocalgetlatestrevnum,
svnralocalgetdatedrevision,
svnralocalchangerevprop,
svnralocalrevproplist,
svnralocalrevprop,
svnra_localgetcommiteditor,
svnralocalgetfile,
svnra_localgetdir,
svnralocalgetmergeinfo,
svnralocaldoupdate,
svnra_localdoswitch,
svnralocaldostatus,
svnralocaldodiff,
svnra_localgetlog,
svnralocaldocheckpath,
svnralocalstat,
svnralocalgetuuid,
svnralocalgetreposroot,
svnralocalgetlocations,
svnralocalgetlocationsegments,
svnralocalgetfilerevs,
svnralocallock,
svnralocalunlock,
svnralocalgetlock,
svnralocalgetlocks,
svnra_localreplay,
svnralocalhascapability,
svnra_localreplay_range
};
さて、ここまででRAが抽象化されていることがわかり、リポジトリをオープンする部分の
手前まで来ている。
再度、svnraopen3関数を読み進めていくと、関数テーブルのうちのopensession関数を呼び出している。
opensession関数は関数テーブルで抽象化されて、ローカルアクセスの場合は、
svnralocalopen関数(libsvnralocal
aplugin.c 411行目付近)となる。
svnra_localopen関数では、svnralocalsplitURL関数(libsvnralocalspliturl.c 26行目付近)を呼び出している。
svnralocalsplitURL関数の中を見ると、ようやくお目当ての関数にたどり着く。
下の方にsvnrepos_open関数があり、この関数でリポジトリを開いていることがわかる。
svnralocalsplitURL関数でリポジトリを開くというのも、なんとなく納得がいかない感じもするが
svnra_localsplit_URL関数の呼び出し元の処理を見ていると、どうもこの関数でオープンの処理を
しているようである。
長々とSubversionの一部について書いてみたけど、これで5時間近くかかった・・・。
ただ、内部構造もなんとなくわかったし、実際に運用されているもののコードを見るというのも勉強になって良い。
バージョン管理ソフトなら、実際にどのようにバージョンを保持しているのか、とか、そういうことを
調べてみるのも良かったのかもしれないが、今回は、ファイルシステムの互換性が保たれるかどうか
ということが知りたかったので、そこら辺を重点的に調べてみた。
Blogなどで誰かが書いてあるものを読んで理解するのも良いが、ソースコードが公開されているのであれば、
そのような情報を探すだけではなく、直接ソースコードを見た方が早い場合もあるだろう。
間違っている部分などありましたら、指摘していただければ幸いです。
また、日本語が書き殴ったような文章になっているのは、すいません。
時間があるときにでも、もう少しまともにまとめたいと思っています。