CmdRunner:Plastic SCM API


簡介

當我們建置 Plastic SCM 時,我們新增使用者、客戶和內部時間軸所要求的功能,因此有一些是我們沒有時間和資源能夠處理的功能。還好有社群,我們能夠為它開發更多外掛程式和功能。

擴充 Plastic SCM 的方式之一就是使用 CmdRunner,這是在命令列上所建置的自動化層。它利用 C# 建置於 .NET 之上,而且在 Github 儲存庫上公開推出,因此您可以進行分支處理並貢獻內容。

Plastic SCM 的命令列具有 --machinereadable--format 等參數,能讓我們自訂此工具的輸出,以便外部工具能輕鬆進行剖析。Eclipse、IntelliJ、TeamCity、Hudson/Jenkins、Crucible、Bamboo 等工具的外掛程式完全是以命令列用戶端為基礎,因此有許多擴充可能性。

在我們開始之前:這些範例需要在您機器上安裝有效 Plastic SCM 用戶端。您可以在這裡取得免費授權 (最多可供 5 名開發者使用)。

Hello World

Code: Samples/HelloWorld.cs

展示包裝函式層的最佳方式是展示它的功用。以下是一個小範例:

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 啟動 difftoolsemanticmerge 之類的其他任何程式。如果可在 PATH 中使用,您可以直接呼叫它 (亦即瀏覽器);否則,您可以使用可執行檔的完整路徑來呼叫它。


開始使用

我們可以從 github 下載儲存庫開始使用。有兩種方式可取得程式碼:

  1. 將最後的變更下載為 zip 檔案
  2. 使用 git-sync (按一下這裡以取得詳細資訊),同步處理本機儲存庫和此儲存庫:

    $ cm repository create plastic-cmdrunner $ cm sync plastic-cmdrunner@localhost:8087 git https://github.com/PlasticSCM/plastic-cmdrunner.git

若要使用您自己的命令擴充儲存庫,您可為儲存庫建立分支,這會提供您自己的儲存庫以進行同步處理,之後如要回饋,您可以執行提取要求,而您的變更將會在經過審查後合併回上層儲存庫。

命令

重要:git-sync 需要 Plastic SCM 4.2 或更新版本。

下載套件後,您可以尋找具有兩個專案的 Visual Studio 2010 解決方案:

  1. CmdRunner,其中包含程式庫。
  2. 範例,其中包含本文件說明的所有範例。

所有範例都有各自的主要方法,因此您只需移至 Visual Studio 上的專案屬性,並選取要執行的範例即可。

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);

此程式碼會在儲存庫上執行基本查詢。此命令的原輸出應如下所示:

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 剖析輸出,並將該輸出轉換為字串清單。最後,針對每個元素產生新的分支物件。

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);
    }
}

此物件現在有名稱和伺服器的資訊,以及可協助我們在後續範例中比較分支的相等覆寫。我們可以使用可從 cm find 取得的所有參數來擴充此資訊。


處理不同的伺服器:複寫器

Code: Samples/Replicator.cs

根據先前的範例以及分支物件,我們現在會將已建立的變更從一個儲存庫複寫至另一個儲存庫。為了複寫分支,我們需要它的完整規格和目的地儲存庫。這可透過下列程式碼進行:

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 命令的原始輸出如下所示:

DataWritten Items 1 Revs 0 ACLs 0 Changesets 0 Labels 0 Applied labels 0 Links 0 Applied links 0 Attributes 0 Applied attributes 0 Reviews 0 Review comments 0 branch /main/task001

如果已在目標儲存庫上建立新分支,將會在最後顯示該名稱。完成命令後,會像上一個範例一樣剖析結果,建立具有每項作業結果的新 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

按照先前的範例,我們現在將追蹤分支上的變更,如此便可以將其從一個伺服器複寫至另一個伺服器。針對此案例,此程式碼將模擬一個儲存庫中的變更,如此我們就會在另一個儲存庫中收到通知。

我們首先需瞭解的是否有新的分支,然後據以尋找兩個伺服器上的分支,並新增只位於來源上的分支。

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 是背景執行緒。

背景執行緒


追蹤本地變更:CMbox

Code: Samples/CMbox/CMbox.cs

在此範例中,我們示範如何透過追蹤本機檔案變更及自動認可變更,將 Plastic SCM 轉換為類似 Dropbox 匣的應用程式。Dropbox 成功的原因之一在於簡單易用,且不太需要使用者互動,因為大多數工作都在背景自動完成。

我們需要做的第一件事是設定快顯視窗,並為設定變數取得伺服器資訊。如果我們不變更設定程式碼,IsSimulation 變數將設定為 true,並且將在暫時工作區中自動新增和變更檔案,我們只需輕鬆坐著靜觀其如何進行。

public CMbox()
{
    ConfigureMenu();
    string server = Configuration.ServerName;
    mSampleRep = SampleHelper.GenerateEmptyRepository(server);
 
    if (Configuration.IsSimulation)
        CMboxHelper.StartSimulation();
}

有了此設定後,我們可以每隔 10 秒呼叫名為 CommitUpdates 的函數。此函數會執行的第一個動作是,使用命令 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 statusmachinereadable 修飾詞,輸出可能如以下所示:

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 addcm 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 儲存庫,或連線至現有儲存庫以使用儲存的遊戲。


建立我們自己的 PlasticSCM GUI

Code: Samples/MiniGUI/MiniGUI.cs

Plastic SCM 的每一個主要功能,均可在命令列上使用。這能讓我們建立外掛程式,而且可用來建立完全自訂的使用者介面。在此範例中,我們建立了一個小工具,其中具有暫止的變更檢視變更集清單。並且能讓我們執行簽入作業 (包括註解),以及瞭解哪些檔案已在各個認可上變更。此介面如下所示:

Plastic SCM 小型 GUI

透過此索引標籤項目可簽入檔案,並新增註解。

Plastic SCM 小型 GUI - [變更集] 索引標籤

此索引標籤項目會顯示變更集,以及已在各個變更集上所做的不同變更。

在我們模擬一般使用行為時,主控台將會輸出自動完成的變更。

Plastic SCM 小型 GUI - 主控台輸出變更

我們不會著重於 GUI 相關層面,只會說明三個特定按鈕的行為,亦即簽入、重新整理,以及從變更集檢視重新整理。


變更集清單的 [重新整理] 按鈕

此按鈕會重新載入變更集清單,而且也會建立該變更集的內容清單。為此,我們要從 cm 使用下列命令:

  • find changeset (取得具有特定格式的變更集)。
  • log (獲得特定變更集後,我們可以取得已新增、已移動、已變更或已刪除的項目)。
private void RefreshChangesetList(object sender, EventArgs e)
{
    string cmdResult = CmdRunner.ExecuteCommandWithStringResult(
        "cm find changeset --nototal --format=\"{changesetid}#{date}#{comment}\"",
        SampleHelper.GetWorkspace());
 
    ArrayList results = SampleHelper.GetListResults(cmdResult, true);
 
    changesetList.Items.Clear();
    foreach (string item in results)
    {
        Changeset cset = new Changeset(item);
        cmdResult = CmdRunner.ExecuteCommandWithStringResult(
            string.Format("cm log {0} --csFormat=\"{{items}}\" --itemFormat=\"{{path}}#{{fullstatus}}#{{newline}}\"",cset.Id),
            SampleHelper.GetWorkspace());
 
        results = SampleHelper.GetListResults(cmdResult, true);
        foreach (string changedItem in results)
            cset.Changes.Add(new Item(changedItem));
 
        changesetList.Items.Add(cset);
    }
}

此程序與其他程序非常相似,會呼叫命令、剖析結果,並產生能讓我們在 GUI 中顯示資料的新物件。

Changeset 類別是簡化的表示法,其只包含 ID、日期、註解和變更清單。同時也包含剖析單一 cm find 命令列輸出所需的程式碼。

public class Changeset
{
    public string Date { get; set; }
    public string Id { get; set; }
    public string Comment { get; set; }
    public List<Item> Changes { get; set; }
 
    public Changeset(string output)
    {
        string[] parsed = output.Split('#');
        Id = parsed[0];
        Date = parsed[1];
        Comment = parsed[2];
        Changes = new List<Item>();
    }
 
    public override string ToString()
    {
        return string.Format("{0}: cs:{1}", Date, Id);
    }
}

Item 類別包含狀態和工作區的相對路徑。

public class Item
{
    public string Path { get; set; }
    public string Status { get; set; }
 
    public Item(string output)
    {
        if (string.IsNullOrEmpty(output))
        {
            Path = string.Empty;
            Status = string.Empty;
            return;
        }
 
        string[] parsed = output.Split('#');
        Path = parsed[0].Replace(SampleHelper.GetWorkspace().ToLowerInvariant(),
            "");
        Status = parsed[1];
    }
 
    public override string ToString()
    {
        if (string.IsNullOrEmpty(Status))
            return string.Empty;
 
        return string.Format("{0} ({1})", Path, Status);
    }
}

暫止的變更檢視上的 [重新整理] 按鈕

[重新整理] 按鈕會清除兩個清單、取得變更清單,以及從已變更的項目篩選已新增和已刪除的項目。它會使用 cm 的單一命令:cm status。完成的作業與我們在 CMBox 範例中完成的 GetChanges 相同。

private void Update(object sender, EventArgs e)
{
    itemsToCommit.Items.Clear();
    itemsNotAdded.Items.Clear();
 
    List<Change> mChanges = GetChanges();
    if (mChanges.Count == 0)
        return;
 
    foreach (var item in mChanges)
    {
        if ((item.ChangeType == ChangeType.Added)
            || (item.ChangeType == ChangeType.Deleted))
        {
            itemsNotAdded.Items.Add(item);
            continue;
        }
 
        itemsToCommit.Items.Add(item);
    }
}

如需將已新增和已刪除的項目加入至清單,只要按兩下即可。


暫止的變更檢視上的 [簽入] 按鈕

[簽入] 按鈕將會新增已新增至清單的項目 (已新增和刪除的項目),將會執行目前工作區狀態的簽入作業。最後,它會清除註解文字方塊,並使用最後變更來更新清單。

private void Checkin(object sender, EventArgs e)
{
    foreach (Change item in itemsToCommit.Items)
    {
        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(
        string.Format("cm ci -c=\"{0}\"", textBox1.Text),
        SampleHelper.GetWorkspace());
 
    textBox1.Text = string.Empty;
    Update(sender, e);
}

透過這些少數命令我們可以擁有自己的 Plastic SCM 用戶端,但仍需要處理錯誤、合併,以及許多在 GUI 內部進行的作業。


使用 CM Shell

所有顯示的範例都會對 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);

查看上一個布林值?這表示我們是使用 Shell 來載入命令,而且只會有一個執行中的 cm.exe。如果沒有執行中的 Shell,則會自動啟動新 Shell。這會等同於下列序列:

cm shell repository list workspace delete .

協助程式和設定

所有程式碼皆已準備就緒,只要在 Configuration.cs 中指定 Plastic SCM 本機伺服器即可執行。為了讓事情簡單化,所有範例也會為此作業建立新儲存庫,讓您可以在安全的環境中試用程式碼。

SampleHelper 包含下列用途的程式碼:

  • 建立空白儲存庫。
  • 建立具有分支的儲存庫。
  • 在儲存庫內新增並簽入項目。
  • 在工作區內新增並修改項目。
  • 剖析輸出,使其變成字串陣列清單。
  • 擷取目前的暫時工作區。

擴充

此程式碼是根據 MIT 授權所發佈的開放原始碼,因此您可以取得、修改和自訂該程式碼,以符合您的需求。我們也非常感謝您所貢獻的內容,因此隨時歡迎來複製並索取您自己貢獻的內容。


上次更新

2019 年 3 月 22 日
  • 我們已使用新的對應項目來取代 cm mkrep 等棄用儲存庫系統管理命令的參考資料。