Контроль версий файлов .sqlproj - PullRequest
2 голосов
/ 30 января 2020

Наш .sqlproj содержит множество таких утверждений для каждого объекта, существующего в проекте:

<Build Include="MySchema\Tables\TableA" />
<Build Include="MySchema\Tables\TableB" />
<Build Include="MySchema\Tables\TableC" />

Каждый раз, когда объект добавляется в проект, SSDT автоматически обновляет sqlproj. файл, добавив запись в какую-то случайную строку файла. Это вызывает много проблем слияния, когда несколько разработчиков работают над одним и тем же проектом.

Я попытался изменить этот файл, добавив символы подстановки во все папки схем, поэтому предыдущая будет выглядеть так:

<Build Include="MySchema\**" />

Но тогда, если я создам TableD в той же схеме, он все равно добавит запись для этого объекта, даже если она будет включена в предыдущий оператор. Так что мой .sqlproj выглядел бы так:

<Build Include="MySchema\**" />
<Build Include="MySchema\Tables\TableD" />

Есть ли какое-нибудь решение, чтобы обойти это?

Ответы [ 2 ]

2 голосов
/ 02 февраля 2020

Слияние файла проекта SSDT sqlproj - это просто боль. Мы создали файл целей MSBuild, который просто сортирует файл проекта каждый раз, когда вы создаете проект. Недостатком этого является то, что когда файл sqlproj отсортирован, он обрабатывается Visual Studio, что он изменен извне и хочет обновить sh проект. В любом случае, это не так уж и сложно по сравнению с адом слияния.

Итак, в папке проекта у нас есть файл build_VS2017.targets (его, возможно, потребуется настроить, если вы хотите использовать его не с версией VS 2017, по крайней мере, я что-то сделал, когда мы мигрировали с 2015 по 2017 год):

<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- This simple inline task displays "Hello, world!" -->
  <UsingTask
    TaskName="ReorderSqlProjFile_Inline"
    TaskFactory="CodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
    <ParameterGroup />
    <Task>
      <Reference Include="System.Xml"/>
      <Reference Include="System.Core"/>
      <Reference Include="System.Xml.Linq"/>
      <Using Namespace="Microsoft.Build.Framework" />
      <Using Namespace="Microsoft.Build.Utilities" />
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="System.Linq"/>
      <Using Namespace="System.Xml.Linq"/>
      <Using Namespace="System.Collections.Generic"/>
      <Code Type="Class" Language="cs">
        <![CDATA[    
    using System.Linq;

    public class ReorderSqlProjFile_Inline : Microsoft.Build.Utilities.Task
    {
        private string _projectFullPath = @"]]>$(MSBuildProjectFullPath)<![CDATA[";


        public override bool Execute()
        {
            try
            {
                System.Xml.Linq.XDocument document = System.Xml.Linq.XDocument.Load(_projectFullPath, System.Xml.Linq.LoadOptions.PreserveWhitespace | System.Xml.Linq.LoadOptions.SetLineInfo);
                System.Xml.Linq.XNamespace msBuildNamespace = document.Root.GetDefaultNamespace();
                System.Xml.Linq.XName itemGroupName = System.Xml.Linq.XName.Get("ItemGroup", msBuildNamespace.NamespaceName);
                var itemGroups = document.Root.Descendants(itemGroupName).ToArray();

                var processedItemGroups = new System.Collections.Generic.List<System.Xml.Linq.XElement>();

                CombineCompatibleItemGroups(itemGroups, processedItemGroups);

                foreach (System.Xml.Linq.XElement itemGroup in processedItemGroups)
                {
                    SortItemGroup(itemGroup);
                }

                var originalBytes = System.IO.File.ReadAllBytes(_projectFullPath);
                byte[] newBytes = null;

                using (var memoryStream = new System.IO.MemoryStream())
                using (var textWriter = new System.IO.StreamWriter(memoryStream, System.Text.Encoding.UTF8))
                {
                    document.Save(textWriter, System.Xml.Linq.SaveOptions.None);
                    newBytes = memoryStream.ToArray();
                }

                if (!AreEqual(originalBytes, newBytes))
                {
                    Log.LogMessageFromText("===    RESULT: Included files in *.sqlproj need to be reordered.          ===", Microsoft.Build.Framework.MessageImportance.High);

                    if (!new System.IO.FileInfo(_projectFullPath).IsReadOnly)
                    {
                        System.IO.File.WriteAllBytes(_projectFullPath, newBytes);

                        Log.LogMessageFromText("===            *.sqlproj has been overwritten.                            ===", Microsoft.Build.Framework.MessageImportance.High);
                        Log.LogMessageFromText("===            Visual Studio will ask to reload project.                  ===", Microsoft.Build.Framework.MessageImportance.High);
                        Log.LogMessageFromText("===                                                                       ===", Microsoft.Build.Framework.MessageImportance.High);
                        Log.LogMessageFromText("=============================================================================", Microsoft.Build.Framework.MessageImportance.High);
                    }
                    else
                    {
                        Log.LogMessageFromText("===            *.sqlproj is readonly. Cannot overwrite *.sqlproj file.    ===", Microsoft.Build.Framework.MessageImportance.High);
                        Log.LogMessageFromText("===                                                                       ===", Microsoft.Build.Framework.MessageImportance.High);
                        Log.LogMessageFromText("=============================================================================", Microsoft.Build.Framework.MessageImportance.High);
                    }
                }
                else
                {
                    Log.LogMessageFromText("===    RESULT: *.sqlproj is OK.                                           ===", Microsoft.Build.Framework.MessageImportance.High);
                    Log.LogMessageFromText("===                                                                       ===", Microsoft.Build.Framework.MessageImportance.High);
                    Log.LogMessageFromText("=============================================================================", Microsoft.Build.Framework.MessageImportance.High);
                }

                return true;
            }
            catch (System.Exception ex)
            {
                Log.LogMessageFromText("===    RESULT: Exception occured trying to reorder *.sqlproj file.        ===", Microsoft.Build.Framework.MessageImportance.High);
                Log.LogMessageFromText("===            Exception:" + ex, Microsoft.Build.Framework.MessageImportance.High);
                Log.LogMessageFromText("===                                                                       ===", Microsoft.Build.Framework.MessageImportance.High);
                Log.LogMessageFromText("=============================================================================", Microsoft.Build.Framework.MessageImportance.High);

                return true;
            }
        }

        public bool AreEqual(byte[] left, byte[] right)
        {
            if (left == null)
            {
                return right == null;
            }

            if (right == null)
            {
                return false;
            }

            if (left.Length != right.Length)
            {
                return false;
            }

            for (int i = 0; i < left.Length; i++)
            {
                if (left[i] != right[i])
                {
                    return false;
                }
            }

            return true;
        }

        public void CombineCompatibleItemGroups(System.Xml.Linq.XElement[] itemGroups, System.Collections.Generic.List<System.Xml.Linq.XElement> processedItemGroups)
        {
            var itemTypeLookup = itemGroups.ToDictionary(i => i, i => GetItemTypesFromItemGroup(i));
            foreach (var itemGroup in itemGroups)
            {
                if (!itemGroup.HasElements)
                {
                    RemoveItemGroup(itemGroup);
                    continue;
                }

                var suitableExistingItemGroup = FindSuitableItemGroup(processedItemGroups, itemGroup, itemTypeLookup);
                if (suitableExistingItemGroup != null)
                {
                    ReplantAllItems(from: itemGroup, to: suitableExistingItemGroup);

                    RemoveItemGroup(itemGroup);
                }
                else
                {
                    processedItemGroups.Add(itemGroup);
                }
            }
        }

        public void RemoveItemGroup(System.Xml.Linq.XElement itemGroup)
        {
            var leadingTrivia = itemGroup.PreviousNode;
            if (leadingTrivia is System.Xml.Linq.XText)
            {
                leadingTrivia.Remove();
            }

            itemGroup.Remove();
        }

        public void ReplantAllItems(System.Xml.Linq.XElement from, System.Xml.Linq.XElement to)
        {
            if (to.LastNode is System.Xml.Linq.XText)
            {
                to.LastNode.Remove();
            }

            var fromNodes = from.Nodes().ToArray();
            from.RemoveNodes();
            foreach (var element in fromNodes)
            {
                to.Add(element);
            }
        }

        public System.Xml.Linq.XElement FindSuitableItemGroup(
            System.Collections.Generic.List<System.Xml.Linq.XElement> existingItemGroups,
            System.Xml.Linq.XElement itemGroup,
            System.Collections.Generic.Dictionary<System.Xml.Linq.XElement, System.Collections.Generic.HashSet<string>> itemTypeLookup)
        {
            foreach (var existing in existingItemGroups)
            {
                var itemTypesInExisting = itemTypeLookup[existing];
                var itemTypesInCurrent = itemTypeLookup[itemGroup];
                if (itemTypesInCurrent.IsSubsetOf(itemTypesInExisting) && AreItemGroupsMergeable(itemGroup, existing))
                {
                    return existing;
                }
            }

            return null;
        }

        public bool AreItemGroupsMergeable(System.Xml.Linq.XElement left, System.Xml.Linq.XElement right)
        {
            if (!AttributeMissingOrSame(left, right, "Label"))
            {
                return false;
            }

            if (!AttributeMissingOrSame(left, right, "Condition"))
            {
                return false;
            }

            return true;
        }

        public bool AttributeMissingOrSame(System.Xml.Linq.XElement left, System.Xml.Linq.XElement right, string attributeName)
        {
            var leftAttribute = left.Attribute(attributeName);
            var rightAttribute = right.Attribute(attributeName);
            if (leftAttribute == null && rightAttribute == null)
            {
                return true;
            }
            else if (leftAttribute != null && rightAttribute != null)
            {
                return leftAttribute.Value == rightAttribute.Value;
            }

            return false;
        }

        public System.Collections.Generic.HashSet<string> GetItemTypesFromItemGroup(System.Xml.Linq.XElement itemGroup)
        {
            var set = new System.Collections.Generic.HashSet<string>();
            foreach (var item in itemGroup.Elements())
            {
                set.Add(item.Name.LocalName);
            }

            return set;
        }

        public void SortItemGroup(System.Xml.Linq.XElement itemGroup)
        {
            System.Collections.Generic.List<System.Xml.Linq.XElement> list = new System.Collections.Generic.List<System.Xml.Linq.XElement>();
            foreach (System.Xml.Linq.XElement element in itemGroup.Elements())
                list.Add(element);
            var original = list.ToArray();
            var sorted = original
                .OrderBy(i => i.Name.LocalName)
                .ThenBy(i => (i.Attribute("Include") ?? i.Attribute("Remove")).Value)
                .ToArray();

            for (int i = 0; i < original.Length; i++)
            {
                original[i].ReplaceWith(sorted[i]);
            }
        }
    }
]]>
      </Code>
    </Task>
  </UsingTask>
  <Target Name="BeforeBuild">
    <Message Text="=============================================================================" Importance="high" />
    <Message Text="===================                                       ===================" Importance="high" />
    <Message Text="===================        RUNNING PREBIULD SCRIPT        ===================" Importance="high" />
    <Message Text="===                                                                       ===" Importance="high" />
    <Message Text="===   This script will order included files in *.sqlproj alphabetically   ===" Importance="high" />
    <Message Text="===           This is done to fix issues during merge process.            ===" Importance="high" />
    <Message Text="===                                                                       ===" Importance="high" />
    <Message Text="===    FYI: To disable this script comment next line in *.sqlproj file:   ===" Importance="high" />
    <Message Text="===      &lt;Import Project=&quot;build_VS2017.targets&quot; /&gt;        ===" Importance="high" />
    <Message Text="===                                                                       ===" Importance="high" />
    <Message Text="===                                                                       ===" Importance="high" />
    <Message Text="===                                                                       ===" Importance="high" />
    <Message Text="=============================================================================" Importance="high" />
    <ReorderSqlProjFile_Inline />
  </Target>
</Project>

и затем в файле проекта добавьте следующую запись перед </Project>:

...
    <Import Project="build_VS2017.targets" Condition="'$(Configuration)'=='Debug'" />
</Project>
1 голос
/ 07 февраля 2020

Аналогично ответу Дмитрия, вот сценарий PowerShell для сортировки элементов в файлах sqlproj:

Function AutoFix-SqlProj([string] $rootDirectory)
{
    $files = Get-ChildItem -Path $rootDirectory -Filter *.sqlproj -Recurse
    $modifiedfiles = @()

    foreach($file in $files)
    {
        $original = [xml] (Get-Content $file.FullName)
        $workingCopy = $original.Clone()

        foreach($itemGroup in $workingCopy.Project.ItemGroup){

            # Sort the Folder elements
            if ($itemGroup.Folder -ne $null){

                $sorted = $itemGroup.Folder | sort { [string]$_.Include }

                $itemGroup.RemoveAll() | Out-Null

                foreach($item in $sorted){
                    $itemGroup.AppendChild($item) | Out-Null
                }
            }

            # Sort the Build elements
            if ($itemGroup.Build -ne $null){

                $sorted = $itemGroup.Build | sort { [string]$_.Include }

                $itemGroup.RemoveAll() | Out-Null

                foreach($item in $sorted){
                    $itemGroup.AppendChild($item) | Out-Null
                }
            }
        }

        $differencesCount = (Compare-Object -ReferenceObject (Select-Xml -Xml $original -XPath "//*") -DifferenceObject (Select-Xml -Xml $workingCopy -XPath "//*")).Length

        if ($differencesCount -ne 0)
        {
            $workingCopy.Save($file.FullName) | Out-Null
            $modifiedfiles += $file.FullName
        }
    }

    return $modifiedfiles
}

$rootDirectory = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "\..\..\"

$exitCode = 0;

$changedfiles = @()
$changedfiles += AutoFix-SqlProj($rootDirectory)

if ($changedfiles.Count -gt 0)
{
    Write-Host "The following files have been auto-formatted"
    Write-Host "to reduce the likelyhood of merge conflicts:"

    foreach($file in $changedfiles)
    {
        Write-Host $file
    }

    Write-Host "Your commit has been aborted. Add the modified files above"
    Write-Host "to your changes to be comitted then commit again."

    $exitCode = 1;
}

exit $exitcode
...