解决 .resx 合并问题






4.88/5 (32投票s)
本文提供了一个控制台实用程序和一种扩展合并工具的方法,以确保在合并来自不同源代码管理分支的文件时,只显示实际的更改(而不是重新排序)。
引言
我过去 13 年左右一直在使用 Windows Forms,虽然它有一些怪癖,但总的来说使用起来相当直接。然而,Windows Forms 给我带来真正痛苦的一个场景是当我需要从源代码系统的两个分支合并一个 .resx 文件时。事实证明,Visual Studio 中的 Windows Forms 实现会在对窗体或自定义控件进行更改时重新排序(貌似是随机的).resx 文件中的元素。虽然这不会影响窗体或控件的行为(顺序无关紧要),但它会严重破坏你使用的任何合并/差异工具,因为每一次重新排序都会被视为一次更改。
本项目使用 .NET 3.5 编写,未在支持 LINQ 的其他任何配置上进行测试。如果您成功地将其与 .NET 4.5、.NET 4.0、.NET 3.0 或 .NET 2.0 SP1 一起使用,请留言概述您的体验,以便他人从中受益。
问题
在源代码系统的两个分支之间合并 Windows Forms 窗体或控件时,合并工具通常会显示许多虚假冲突,因为元素序列中的不显著更改被视为显著更改。
解决方案
本文介绍了一个简单的控制台应用程序,可用作预比较转换,按名称属性对 .resx 文件中的元素进行排序。虽然您可以独立使用此过滤器来修改 .resx 文件,然后进行比较,但许多合并/差异实用程序都可以配置为在比较和合并之前运行此转换。
由于这两个文件都以确定性的方式排序,因此合并/差异实用程序可以准确地确定哪些元素是新的和已更改的,而不会因为元素位置的偶然差异而引入虚假冲突。
实现
最初我以为实现 XSLT 转换来排序 .resx XML 中的各种元素是最简单的,但令我高兴的是,我可以使用 LINQ 轻松地实现相同的结果。项目中的全部代码如下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace SortRESX
{
//
// 0 cmd line params ==> input from stdin and output is stdout.
// 1 cmd line param ==> input source .resx (arg[0]). Output is stdout.
// 2 cmd line params ==> input source .resx (arg[0]). Output is target .resx (arg[1])
// The program reads the source and writes a sorted version of it to the output.
//
class Program
{
static void Main(string[] args)
{
XmlReader inputStream = null;
if (args.Length > 2)
{
ShowHelp();
return;
}
if (args.Length == 0) // Input resx is coming from stdin
{
try
{
Stream s = Console.OpenStandardInput();
inputStream = XmlReader.Create(s);
}
catch (Exception ex)
{
Console.WriteLine("Error reading from stdin: {0}", ex.Message);
return;
}
}
else // Input resx is from file specified by first argument
{
string arg0 = args[0].ToLower();
if( arg0.StartsWith(@"/h") || arg0.StartsWith(@"/?"))
{
ShowHelp();
return;
}
try
{
inputStream = XmlReader.Create(args[0]);
}
catch(Exception ex)
{
Console.WriteLine("Error opening file '{0}': {1}", args[0], ex.Message);
}
}
try
{
// Create a linq XML document from the source.
XDocument doc = XDocument.Load(inputStream);
// Create a sorted version of the XML
XDocument sortedDoc = SortDataByName(doc);
// Save it to the target
if (args.Length == 2)
sortedDoc.Save(args[1]);
else
{
Console.OutputEncoding = Encoding.UTF8;
sortedDoc.Save(Console.Out);
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
return;
}
//
// Use Linq to sort the elements. The comment, schema, resheader, assembly, metadata, data appear in that order,
// with resheader, assembly, metadata and data elements sorted by name attribute.
private static XDocument SortDataByName(XDocument resx)
{
return new XDocument(
new XElement(resx.Root.Name,
from comment in resx.Root.Nodes() where comment.NodeType == XmlNodeType.Comment select comment,
from schema in resx.Root.Elements() where schema.Name.LocalName == "schema" select schema,
from resheader in resx.Root.Elements("resheader") orderby (string) resheader.Attribute("name") select resheader,
from assembly in resx.Root.Elements("assembly") orderby (string) assembly.Attribute("name") select assembly,
from metadata in resx.Root.Elements("metadata") orderby (string)metadata.Attribute("name") select metadata,
from data in resx.Root.Elements("data") orderby (string)data.Attribute("name") select data
)
);
}
//
// Write invocation instructions to stderr.
//
private static void ShowHelp()
{
Console.Error.WriteLine(
"0 arguments ==> Input from stdin. Output to stdout.\n" +
"1 argument ==> Input from specified .resx file. Output to stdout.\n" +
"2 arguments ==> Input from first specified .resx file." +
"Output to second specified .resx file.");
}
}
}
没有什么复杂的。输入文件用于初始化 LINQ 的 XDocument XML 文档。调用 SortDataByName() 方法返回文档的排序版本,并将转换后的文件保存到目标路径或流。真正的繁重工作由 SortDataByName() 方法完成,其中包含一个 LINQ 语句。
LINQ 语句构建了一个新文档,其中只有一个根元素,其名称与源文档中的根元素相同。该元素的内容由六个 LINQ 查询定义,这些查询对应于目标中所需的节点组。
- 标准的注释节点。
- .resx 架构元素。
- resheader 元素,按名称属性排序。
- assembly 元素,按名称属性排序。
- metadata 元素,按名称属性排序。
- data 元素,按名称属性排序。
就是这样。SortRESX.exe 程序现在已准备好与合并/差异工具集成。
更新
我已经更新了代码和文章,以便该实用程序可以使用文件名命令行参数,或者使用 stdin 和 stdout 流,因此它可以直接用于在比较前预处理文件的比较工具。 以下任何方法均有效:
- SortRESX filename1.resx filename2.resx
- SortRESX < filename1.resx filename2.resx
- SortRESX < filename1.resx > filename2.resx
- Type filename1.resx | SortResx filename2.resx
- Type filename1.resx | SortResx > filename2.resx
如果命令参数为零,则实用程序从 stdin 读取并写入 stdout。 如果只有一个参数,它将从指定的文件读取并写入 stdout。 如果有两个参数,它们将被视为输入和输出文件名。
与合并/差异工具集成
许多商业合并/差异实用程序提供了一种机制,允许您在比较或合并文件之前,使用外部程序预处理具有特定扩展名的文件。通过将 SortRESX.exe 与 .resx 文件类型关联,可以在排序后的文件或流上进行比较,从而只显示显著的更改。要为此类工具将 SortRESX.exe 与 .resx 文件类型关联,您可以将 SortRESX.exe 复制到实用程序的安装目录中,然后按照实用程序中提供的说明将其与“ .resx”文件类型关联。
关注点
本项目中使用的相同 .resx 排序代码可以作为 Visual Studio 的一个插件,在每次保存 .resx 文件时执行排序。这种替代方法的好处是,如果所有 .resx 文件都已按此方式排序,则所有 diff/merge 工具的行为都不会出现异常。这种方法的缺点是,如果合并涉及在采用该插件之前创建的文件,那么上述合并问题仍然会显现出来。
历史
- 2009年6月3日:初始版本。
- 2009年6月15日:修改了介绍段落。
- 2014年7月17日:添加了对 stdin 和 stdout 的支持。