From e0be1bbe9b0077aa52fd1d050924b510353e935a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:03:50 +0000 Subject: [PATCH 1/2] Initial plan for issue From 8a824d3df2da1aae51fd539954e7131af5650f49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:13:05 +0000 Subject: [PATCH 2/2] Add GitRemotePrune method and modify GitFetch to call it when needed Co-authored-by: sanjuyadav24 <185911972+sanjuyadav24@users.noreply.github.com> --- src/Agent.Plugins/GitCliManager.cs | 20 ++++++++ src/Agent.Worker/Build/GitCommandManager.cs | 24 ++++++++++ .../L0/Worker/Build/GitSourceProviderL0.cs | 48 +++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/Agent.Plugins/GitCliManager.cs b/src/Agent.Plugins/GitCliManager.cs index b3a9d72a54..0dfc33ab82 100644 --- a/src/Agent.Plugins/GitCliManager.cs +++ b/src/Agent.Plugins/GitCliManager.cs @@ -235,6 +235,19 @@ public async Task GitFetch(AgentTaskPluginExecutionContext context, string //define options for fetch string options = $"{forceTag} {tags} --prune {pruneTags} {progress} --no-recurse-submodules {remoteName} {depth} {string.Join(" ", filters.Select(filter => "--filter=" + filter))} {string.Join(" ", refSpec)}"; + + // When fetching with specific refspecs and tags enabled, git doesn't prune conflicting tag refs properly + // even with --prune-tags. Run git remote prune to clean up conflicting tag refs before fetch. + if (refSpec != null && refSpec.Count > 0 && fetchTags && !string.IsNullOrEmpty(pruneTags)) + { + context.Debug("Running git remote prune before fetch to clean up conflicting tag refs."); + int pruneExitCode = await GitRemotePrune(context, repositoryPath, remoteName); + if (pruneExitCode != 0) + { + context.Debug($"Git remote prune completed with exit code {pruneExitCode}. Continuing with fetch."); + } + } + int retryCount = 0; int fetchExitCode = 0; while (retryCount < 3) @@ -602,6 +615,13 @@ public async Task GitPrune(AgentTaskPluginExecutionContext context, string return await ExecuteGitCommandAsync(context, repositoryPath, "prune", "-v"); } + // git remote prune + public async Task GitRemotePrune(AgentTaskPluginExecutionContext context, string repositoryPath, string remoteName) + { + context.Debug($"Prune remote tracking branches for remote: {remoteName}."); + return await ExecuteGitCommandAsync(context, repositoryPath, "remote", $"prune {remoteName}"); + } + // git lfs prune public async Task GitLFSPrune(AgentTaskPluginExecutionContext context, string repositoryPath) { diff --git a/src/Agent.Worker/Build/GitCommandManager.cs b/src/Agent.Worker/Build/GitCommandManager.cs index 849f1af4e5..61703aa0ae 100644 --- a/src/Agent.Worker/Build/GitCommandManager.cs +++ b/src/Agent.Worker/Build/GitCommandManager.cs @@ -54,6 +54,9 @@ public interface IGitCommandManager : IAgentService // get remote set-url --push Task GitRemoteSetPushUrl(IExecutionContext context, string repositoryPath, string remoteName, string remoteUrl); + // git remote prune + Task GitRemotePrune(IExecutionContext context, string repositoryPath, string remoteName); + // git submodule foreach --recursive "git clean -ffdx" Task GitSubmoduleClean(IExecutionContext context, string repositoryPath); @@ -295,6 +298,18 @@ public async Task GitFetch(IExecutionContext context, string repositoryPath //define options for fetch string options = $"{tags} --prune {pruneTags} --progress --no-recurse-submodules {remoteName} {depth} {string.Join(" ", refSpec)}"; + // When fetching with specific refspecs and tags enabled, git doesn't prune conflicting tag refs properly + // even with --prune-tags. Run git remote prune to clean up conflicting tag refs before fetch. + if (refSpec != null && refSpec.Count > 0 && fetchTags && !string.IsNullOrEmpty(pruneTags)) + { + context.Debug("Running git remote prune before fetch to clean up conflicting tag refs."); + int pruneExitCode = await GitRemotePrune(context, repositoryPath, remoteName); + if (pruneExitCode != 0) + { + context.Debug($"Git remote prune completed with exit code {pruneExitCode}. Continuing with fetch."); + } + } + return await ExecuteGitCommandAsync(context, repositoryPath, "fetch", options, additionalCommandLine, cancellationToken); } @@ -408,6 +423,15 @@ public async Task GitRemoteSetPushUrl(IExecutionContext context, string rep return await ExecuteGitCommandAsync(context, repositoryPath, "remote", StringUtil.Format($"set-url --push {remoteName} {remoteUrl}")); } + // git remote prune + public async Task GitRemotePrune(IExecutionContext context, string repositoryPath, string remoteName) + { + ArgUtil.NotNull(context, nameof(context)); + + context.Debug($"Prune remote tracking branches for remote: {remoteName}."); + return await ExecuteGitCommandAsync(context, repositoryPath, "remote", StringUtil.Format($"prune {remoteName}")); + } + // git submodule foreach --recursive "git clean -ffdx" public async Task GitSubmoduleClean(IExecutionContext context, string repositoryPath) { diff --git a/src/Test/L0/Worker/Build/GitSourceProviderL0.cs b/src/Test/L0/Worker/Build/GitSourceProviderL0.cs index 000c879f62..5939e16bca 100644 --- a/src/Test/L0/Worker/Build/GitSourceProviderL0.cs +++ b/src/Test/L0/Worker/Build/GitSourceProviderL0.cs @@ -352,6 +352,54 @@ public void GetSourceGitFetchPR() } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetSourceGitFetchPR_CallsGitRemotePrune() + { + using (TestHostContext tc = new TestHostContext(this)) + { + var trace = tc.GetTrace(); + // Arrange. + string dumySourceFolder = Path.Combine(tc.GetDirectory(WellKnownDirectory.Bin), "SourceProviderL0"); + try + { + Directory.CreateDirectory(dumySourceFolder); + string dumyGitFolder = Path.Combine(dumySourceFolder, ".git"); + Directory.CreateDirectory(dumyGitFolder); + string dumyGitConfig = Path.Combine(dumyGitFolder, "config"); + File.WriteAllText(dumyGitConfig, "test git confg file"); + + var executionContext = GetTestExecutionContext(tc, dumySourceFolder, "refs/pull/12345/merge", "a596e13f5db8869f44574be0392fb8fe1e790ce4", false); + var endpoint = GetTestSourceEndpoint("https://github.com/microsoft/azure-pipelines-agent", false, false); + + var _gitCommandManager = GetDefaultGitCommandMock(); + tc.SetSingleton(_gitCommandManager.Object); + tc.SetSingleton(new VstsAgentWebProxy()); + var _configStore = new Mock(); + _configStore.Setup(x => x.GetSettings()).Returns(() => new AgentSettings() { ServerUrl = "http://localhost:8080/tfs" }); + tc.SetSingleton(_configStore.Object); + tc.SetSingleton(new AgentCertificateManager()); + + GitSourceProvider gitSourceProvider = new ExternalGitSourceProvider(); + gitSourceProvider.Initialize(tc); + gitSourceProvider.SetVariablesInEndpoint(executionContext.Object, endpoint); + + // Act. + gitSourceProvider.GetSourceAsync(executionContext.Object, endpoint, default(CancellationToken)).GetAwaiter().GetResult(); + + // Assert. + // Verify that GitRemotePrune is called before GitFetch when we have refspecs and fetchTags is true + _gitCommandManager.Verify(x => x.GitRemotePrune(executionContext.Object, dumySourceFolder, "origin"), Times.Once); + _gitCommandManager.Verify(x => x.GitFetch(executionContext.Object, dumySourceFolder, "origin", It.IsAny(), It.IsAny(), new List() { "+refs/heads/*:refs/remotes/origin/*", "+refs/pull/12345/merge:refs/remotes/pull/12345/merge" }, It.IsAny(), It.IsAny()), Times.Once); + } + finally + { + IOUtil.DeleteDirectory(dumySourceFolder, CancellationToken.None); + } + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")]