概要
DelphiのIDE(統合開発環境、V2005以降)では、
ソースやプロジェクトを保存するたびに「__history」という
隠しディレクトリに保存前のソースが連番でバックアップされていきます。
(ファイル名の形式:「元のソース名と拡張子.~番号~」)

このバックアップソースは、
[ツール]メニューの[オプション]にあるエディタ設定で
何個まで保持するか設定可能で「1~90」の間で任意の値を設定できます。(初期値は10)
また「バックアップ ファイルの作成」のチェックを外せばバックアップ機能自体をオフにできます。
(バックアップしたソースもディスクの容量を多少は消費するため、完全に不要な場合はオフに設定して下さい。)



 
ここで設定した上限数をこえて保存されると、
古いバックアップから順に削除されていきます。
『たったの最新90回までしかバックアップが保持されない!
 無制限にバックアップできないの?』
と思われる方もおられるかもしれません。
(ソースを頻繁に保存する方だと90回なんてすぐですよね……)
そこで今回のTipsでは、ツールを作成して
これらのローカルのソースのバックアップをさらにバックアップして
古いソース履歴も保管し続ける手順をご紹介します。
(別のフォルダに毎回手動でコピーしていっても良いのですが、
 いつ90回をこえるか不確実なのでツールで自動化するのが確実です。)
ツールの作成(画面設計)
今回はこちらのような画面を作成しました。

それぞれ以下のような動作を想定しています。
- 画面生成時
- 別のテキストからバックアップ対象のフォルダパス一覧を読み込んでTMemoに表示します。
 - 特定の実行時引数付きで起動した場合、バックアップ処理を自動実行して画面を閉じます。
(タスクスケジューラやスタートアップでの呼出を想定) 
 - 「保存先を開く」ボタン
- バックアップ先のフォルダを開きます。
 
 - 「バックアップ実行」ボタン
- バックアップ処理を手動実行します。
 
 
ツールの作成(ロジック記述)
ここからは、上記のそれぞれの処理(イベント)について記載していきます。
中身を紐解けば、そこまで上級テクニックは使っていません。
※それぞれのロジックはあくまで一例のため、要件に応じてカスタマイズして下さい。
画面生成時(FormCreate)
{*******************************************************************************
 目的: 画面生成時処理
 引数:
 戻値:
*******************************************************************************}
procedure TForm1.FormCreate(Sender: TObject);
const
  cPath_List = 'Historier_List.txt';
begin
  // 対象フォルダパス一覧の読込(EXEと同階層の対象テキスト)
  if (FileExists(ExtractFilePath(Application.Exename) + cPath_List)) then
  begin
    try
      Memo1.Lines.LoadFromFile(ExtractFilePath(Application.Exename) + cPath_List);
    except
 // 例外を無視
    end;
  end;
  // バックアップ先のフォルダ指定
  //(sDocはグローバルString変数)
  //(本サンプルではEXEと同階層に「B17」フォルダを配置する想定)
  sDoc := ExtractFilePath(Application.Exename) + 'B17\';
  if not(DirectoryExists(sDoc)) then
  begin
    ForceDirectories(sDoc); // フォルダが無いと保存時エラーになるので生成しておく
  end;
  // 特定の実行時引数付きで起動した場合はサイレントで自動バックアップ
  if (ParamStr(1) = 'SILENT') then
  begin
    try
      DoHistorier;  // DoHistorier関数については後述
    finally
      // サイレント実行後、画面を閉じる
      //(FormCreateやFormShowの間はCloseが効かないため、これで閉じる)
      PostMessage(Handle, WM_Close, 0, 0);
    end;
  end;
end;
「保存先を開く」ボタン押下時
(ShellExecuteを使うため、uses節にWinapi.ShellApiが必要)
{*******************************************************************************
 目的: 保存先を開くボタン 押下時処理
 引数:
 戻値:
*******************************************************************************}
procedure TForm1.bbtnOpenClick(Sender: TObject);
begin
  // バックアップ先のフォルダを開く
  ShellExecute(Application.Handle,'OPEN',PChar(sDoc),'','',SW_SHOW);
end;
「バックアップ実行」ボタン押下時
{*******************************************************************************
 目的: バックアップ実行ボタン 押下時処理
 引数:
 戻値:
*******************************************************************************}
procedure TForm1.bbtnBackupClick(Sender: TObject);
begin
  if DoHistorier then  // DoHistorier関数については後述
  begin
    Application.MessageBox('処理完了しました。', 'バックアップ', MB_ICONASTERISK);
  end;
end;
バックアップ処理
この中で使用している、
フォルダ内のファイル名一覧を取得する関数「ListDirTree」についてはこちらを参照。
(※ListDirTree内で使うため、uses節にSystem.IOUtils, System.Typesが必要)
{*******************************************************************************
 目的: バックアップ実行処理
 引数:
 戻値: 処理成否
*******************************************************************************}
function TForm1.DoHistorier: Boolean;
var
  i, j: Integer;
  sPath: String;
  sFold: String;
  sSave: String;
  slFileList: TStringList;
begin
  Result := True;
  slFileList := TStringList.Create;
  try
    for i := 0 to (Memo1.Lines.Count - 1) do
    begin
      sPath := Memo1.Lines[i];
      slFileList.Clear;
      ListDirTree(sPath, slFileList, ldBoth);
      for j := 0 to (slFileList.Count - 1) do
      begin
        // フルパスに「__history」が含まれており、
        // かつフルパスが「~」で終わる場合にバックアップファイルと判断する
        if (Pos('__history', LowerCase(slFileList[j])) > 0) and
           (Copy(slFileList[j], Length(slFileList[j]), 1) = '~') then
        begin
          // コピー元フォルダパス取得(パスに使えない半角「:」「\」を全角に変換する)
          sFold := ExtractFileDir(slFileList[j]); // ~~Dir使用時、最後の「\」は除かれる
          sFold := StringReplace(sFold, ':', ':', [rfReplaceAll]);
          sFold := StringReplace(sFold, '\', '¥', [rfReplaceAll]);
          // コピー先ファイルパスの指定
          sSave := sDoc + sFold + '\' + ExtractFileName(slFileList[j]);
          // コピー先フォルダ作成
          if (not(DirectoryExists(ExtractFilePath(sSave)))) then
          begin
            ForceDirectories(ExtractFilePath(sSave));
          end;
          // コピー実行(既に同名のファイルがある場合はコピーしない)
          if (not(FileExists(sSave))) then
          begin
            Result := Result and // 失敗時ResultがFalseになる
                      CopyFile(PChar(slFileList[j]), PChar(sSave), False);
          end;
        end;
      end;
    end;
  finally
    FreeAndNil(slFileList);
  end;
  if not Result then
  begin
    Application.MessageBox('処理失敗したファイルがあります。', '警告', MB_ICONEXCLAMATION);
  end;
end;
これでプログラムを実行して「バックアップ実行」ボタンを押します。
すると、例えばMemo1に【C:\Support\07_WP_tips\T202401\B17_NEW】を指定していた場合、
【C:\Support\07_WP_tips\T202401\B17_NEW\__history】にあったソースが
【(中略)\B17_NEW\B17\C:¥Support¥07_WP_tips¥T202401¥B17_NEW¥__history】
というフォルダにコピーされます。
(緑字部分は元々のパスを、「:」「\」を全角文字に置き換えた文字列です)


この方法で「__history」フォルダのバックアップを取り続けることで、
90回よりも古い保存時バックアップソースを自動でさらにバックアップすることが可能です。
補足事項
この方法でバックアップを溜め続けると、
前述の通り、開発端末のディスク容量を多少は消費します。
特に画像を埋め込んだDFMなどはファイルサイズが大きくなりがちです。
バックアップ(本記事のツールでさらにバックアップ)したソースは
基本的に不具合発生時といった非常時に使われるものです。
平時においては定期的にZIP等に圧縮しておくなど、
ファイルサイズの節約を別途ご検討ください。
<サンプルソースダウンロード>
ここまで紹介してきたプログラムのサンプルソースは、以下のリンクからダウンロード可能です。
(※Delphi 10.2 Tokyoで作成しています。お客様にてコンパイルして下さい。)
=======================================
【免責事項】
本ページに掲載しているソースコードは情報提供の為のサンプルプログラムとなります。
これらのソースコードやサンプルプログラムを使用したことによって生じた、
いかなる障害・損失に関しても一切の責を負いかねますので、ご了承下さい。
=======================================