Plastic SCM をビルドする際にはユーザー、お客様、社内のタイムラインから要望のあった機能を追加するので、追加のための時間やリソースを割けない機能もあります。幸いなことに、コミュニティのおかげでより多くの Plastic SCM 向けプラグインと機能の開発が可能になっています。
Plastic SCM を拡張する方法の 1 つとして、CmdRunner の使用があります。これはコマンドラインをベースとした自動化レイヤーです。C# を使用して .NET 上に構築されており、Github リポジトリ上で公開されるので、フォークや共同作成を行うことができます。
Plastic SCM のコマンドラインには --machinereadable や --format などのパラメーターがあり、これを使用してツールの出力をカスタマイズして、外部ツールで解析しやすくすることができます。Eclipse、IntelliJ、TeamCity、Hudson / Jenkins、Crucible、Bamboo、その他のプラグインはすべてコマンドラインクライアントをベースとしているため、さまざまな拡張の可能性があります。
Code: Samples/HelloWorld.cs
Plastic のラッパーレイヤーが何であるかを示す最もよい方法は、実際に何ができるかを示すことです。わかりやすい例がこちらです。
string cmVersion = CmdRunner.ExecuteCommandWithStringResult("cm version", Environment.CurrentDirectory); Console.WriteLine(string.Format("The cm version is: {0}", cmVersion));
.NET 方式での起動プロセスに慣れている方であれば、そのすべてのロジックをシンプルな静的メソッドに抽出し、通常であれば標準出力から取得するであろう出力を復元していることを理解できると思います。ある特定のサーバーにあるすべてのリポジトリの一覧を表示するなど、もう少し本格的なことをやってみましょう。
string server = "remoteserver:8087"; string repoList = CmdRunner.ExecuteCommandWithStringResult(string.Format("cm repository list {0}", server), Environment.CurrentDirectory); Console.WriteLine(string.Format("{0}", repoList));
この簡単なコードの出力は、次のようになります。
The cm version is: 5.4.16.633 1 default localhost:8084 4 cmdSamplesToyRepo localhost:8084また、さまざまな引数をどのように扱っているか見たことはありますか?このセクションを締めくくる前のちょっとしたヒントとして、CmdRunner を使用して、difftool や semanticmerge などの他のプログラムを起動できます。PATH にある場合は、直接(エクスプローラーから)呼び出すことができます。ない場合は、実行ファイルのフルパスを使用して呼び出すことができます。
GitHub からリポジトリをダウンロードすることで開始できます。コードを取得する方法は 2 つあります。
独自のコマンドで拡張する必要がある場合は、リポジトリをフォークできます。これにより、同期元となる独自のリポジトリが作成されます。後で貢献を反映する必要がある場合には、プルリクエストを発行してください。これにより、自分が加えた変更がレビューされ、親リポジトリにマージバックされます。
パッケージをダウンロードすると、次の 2 つのプロジェクトがある Visual Studio 2010 ソリューションが見つかります。
すべてのサンプルには独自の main メソッドがあるため、Visual Studio のプロジェクトのプロパティに移動して、実行するサンプルを選択するだけで済みます。
Code: Samples/ListBranches.cs
このサンプルでは、リポジトリで使用可能なすべてのブランチをリスト表示します。cm find の詳細については、こちらのガイドを確認してください。
string cmdResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm find branch on repositories '{0}' --format={{id}}#{{name}} --nototal", repository), Environment.CurrentDirectory); ArrayList results = SampleHelper.GetListResults(cmdResult, true); List<Branch> branches = GetBranchListFromCmdResult(results); foreach (Branch branch in branches) Console.WriteLine(branch);
このコードは、リポジトリに対して基本的なクエリを実行します。このコマンドの raw の出力は次のようになります。
3#/main 12#/main/task0 13#/main/task1 14#/main/task2 15#/main/task3 16#/main/task4 17#/main/task5 18#/main/task6 19#/main/task7 20#/main/task8 21#/main/task9
その後、サンプルヘルパーを使用して、メソッド GetListResults
でその出力を解析し、文字列のリストに変換します。最後に、各要素に対して新しい Branch オブジェクトを生成します。
public class Branch { public string Id { get; set; } public string Name { get; set; } public Branch(string output) { string[] parsed = output.Split('#'); Id = parsed[0]; Name = parsed[1]; } public override string ToString() { return Id + " - " + Name; } public override bool Equals(object obj) { if (!(obj is Branch)) return base.Equals(obj); return ((Branch)obj).Name.Equals(Name); } }
このオブジェクトには、名前とサーバー、および、以降のサンプルでブランチを比較するのに役立つ equal のオーバーライドの情報が含まれています。この情報を、cm find から取得できるすべてのパラメーターで拡張できます。
Code: Samples/Replicator.cs
前のサンプルを基に、Branch オブジェクトを使用して、1 つのリポジトリで作成された変更を別のリポジトリにレプリケートしていきましょう。ブランチをレプリケートするには、そのブランチの完全な指定と、同期先リポジトリが必要です。これは、次のコードを使用して実行できます。
private static ReplicationResult Replicate(Branch branch, string repository, string secondRepository) { Console.WriteLine("Replicating branch {0}", branch.Name); string cmdResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm replicate \"br:{0}@{1}\" \"rep:{2}\"", branch.Name, repository, secondRepository), Environment.CurrentDirectory); return new ReplicationResult(SampleHelper.GetListResults(cmdResult, true), branch); }
レプリケートコマンドが呼び出され、結果が解析されて、Replication Result
というシンプルなクラスに変換されます。Replicate
コマンドの生の出力は次のようになります。
ターゲットリポジトリで新規ブランチが作成されると、最後に名前が表示されます。コマンドが完了すると、前のサンプルのように結果が解析され、各操作の結果を含んだ新しい ReplicationResult
オブジェクトが作成されます。
class ReplicationResult { public long Items { get; set; } public Branch Branch { get; set; } public ReplicationResult(ArrayList cmdResult, Branch branch) { Branch = branch; string buffer = (string)cmdResult[1]; Items = long.Parse(buffer.Substring(buffer.LastIndexOf(' '))); } }
レプリケーション結果には、レプリケートされた項目の数と、関連するブランチが含まれます。これは、前に表示された項目を使って拡張することもできます。
最後に、レプリケーション全体の簡単なレポートが生成されます。
private static void PrintReplicationResult(List<ReplicationResult> resultList) { Console.WriteLine("Branches replicated: {0}" , resultList.Count); foreach (ReplicationResult item in resultList) Console.WriteLine("- {0} ({1} item{2})", item.Branch.Name, item.Items, item.Items == 1 ? "" : "s"); }
レプリケーションの出力は次のようになります。
Replicating branch /main Replicating branch /main/task0 Replicating branch /main/task1 Replicating branch /main/task2 Replicating branch /main/task3 Replicating branch /main/task4 Replication complete Branches replicated: 6 - /main (0 items) - /main/task0 (3 items) - /main/task1 (4 items) - /main/task2 (2 items) - /main/task3 (3 items) - /main/task4 (2 items)Code: Samples/Notifier.cs
前の例を基に、今度はブランチへの変更を追跡して、変更を 1 つのサーバーから別のサーバーへとレプリケートできるようにしましょう。このシナリオでは、コードで一方のリポジトリの変更をシミュレートすることにより、もう一方で通知を受け取れるようにします。
まず把握する必要があるのは、新しいブランチがあるかどうかということなので、両方のサーバーでブランチを検索し、ソースにしかないブランチを追加します。
List<Branch> GetBranchesToSync() { List<Branch> srcBranches = GetBranchesFromRepo(mSampleRep); List<Branch> dstBranches = GetBranchesFromRepo(mSecondRep); List<Branch> newBranches = new List<Branch>(); foreach (Branch item in srcBranches) { if (!dstBranches.Contains(item)) { newBranches.Add(item); continue; } if (HasChangesInBranch(item)) newBranches.Add(item); } return newBranches; }
次のステップは、共通のブランチに変更があるかどうかを確認することです。このプロセスのための最も簡単な方法は、cm find をもう一度使用することです。
private bool HasChangesInBranch(Branch branch) { string srcResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm find changeset where branch = 'br:{0}' on repositories '{1}' --format={{id}} --nototal", branch.Name, mSampleRep), Environment.CurrentDirectory); ArrayList srcResults = SampleHelper.GetListResults(srcResult, true); string dstResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm find changeset where branch = 'br:{0}' on repositories '{1}' --format={{id}} --nototal", branch.Name, mSecondRep), Environment.CurrentDirectory); ArrayList dstResults = SampleHelper.GetListResults(dstResult, true); return srcResults.Count != dstResults.Count; }
すべての変更セットが取得できたら、それらを比較し、一致しない場合はブランチをレプリケートする必要があります。このプロセスはすべて、バルーン内に表示されます。
バルーンをクリックすると、レプリカプロセスが起動します。もう一方のリポジトリでの作業をシミュレートするために、項目を継続的に追加してチェックインするバックグラウンドスレッドが存在するので、ユーザーは変更が表示されるのを待つだけです。このサンプルでは、スレッド番号が追加されています。画像のスレッド 9 がメインスレッドで、スレッド 10 がバックグラウンドスレッドです。
Code: Samples/CMbox/CMbox.cs
このサンプルでは、ローカルファイルの変更を追跡し、それらを自動的にコミットすることで、Plastic SCM を Dropbox のようなトレイアプリに変換する方法について説明します。Dropbox が成功した理由の 1 つは、シンプルさとユーザーの対応の少なさです。つまり、多くの操作がバックグラウンドで自動的に実行されるという点です。
最初に行わなければならないことは、ポップアウトウィンドウを設定することと、設定変数用のサーバー情報を取得することです。設定コード(IsSimulation
)を変更しなかった場合、変数は true に設定され、ファイルが一時ワークスペースに自動的に追加されて変更されるので、ユーザーは操作が実行されるのを座って待つだけです。
public CMbox() { ConfigureMenu(); string server = Configuration.ServerName; mSampleRep = SampleHelper.GenerateEmptyRepository(server); if (Configuration.IsSimulation) CMboxHelper.StartSimulation(); }
設定が完了したら、CommitUpdates
という関数を 10 秒ごとに呼び出します。この関数が最初に行うことは、コマンド cm status を使用して、ワークスペースの既存の変更を確認することです。
private List<Change> GetChanges() { List<Change> changes = new List<Change>(); string cmdResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm status --all --machinereadable"), SampleHelper.GetWorkspace()); ArrayList results = SampleHelper.GetListResults(cmdResult, true); for (int i = 1; i < results.Count; i++) changes.Add(new Change((string)results[i])); return changes; }
cm status を machinereadable 修飾子とともに使用した場合、出力は次のようになります。
STATUS 4 cmdSample5d4ce30 localhost:8084 PR c:\Users\rbisbe\AppData\Local\Temp\c10d4d2e-1e14-43e4-af1d-a24df76176d5\sampleb165621 False NO_MERGES PR c:\Users\rbisbe\AppData\Local\Temp\c10d4d2e-1e14-43e4-af1d-a24df76176d5\samplef3390c5 False NO_MERGES
生成された出力は、先にも示したように配列リストに変換され、各行は解析されて単一の Change
要素になります。
Code: /Samples/CMbox/Change.cs
class Change { public string Name { get; set; } public string FullPath { get; set; } public ChangeType ChangeType { get; set; } public Change(string resultRow) { string[] separated = resultRow.Split(' '); ChangeType = GetFromString(separated[0]); FullPath = separated[1]; Name = Path.GetFileName(separated[1]); } private ChangeType GetFromString(string source) { switch (source.ToUpperInvariant()) { case "AD+LD": case "LD+CO": return CmdRunnerExamples.ChangeType.Deleted; case "PR": case "AD": return CmdRunnerExamples.ChangeType.Added; case "CO": return CmdRunnerExamples.ChangeType.Changed; default: return CmdRunnerExamples.ChangeType.None; } } }
返される変更には、項目の名前(バルーンでの表示用)、項目のフルパス、および変更の種類が含まれます(結果の解析から復元されます)。
変更が復元された後、チェックインされていない変更がある場合はチェックイン操作が実行され、終了時にユーザーへの通知が表示されます。
private void CheckinUpdates(object sender, EventArgs e) { List<Change> mChanges = GetChanges(); if (mChanges.Count == 0) return; StringBuilder builder = new StringBuilder(); foreach (var item in mChanges) builder.Append(string.Format("{0} {1}\n", item.ChangeType, item.Name)); foreach (var item in mChanges) { if (item.ChangeType == ChangeType.Added) CmdRunner.ExecuteCommandWithStringResult( string.Format("cm add {0}", item.Name), SampleHelper.GetWorkspace()); if (item.ChangeType == ChangeType.Deleted) CmdRunner.ExecuteCommandWithStringResult( string.Format("cm rm {0}", item.Name), SampleHelper.GetWorkspace()); } CmdRunner.ExecuteCommandWithStringResult("cm ci ", SampleHelper.GetWorkspace()); mTrayIcon.ShowBalloonTip(3, string.Format("{0} new changes saved", mChanges.Count), string.Format("The following changes have been checked in.\n{0}", builder.ToString()), ToolTipIcon.Info); }
このサンプルでは、cm add と cm rm を使用して、追加済みおよび削除済みとしてマークされた項目の追加と削除も行っています。
通知には、チェックインされた変更の数と、各変更の簡単な説明が含まれます。
Code: Samples/Game.cs
このサンプルでは、ソース管理をゲームセーバーとして使用し、レベルを変更するたびに、そのレベル情報を使用して新しい変更セットが作成されるようにします。これにより、以前のゲームステータスを常に復元できるようになります。
このゲームでは、ランダムファイルの新規変更セットを作成することで、ステータスが保存されます。変更セットのコメントには、ゲームのステータスが含まれます。
private void SaveGame() { string item = SampleHelper.AddRandomItem(); SampleHelper. CheckinItem(item, string.Format("{0}#{1}", mColors, mScore)); }
これにより、ゲームのロード時には、すべての変更セットを検索し、コメントでフィルタリングして、そのコメントを解析するだけで済むようになります。各コメントは、SaveGame
という新しいクラス内で解析されます。これにより、スコアと色番号が格納されます。
private void ReloadSavedGames() { string cmdResult = CmdRunner.ExecuteCommandWithStringResult( string.Format("cm find changeset on repositories '{0}' --format={{comment}} --nototal", textBox1.Text), Environment.CurrentDirectory); ArrayList results = SampleHelper.GetListResults(cmdResult, true); listBox1.Items.Clear(); results.RemoveAt(0); foreach (string item in results) listBox1.Items.Add(new SavedGame(item)); }
このコンテンツは後でゲームにロードされます、これにより、前回のプレイ終了時と同じステータスでプレイを続行できるようになります。
「ロード」ボタンと「新規作成」ボタンを使用すると、このゲーム用の新しい Plastic SCM リポジトリを作成したり、既存のリポジトリに接続して保存済みのゲームを使用することができます。
Code: Samples/MiniGUI/MiniGUI.cs
Plastic SCM の主な機能はすべて、コマンドラインで実行できます。これにより独自のプラグインを作成できるようになっただけでなく、全カスタマイズ化したユーザーインターフェースを作成するために活用もできるようになったのではないでしょうか。このサンプルでは、保留中の変更ビューと変更セットの一覧がある小さなツールを作成しています。ツールではチェックイン操作(コメントを含む)を実行できるほか、各コミットで変更されたファイルを確認できます。このような見た目になります。
このタブ項目では、ファイルをチェックインしてコメントを追加できます。
このタブ項目には、変更セットと、各変更セットで加えられたさまざまな変更が表示されます。
ここでは通常の使用における動作についてシミュレーションを行っているため、コンソールには自動的に行われる変更が出力されるようになっています。
GUI 関連の側面には注視せず、3 つの具体的なボタンの動作、チェックイン、更新、変更セットビューからの更新にのみ注目していきます。
ここで示したすべてのサンプルは、cm.exe プロセスへの個別呼び出しを行うものですが、複数のコマンドを実行する必要がある場合は、同じ結果をより迅速に達成できる方法があります。
string repos = CmdRunner.ExecuteCommandWithStringResult("cm repository list", Environment.CurrentDirectory, true); string output; string error; CmdRunner.ExecuteCommandWithResult( "cm workspace delete .", Environment.CurrentDirectory, out output, out error, true);
最後の boolean 値に注目してください。これは、シェルを使用してコマンドをロードし、単一の cm.exe だけを実行することを意味します。実行中のシェルがない場合は、新しいシェルが自動的に起動されます。これは、次のシーケンスに相当するものです。
cm shell repository list workspace delete .
すべてのコードは Configuration.cs
に Plastic SCM のローカルサーバーを指定するだけで実行する準備が整うようになっています。便宜上、すべてのサンプルはそのサンプルを操作するための新規リポジトリも作成するため、セキュリティで保護された環境でコードを試すことができます。
SampleHelper
には、次のことを行うコードが含まれます。
このコードはオープンソースであり、MIT ライセンスの下で公開されているため、必要に合わせて入手や変更、カスタマイズを行うことができます。また、皆様からのご協力にも大変感謝しております。クローンとプルリクエストの発行により、ご助力いただくことについても、いつでも大歓迎です。