From 507d9da4116ef2211c466b9edffc4d1c23c79b9b Mon Sep 17 00:00:00 2001 From: Bob Arnson Date: Fri, 23 Oct 2020 17:48:34 -0400 Subject: Reorganize Product/Package to Package/SummaryInformation. --- src/WixToolset.Converters/WixConverter.cs | 111 ++++++++++- .../WixToolsetTest.Converters/ConditionFixture.cs | 8 +- .../ConverterIntegrationFixture.cs | 8 +- .../WixToolsetTest.Converters/ExtensionFixture.cs | 80 ++++++++ .../FirewallExtensionFixture.cs | 80 -------- .../ProductPackageFixture.cs | 209 +++++++++++++++++++++ .../PackageSummaryInformation/TypicalV3.msi | Bin 0 -> 32768 bytes .../PackageSummaryInformation/TypicalV3.wxs | 37 ++++ .../Preprocessor/ConvertedPreprocessor.wxs | 8 +- .../TestData/QtExec.bad/v4_expected.wxs | 8 +- .../TestData/QtExec/v4_expected.wxs | 8 +- .../TestData/SingleFile/ConvertedSingleFile.wxs | 8 +- .../WixToolsetTest.Converters.csproj | 2 + 13 files changed, 463 insertions(+), 104 deletions(-) create mode 100644 src/test/WixToolsetTest.Converters/ExtensionFixture.cs delete mode 100644 src/test/WixToolsetTest.Converters/FirewallExtensionFixture.cs create mode 100644 src/test/WixToolsetTest.Converters/ProductPackageFixture.cs create mode 100644 src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.msi create mode 100644 src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.wxs (limited to 'src') diff --git a/src/WixToolset.Converters/WixConverter.cs b/src/WixToolset.Converters/WixConverter.cs index 1fcaacb4..bfdaa31b 100644 --- a/src/WixToolset.Converters/WixConverter.cs +++ b/src/WixToolset.Converters/WixConverter.cs @@ -57,9 +57,11 @@ namespace WixToolset.Converters private static readonly XName LaunchElementName = WixNamespace + "Launch"; private static readonly XName LevelElementName = WixNamespace + "Level"; private static readonly XName ExePackageElementName = WixNamespace + "ExePackage"; + private static readonly XName ModuleElementName = WixNamespace + "Module"; private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage"; private static readonly XName MspPackageElementName = WixNamespace + "MspPackage"; private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage"; + private static readonly XName PackageElementName = WixNamespace + "Package"; private static readonly XName PayloadElementName = WixNamespace + "Payload"; private static readonly XName PermissionExElementName = WixNamespace + "PermissionEx"; private static readonly XName ProductElementName = WixNamespace + "Product"; @@ -67,7 +69,6 @@ namespace WixToolset.Converters private static readonly XName PublishElementName = WixNamespace + "Publish"; private static readonly XName MultiStringValueElementName = WixNamespace + "MultiStringValue"; private static readonly XName RequiredPrivilegeElementName = WixNamespace + "RequiredPrivilege"; - private static readonly XName RowElementName = WixNamespace + "Row"; private static readonly XName ServiceArgumentElementName = WixNamespace + "ServiceArgument"; private static readonly XName SetDirectoryElementName = WixNamespace + "SetDirectory"; private static readonly XName SetPropertyElementName = WixNamespace + "SetProperty"; @@ -86,6 +87,7 @@ namespace WixToolset.Converters private static readonly XName Include4ElementName = WixNamespace + "Include"; private static readonly XName Include3ElementName = Wix3Namespace + "Include"; private static readonly XName IncludeElementWithoutNamespaceName = XNamespace.None + "Include"; + private static readonly XName SummaryInformationElementName = WixNamespace + "SummaryInformation"; private static readonly Dictionary OldToNewNamespaceMapping = new Dictionary() { @@ -147,6 +149,7 @@ namespace WixToolset.Converters { WixConverter.EmbeddedChainerElementName, this.ConvertEmbeddedChainerElement }, { WixConverter.ErrorElementName, this.ConvertErrorElement }, { WixConverter.ExePackageElementName, this.ConvertSuppressSignatureValidation }, + { WixConverter.ModuleElementName, this.ConvertModuleElement }, { WixConverter.MsiPackageElementName, this.ConvertSuppressSignatureValidation }, { WixConverter.MspPackageElementName, this.ConvertSuppressSignatureValidation }, { WixConverter.MsuPackageElementName, this.ConvertSuppressSignatureValidation }, @@ -669,6 +672,36 @@ namespace WixToolset.Converters private void ConvertProgressTextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Message"); + private void ConvertModuleElement(XElement element) + { + if (element.Attribute("Guid") == null // skip already-converted Module elements + && this.OnError(ConverterTestType.ModuleAndPackageRenamed, element, "The Module and Package elements have been renamed and reorganized for simplicity.")) + { + var xModule = element; + + var xSummaryInformation = xModule.Element(PackageElementName); + if (xSummaryInformation != null) + { + xSummaryInformation.Name = SummaryInformationElementName; + + RemoveAttribute(xSummaryInformation, "AdminImage"); + RemoveAttribute(xSummaryInformation, "Comments"); + MoveAttribute(xSummaryInformation, "Id", xModule, "Guid"); + MoveAttribute(xSummaryInformation, "InstallerVersion", xModule); + RemoveAttribute(xSummaryInformation, "Languages"); + RemoveAttribute(xSummaryInformation, "Platform"); + RemoveAttribute(xSummaryInformation, "Platforms"); + RemoveAttribute(xSummaryInformation, "ReadOnly"); + MoveAttribute(xSummaryInformation, "SummaryCodepage", xSummaryInformation, "Codepage", defaultValue: "1252"); + + if (!xSummaryInformation.HasAttributes) + { + xSummaryInformation.Remove(); + } + } + } + } + private void ConvertProductElement(XElement element) { var id = element.Attribute("Id"); @@ -696,6 +729,72 @@ namespace WixToolset.Converters xCondition.Remove(); } } + + if (this.OnError(ConverterTestType.ProductAndPackageRenamed, element, "The Product and Package elements have been renamed and reorganized for simplicity.")) + { + var xPackage = element; + xPackage.Name = PackageElementName; + + var xSummaryInformation = xPackage.Element(PackageElementName); + if (xSummaryInformation != null) + { + xSummaryInformation.Name = SummaryInformationElementName; + + RemoveAttribute(xSummaryInformation, "AdminImage"); + RemoveAttribute(xSummaryInformation, "Comments"); + MoveAttribute(xSummaryInformation, "Compressed", xPackage); + RemoveAttribute(xSummaryInformation, "Id"); + MoveAttribute(xSummaryInformation, "InstallerVersion", xPackage, defaultValue: "500"); + MoveAttribute(xSummaryInformation, "InstallScope", xPackage, "Scope", defaultValue: "perMachine"); + RemoveAttribute(xSummaryInformation, "Platform"); + RemoveAttribute(xSummaryInformation, "Platforms"); + RemoveAttribute(xSummaryInformation, "ReadOnly"); + MoveAttribute(xSummaryInformation, "ShortNames", xPackage); + MoveAttribute(xSummaryInformation, "SummaryCodepage", xSummaryInformation, "Codepage", defaultValue: "1252"); + MoveAttribute(xPackage, "Id", xPackage, "ProductCode"); + + var xInstallPrivileges = xSummaryInformation.Attribute("InstallPrivileges"); + switch (xInstallPrivileges?.Value) + { + case "limited": + xPackage.SetAttributeValue("Scope", "perUser"); + break; + case "elevated": + { + var xAllUsers = xPackage.Elements(PropertyElementName).SingleOrDefault(p => p.Attribute("Id")?.Value == "ALLUSERS"); + if (xAllUsers?.Attribute("Value")?.Value == "1") + { + xAllUsers?.Remove(); + } + } + break; + } + + xInstallPrivileges?.Remove(); + + if (!xSummaryInformation.HasAttributes) + { + xSummaryInformation.Remove(); + } + } + } + } + + private static void MoveAttribute(XElement xSource, string attributeName, XElement xDestination, string destinationAttributeName = null, string defaultValue = null) + { + var xAttribute = xSource.Attribute(attributeName); + if (xAttribute != null && (defaultValue == null || xAttribute.Value != defaultValue)) + { + xDestination.SetAttributeValue(destinationAttributeName ?? attributeName, xAttribute.Value); + } + + xAttribute?.Remove(); + } + + private static void RemoveAttribute(XElement xSummaryInformation, string attributeName) + { + var xAttribute = xSummaryInformation.Attribute(attributeName); + xAttribute?.Remove(); } private void ConvertPublishElement(XElement element) @@ -1300,6 +1399,16 @@ namespace WixToolset.Converters /// The CustomAction attributes have been renamed from BinaryKey and FileKey to BinaryRef and FileRef. /// CustomActionKeysAreNowRefs, + + /// + /// The Product and Package elements have been renamed and reorganized. + /// + ProductAndPackageRenamed, + + /// + /// The Module and Package elements have been renamed and reorganized. + /// + ModuleAndPackageRenamed, } } } diff --git a/src/test/WixToolsetTest.Converters/ConditionFixture.cs b/src/test/WixToolsetTest.Converters/ConditionFixture.cs index 629fbd2a..75ceec31 100644 --- a/src/test/WixToolsetTest.Converters/ConditionFixture.cs +++ b/src/test/WixToolsetTest.Converters/ConditionFixture.cs @@ -220,6 +220,7 @@ namespace WixToolsetTest.Converters "", "", " ", + " ", " ", " 1<2", " ", @@ -232,10 +233,11 @@ namespace WixToolsetTest.Converters var expected = new[] { "", - " ", + " ", + " ", " ", " ", - " ", + " ", "" }; @@ -245,7 +247,7 @@ namespace WixToolsetTest.Converters var converter = new WixConverter(messaging, 2, null, null); var errors = converter.ConvertDocument(document); - Assert.Equal(4, errors); + Assert.Equal(5, errors); var actualLines = UnformattedDocumentLines(document); WixAssert.CompareLineByLine(expected, actualLines); diff --git a/src/test/WixToolsetTest.Converters/ConverterIntegrationFixture.cs b/src/test/WixToolsetTest.Converters/ConverterIntegrationFixture.cs index 09387590..38afca72 100644 --- a/src/test/WixToolsetTest.Converters/ConverterIntegrationFixture.cs +++ b/src/test/WixToolsetTest.Converters/ConverterIntegrationFixture.cs @@ -58,7 +58,7 @@ namespace WixToolsetTest.Converters var converter = new WixConverter(messaging, 4); var errors = converter.ConvertFile(targetFile, true); - Assert.Equal(7, errors); + Assert.Equal(8, errors); var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n"); var actual = File.ReadAllText(targetFile).Replace("\r\n", "\n"); @@ -84,7 +84,7 @@ namespace WixToolsetTest.Converters var settingsFile = Path.Combine(folder, "wixcop.settings.xml"); var result = RunConversion(targetFile, settingsFile: settingsFile); - Assert.Equal(7, result.ExitCode); + Assert.Equal(8, result.ExitCode); var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n"); var actual = File.ReadAllText(targetFile).Replace("\r\n", "\n"); @@ -108,7 +108,7 @@ namespace WixToolsetTest.Converters File.Copy(Path.Combine(folder, beforeFileName), Path.Combine(baseFolder, beforeFileName)); var result = RunConversion(targetFile); - Assert.Equal(11, result.ExitCode); + Assert.Equal(12, result.ExitCode); var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n"); var actual = File.ReadAllText(targetFile).Replace("\r\n", "\n"); @@ -133,7 +133,7 @@ namespace WixToolsetTest.Converters var result = RunConversion(targetFile); - Assert.Equal(11, result.ExitCode); + Assert.Equal(12, result.ExitCode); Assert.Single(result.Messages.Where(message => message.ToString().EndsWith("(QtExecCmdTimeoutAmbiguous)"))); var expected = File.ReadAllText(Path.Combine(folder, afterFileName)).Replace("\r\n", "\n"); diff --git a/src/test/WixToolsetTest.Converters/ExtensionFixture.cs b/src/test/WixToolsetTest.Converters/ExtensionFixture.cs new file mode 100644 index 00000000..4bf2ed3d --- /dev/null +++ b/src/test/WixToolsetTest.Converters/ExtensionFixture.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.Converters +{ + using System; + using System.Xml.Linq; + using WixBuildTools.TestSupport; + using WixToolset.Converters; + using WixToolsetTest.Converters.Mocks; + using Xunit; + + public class ExtensionFixture : BaseConverterFixture + { + [Fact] + public void FixRemoteAddressValue() + { + var parse = String.Join(Environment.NewLine, + "", + " ", + " ", + " 127.0.0.1", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + + var messaging = new MockMessaging(); + var converter = new WixConverter(messaging, 2, null, null); + + var errors = converter.ConvertDocument(document); + Assert.Equal(3, errors); + + var actualLines = UnformattedDocumentLines(document); + WixAssert.CompareLineByLine(expected, actualLines); + } + + [Fact] + public void FixXmlConfigValue() + { + var parse = String.Join(Environment.NewLine, + "", + " ", + " ", + " a<>b", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + + var messaging = new MockMessaging(); + var converter = new WixConverter(messaging, 2, null, null); + + var errors = converter.ConvertDocument(document); + Assert.Equal(3, errors); + + var actualLines = UnformattedDocumentLines(document); + WixAssert.CompareLineByLine(expected, actualLines); + } + } +} diff --git a/src/test/WixToolsetTest.Converters/FirewallExtensionFixture.cs b/src/test/WixToolsetTest.Converters/FirewallExtensionFixture.cs deleted file mode 100644 index e6ec8568..00000000 --- a/src/test/WixToolsetTest.Converters/FirewallExtensionFixture.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolsetTest.Converters -{ - using System; - using System.Xml.Linq; - using WixBuildTools.TestSupport; - using WixToolset.Converters; - using WixToolsetTest.Converters.Mocks; - using Xunit; - - public class FirewallExtensionFixture : BaseConverterFixture - { - [Fact] - public void FixRemoteAddressValue() - { - var parse = String.Join(Environment.NewLine, - "", - " ", - " ", - " 127.0.0.1", - " ", - " ", - ""); - - var expected = new[] - { - "", - " ", - " ", - " ", - "" - }; - - var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); - - var messaging = new MockMessaging(); - var converter = new WixConverter(messaging, 2, null, null); - - var errors = converter.ConvertDocument(document); - Assert.Equal(3, errors); - - var actualLines = UnformattedDocumentLines(document); - WixAssert.CompareLineByLine(expected, actualLines); - } - - [Fact] - public void FixXmlConfigValue() - { - var parse = String.Join(Environment.NewLine, - "", - " ", - " ", - " a<>b", - " ", - " ", - ""); - - var expected = new[] - { - "", - " ", - " ", - " ", - "" - }; - - var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); - - var messaging = new MockMessaging(); - var converter = new WixConverter(messaging, 2, null, null); - - var errors = converter.ConvertDocument(document); - Assert.Equal(3, errors); - - var actualLines = UnformattedDocumentLines(document); - WixAssert.CompareLineByLine(expected, actualLines); - } - } -} diff --git a/src/test/WixToolsetTest.Converters/ProductPackageFixture.cs b/src/test/WixToolsetTest.Converters/ProductPackageFixture.cs new file mode 100644 index 00000000..9407ff16 --- /dev/null +++ b/src/test/WixToolsetTest.Converters/ProductPackageFixture.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.Converters +{ + using System; + using System.IO; + using System.Xml.Linq; + using WixBuildTools.TestSupport; + using WixToolset.Converters; + using WixToolset.Core.TestPackage; + using WixToolsetTest.Converters.Mocks; + using Xunit; + + public class ProductPackageFixture : BaseConverterFixture + { + [Fact] + public void FixesCompressed() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + private static void AssertSuccess(string input, int expectedErrorCount, string[] expected) + { + var document = XDocument.Parse(input, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + + var messaging = new MockMessaging(); + var converter = new WixConverter(messaging, 2, null, null); + + var errors = converter.ConvertDocument(document); + Assert.Equal(expectedErrorCount, errors); + + var actualLines = UnformattedDocumentLines(document); + WixAssert.CompareLineByLine(expected, actualLines); + } + + [Fact] + public void FixesInstallerVersion() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + [Fact] + public void FixesDefaultInstallerVersion() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + [Fact] + public void FixesNonDefaultInstallerVersion() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + [Fact] + public void FixesLimitedInstallerPrivileges() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + [Fact] + public void FixesElevatedInstallerPrivileges() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + " ", + ""); + + var expected = new[] + { + "", + " ", + " ", + " ", + " ", + "" + }; + + AssertSuccess(parse, 3, expected); + } + + [Fact] + public void CanDecompileAndRecompile() + { + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var decompiledWxsPath = Path.Combine(baseFolder, "TypicalV3.wxs"); + + var folder = TestData.Get(@"TestData\PackageSummaryInformation"); + var v3msiPath = Path.Combine(folder, "TypicalV3.msi"); + var result = WixRunner.Execute(new[] + { + "decompile", v3msiPath, + "-intermediateFolder", intermediateFolder, + "-o", decompiledWxsPath + }); + + result.AssertSuccess(); + + var v4msiPath = Path.Combine(intermediateFolder, "TypicalV4.msi"); + result = WixRunner.Execute(new[] + { + "build", decompiledWxsPath, + "-arch", "x64", + "-intermediateFolder", intermediateFolder, + "-o", v4msiPath + }); + + result.AssertSuccess(); + + Assert.True(File.Exists(v4msiPath)); + + var v3results = Query.QueryDatabase(v3msiPath, new[] { "_SummaryInformation", "Property" }); + var v4results = Query.QueryDatabase(v4msiPath, new[] { "_SummaryInformation", "Property" }); + WixAssert.CompareLineByLine(v3results, v4results); + } + } + } +} diff --git a/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.msi b/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.msi new file mode 100644 index 00000000..0d7e1b21 Binary files /dev/null and b/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.msi differ diff --git a/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.wxs b/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.wxs new file mode 100644 index 00000000..8c5027b4 --- /dev/null +++ b/src/test/WixToolsetTest.Converters/TestData/PackageSummaryInformation/TypicalV3.wxs @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs index ea52c71f..e601ae2a 100644 --- a/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs +++ b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs @@ -1,12 +1,12 @@ - + - - + + @@ -57,5 +57,5 @@ - + diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs index 0266e177..169eb07e 100644 --- a/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs +++ b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs @@ -1,12 +1,12 @@ - + - - + + @@ -59,5 +59,5 @@ - + diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs index da4f5135..71df9fd9 100644 --- a/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs +++ b/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs @@ -1,12 +1,12 @@ - + - - + + @@ -58,5 +58,5 @@ - + diff --git a/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs b/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs index ec4d84d5..b52f5855 100644 --- a/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs +++ b/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs @@ -1,12 +1,12 @@ - + - - + + @@ -55,5 +55,5 @@ - + diff --git a/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj b/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj index 9f761738..b09b5418 100644 --- a/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj +++ b/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj @@ -23,6 +23,8 @@ + + -- cgit v1.2.3-55-g6feb