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 来启动任何其他程序,例如 difftool 或 semanticmerge。如果在 PATH 中可用,您可以直接调用它(如资源管理器);否则,您可以使用可执行文件的完整路径调用它。
开始使用
我们可以首先从 github 下载存储库。获取代码有两种方式:
- 将最后的更改下载到 zip 文件中。
- 使用 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 解决方案:
- CmdRunner,其中包含库。
- Samples,其中包含本文档中介绍的所有示例。
所有示例都有自己的 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);
此代码对我们的存储库执行基本查询。命令的原始输出应如下所示:
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 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 存储库,或连接到现有存储库以使用已保存的游戏。
创建您自己的 Plastic SCM GUI
Code: Samples/MiniGUI/MiniGUI.cs
Plastic SCM 的每个主要功能都可以通过命令行提供。因此,我们可以创建自己的插件,并可以创建完全自定义的用户界面。在此示例中,我们将创建一个小工具,它有一个待定更改视图和一个变更集列表。使用这个工具可以进行签入操作(包括注释),以及了解每次提交时更改了哪些文件。这个工具如下所示:
此选项卡项用于签入文件和添加注释。
此选项卡项显示变更集,以及对每个变更集所做的不同更改。
当我们模拟正常使用的行为时,控制台将自动输出正在执行的更改。
我们不会关注与 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)的引用,改为新的对应命令。