Git管理されたプロジェクトにおいて差分ファイルを納品する時に便利なスクリプトの紹介です。これは Git から特定のコミット間の差分ファイルを集め、それを ZIP 化し、いい感じのメールの文面にまとめるスクリプトです。
使用方法はソースコード中のリビジョンID、納品先プロジェクトルートフルパスを都度、手で書き換え、次のコマンドで実行します。すべて PHP 組み込み関数で記述されているので Windows、Git、PHPがそろっていれば、それだけで実行可能です。
php makeDiffFiles.php
すると次の画像の様に差分ファイルまとめが出力され、エクスプローラで開かれます。

<?php
/**
* ファイルを上書きして修正するタイプの納品対応用スクリプト
*/
main();
function main()
{
$projectRootPath = '納品先プロジェクトルートフルパス';
// Git のコミットのIDから差分ファイルらのパスを取得
$paths = getDiffFilePaths(
'e86f870040884033c27651a52b4b9aee441c513e',
'e8e65296e35ab44037dcc14722ae6eb64e243d84'
);
$mainDirName = 'fix_' . date('YmdHis');// 差分ファイル出力関連(ディレクトリ名など)で使う名前を指定
$rootDir = __DIR__ . '/patch/' . $mainDirName;// 差分ファイル出力先のルートディレクトリを指定
copyToDir($paths, $rootDir . "/" . $mainDirName);// 差分ファイルらを差分ファイル出力先ディレクトリ以下にコピー
// 差分ファイルを ZIP ファイルにまとめる
zip(
$rootDir . '/' . $mainDirName . '.zip',
$rootDir . "/" . $mainDirName,
// ZIP ファイルのパスワードは password_hash 関数で生まれたランダム文字列の末尾 8 文字を使用
$password = substr(password_hash(random_int(1e4, 1e5), PASSWORD_DEFAULT), -8)
);
// 差分ファイルを顧客に送付する際の定型文を生成
dumpMailTemplate($projectRootPath, $rootDir, $mainDirName, $password);
// 差分ファイル出力先をエクスプローラーで開いて終了
exec('explorer ' . str_replace('/', '\\', $rootDir));
}
/**
* メールで送る文面のテンプレートを生成
* @param string $productRoot
* @param string $rootDir
* @param string $mainName
* @param string $password
*/
function dumpMailTemplate(string $productRoot, string $rootDir, string $mainName, string $password): void
{
$tree = <<<TREE
これらの修正版のファイルをまとめた zip ファイル $mainName.zip を添付させていただきました。
解凍パスワードは $password です。
これを解凍し、$productRoot 以下に上書きしていただければ更新が反映されます。
上書き後は以下のファイル・ディレクトリ構成になります。
$productRoot
TREE;
// windows の tree コマンドで生成した差分ファイルの構成を木構造で取得
// 自分の環境ではコマンド実行結果の頭三行に不要な結果が出力されるので、これを行単位の配列への分解と array_slice でカット。implode で再構築
$tree .= implode(
"\n",
array_slice(
explode("\n", shell_exec('tree /A /F ' . $rootDir . '\\' . $mainName)),
3
)
);
// 生成した木構造文字列をテキストファイルに出力
file_put_contents("$rootDir/_tree.txt", $tree);
}
/**
* Git のコミットのIDから差分ファイルのパスを取得
* @param string $revisionId
* @param string $revisionId2
* @return false|string[]
*/
function getDiffFilePaths(string $revisionId, string $revisionId2)
{
// git diff コマンドでコミット間の差分のあるファイル群を名前だけ取得
// 改行で区切り、空文字列を消すことでファイル群のパスを格納した配列を生成
return array_filter(
explode(
"\n",
shell_exec("git diff --name-only $revisionId $revisionId2")
)
);
}
/**
* 与えられたファイルパスらを宛先ディレクトリにコピーする
* @param string[] $paths
* @param string $distDir
*/
function copyToDir(array $paths, string $distDir): void
{
foreach($paths as $from) {
// 特定のファイルについては次の様にコピーから除外
if(strpos($from, 'package.json') === 0) {
continue;
}
// 出力先ディレクトリ内に $mainName のディレクトリが用意され、その中に差分ファイルの生をコピー
app_copy($from, $distDir . '/' . $from);
}
}
/**
* ディレクトリ存在しませんエラー対策付きのコピー関数
* @param $src
* @param $dist
*/
function app_copy($src, $dist)
{
$path = pathinfo($dist);
// コピー先パスのディレクトリが存在しないならば生成
if(!file_exists($path['dirname']) && !mkdir($concurrentDirectory = $path['dirname'], 0777, true) && !is_dir($concurrentDirectory)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
}
// ファイルコピー実行
if(!copy($src, $dist)) {
echo "copy failed \n";
}
}
/**
* ZIPファイルを作る
* @param string $dist ZIPファイル出力先古パス
* @param string $src ZIPファイルにしたいディレクトリのパス
* @param string $password ZIPファイルに設定するパスワード
*/
function zip(string $dist, string $src, string $password)
{
$za = new ZipArchive();
$za->open($dist, ZIPARCHIVE::CREATE);
$za->setPassword($password);
zipAddRecursive($za, $src, '');
$za->close();
}
/**
* $src 以下のファイルをディレクトリ構造に従って再帰的に ZIP に登録
* @param ZipArchive $zip
* @param string $src
* @param string $parentPath
*/
function zipAddRecursive(ZipArchive $zip, string $src, string $parentPath = '')
{
$dh = opendir($src);
while(($entry = readdir($dh)) !== false) {
if($entry === '.' || $entry === '..') {
continue;
}
$localPath = $parentPath . $entry;
$fullpath = $src . '/' . $entry;
if(is_file($fullpath)) {
if(!$zip->addFile($fullpath, $localPath)) {
echo sprintf('Add file failed: %s', $localPath);
}
if(!$zip->setEncryptionName($localPath, ZipArchive::EM_AES_256)) {
echo sprintf('Set encryption failed: %s', $localPath);
}
} elseif(is_dir($fullpath)) {
$zip->addEmptyDir($localPath);
zipAddRecursive($zip, $fullpath, $localPath . '/');
}
}
closedir($dh);
}