我们在开发 Plastic SCM 时,按照用户、客户和我们内部时间表的要求添加相应的功能,因此有些功能我们没有时间和资源去完成。幸运的是,感谢社区,我们已经能够开发更多插件和功能。
扩展 Plastic SCM 的方法之一是使用 CmdRunner,这是构建在命令行之上的自动化层。它使用 C# 构建在 .NET 上,并且可在我们的 Github 存储库中公开获得,因此您可以进行分叉和参与。
Plastic SCM 的命令行具有 --machinereadable 和 --format 等参数,可以让我们自定义工具的输出,方便外部工具进行解析。适用于 Eclipse、IntelliJ、TeamCity、Hudson/Jenkins、Crucible、Bamboo 等的插件完全基于命令行客户端,因此有很多扩展可能性。
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 下载存储库。获取代码有两种方式:
如果您想用自己的命令进行扩展,则可以对存储库进行分叉,从而使用您自己的存储库来同步。然后,如果您想回传自己的成果,则可以发出拉取请求,这样,您的更改将被审查并合并回父存储库。
下载包后,可以找到一个包含以下两个项目的 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);
此代码对我们的存储库执行基本查询。命令的原始输出应如下所示:
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
命令的原始输出如下所示:
如果在目标存储库上创建了新分支,则名称将显示在末尾。命令完成后,结果会像前面的示例一样解析,使用每个操作的结果创建一个新的 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 是后台线程。
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 存储库,或连接到现有存储库以使用已保存的游戏。
Code: Samples/MiniGUI/MiniGUI.cs
Plastic SCM 的每个主要功能都可以通过命令行提供。因此,我们可以创建自己的插件,并可以创建完全自定义的用户界面。在此示例中,我们将创建一个小工具,它有一个待定更改视图和一个变更集列表。使用这个工具可以进行签入操作(包括注释),以及了解每次提交时更改了哪些文件。这个工具如下所示:
此选项卡项用于签入文件和添加注释。
此选项卡项显示变更集,以及对每个变更集所做的不同更改。
当我们模拟正常使用的行为时,控制台将自动输出正在执行的更改。
我们不会关注与 GUI 相关的方面,只关注三个特定按钮(签入、刷新以及变更集视图刷新)的行为。
显示的所有示例都对 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)。