技研のまつけんです。

 

私は普段、ファイルのバージョン管理にSubversion (TortoiseSVN) を使っています。大量のファイルを法則に従って、一挙に処理したいときなどは、cygwinでsvnコマンドを使ったりもします。しかし、ある程度の分量があるのに法則性が無く、マウス操作で複数のファイルを移動したり改名したりするのは大変です。1つ1つ、右クリックしてSVN rename…を選んだり、マウスの右ボタンでドラッグ&ドロップしてSVN move versioned item(s) hereを選んだりして処理しなければなりません (gitならファイルの移動も自動検出なんですけどね)。今回は、それを手軽に行う方法を紹介したいと思います。

具体的には、ターゲットとなるディレクトリの「クローン」(正確には「ダミー」) を作って、そこの中で行われた移動やコピーを探知し、それをsvn cpやsvn rmのコマンドに変換します。これで、WindowsのExplorerのみならず、あらゆるファイラーで処理をすることが出来ます。個人的には、Explorerでctrl-a、ctrl-c (またはctrl-x)、backspace、backspace、ctrl-vで「全アイテムを2階層上にコピー (または移動)」したり出来るようになったのが、とても気に入ってます。

なお、svn mvは、svn cpとsvn rmの結果を一緒にcommitすれば、(TortoseSVNのログ画面で見る限り) 同じ扱いになるようです。コピーと移動が混ざっている場合 (svn cp A B ; svn mv A C) のような場合に扱いが難しいので、今回の場合は、コピーと削除 (svn cp A B ; svn cp A C ; svn rm A)  として扱うこととしました。

スクリプトと手順

今回、紹介するスクリプトは、checkout.sh、diff.sh、commit.shの3つです (記事の末尾に掲載)。これらはsvnのコマンドcheckout、diff、commitに合わせてあります。手順としては

  1. checkout.shでクローン (ダミー) を作成し、
  2. それを編集したあと、
  3. diff.shで差分を解析し、そして、
  4. commit.shで現状を一時保存し、(必要に応じて) 再び、「編集」に戻る

となります。後述の制約条件の範囲内ならば1、2、3だけで終わり (4は不要) となります。

今回の題材

今回は、proj001/trunk/srcをターゲットとして、使い方などを説明していきます。その内容は

$ tree proj001/trunk/src/
proj001/trunk/src/
├── calculate
│   ├── calculate.h
│   ├── calculate_main.c
│   └── calculate_sub.c
└── plot
├── plot.h
├── plot_main.c
└── plot_sub.c
2 directories, 6 files

だとします。これを最終的に

proj001/trunk/src/
src
├── c
│   ├── calc
│   │   ├── calc.h
│   │   ├── calc_main.c
│   │   └── calc_sub.c
│   └── plot
│   ├── plot.h
│   ├── plot_main.c
│   └── plot_sub.c
└── py
4 directories, 6 files

に変更したいと仮定します (新しいディレクトリcとpyを作成し、既存のものを両方ともcに移動し、calculateをcalcに変更)。これを手動でbashなどで行う場合、

mkdir src/c ; svn add src/c
mkdir src/py ; svn add src/py
svn mv src/calculate/ src/c/calc
svn mv src/plot/ src/c/plot
svn mv src/c/calc/calculate.h src/c/calc/calc.h
svn mv src/c/calc/calculate_main.c src/c/calc/calc_main.c
svn mv src/c/calc/calculate_sub.c src/c/calc/calc_sub.c

となります (ディレクトリの移動を先に行う場合)。上述の3つのシェルスクリプトを使って、こちらのコマンド (ただし、svn mvをsvn cp + svn rmの組に置き換えたものとする) を生成する手順を説明していきます。

クローン (ダミー) の作成

まずは、checkout.shで、ターゲットのディレクトリをクローン (ダミー作成) します 。この「クローン (ダミー)」は、ディレクトリ構造をコピーし、その中に、元の相対パスが描かれたダミーファイルを置いたものです。なお、ディレクトリ構造だけコピーする処理は、rsync -include “/” -exclude ““でも可能で、そちらの方が高速かも知れませんが、今回はリストを作る関係でfindとmkdirの組み合わせを用いています。例えば、proj001/trunk/srcであれば、以下のように実行します:

./checkout.sh /home/user/proj001/trunk/src/ # 一般的なLinuxシステムの場合
./checkout.sh /cygdrive/c/Users/user/proj001/trunk/src/ # cygwinの場合

編集と差分解析

checkout.shが完了したら、カレントディレクトリにsrcというダミーが作られています。このダミーに対して、(後述する制約の範囲内で) フォルダやファイルのコピー、移動、削除、改名などの変更を自由に行います。一通り、変更が終わったら、diff.shを実行します。こちらのdiff.shは、checkout.shで生成したダミーファイルの内容 (元の相対パス) と現在の相対パスを比較したり、フォルダとファイルのリストを比較したりして、フォルダやファイルのコピー、移動、改名を追跡します。追跡の結果は、svnコマンドの形で出力されます。

制約事項と一時保存

diff.shを使う上で、制約があります。それは「フォルダのコピー、移動、改名」と「ファイルのコピー、移動、改名」を同時に処理できないことです。よって、フォルダ構造を整理したら、いったんdiff.shを実行して差分を記録したら、commit.shで一時保存してから、ファイルの移動や改名を行い、改めてdiff.shを実行する必要があります。

例えば、今回の例では

mkdir src/c ; svn add src/c
mkdir src/py ; svn add src/py
svn mv src/calculate/ src/c/calc
svn mv src/plot/ src/c/plot
svn mv src/c/calc/calculate.h src/c/calc/calc.h
svn mv src/c/calc/calculate_main.c src/c/calc/calc_main.c
svn mv src/c/calc/calculate_sub.c src/c/calc/calc_sub.c

の前半の4行 (フォルダの作成、移動、改名) に相当する処理と、後半の3行 (ファイルの改名) に相当する処理は別々に行う必要があります。

まず、前半4行に相当する操作 (cとpyを作成、cにcalculateとplotを移動、calculateをcalcに改名) を行います。diff.shを実行することで、その結果をbashコマンドとして得ます:

$ ./diff.sh src
# directories:
mkdir -p 'src/c' ; svn add 'src/c' # dir
svn cp 'src/calculate' 'src/c/calc' # dir
svn cp 'src/plot' 'src/c/plot' # dir
svn rm 'src/calculate' # dir
svn rm 'src/plot' # dir
mkdir 'src/py' ; svn add src/py # dir
# files:
svn cp 'src/calculate/calculate.h' 'src/c/calc/calculate.h' # file
svn cp 'src/calculate/calculate_main.c' 'src/c/calc/calculate_main.c' # file
svn cp 'src/calculate/calculate_sub.c' 'src/c/calc/calculate_sub.c' # file
svn cp 'src/plot/plot.h' 'src/c/plot/plot.h' # file
svn cp 'src/plot/plot_main.c' 'src/c/plot/plot_main.c' # file
svn cp 'src/plot/plot_sub.c' 'src/c/plot/plot_sub.c' # file
svn rm 'src/calculate/.dir_info' # file
svn rm 'src/calculate/calculate.h' # file
svn rm 'src/calculate/calculate_main.c' # file
svn rm 'src/calculate/calculate_sub.c' # file
svn rm 'src/plot/.dir_info' # file
svn rm 'src/plot/plot.h' # file
svn rm 'src/plot/plot_main.c' # file
svn rm 'src/plot/plot_sub.c' # file

この出力の前半部分はディレクトリの作成、移動、改名によるものです。が、後半部分 (# files:以降) はディレクトリの移動、改名に伴うファイルの位置の変化です (これが制約条件の元凶)。ディレクトリ関連のコマンドは行末に# dirを付加するようにしてあるので、grep “# dir$”で前半部のみを取り出して保存します:

$ ./diff.sh src | grep "# dir$" > commands.sh
$ cat commands.sh
mkdir -p 'src/c' ; svn add 'src/c' # dir
svn cp 'src/calculate' 'src/c/calc' # dir
svn cp 'src/plot' 'src/c/plot' # dir
svn rm 'src/calculate' # dir
svn rm 'src/plot' # dir
mkdir 'src/py' ; svn add src/py # dir

問題なさそうなので、commit.shで一時保存します:

$ ./commit.sh src

次にファイルの改名を行います。再び、diff.shを実行するとファイルの改名についてのbashコマンドが得られます:

$ ./diff.sh src
# directories:
# files:
svn cp 'src/c/calc/calculate.h' 'src/c/calc/calc.h' # file
svn cp 'src/c/calc/calculate_main.c' 'src/c/calc/calc_main.c' # file
svn cp 'src/c/calc/calculate_sub.c' 'src/c/calc/calc_sub.c' # file
svn rm 'src/c/calc/calculate.h' # file
svn rm 'src/c/calc/calculate_main.c' # file
svn rm 'src/c/calc/calculate_sub.c' # file

今度はそのままcommands.shに追加しても問題ないので、そのまま追加します (もちろんgrep “#files$”しても良い) :

$ ./diff.sh src >> commands.sh

これで準備が整いました。あとは、オリジナルのディレクトリで

$ ./diff.sh src >> commands.sh

これで準備が整いました。あとは、オリジナルのディレクトリ (/home/user/proj001/trunk/src/や/cygdrive/c/Users/user/proj001/trunk/src/)で、このcommands.shを実行するだけです。

まとめ

今回は、特定のディレクトリのクローン (ダミー) を作ることにより、その中でWindowsのExplorerや、その他のファイラーでコピー、移動、改名した結果を解析し、svnコマンド (とmkdirコマンド) を生成する方法について紹介しました。フォルダのコピー、移動、改名とファイルのコピー、移動、改名が同時に解析できないのが少し面倒ですが、それについては今後、改良していきたいと思います。

スクリプトなど

今回使用したスクリプトは、以下の通りです (ファイル先頭の#!/bin/bashは省略)。

checkout.sh:

TGT="$1"
if [ "" = "$TGT" ]
then
echo "usage: $0 target_dir"
exit
fi
# target_dirにsvn管理外のファイルがある場合は、findの代わりにsvn listを使う
( cd "$TGT"/../ ; find `basename "$TGT"` -type d | sort ) > d0
( cd "$TGT"/../ ; find `basename "$TGT"` -type f | sort ) > f0
cat d0 | xargs mkdir
cat d0 | while read P
do
echo "$P/.dir_info" > "$P/.dir_info"
attrib +h "$P/.dir_info"
done
cat f0 | while read P
do
echo "$P" > "$P"
done
find `basename "$TGT"` -type f | sort > f0

diff.sh:

TGT="$1"
if [ "" = "$TGT" ]
then
#echo "usage: $0 target_dir"
#exit
TGT=`find -maxdepth 1 -type d | tail -n 1 | cut -c 3-`
echo "# target_dir: $TGT"
fi
find "$TGT" -type f | sort > f2
cp -f d0 d2
echo '# directories:'
grep .dir_info < f2 | while read DST
do
SRC=`cat "$DST"`
diff <( echo "$DST" ) <( echo "$SRC" ) >& /dev/null
if [ 1 -eq $? ]
then
SRC=`dirname "$SRC"`
DST=`dirname "$DST"`
DIR=`dirname "$DST"`
N=`grep "^$DIR$" < d2 | wc -l`
if [ 0 -eq $N ]
then
echo "mkdir -p '$DIR' ; svn add '$DIR' # dir"
echo $DIR >> d2
fi
echo "svn cp '$SRC' '$DST' # dir"
echo $DST >> d2
fi
done
find "$TGT" -type d | sort > d3
diff d0 d3 | grep "^< " | cut -c 3- | xargs -I{} echo "svn rm '{}' # dir"
diff <( sort < d2 ) d3 | grep "^> " | cut -c 3- | xargs -I{} echo "mkdir '{}' ; svn add {} # dir"
echo '# files:'
grep -v .dir_info < f2 | while read DST
do
SRC=`cat "$DST"`
diff <( echo "$DST" ) <( echo "$SRC" ) >& /dev/null
if [ 1 -eq $? ]
then
DIR=`dirname "$DST"`
N=`grep "^$DIR$" < d2 | wc -l`
if [ 0 -eq $N ]
then
echo "mkdir -p '$DIR' ; svn add '$DIR' # file"
echo $DIR >> d2
fi
echo "svn cp '$SRC' '$DST' # file"
fi
done
diff f0 f2 | grep "^< " | cut -c 3- | xargs -I{} echo "svn rm '{}' # file"

commit.sh:

TGT="$1"
if [ "" = "$TGT" ]
then
#echo "usage: $0 target_dir"
#exit
TGT=`find -maxdepth 1 -type d | tail -n 1 | cut -c 3-`
echo "# target_dir: $TGT"
fi
find "$TGT" -type d | sort > d0
find "$TGT" -type f | sort > f0
cat d0 | xargs mkdir -p
cat d0 | while read P
do
echo "$P/.dir_info" > "$P/.dir_info"
attrib +h "$P/.dir_info"
done
cat f0 | while read P
do
echo "$P" > "$P"
done
find "$TGT" -type f | sort > f0

なお、これらのスクリプトはカレントディレクトリにテンポラリファイルを作成します。それらは

rm -f d? f?

を実行することで削除できますが、空の作業用ディレクトリ作成し、そこに3つのスクリプトをコピーしてきて、その中で処理し、終わったら作業用ディレクトリごと削除してしまうのがオススメです。

おまけ

今回のような方法の他にも

find -type f | xargs md5sum > sum_before

で、全ファイルのMD5の一覧を保存しておき、移動やコピーをした後に

find -type f | xargs md5sum > sum_after

で保存したMD5の一覧を比較するという方法も考えられます。機会があれば紹介したいと思いますが、こちらは、ダミーのディレクトリツリーを作る必要が無い反面、同一内容のファイルが無いことが前提になります。また、全ファイルのMD5を計算するので時間も掛かってしまいます。