Subversionで、あるリビジョン以降の差分をいきなりFTP

昨日の
Subversionで、あるリビジョン以降の差分をファイルで抽出 - なんたらノート 第二期
をもとに、エクスポートファイルを吐出さず、直接FTPするツールを作りました。

本当なら、ファイルとしてエクスポートして、きっちり確認しながらアップロードするべきだけど、もう「最後3リビジョンの変更だけ、さっさとFTPしたいんよ」というような場合もありますね。

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

import sys, os, re, getpass
from optparse import OptionParser
from ftplib import FTP

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 [options] <workspace> <rev>[:<rev>]",
        conflict_handler="resolve")
    parser.add_option("-f", "--force", dest="force", action="store_true", default=False,
                      help="force executuion", metavar="PATH")
    parser.add_option("-h", "--host", dest="host",
                      help="target host name", metavar="HOST")
    parser.add_option("-u", "--user", dest="user",
                      help="user account name", metavar="USER")
    parser.add_option("-p", "--password", dest="password",
                      help="password", metavar="PASSWORD")
    parser.add_option("-d", "--destpath", dest="destpath",
                      help="destination path on FTP server", metavar="PATH")
    (options, args) = parser.parse_args()
    
    if(len(args) != 2):
        parser.error("Must be specified both of workspace and rev!")
    
    ws,rev = args
    
    def checked_input(prompt, test=lambda i: i != "", echo=True):
        i = raw_input(prompt) if echo else getpass.getpass(prompt)
        pad = " " * len(prompt)
        while not test(i):
            i = raw_input(pad) if echo else getpass.getpass(pad)
        return i
    
    # parse svn diff summary
    overwrite, remove = list_changed_path(ws, rev)
    
    # confirm
    if options.force:
        do_ftp = True
    else:
        do_ftp = dict(y=True, n=False).get(checked_input("Upload these files? (y/n) ", test=lambda i: i[0].lower() in ('y', 'n'))[0])
    if not do_ftp:
        sys.exit()
    
    # connect ftp
    host = options.host or checked_input("host:")
    user = options.user or checked_input("user:")
    password = options.password or checked_input("password:", test=lambda x: True, echo=False)
    destpath = options.destpath or checked_input("destination path:")
    
    ftp = FTP(host, user, password)
    ftp.cwd(destpath)
    
    # upload
    ASCII_TYPES = (
        'html', 'htm', 'xhtml', 'xhtm', 'css', 'js',
        'txt', 'csv', 'xml', 'json', 'yml', 'yaml',
        'cgi', 'pl', 'php', 'inc', 'phtml', 'tpl', 'py', 'rb'
    )
    for p in overwrite:
        s = "/".join([ws, p])
        d = "/".join([destpath, p.replace("\\", "/")])
        if os.path.isfile(s):
            ext = s[s.rfind(".")+1:]
            try:
                if ext in ASCII_TYPES:
                    sf = file(s, 'rt')
                    ftp.storlines("STOR %s" % d, sf)
                    sf.close()
                    print "copy(a) %s" % d
                else:
                    sf = file(s, 'rb')
                    ftp.storbinary("STOR %s" % d, sf)
                    sf.close()
                    print "copy(b) %s" % d
            except:
                print "!copy %s ==> failed" % d
        elif os.path.isdir(s):
            try:
                ftp.mkd(d)
                print "mkdir %s" % d
            except:
                print "!mkdir %s ==> failed" % d
    
    # remove
    for p in reversed(remove):
        d = "/".join([destpath, p.replace("\\", "/")])
        try:
            ftp.delete(d)
            print "rm %s" % d
        except:
            print "!rm %s ==> failed" % d
    
    ftp.quit();

使い方は

svn-ftp-changes --help

で確認してください。

FTPでの同期そのものはあまりがんばって作ってないので、ちょくちょく、ディレクトリが作れなかったりして失敗します。失敗した処理は手作業で。

そもそも、数コミット分まで程度の、ごく小さな変更を即座にアップロードすることしか想定してないので、リビジョン1からごっそり全自動FTPというのは、避けたほうが安全です。