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. Samples,其中包含本文档中介绍的所有示例。

所有示例都有自己的 main 方法,因此您只需转到 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 方法解析该输出并转换为字符串列表。最后,我们为每个元素生成一个新的 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);
    }
}

此对象现在有名称和服务器的信息,以及一个相等覆盖可以帮助我们比较下一个示例中的分支。我们可以使用从 cm find 获得的所有参数来扩展此信息。


处理不同的服务器:复制器

Code: Samples/Replicator.cs

根据我们前面的示例并使用我们的 Branch 对象,我们现在要将创建的更改从一个存储库复制到另一个存储库。为了复制分支,我们需要该分支的完整规格以及目标存储库。这一过程可通过以下代码完成:

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

这段代码将调用 replicate 命令,然后解析结果并转换为一个名为 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 存储库,或连接到现有存储库以使用已保存的游戏。


创建您自己的 Plastic SCM GUI

Code: Samples/MiniGUI/MiniGUI.cs

Plastic SCM 的每个主要功能都可以通过命令行提供。因此,我们可以创建自己的插件,并可以创建完全自定义的用户界面。在此示例中,我们将创建一个小工具,它有一个待定更改视图和一个变更集列表。使用这个工具可以进行签入操作(包括注释),以及了解每次提交时更改了哪些文件。这个工具如下所示:

Plastic SCM Mini GUI

此选项卡项用于签入文件和添加注释。

Plastic SCM Mini GUI -“变更集”选项卡

此选项卡项显示变更集,以及对每个变更集所做的不同更改。

当我们模拟正常使用的行为时,控制台将自动输出正在执行的更改。

Plastic SCM Mini 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 许可下发布,因此您可以获取、修改此代码以及根据需要对其进行自定义。我们也感谢您的参与,因此欢迎您进行克隆和为您自己贡献的内容发送拉取请求 (pull requesting)。


上次更新

2019 年 3 月 22 日
  • 我们替换了对已弃用的存储库管理命令(如 cm mkrep)的引用,改为新的对应命令。