[レガシーとの闘い] SVNのsyncをpythonで半自動化してみた

# はじめに

この記事の目的は私が ~~押し付けられた~~ 担当していた
「なんでこんな作業せなあかんねん…」的なレガシーな作業から逃れるために、
どうせなら流行りのpythonでツールを作ろうとして手探りで作成したツールをご紹介します。
明日使えるかどうかわからないムダ知識を探している方にお勧めです。

(自己紹介)
2016年新卒入社の5年目社員です。
レガシーと戦う雑用係です。

# 背景
まずこのツールの対象となるタスクについてですが、一言でいうと
「本番環境のSVNリポジトリをテスト環境のSVNリポジトリにsyncさせる」
といったようなタスクになります。


## SVNってなに？
SVN(Subversion)とはいわゆるバージョン管理システムの1つで、分散型のGitとは異なり、
集中型(クライアント・サーバ型)のバージョン管理システムになります。
SVNとGitの違いなどは他に詳しくまとめているページがあるので詳細は省きますが、
近年ではSVNよりもGitの方が圧倒的に主流のようで、SVNはレガシーなツールになっているようです。

今回の場合は本番環境とテスト環境の2つを使用して、開発と進捗管理を行っていました。
この際リポジトリとしては2つですが、基本的に開発者がソースをコミットするのは本番環境のみで、
テスト環境の方にはsvnsyncを利用して本番環境へのコミット内容を反映させていました。

ただこのsvnsyncも万能ではなく、sync時に対象ファイルに差分がある場合に失敗することが多々ありました。

## 従来の対応方法
このsvnsyncに失敗した場合にエラーを解消させることが課せられた使命だったのですが、
その方法が下記の通りになります。

1. テスト環境のリポジトリに接続してログを表示し最新のリビジョン番号を確認
2. 本番環境のリポジトリに接続してログを表示
3. 1で確認したリビジョン番号+1のコミット内容を確認
4. 同じコミット内容になるようにテスト環境のファイルを修正

これだけでももうめんどくさいのですが、開発過渡期にはこの対応が1日に何度も発生することになり、
なんとかこれを自動化できないだろうかと考えました。

# 対応内容

自動化に取り掛かるにあたってまずは作業内容を大きく2つの手順に切り分け、それぞれの手順をスクリプト化しました。

1. 対象ファイルの抽出
2. 対象ファイルの反映

これ以外にも対応内容に従った別処理の作成とコミット完了までを通しで行うスクリプトも作成しましたが、ここでは省きます。

## 対象ファイルの抽出

実際のスクリプトが下記になります。

```python:sync_target.py
import os
import subprocess

# テストのリビジョン取得
os.chdir('C:\\Users\\svn\\svn_sync\\latest_test')
# cmd = 'svn update'
returncode = subprocess.call("svn update")
cmd = 'svnversion'
returncode = subprocess.Popen("svnversion", stdout=subprocess.PIPE)
out = str(returncode.stdout.read().decode('cp932'))
print(out)
stIdx = out.find("'") + 1
edIdx = out.find("P")
rev = int(out[stIdx:edIdx])
print("rev=" + str(rev))

# 更新されたファイルパス取得
os.chdir('C:\\Users\\svn\\svn_sync\\latest_main')
cmd = 'svn update'
returncode = subprocess.call(cmd)
cmd = 'svn diff --summarize -r' + ' r' + str(rev) + ':r' + str(rev +1)
print(cmd)
getlst = subprocess.Popen(cmd, stdout=subprocess.PIPE)
pathlst = getlst.stdout.read().decode('cp932')

with open('C:\\Users\\svn\\svn_sync\\bat\\sync_path.txt', mode='w') as f:
    f.writelines(pathlst)

# パスのリスト成形
cmd = '"C:\\Program Files (x86)\\Hidemaru\\Hidemaru.exe" /x "C:\\Users\\works\\AppData\\Roaming\\Hidemaruo\\Hidemaru\\Macro\\replace.mac" "C:\\Users\\svn\\svn_sync\\bat\\sync_path.txt"'
print("cmd" + cmd)
editfile = subprocess.call(cmd)
```
ごちゃごちゃとやっているのですが、ここでやっているのは最初の手順をほぼそのまま自動でやらせたというものに近いです。
エラーとなるリビジョン取得
　　　↓
更新されたパスをテキストに書き出し
　　　↓
更新対象のパスを整形
最後に至ってはパスの成形のためにマクロを使っています。

このスクリプト部分で躓きやすいのがパスの書き方と文字コードになっており、
これはpythonあるあるかとも思いますが、パスの区切りやファイル書き込み時の文字コード指定などが厳しいので、慣れるまでは結構迷いました。

## 対象ファイルの反映

前述した対象ファイルの抽出を実施した後に、実際にその変更部分をチェックアウトしたリポジトリに反映させます。

```python:sync_main.py
import re
import os
import subprocess
import logging
import shutil
import glob as gb
import sync_target

# 対象ファイル抽出
target:sync_target

envpath = "set path=%PATH%;C:\\Users\\svn\\svn_sync\\svn_cmd_tool\\Apache-Subversion-1.9.5\\bin"
os.system(envpath)

# 一時チェックアウト用フォルダ指定
os.chdir('../')
cd = os.getcwd()
print(cd)
MAIN_PATH = cd + "\\main"
TEST_PATH = cd + "\\test"
num = 1
os.chdir(cd + '\\bat')
print(num)
print(MAIN_PATH)
print(TEST_PATH)

# フォルダ削除&作成
os.makedirs(MAIN_PATH)
os.makedirs(TEST_PATH)

svn_info = "svn info http://testenv/svn/test --show-item revision"
REVISION = os.system(svn_info)
REVISION = REVISION + 1
print(REVISION)

file = open('sync_path.txt', 'r')
for fpath in file:
  path = fpath.rsplit('/',1)[0]
  name = fpath.rsplit('/',1)[1]

  print('path:' + path)
  print('name:' + name)

  SUB_MAIN_PATH = MAIN_PATH + '\\' + str(num) 
  SUB_TEST_PATH = TEST_PATH + '\\' + str(num)
  FIN_MAIN_PATH = SUB_MAIN_PATH + '\\' + name
  FIN_TEST_PATH = SUB_TEST_PATH + '\\' + name

  os.makedirs(SUB_MAIN_PATH)
  os.makedirs(SUB_TEST_PATH)

  os.chdir(SUB_TEST_PATH)

  MARGE_TEST_PATH = 'http://testenv/svn/test' + path
  print('MERGE : ' + MARGE_TEST_PATH)
  print('SUB : ' + SUB_TEST_PATH)
  print('FIN : ' + FIN_TEST_PATH)
  os.system('svn co --depth=empty ' + MARGE_TEST_PATH + ' ' + SUB_TEST_PATH)
  os.system('svn up --set-depth=empty ' + SUB_TEST_PATH)
  os.system('svn up --set-depth=infinity ' + FIN_TEST_PATH)

  os.chdir(SUB_MAIN_PATH)

  MARGE_MAIN_PATH = 'http://mainenv' + path
  print(MARGE_MAIN_PATH)
#  os.system('pause')
  os.system('svn co --depth=empty ' + MARGE_MAIN_PATH + ' ' + SUB_MAIN_PATH)
  os.system('svn up --set-depth=empty ' + SUB_MAIN_PATH)
  os.system('svn up --set-depth=infinity ' + FIN_MAIN_PATH)

  num = num + 1

file.close

# sync_copy
sec = 1

os.chdir('C:\\Users\\svn\\svn_sync\\bat')
cnt = len(open('sync_path.txt').readlines())

for i in range(cnt):
  files = gb.glob('C:\\Users\\svn\\svn_sync\\main\\' + str(sec) + '\\*.*')
  for j in range(len(files)):
    file = os.path.basename(files[j])
    mPath = str("C:/Users/svn/svn_sync/main/" + str(sec) + "/" + file)
    tPath = str("C:/Users/svn/svn_sync/test/" + str(sec) + "/" + file)
    shutil.copyfile(mPath, tPath)
  sec = sec + 1

```
最初にsync_targetを呼び出すことで更新対象のパスのリストを作成し、
コミットのファイル単位でフォルダを1つ作成し、空のままチェックアウトします。
その後フォルダ毎にアップデートをかけていくことで更新対象のパスを用意しています。

これを本番とテストでそれぞれ実施し、最後はファイルの上書きという力技で対応しています。

## おわりに
このタスクを通じて得た学びとしては下記2点

* pythonってすごい
* 身近なタスクをコードに起こすことから開発って始められる

歴史の長い製品ではどうしてもレガシーとの闘いになることがありますが、
それを改善していくのも楽しみの一つになると思いますので、それこそWork Funの精神を忘れずに取り組んでみるといいのではないかと思います。