diff --git a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/DependencyFileManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/DependencyFileManager.cs index 3dfff2bf36..6e1d9fb4f6 100644 --- a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/DependencyFileManager.cs +++ b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/DependencyFileManager.cs @@ -211,6 +211,86 @@ public async Task AddDependencyAsync( await AddDependencyToVersionDetailsAsync(repoUri, branch, dependency); } + public async Task RemoveDependencyAsync(DependencyDetail dependency, string repoUri, string branch, bool repoIsVmr = false) + { + var updatedDependencyVersionFile = + new GitFile(VersionFiles.VersionDetailsXml, await RemoveDependencyFromVersionDetailsAsync(dependency, repoUri, branch)); + var updatedVersionPropsFile = + new GitFile(VersionFiles.VersionProps, await RemoveDependencyFromVersionPropsAsync(dependency, repoUri, branch)); + List gitFiles = [updatedDependencyVersionFile, updatedVersionPropsFile]; + + var updatedDotnetTools = await RemoveDotnetToolsDependencyAsync(dependency, repoUri, branch, repoIsVmr); + if (updatedDotnetTools != null) + { + gitFiles.Add(new(VersionFiles.DotnetToolsConfigJson, updatedDotnetTools)); + } + + await GetGitClient(repoUri).CommitFilesAsync( + gitFiles, + repoUri, + branch, + $"Remove {dependency.Name} from Version.Details.xml and Version.props'"); + + _logger.LogInformation($"Dependency '{dependency.Name}' successfully removed from '{VersionFiles.VersionDetailsXml}'"); + } + + private async Task RemoveDotnetToolsDependencyAsync(DependencyDetail dependency, string repoUri, string branch, bool repoIsVmr) + { + var dotnetTools = await ReadDotNetToolsConfigJsonAsync(repoUri, branch, repoIsVmr); + + if (dotnetTools == null) + { + return null; + } + + if (dotnetTools["tools"] is not JObject tools) + { + return null; + } + + // we have to do this because JObject is case sensitive + var toolProperty = tools.Properties().FirstOrDefault(p => p.Name.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)); + if (toolProperty != null) + { + tools.Remove(toolProperty.Name); + } + + return dotnetTools; + } + + private async Task RemoveDependencyFromVersionPropsAsync(DependencyDetail dependency, string repoUri, string branch) + { + var versionProps = await ReadVersionPropsAsync(repoUri, branch); + string nodeName = VersionFiles.GetVersionPropsPackageVersionElementName(dependency.Name); + XmlNode element = versionProps.SelectSingleNode($"//{nodeName}"); + if (element == null) + { + string alternateNodeName = VersionFiles.GetVersionPropsAlternatePackageVersionElementName(dependency.Name); + element = versionProps.SelectSingleNode($"//{alternateNodeName}"); + if (element == null) + { + throw new DependencyException($"Couldn't find dependency {dependency.Name} in Version.props"); + } + } + element.ParentNode.RemoveChild(element); + + return versionProps; + } + + private async Task RemoveDependencyFromVersionDetailsAsync(DependencyDetail dependency, string repoUri, string branch) + { + var versionDetails = await ReadVersionDetailsXmlAsync(repoUri, branch); + XmlNode dependencyNode = versionDetails.SelectSingleNode($"//{VersionDetailsParser.DependencyElementName}[@Name='{dependency.Name}']"); + + if (dependencyNode == null) + { + throw new DependencyException($"Dependency {dependency.Name} not found in Version.Details.xml"); + } + + dependencyNode.ParentNode.RemoveChild(dependencyNode); + return versionDetails; + } + private static void SetAttribute(XmlDocument document, XmlNode node, string name, string value) { XmlAttribute attribute = node.Attributes[name]; @@ -805,8 +885,8 @@ private async Task AddDependencyToVersionsPropsAsync(string repo, string branch, // Attempt to find the element name or alternate element name under // the property group nodes - XmlNode existingVersionNode = versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{packageVersionElementName}' and parent::PropertyGroup]"); - existingVersionNode ??= versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{packageVersionAlternateElementName}' and parent::PropertyGroup]"); + XmlNode existingVersionNode = versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{packageVersionElementName}' and parent::PropertyGroup]") + ?? versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{packageVersionAlternateElementName}' and parent::PropertyGroup]"); if (existingVersionNode != null) { diff --git a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/IDependencyFileManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/IDependencyFileManager.cs index 704ebeeb83..6d1784d1d4 100644 --- a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/IDependencyFileManager.cs +++ b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/IDependencyFileManager.cs @@ -21,6 +21,8 @@ public interface IDependencyFileManager { Task AddDependencyAsync(DependencyDetail dependency, string repoUri, string branch); + Task RemoveDependencyAsync(DependencyDetail dependency, string repoUri, string branch, bool repoIsVmr = false); + Dictionary> FlattenLocationsAndSplitIntoGroups(Dictionary> assetLocationMap); List<(string key, string feed)> GetPackageSources(XmlDocument nugetConfig, Func? filter = null); diff --git a/test/Microsoft.DotNet.DarcLib.Tests/Helpers/DependencyFileManagerTests.cs b/test/Microsoft.DotNet.DarcLib.Tests/Helpers/DependencyFileManagerTests.cs new file mode 100644 index 0000000000..ffa122a16a --- /dev/null +++ b/test/Microsoft.DotNet.DarcLib.Tests/Helpers/DependencyFileManagerTests.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.DotNet.DarcLib.Helpers; +using Microsoft.DotNet.DarcLib.Models.Darc; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualStudio.Services.Profile; +using Moq; +using NUnit.Framework; + +namespace Microsoft.DotNet.DarcLib.Tests.Helpers; + +[TestFixture] +public class DependencyFileManagerTests +{ + private const string VersionDetails = """ + + + + + + https://github.com/dotnet/foo + sha1 + + + https://github.com/dotnet/bar + sha1 + + + + + + """; + + private const string VersionProps = """ + + + + + + + 1.0.0 + 1.0.0 + + + """; + + private const string DotnetTools = """ + { + "version": 1, + "isRoot": true, + "tools": { + "microsoft.dnceng.secretmanager": { + "version": "1.1.0-beta.25071.2", + "commands": [ + "secret-manager" + ] + }, + "foo": { + "version": "8.0.0", + "commands": [ + "foo" + ] + }, + "microsoft.dnceng.configuration.bootstrap": { + "version": "1.1.0-beta.25071.2", + "commands": [ + "bootstrap-dnceng-configuration" + ] + } + } + } + """; + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task RemoveDependencyShouldRemoveDependency(bool dotnetToolsExists) + { + var expectedVersionDetails = """ + + + + + + https://github.com/dotnet/bar + sha1 + + + + + + """; + var expectedVersionProps = """ + + + + + + + 1.0.0 + + + """; + var expectedDotNetTools = """ + { + "version": 1, + "isRoot": true, + "tools": { + "microsoft.dnceng.secretmanager": { + "version": "1.1.0-beta.25071.2", + "commands": [ + "secret-manager" + ] + }, + "microsoft.dnceng.configuration.bootstrap": { + "version": "1.1.0-beta.25071.2", + "commands": [ + "bootstrap-dnceng-configuration" + ] + } + } + } + """; + + var tmpVersionDetailsPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var tmpVersionPropsPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var tmpDotnetToolsPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + DependencyDetail dependency = new() + { + Name = "Foo" + }; + + Mock repo = new(); + Mock repoFactory = new(); + + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.VersionDetailsXml, It.IsAny(), It.IsAny())) + .ReturnsAsync(VersionDetails); + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.VersionProps, It.IsAny(), It.IsAny())) + .ReturnsAsync(VersionProps); + if (!dotnetToolsExists) + { + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.DotnetToolsConfigJson, It.IsAny(), It.IsAny())) + .Throws(); + } + else + { + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.DotnetToolsConfigJson, It.IsAny(), It.IsAny())) + .ReturnsAsync(DotnetTools); + } + + repo.Setup(r => r.CommitFilesAsync( + It.Is>(files => + files.Count == (dotnetToolsExists ? 3 : 2) && + files.Any(f => f.FilePath == VersionFiles.VersionDetailsXml) && files.Any(f => f.FilePath == VersionFiles.VersionProps)), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback, string, string, string>((files, repoUri, branch, commitMessage) => + { + File.WriteAllText(tmpVersionDetailsPath, files[0].Content); + File.WriteAllText(tmpVersionPropsPath, files[1].Content); + if (dotnetToolsExists) + { + File.WriteAllText(tmpDotnetToolsPath, files[2].Content); + } + }); + + repoFactory.Setup(repoFactory => repoFactory.CreateClient(It.IsAny())).Returns(repo.Object); + + DependencyFileManager manager = new( + repoFactory.Object, + new VersionDetailsParser(), + NullLogger.Instance); + + try + { + await manager.RemoveDependencyAsync(dependency, string.Empty, string.Empty); + + File.ReadAllText(tmpVersionDetailsPath).Replace("\r\n", "\n").TrimEnd().Should() + .Be(expectedVersionDetails.Replace("\r\n", "\n").TrimEnd()); + File.ReadAllText(tmpVersionPropsPath).Replace("\r\n", "\n").TrimEnd().Should() + .Be(expectedVersionProps.Replace("\r\n", "\n").TrimEnd()); + if (dotnetToolsExists) + { + File.ReadAllText(tmpDotnetToolsPath).Replace("\r\n", "\n").TrimEnd().Should() + .Be(expectedDotNetTools.Replace("\r\n", "\n").TrimEnd()); + } + } + finally + { + if (File.Exists(tmpVersionDetailsPath)) + { + File.Delete(tmpVersionDetailsPath); + } + if (File.Exists(tmpVersionPropsPath)) + { + File.Delete(tmpVersionPropsPath); + } + if (File.Exists(tmpDotnetToolsPath)) + { + File.Delete(tmpDotnetToolsPath); + } + } + } + + [Test] + public async Task RemoveDependencyShouldThrowWhenDependencyDoesNotExist() + { + DependencyDetail dependency = new() + { + Name = "gaa" + }; + + Mock repo = new(); + Mock repoFactory = new(); + + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.VersionDetailsXml, It.IsAny(), It.IsAny())) + .ReturnsAsync(VersionDetails); + repo.Setup(r => r.GetFileContentsAsync(VersionFiles.VersionProps, It.IsAny(), It.IsAny())) + .ReturnsAsync(VersionProps); + repoFactory.Setup(repoFactory => repoFactory.CreateClient(It.IsAny())).Returns(repo.Object); + + DependencyFileManager manager = new( + repoFactory.Object, + new VersionDetailsParser(), + NullLogger.Instance); + + Func act = async () => await manager.RemoveDependencyAsync(dependency, string.Empty, string.Empty); + await act.Should().ThrowAsync(); + } +}