読者です 読者をやめる 読者になる 読者になる

Subversionで、あるリビジョン以降の差分をファイルで抽出

Subversion Python

Subversionのexportコマンドは作業コピー内の全ファイルを吐き出してしまいます。ぜんぶですよ、ぜんぶ!! Web仕事の場合、この大量のファイルをすべてFTPで再アップロードしなくちゃいけない、となるとたいへんです。できれば、内容が変わってないファイルはそのままにしたいですね。

できます。

svnコマンドのdiffには、パッチファイルのような行単位の差分を省略して、ファイル名のみ出力する機能があります。しかも、A/M/Dのマーク付きで。

svn diff -r100:HEAD --summarize .

で、こんな感じの出力が得られます。

D       admin/htdocs/articles/_default
D       admin/htdocs/articles/_list.html
M       admin/htdocs/articles/_edit.html
A       admin/htdocs/articles/css/view.css
M       admin/htdocs/articles/index.html
M       admin/htdocs/articles/_view.html
M       admin/htdocs/css/common.css
M       admin/htdocs/index.html

これをPythonでばばっと処理してみました。

#!/usr/bin/env python
#!coding: utf-8

import sys, os, re, shutil
from optparse import OptionParser

def list_changed_path(ws, rev):
    if not re.search(":", rev):
        rev += ":HEAD"
    pwd = os.getcwd()
    try:
        os.chdir(ws)
        svnfd = os.popen('svn diff -r%s --summarize .' % rev, 'r')
        overwrite = []
        remove = []
        for line in svnfd:
            mo = re.match(r'^\s*([AMD])\s*(.*)$', line.strip())
            if mo:
                print line.strip()
                if mo.group(1) in ['A', 'M']:
                    overwrite.append(mo.group(2).strip(" \n"))
                elif mo.group(1) in ['D']:
                    remove.append(mo.group(2).strip(" \n"))
        svnfd.close()
    finally:
        os.chdir(pwd)
        
    overwrite.sort()
    remove.sort()
    return (overwrite, remove)
    
if __name__ == '__main__':
    parser = OptionParser(usage="%prog <workspace> <rev>[:<rev>] <target>")
    (options, args) = parser.parse_args()
    if len(args) != 3:
        parser.error("Parameters not matched!")
    
    ws,rev,target = args
    
    # clean target dir
    if os.path.exists(target):
        for root, dirs, files in os.walk(target, topdown=False):
            for name in files:
                os.remove(os.path.join(root, name))
            for name in dirs:
                os.rmdir(os.path.join(root, name))
    else:
        os.makedirs(target)
    
    # parse svn diff summary
    overwrite, remove = list_changed_path(ws, rev)
    
    # copy added/modified files
    for p in overwrite:
        s = os.path.join(ws, p)
        d = os.path.join(target, p)
        if os.path.isfile(s):
            if not os.path.exists(os.path.dirname(d)):
                os.makedirs(os.path.dirname(d))
            shutil.copy2(s, d)
        elif os.path.isdir(s):
            if not os.path.exists(d):
                os.makedirs(d)
    
    # report if removed files found
    if remove:
        fd = file(os.path.join(target, "__TO_BE_REMOVED__.txt"), "wt")
        for p in remove:
            fd.write("D %s\n" % p)
        fd.close()

上書きファイルのセットが吐き出せるコマンドのできあがりです。

$ svn-export-changed myworkspace 100 upload_files

で、myworkspaceのリビジョン100以降に変更があったファイルを、upload_filesにコピーします。ワークスペースは最新で、手元にコミット保留中のファイルがないのが前提です。

$ svn update -r200 myworkspace
$ svn-export-changed myworkspace 100:200 upload_files

とかすれば、100から200の変更のみ取り出せます。(たぶんね。テストしてないからだめかも)

もし削除すべきファイルがあれば、出力先のフォルダの__TO_BE_REMOVED__.txt ファイルにレポートされます。

こりゃ便利。