Skip to content

Commit 7a2b78c

Browse files
Add mechanism to download Git on Initialize step (#4850)
* Add GitManager * Add Git 2.42.0.2 handling * Set default version as 2.39.4 * Handle error on extracting * Add debug message * Move consts * Set defaultVersion as property * Update unit test * Change Directory to File check * Set Git only on windows platform --------- Co-authored-by: Kirill Ivlev <[email protected]>
1 parent 00f479d commit 7a2b78c

File tree

6 files changed

+210
-3
lines changed

6 files changed

+210
-3
lines changed

src/Agent.Plugins/GitCliManager.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,22 @@ public bool EnsureGitLFSVersion(Version requiredVersion, bool throwOnNotMatch)
8787
string agentHomeDir = context.Variables.GetValueOrDefault("agent.homedirectory")?.Value;
8888
ArgUtil.NotNullOrEmpty(agentHomeDir, nameof(agentHomeDir));
8989

90-
string gitPath = Path.Combine(agentHomeDir, "externals", "git", "cmd", $"git.exe");
90+
string gitPath = null;
91+
92+
if (AgentKnobs.UseGit2_39_4.GetValue(context).AsBoolean())
93+
{
94+
gitPath = Path.Combine(agentHomeDir, "externals", "git-2.39.4", "cmd", $"git.exe");
95+
}
96+
else if (AgentKnobs.UseGit2_42_0_2.GetValue(context).AsBoolean())
97+
{
98+
gitPath = Path.Combine(agentHomeDir, "externals", "git-2.42.0.2", "cmd", $"git.exe");
99+
}
100+
101+
if (gitPath is null || !File.Exists(gitPath))
102+
{
103+
context.Debug("gitPath is null or does not exist. Falling back to default git path.");
104+
gitPath = Path.Combine(agentHomeDir, "externals", "git", "cmd", $"git.exe");
105+
}
91106

92107
string gitLfsPath;
93108

src/Agent.Sdk/Knob/AgentKnobs.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ public class AgentKnobs
107107
new EnvironmentKnobSource("system.prefergitfrompath"),
108108
new BuiltInDefaultKnobSource("false"));
109109

110+
public static readonly Knob UseGit2_39_4 = new Knob(
111+
nameof(UseGit2_39_4),
112+
"If true, Git v2.39.4 will be used instead of the default version.",
113+
new RuntimeKnobSource("USE_GIT_2_39_4"),
114+
new EnvironmentKnobSource("USE_GIT_2_39_4"),
115+
new BuiltInDefaultKnobSource("false"));
116+
117+
public static readonly Knob UseGit2_42_0_2 = new Knob(
118+
nameof(UseGit2_42_0_2),
119+
"If true, Git v2.42.0.2 will be used instead of the default version.",
120+
new RuntimeKnobSource("USE_GIT_2_42_0_2"),
121+
new EnvironmentKnobSource("USE_GIT_2_42_0_2"),
122+
new BuiltInDefaultKnobSource("false"));
123+
110124
public static readonly Knob DisableGitPrompt = new Knob(
111125
nameof(DisableGitPrompt),
112126
"If true, git will not prompt on the terminal (e.g., when asking for HTTP authentication).",

src/Agent.Worker/GitManager.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
using System.IO.Compression;
7+
using System.Net.Http;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
using Agent.Sdk;
12+
using Microsoft.VisualStudio.Services.Agent.Util;
13+
14+
namespace Microsoft.VisualStudio.Services.Agent.Worker
15+
{
16+
[ServiceLocator(Default = typeof(GitManager))]
17+
public interface IGitManager : IAgentService
18+
{
19+
Task DownloadAsync(IExecutionContext executionContext, string version = GitManager.defaultGitVersion);
20+
}
21+
22+
public class GitManager : AgentService, IGitManager
23+
{
24+
private const int timeout = 180;
25+
private const int defaultFileStreamBufferSize = 4096;
26+
private const int retryDelay = 10000;
27+
private const int retryLimit = 3;
28+
29+
public const string defaultGitVersion = "2.39.4";
30+
31+
public async Task DownloadAsync(IExecutionContext executionContext, string version = defaultGitVersion)
32+
{
33+
Trace.Entering();
34+
ArgUtil.NotNull(executionContext, nameof(executionContext));
35+
ArgUtil.NotNullOrEmpty(version, nameof(version));
36+
37+
Uri gitUrl = GitStore.GetDownloadUrl(version);
38+
var gitFileName = gitUrl.Segments[^1];
39+
var externalsFolder = HostContext.GetDirectory(WellKnownDirectory.Externals);
40+
var gitExternalsPath = Path.Combine(externalsFolder, $"git-{version}");
41+
var gitPath = Path.Combine(gitExternalsPath, gitFileName);
42+
43+
if (File.Exists(gitPath))
44+
{
45+
executionContext.Debug($"Git instance {gitFileName} already exists.");
46+
return;
47+
}
48+
49+
var tempDirectory = Path.Combine(externalsFolder, "git_download_temp");
50+
Directory.CreateDirectory(tempDirectory);
51+
var downloadGitPath = Path.ChangeExtension(Path.Combine(tempDirectory, gitFileName), ".completed");
52+
53+
if (File.Exists(downloadGitPath))
54+
{
55+
executionContext.Debug($"Git intance {version} already downloaded.");
56+
return;
57+
}
58+
59+
Trace.Info($@"Git zip file will be downloaded and saved as ""{downloadGitPath}""");
60+
61+
int retryCount = 0;
62+
63+
while (true)
64+
{
65+
using CancellationTokenSource downloadToken = new(TimeSpan.FromSeconds(timeout));
66+
using var downloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(downloadToken.Token, executionContext.CancellationToken);
67+
68+
try
69+
{
70+
using HttpClient client = new();
71+
using Stream stream = await client.GetStreamAsync(gitUrl);
72+
using FileStream fs = new(downloadGitPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: defaultFileStreamBufferSize, useAsync: true);
73+
74+
await stream.CopyToAsync(fs);
75+
Trace.Info("Finished Git downloading.");
76+
await fs.FlushAsync(downloadCancellation.Token);
77+
fs.Close();
78+
break;
79+
}
80+
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
81+
{
82+
Trace.Info($"Git download has been cancelled.");
83+
throw;
84+
}
85+
catch (Exception ex)
86+
{
87+
retryCount++;
88+
Trace.Info("Failed to download Git");
89+
Trace.Error(ex);
90+
91+
if (retryCount > retryLimit)
92+
{
93+
Trace.Info($"Retry limit to download Git has been reached.");
94+
break;
95+
}
96+
else
97+
{
98+
Trace.Info("Retry Git download in 10 seconds.");
99+
await Task.Delay(retryDelay, executionContext.CancellationToken);
100+
}
101+
}
102+
}
103+
104+
try
105+
{
106+
ZipFile.ExtractToDirectory(downloadGitPath, gitExternalsPath);
107+
File.WriteAllText(downloadGitPath, DateTime.UtcNow.ToString());
108+
Trace.Info("Git has been extracted and cleaned up");
109+
}
110+
catch (Exception ex)
111+
{
112+
Trace.Error(ex);
113+
}
114+
}
115+
}
116+
117+
internal class GitStore
118+
{
119+
private static readonly string baseUrl = "https://vstsagenttools.blob.core.windows.net/tools/mingit";
120+
private static readonly string bit = PlatformUtil.BuiltOnX86 ? "32" : "64";
121+
internal static Uri GetDownloadUrl(string version = GitManager.defaultGitVersion)
122+
{
123+
return new Uri($"{baseUrl}/{version}/MinGit-{version}-{bit}-bit.zip");
124+
}
125+
}
126+
}

src/Agent.Worker/JobExtension.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public async Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipel
239239
context.Output("Finished checking job knob settings.");
240240

241241
// Ensure that we send git telemetry before potential path env changes during the pipeline execution
242-
var isSelfHosted = StringUtil.ConvertToBoolean(jobContext.Variables.Get(Constants.Variables.Agent.IsSelfHosted));
242+
var isSelfHosted = StringUtil.ConvertToBoolean(jobContext.Variables.Get(Constants.Variables.Agent.IsSelfHosted));
243243
if (PlatformUtil.RunningOnWindows && isSelfHosted)
244244
{
245245
try
@@ -276,6 +276,24 @@ public async Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipel
276276
prepareStep.Condition = ExpressionManager.Succeeded;
277277
preJobSteps.Add(prepareStep);
278278
}
279+
280+
string gitVersion = null;
281+
282+
if (AgentKnobs.UseGit2_39_4.GetValue(jobContext).AsBoolean())
283+
{
284+
gitVersion = "2.39.4";
285+
}
286+
else if (AgentKnobs.UseGit2_42_0_2.GetValue(jobContext).AsBoolean())
287+
{
288+
gitVersion = "2.42.0.2";
289+
}
290+
291+
if (gitVersion is not null)
292+
{
293+
context.Debug($"Downloading Git v{gitVersion}");
294+
var gitManager = HostContext.GetService<IGitManager>();
295+
await gitManager.DownloadAsync(context, gitVersion);
296+
}
279297
}
280298

281299
// build up 3 lists of steps, pre-job, job, post-job

src/Test/L0/Plugin/TestGitCliManager/TestGitCliManagerL0.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Microsoft.VisualStudio.Services.Agent.Util;
33
using Moq;
44
using System;
5-
using System.Collections.Generic;
65
using System.IO;
76
using System.Threading;
87
using System.Threading.Tasks;

src/Test/L0/Worker/GitManagerL0.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.IO;
5+
using System.Threading;
6+
using Microsoft.VisualStudio.Services.Agent.Worker;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker
11+
{
12+
public sealed class GitManagerL0
13+
{
14+
[Fact]
15+
[Trait("Level", "L0")]
16+
[Trait("Category", "Worker")]
17+
[Trait("SkipOn", "darwin")]
18+
[Trait("SkipOn", "linux")]
19+
public async void DownloadAsync()
20+
{
21+
using var tokenSource = new CancellationTokenSource();
22+
using var hostContext = new TestHostContext(this);
23+
GitManager gitManager = new();
24+
gitManager.Initialize(hostContext);
25+
var executionContext = new Mock<IExecutionContext>();
26+
executionContext.Setup(x => x.CancellationToken).Returns(tokenSource.Token);
27+
await gitManager.DownloadAsync(executionContext.Object);
28+
29+
var externalsPath = hostContext.GetDirectory(WellKnownDirectory.Externals);
30+
31+
Assert.True(Directory.Exists(Path.Combine(externalsPath, "git-2.39.4")));
32+
Assert.True(File.Exists(Path.Combine(externalsPath, "git-2.39.4", "cmd", "git.exe")));
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)