プロジェクトのメタデータにGitのコミットハッシュを埋め込むバージョン管理連携
MS Projectを用いた大規模なプロジェクトマネジメントにおいて、長年エンジニアを悩ませてきた普遍的な課題がある。それは、「今、手元にあるこの `.mpp` ファイルが、Gitリポジトリのどのコミット時点の状態と同期しているのか」を正確に把握する術が標準機能には存在しないという事実だ。
コード(VBAモジュールや外部スクリプト)とスケジュール(WBS、リソースアサイン)が乖離した瞬間、プロジェクトは暗礁に乗り上げる。バックアップとしてのファイルを乱立させ、日付や「最終版.mpp」といった無意味な文字列で管理する泥臭いワークフローは、もはやシニアエンジニアの選択肢たり得ない。
今回は、Project VBAのイベントハンドリング、WScriptを用いたGitプロセスの非同期ハッキング、そしてメタデータ(プロジェクトプロパティ)の極限まで最適化された書き込み手法を統合し、「保存時に自動でGitコミットハッシュをプロジェクトのDNAに刻み込む」ためのアーキテクチャを解説する。
—
1. アーキテクチャの全体像と設計思想
本機構の核心は、MS Projectのライフサイクルイベント(`FileSave` または `ProjectBeforeSave`)をフックし、OSのシェルを叩いてGitの最新コミットハッシュを取得、それをProjectの「カスタムプロジェクトプロパティ」へ不可視のメタデータとして埋め込む点にある。
ここで考慮すべき技術的要件は以下の通りだ。
1. パフォーマンスの担保: 保存操作のたびに重い外部プロセスを同期実行し、UIスレッドをブロックしてはならない。
2. レガシー環境への配慮: 組織内の端末には、旧型のOffice環境やセキュリティポリシーで制限されたPowerShell実行環境が混在している。WScript.Shellを用いた堅牢なパイプ処理が必須となる。
3. オブジェクトのライフサイクル管理: VBAからCOMオブジェクトや外部シェルを操作する際、メモリリークは致命傷となる。参照の即座の解放(`Set … = Nothing`)を徹底する。
—
2. 実装コード:`ThisProject` モジュール
以下のコードを、対象となるMS Projectファイルの `ThisProject` クラスモジュールに実装する。
Option Explicit
‘ =========================================================================
‘ クラス名: ThisProject (イベントハンドラ)
‘ 概要: プロジェクト保存時にGitのHEADコミットハッシュをカスタムプロパティに埋め込む
‘ =========================================================================
Private Sub Project_BeforeSave(ByVal pj As Project, ByRef Cancel As Boolean)
On Error GoTo ErrorHandler
Dim gitHash As String
Dim gitBranch As String
Dim repoPath As String
‘ プロジェクトファイルの親ディレクトリをGitリポジトリのルートと仮定
repoPath = GetParentDirectory(pj.FullName)
If repoPath = “” Then
‘ パスが未保存(新規ファイル等)の場合はスキップ
Exit Sub
End If
‘ Gitから最新のコミットハッシュとブランチ名を取得
gitHash = GetGitMetadata(repoPath, “HEAD”)
gitBranch = GetGitMetadata(repoPath, “branch”)
If gitHash = “” Then
‘ Gitリポジトリではない、または取得に失敗した場合は処理を継続(保存自体は妨げない)
Exit Sub
End If
‘ プロジェクトのカスタムプロパティへ書き込み
Call SetCustomProperty(pj, “Git_CommitHash”, gitHash)
Call SetCustomProperty(pj, “Git_Branch”, gitBranch)
Call SetCustomProperty(pj, “Git_SyncTimestamp”, Format(Now, “yyyy-mm-dd hh:nn:ss”))
Exit Sub
ErrorHandler:
‘ 予期せぬエラーでプロジェクトの保存を阻害しないためのフェイルセーフ
Debug.Print “[Git Integration Error] ” & Err.Description
Resume Next
End Sub
‘ ————————————————————————-
‘ 外部プロセスを叩き、Gitの情報を取得する関数
‘ ————————————————————————-
Private Function GetGitMetadata(ByVal workingDir As String, ByVal queryType As String) As String
Dim wsh As Object
Dim execObj As Object
Dim cmd As String
Dim result As String
On Error GoTo CleanUp
‘ WScript.Shellの生成
Set wsh = CreateObject(“WScript.Shell”)
‘ カレントディレクトリをプロジェクトの場所に移動しつつGitコマンドを実行
‘ 標準出力のみをキャプチャするため、エラー出力はリダイレクトする
If queryType = “HEAD” Then
cmd = “cmd.exe /c cd /d “”” & workingDir & “”” && git rev-parse –short HEAD 2>nul”
ElseIf queryType = “branch” Then
cmd = “cmd.exe /c cd /d “”” & workingDir & “”” && git rev-parse –abbrev-ref HEAD 2>nul”
Else
Exit Function
End If
‘ 非同期実行オブジェクトの取得
Set execObj = wsh.Exec(cmd)
‘ プロセスが終了するまで待機(タイムアウト処理を入れるのが望ましいが簡易的に標準ストリームを読む)
Do While execObj.Status = 0
DoEvents
Loop
If execObj.ExitCode = 0 Then
result = execObj.StdOut.ReadAll
‘ 改行コードの除去
result = Replace(Replace(result, vbCr, “”), vbLf, “”)
GetGitMetadata = Trim(result)
Else
GetGitMetadata = “”
End If
CleanUp:
‘ オブジェクトの明示的解放によるメモリ最適化
Set execObj = Nothing
Set wsh = Nothing
End Function
‘ ————————————————————————-
‘ カスタムプロジェクトプロパティへの安全な書き込み(存在チェック・更新)
‘ ————————————————————————-
Private Sub SetCustomProperty(ByVal pj As Project, ByVal propName As String, ByVal propValue As String)
Dim prop As DocumentProperty
Dim propExists As Boolean
propExists = False
On Error Resume Next
For Each prop In pj.CustomDocumentProperties
If prop.Name = propName Then
prop.Value = propValue
propExists = True
Exit For
End If
Next prop
On Error GoTo 0
‘ プロパティが存在しない場合は新規追加
If Not propExists Then
On Error Resume Next
pj.CustomDocumentProperties.Add Name:=propName, _
LinkToContent:=False, _
Type:=msoPropertyTypeString, _
Value:=propValue
On Error GoTo 0
End If
‘ コレクション内オブジェクトの解放
Set prop = Nothing
End Sub
‘ ————————————————————————-
‘ フルパスからディレクトリパスを抽出するヘルパー
‘ ————————————————————————-
Private Function GetParentDirectory(ByVal fullPath As String) As String
Dim pos As Long
pos = InStrRev(fullPath, “\”)
If pos > 0 Then
GetParentDirectory = Left(fullPath, pos – 1)
Else
GetParentDirectory = “”
End If
End Function
—
3. コードの深層解説:シニアエンジニアの視点
A. イベントハンドラの選択とフェイルセーフ
`Project_BeforeSave` を採用した理由は、ユーザーが「保存」を押した瞬間に必ず割り込み、ストレージへ書き込まれるメタデータの整合性を強制するためだ。
ここで最も重要なのは、「万が一Gitがインストールされていない環境や、リポジトリ外のローカルフォルダで保存された場合でも、プロジェクトの保存自体を絶対に失敗(クラッシュ)させてはならない」という原則である。
コード内の `On Error GoTo ErrorHandler` と `Resume Next` の組み合わせは、インフラ起因のエラーがプロジェクトファイルの破損や保存失敗に繋がる最悪のシナリオを完全に防ぐための防壁となっている。
B. COMオブジェクトのライフサイクルとメモリ管理
VBAにおける最大の悪習は、`CreateObject` やコレクションの走査で生成されたオブジェクトを野放しにすることだ。特にMS Projectのセッションが長期間持続する環境では、些細なメモリリークがCOMコンポーネントの肥大化を招く。
上記のコードでは、`GetGitMetadata` や `SetCustomProperty` のスコープ脱出直前で、必ず `Set obj = Nothing` を明示的に実行している。ガベージコレクションに依存しない、リソース管理の鉄則を遵守している。
C. WScript.Shellの `Exec` メソッドによる同期制御
`WshShell.Run` は非同期実行であり戻り値のキャプチャが困難、かといって簡易的なシェルスクリプト経由ではセキュリティポリシーに引っかかる。
`WshShell.Exec` を用いることで、標準出力(`StdOut`)を直接VBAのメモリ空間にストリームとして取り込むことが可能になる。`Do While execObj.Status = 0` によるポーリングループは、UIのスレッドを完全には殺さずにプロセスの終了をスマートに待機させるための、レガシー環境における最適解である。
—
4. システム間連携への拡張:CI/CDパイプラインとの結合
この設計の真価は、単にローカルでのバージョン追跡に留まらない。
埋め込まれたカスタムプロパティ(`Git_CommitHash`)は、COMインターフェースを介して外部のC#アプリケーションやPythonスクリプトから一撃で読み取ることができる。
例えば、社内のサーバーサイドで動くバッチ処理(Pythonなど)から、以下のようにして(`.mpp`)ファイルを開くことなくメタデータを抽出することが可能だ。
import win32com.client
MS Projectを非表示で起動
project_app = win32com.client.Dispatch(“MSProject.Application”)
project_app.Visible = False
ファイルを読み取り専用で開く
project_app.FileOpenEx(
Name=”C:\\Projects\\MasterPlan.mpp”, ReadOnly=True, opentrustagreed=True
)
active_project = project_app.ActiveProject
埋め込まれたGitコミットハッシュの抽出
commit_hash = “Not Found”
branch_name = “Not Found”
for prop in active_project.CustomDocumentProperties:
if prop.Name == “Git_CommitHash”:
commit_hash = prop.Value
elif prop.Name == “Git_Branch”:
branch_name = prop.Value
print(f”Sync Target Branch: {branch_name} [Commit: {commit_hash}]”)
クリーンアップ
project_app.FileCloseEx(2) # pjDoNotSave
project_app.Quit()
この連携により、「どのコミット時点のソースコードに対して、どのバージョンのスケジュールが合意されていたか」を、CI/CDパイプライン上で完全に自動検証・監査できる体制が整う。
—
5. 総括
VBAは、しばしば「レガシーで時代遅れの言語」とやゆされる。しかし、それは使い手のアーキテクチャ設計能力が欠如している場合の言い訳に過ぎない。
OSの低レイヤー、プロセスのライフサイクル、そしてホストアプリケーションのイベントモデルを完全に掌握していれば、VBAはモダンな開発エコシステム(Git等)と極めて強固に統合された、現場の武器となる。
規律ある設計と妥協のないコードによって、あなたのプロジェクト管理環境を「属人化の泥沼」から「エンジニアリングの要塞」へと昇華させてほしい。
