我们在开发 Plastic SCM 时,按照用户、客户和我们内部时间表的要求添加相应的功能,因此有些功能我们没有时间和资源去完成。幸运的是,感谢社区,我们已经能够开发更多插件和功能。
扩展 Plastic SCM 的方法之一是使用
CmdRunner
,这是构建在命令行之上的自动化层。它使用 C# 构建在 .NET 上,并且可在我们的 Github 存储库中公开获得,因此您可以进行分叉和参与。
Plastic SCM 的命令行具有
--machinereadable
和
--format
等参数,可以让我们自定义工具的输出,方便外部工具进行解析。适用于 Eclipse、IntelliJ、TeamCity、Hudson/Jenkins、Crucible、Bamboo 等的插件完全基于命令行客户端,因此有很多扩展可能性。
在我们开始之前:
这些示例需要在您的机器上安装一个可运行的 Plastic SCM 客户端。您可以在
此处
获得免费许可证(最多可供 5 名开发者使用)。
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 中可用,您可以直接调用它(如资源管理器);否则,您可以使用可执行文件的完整路径调用它。
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;
一旦我们拥有所有变更集,我们便可以比较它们,如果它们不匹配,则必须复制分支。整个这一过程会以提示框的形式显示给用户:
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
添加和删除标记为已添加和已删除的项。
通知中包含已签入的更改数量以及每个更改的简短描述。
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 存储库,或连接到现有存储库以使用已保存的游戏。
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 内部完成的许多事项。
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
包含以下方面的代码:
创建空的存储库。
创建具有分支的存储库。
添加和签入存储库内的项。
添加和修改工作区内的项。
解析输出,使其成为字符串数组列表。
检索当前的临时工作区。