Think Twice
IT技術メモ | PowerShellのメモ
Created: 2021-06-19 / Updated: 2021-07-26

PowerShellでSVNリポジトリをpushしたりpullしたりするツール


目次


これは何?

分散バージョンシ管理ステムであるGitでは、ローカルにリポジトリを作成してそこにいつでも好きなタイミングでコミットできるようになっています。
そして、任意のタイミングでリモートリポジトリ(他の開発者と共有しているリポジトリ)にプッシュすることで、自分の作業を公開することができます。

ところが、同じバージョン管理システムで古くからあるSubversionは、分散型ではないため基本的に中央リポジトリが一つのみとなっており、
開発者はコミットする時にすべて中央リポジトリにコミットされる形となっています。

どちらが良いとか悪いとかの議論はここでは避けるとして、とある環境でSubversionしか使えない状況であるが中央リポジトリを立ててもらえないという残念な状況に陥ることがありました。
そこで無理やりではありますが、SubversionのリポジトリをGitのそれのように共有フォルダにプッシュしたり、プルしたりできるようにしてみたツールがこれです。

ご注意

なお、スクリプト内部でrobocopyコマンドを使っていますので、Windows専用です。

リポジトリ構造

まず、ツールを動作させるためのSVNリポジトリの構造はこんな風にします。

ローカル側

C:\temp\testrepos
Copy
│
├─.svnrepos
│       ここの中身がSVNリポジトリ
│
├─wc
│       ここの中身がreposをチェックアウトした内容
└─scripts
       push.ps1
       pull.ps1

リモート側

N:\repos\svn
Copy
│
├─testrepos.svnrepos
│
└─testrepos.lnk (TortoiseSVNでRepository Browserを開くショートカット)

スクリプト

push.ps1
Copy
$reposName = 'testrepos'
$remoteBaseDir = 'N:\repos\svn\'

# プッシュを実装
function doPush {
    param(
        $reposName,
        $remoteBaseDir,
        $localReposDir,
        $remoteReposDir
    )
    # リポジトリフォルダのコピーを実行
    robocopy $localReposDir $remoteReposDir /MIR /E /R:1
    
    if ($remoteBaseDir.Substring(1, 1) -eq ':') {
        $remoteReposSvnPath = 'file:///' + ($remoteReposDir -replace '\\', '/') + '/trunk'
    } else {
        $remoteReposSvnPath = 'file:' + ($remoteReposDir -replace '\\', '/') + '/trunk'
    }

    # TortoiseSvnのRepository Browserのショートカットを作成
    $wshShell = New-Object -ComObject WScript.Shell
    $linkPath = Join-Path $remoteBaseDir "$($reposName).lnk"
    $shortcut = $wshShell.CreateShortcut($linkPath)
    $shortcut.TargetPath = "`"C:\Program Files\TortoiseSVN\bin\TortoiseProc.exe`""
    $shortcut.Arguments = "/command:repobrowser /path:`"$($remoteReposSvnPath)`""
    $shortcut.IconLocation = "C:\Program Files\TortoiseSVN\bin\TortoiseProc.exe"
    $shortcut.WorkingDirectory = "C:\Program Files\TortoiseSVN\bin"
    $shortcut.Save()
}

# リポジトリのリビジョン番号を調べる
# リポジトリがない場合、リビジョン0を返却
function getReposRevision {
    param(
        $reposDir # リポジトリのディレクトリ
    )
    if (($reposDir -ne $null) -and (Test-Path $reposDir) -eq $True) {
        return [int](Get-Content (Join-Path (Join-Path $reposDir 'db') 'current'))
    } else {
        return [int]0
    }
}


# 同期化したリビジョンを更新
function updateSyncRevision{
    param(
        $newRevision
    )
    Set-Content -Path (Join-Path $PSScriptRoot \..\.syncrevision) -Value $newRevision
}

######################################
# ここからメイン処理
######################################

$localReposDir = Resolve-Path (Join-Path $PSScriptRoot \..\.svnrepos)
$remoteReposDir = Join-Path $remoteBaseDir "$($reposName).svnrepos"

# リポジトリのリビジョンを調べる
$localRevision = getReposRevision -reposDir $localReposDir
$remoteRevision = getReposRevision -reposDir $remoteReposDir
if ((Test-Path (Join-Path $PSScriptRoot \..\.syncrevision))) {
    $syncRevision = [int](Get-Content (Join-Path $PSScriptROot \..\.syncrevision))
} else {
    $syncRevision = [int]0
}
Write-Host "local  : $localRevision"
Write-Host "remote : $remoteRevision"
Write-Host "sync   : $syncRevision"

# 前回同期時からローカルリビジョンが同じ場合、push不要
if ($localRevision -eq $syncRevision) {
    Write-Host '前回同期化時からローカルリポジトリに更新がないため、Pushの必要がありません。'
    exit 0

# 前回同期化時よりもローカルリビジョンが下がった場合、状態が変
} elseif ($localRevision -lt $syncRevision) {
    Write-Host "状態が変です。前回同期化時よりもローカルリポジトリが古くなっています。前回同期化時のリビジョン:$syncRevision、ローカルのリビジョン:$localRevision"
    exit 1

# 前回同期化後にローカルリビジョンが上がった場合、push可能
} else {
    # Write-Host 'Push可能です。'
    
    # 前回同期化時よりもリモートリビジョンが上がっている場合
    if ($syncRevision -lt $remoteRevision) {
        Write-Host 'リモートが最新です。先にPullして下さい。'
        Write-Host '※Pullするとローカルにコミットした内容は失われますので、データを退避するなりして備えて下さい。'
        exit 0

    # 前回同期化時よりもリモートリビジョンが下がっている場合
    } elseif ($syncRevision -gt $remoteRevision) {
        Write-Host "状態が変です。前回同期時よりもリモートリポジトリが古くなっています。前回同期化時のリビジョン:$syncRevision、リモートのリビジョン:$remoteRevision"
        exit 1

    # 前回同期化時とリモートリビジョンが同じ場合
    } else {
        # Write-Host 'Push可能です。'
        
        # プッシュを実行
        doPush -reposName $reposName `
               -remoteBaseDir $remoteBaseDir `
               -localReposDir $localReposDir `
               -remoteReposDir $remoteReposDir

        # 同期化したリビジョンを更新
        updateSyncRevision -newRevision $localRevision
    }
}
pull.ps1
Copy
$reposName = 'testrepos'
$remoteBaseDir = 'N:\repos\svn\'

# プルを実装
function doPull {
    param(
        $localReposDir,
        $remoteReposDir,
        $syncRevision,
        $localRevision
    )

    # ローカルリポジトリを退避
    robocopy $localReposDir (Join-Path $PSScriptRoot "\..\.svnrepos_bk\.svnrepos_$((Get-Date).ToString('yyyyMMddHHmmss'))_r$($($syncRevision))to$($localRevision)") /MIR /E /R:1
    # リポジトリフォルダのコピーを実行
    robocopy $remoteReposDir $localReposDir /MIR /E /R:1
}

# リポジトリのリビジョン番号を調べる
# リポジトリがない場合、リビジョン0を返却
function getReposRevision {
    param(
        $reposDir # リポジトリのディレクトリ
    )
    if (($reposDir -ne $null) -and (Test-Path $reposDir) -eq $True) {
        return [int](Get-Content (Join-Path (Join-Path $reposDir 'db') 'current'))
    } else {
        return [int]0
    }
}

# 同期化したリビジョンを更新
function updateSyncRevision{
    param(
        $newRevision
    )
    Set-Content -Path (Join-Path $PSScriptRoot \..\.syncrevision) -Value $newRevision
}

######################################
# ここからメイン処理
######################################

$localReposDir = Resolve-Path (Join-Path $PSScriptRoot \..\.svnrepos)
$remoteReposDir = Join-Path $remoteBaseDir "$($reposName).svnrepos"

# リポジトリのリビジョンを調べる
$localRevision = getReposRevision -reposDir $localReposDir
$remoteRevision = getReposRevision -reposDir $remoteReposDir
if ((Test-Path (Join-Path $PSScriptRoot \..\.syncrevision))) {
    $syncRevision = [int](Get-Content (Join-Path $PSScriptROot \..\.syncrevision))
} else {
    $syncRevision = [int]0
}
Write-Host "local  : $localRevision"
Write-Host "remote : $remoteRevision"
Write-Host "sync   : $syncRevision"

# 前回同期時とリモートリビジョンが同じ場合、pull不要
if ($remoteRevision -eq $syncRevision) {
    Write-Host '前回同期化時からリモートリポジトリに更新がないため、Pullの必要がありません。'
    exit 0

# 前回同期化時よりもリモートリビジョンが下がった場合、状態が変
} elseif ($remoteRevision -lt $syncRevision) {
    Write-Host "状態が変です。前回同期化時よりもリモートリポジトリが古くなっています。前回同期化時のリビジョン:$syncRevision、リモートのリビジョン:$remoteRevision"
    exit 1

# 前回同期化後にリモートリビジョンが上がった場合、pull可能
} else {
    # Write-Host 'Pull可能です。'
    
    # 前回同期化時よりもローカルリビジョンが上がっている場合、確認メッセージを入れてからPullする
    if ($syncRevision -lt $localRevision) {
        $objYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "実行する"
        $objNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "やめておく"
        $objOptions = [System.Management.Automation.Host.ChoiceDescription[]]($objYes, $objNo)
        $objMessage = @"
リモートが最新ですが、ローカルにもコミットしています。
ローカル: $($syncRevision) -> $($localRevision)
リモート: $($syncRevision) -> $($remoteRevision)
※Pullするとローカルにコミットした内容は失われますので、データを退避するなりして備えて下さい。
"@
        $resultVal = $host.ui.PromptForChoice('実行しますか?', $objMessage, $objOptions, 1)
        if ($resultVal -ne 0) {
            Write-Host "処理が中断されました。"
            exit 0
        }
    }
    
    # プルを実行
    doPull -localReposDir $localReposDir `
           -remoteReposDir $remoteReposDir `
           -syncRevision $syncRevision `
           -localRevision $localRevision

    # 同期化したリビジョンを更新
    updateSyncRevision -newRevision $remoteRevision

    exit 0
}

挙動

ローカル側

C:\temp\testrepos
Copy
│
├─.svnrepos
│       ここの中身がSVNリポジトリ
│
├─wc
│       ここの中身がreposをチェックアウトした内容
├─scripts
│      push.ps1
│      pull.ps1
│ 
└─.syncrevision  ※同期したときのリビジョン番号を保持しているテキストファイル

C:\temp\testrepos
Copy
│
├─.svnrepos
│       ここの中身がSVNリポジトリ
│
├─.svnrepos_bk
│  ├─.svnrepos_yyyyMMddHHmmss_r4to6
│  ├─.svnrepos_yyyyMMddHHmmss_r8to10
│  ├─.svnrepos_yyyyMMddHHmmss_r12to16
│
├─wc
│       ここの中身がreposをチェックアウトした内容
├─scripts
│      push.ps1
│      pull.ps1
│ 
└─.syncrevision  ※同期したときのリビジョン番号を保持しているテキストファイル

リモート側


参考

ソース