Skip to content

Commit 2fa6482

Browse files
authored
Fix several small bugs that an AI agent detected (#2303)
A collection of small bug fixes found with the help of an AI agent scanning the codebase. None of these are known to be actively exploited or causing user-reported issues, but each is a latent defect that could cause crashes, hangs, or incorrect behaviour under the right conditions. ## Fixes **Credential UI** — `github/gitlab: use correct param order` - Custom credential UI commands had swapped URL and username arguments. **Stream extensions** — `streamextensions: fix a bug in multi-var reset handling` - Multi-dictionary writer emitted the wrong value when a reset marker was followed by a single entry. **GitHub auth** — `github: handle empty domain or enterprise hints` - Null-reference exception when WWW-Authenticate header is missing domain/enterprise hints. **GitHub account filtering** — `github: do not filter accounts outside of dotcom` - Account filtering was applied outside of GitHub.com despite detecting it shouldn't be. **Network diagnostics** — `diagnose: fix network diag to await HTTP requests` - HTTP requests in the network diagnostic were not properly awaited. **macOS notarize script** — `macos: add die function to notarize.sh script` - Missing `die` function. **Git stderr handling** — `git: drain stderr on IsInsideRepository` - Potential hang when Git writes enough to stderr to fill the pipe buffer. **Windows layout script** — `windows: fix layout.ps1 if symboloutput is not set` - Null-reference trimming `SymbolOutput` when the variable is unset. **OAuth device code UI** — `oauth: pass cancellation token to in-proc device code UI` - Cancellation token was not forwarded, so dismissing the dialog didn't work. **TRACE2 thread ID** — `trace2: fix main thread identification` - Main thread ID starts at 1, not 0. **TRACE2 perf format** — `trace2: fix crash in perf format for large elapsed times` - `ArgumentOutOfRangeException` crash when elapsed time exceeds 9999 seconds. **HTTP config** — `http: use correct http.sslAutoClientCert setting name` - Setting was read from the wrong Git config section. **TRACE2 writer cleanup** — `trace2: fix incomplete disposal of writers on cleanup` - Forward iteration with removal skipped every other writer, leaking file handles. **Git stderr redirect** — `git: fix crash when reading stderr from non-redirected processes` - `InvalidOperationException` when `GetRemotes`/`CreateGitException` read non-redirected stderr. **Executable lookup** — `environment: check execute permission in TryLocateExecutable` - PATH scan didn't verify execute bits on POSIX, unlike the `which` it replaced. **Installer Exec command** — `windows: fix en-dash characters in installer Exec command` - Unicode en-dash characters instead of ASCII hyphens broke PowerShell 5.1 parameter parsing.
2 parents 632413f + fa7b374 commit 2fa6482

21 files changed

Lines changed: 141 additions & 26 deletions

src/osx/Installer.Mac/notarize.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#!/bin/bash
2+
die () {
3+
echo "$*" >&2
4+
exit 1
5+
}
26

37
for i in "$@"
48
do

src/shared/Core.Tests/Commands/DiagnoseCommandTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http;
33
using System.Security.AccessControl;
44
using System.Text;
5+
using System.Threading.Tasks;
56
using GitCredentialManager.Diagnostics;
67
using GitCredentialManager.Tests.Objects;
78
using Xunit;
@@ -11,7 +12,7 @@ namespace Core.Tests.Commands;
1112
public class DiagnoseCommandTests
1213
{
1314
[Fact]
14-
public void NetworkingDiagnostic_SendHttpRequest_Primary_OK()
15+
public async Task NetworkingDiagnostic_SendHttpRequest_Primary_OK()
1516
{
1617
var primaryUriString = "http://example.com";
1718
var sb = new StringBuilder();
@@ -24,14 +25,14 @@ public void NetworkingDiagnostic_SendHttpRequest_Primary_OK()
2425

2526
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
2627

27-
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
28+
await networkingDiagnostic.SendHttpRequestAsync(sb, new HttpClient(httpHandler));
2829

2930
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
3031
Assert.Contains(expected, sb.ToString());
3132
}
3233

3334
[Fact]
34-
public void NetworkingDiagnostic_SendHttpRequest_Backup_OK()
35+
public async Task NetworkingDiagnostic_SendHttpRequest_Backup_OK()
3536
{
3637
var primaryUriString = "http://example.com";
3738
var backupUriString = "http://httpforever.com";
@@ -48,15 +49,15 @@ public void NetworkingDiagnostic_SendHttpRequest_Backup_OK()
4849
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
4950
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);
5051

51-
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
52+
await networkingDiagnostic.SendHttpRequestAsync(sb, new HttpClient(httpHandler));
5253

5354
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
5455
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);
5556
Assert.Contains(expected, sb.ToString());
5657
}
5758

5859
[Fact]
59-
public void NetworkingDiagnostic_SendHttpRequest_No_Network()
60+
public async Task NetworkingDiagnostic_SendHttpRequest_No_Network()
6061
{
6162
var primaryUriString = "http://example.com";
6263
var backupUriString = "http://httpforever.com";
@@ -73,7 +74,7 @@ public void NetworkingDiagnostic_SendHttpRequest_No_Network()
7374
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
7475
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);
7576

76-
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
77+
await networkingDiagnostic.SendHttpRequestAsync(sb, new HttpClient(httpHandler));
7778

7879
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
7980
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);

src/shared/Core.Tests/EnvironmentTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public void PosixEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()
9494
[expectedPath] = Array.Empty<byte>(),
9595
}
9696
};
97+
fs.SetExecutable(expectedPath);
9798
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
9899
var env = new PosixEnvironment(fs, envars);
99100

@@ -116,6 +117,32 @@ public void PosixEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFir
116117
["/bin/foo"] = Array.Empty<byte>(),
117118
}
118119
};
120+
fs.SetExecutable(expectedPath);
121+
fs.SetExecutable("/usr/local/bin/foo");
122+
fs.SetExecutable("/bin/foo");
123+
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
124+
var env = new PosixEnvironment(fs, envars);
125+
126+
bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);
127+
128+
Assert.True(actualResult);
129+
Assert.Equal(expectedPath, actualPath);
130+
}
131+
132+
[PosixFact]
133+
public void PosixEnvironment_TryLocateExecutable_NotExecutable_SkipsToNextMatch()
134+
{
135+
string nonExecPath = "/home/john.doe/bin/foo";
136+
string expectedPath = "/usr/local/bin/foo";
137+
var fs = new TestFileSystem
138+
{
139+
Files = new Dictionary<string, byte[]>
140+
{
141+
[nonExecPath] = Array.Empty<byte>(),
142+
[expectedPath] = Array.Empty<byte>(),
143+
}
144+
};
145+
fs.SetExecutable(expectedPath);
119146
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
120147
var env = new PosixEnvironment(fs, envars);
121148

@@ -142,6 +169,8 @@ public void MacOSEnvironment_TryLocateExecutable_Paths_Are_Ignored()
142169
[expectedPath] = Array.Empty<byte>(),
143170
}
144171
};
172+
fs.SetExecutable(pathsToIgnore.FirstOrDefault());
173+
fs.SetExecutable(expectedPath);
145174
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
146175
var env = new PosixEnvironment(fs, envars);
147176

src/shared/Core.Tests/StreamExtensionsTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,13 @@ public void StreamExtensions_WriteDictionary_MultiEntriesWithEmpty_WritesKVPList
381381
{
382382
["a"] = new[] {"1", "2", "", "3", "4"},
383383
["b"] = new[] {"5"},
384-
["c"] = new[] {"6", "7", ""}
384+
["c"] = new[] {"6", "7", ""},
385+
["d"] = new[] {"8", "", "9"}
385386
};
386387

387388
string output = WriteStringStream(input, StreamExtensions.WriteDictionary, newLine: LF);
388389

389-
Assert.Equal("a[]=3\na[]=4\nb=5\n\n", output);
390+
Assert.Equal("a[]=3\na[]=4\nb=5\nd=9\n\n", output);
390391
}
391392

392393
#endregion

src/shared/Core.Tests/Trace2MessageTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public class Trace2MessageTests
1212
[InlineData(26.316083, " 26.316083 ")]
1313
[InlineData(100.316083, "100.316083 ")]
1414
[InlineData(1000.316083, "1000.316083")]
15+
[InlineData(10000.316083, "10000.316083")]
16+
[InlineData(100000.31608, "100000.316080")]
1517
public void BuildTimeSpan_Match_Returns_Expected_String(double input, string expected)
1618
{
1719
var actual = Trace2Message.BuildTimeSpan(input);

src/shared/Core/Authentication/OAuthAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationTo
247247
VerificationUrl = dcr.VerificationUri.ToString(),
248248
};
249249

250-
return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentWindowHandle(), CancellationToken.None);
250+
return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentWindowHandle(), ct);
251251
}
252252

253253
private Task ShowDeviceCodeViaHelperAsync(

src/shared/Core/Diagnostics/NetworkingDiagnostic.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st
2929
bool hasNetwork = NetworkInterface.GetIsNetworkAvailable();
3030
log.AppendLine($"IsNetworkAvailable: {hasNetwork}");
3131

32-
SendHttpRequest(log, httpClient);
32+
await SendHttpRequestAsync(log, httpClient);
3333

3434
log.Append($"Sending HEAD request to {TestHttpsUri}...");
3535
using var httpsResponse = await httpClient.HeadAsync(TestHttpsUri);
@@ -98,7 +98,7 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st
9898
return true;
9999
}
100100

101-
internal /* For testing purposes */ async void SendHttpRequest(StringBuilder log, HttpClient httpClient)
101+
internal /* For testing purposes */ async Task SendHttpRequestAsync(StringBuilder log, HttpClient httpClient)
102102
{
103103
foreach (var uri in new List<string> { TestHttpUri, TestHttpUriFallback })
104104
{

src/shared/Core/EnvironmentBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ internal virtual bool TryLocateExecutable(string program, ICollection<string> pa
138138
{
139139
string candidatePath = Path.Combine(basePath, program);
140140
if (FileSystem.FileExists(candidatePath) && (pathsToIgnore is null ||
141-
!pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase)))
141+
!pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase))
142+
&& FileSystem.FileIsExecutable(candidatePath))
142143
{
143144
path = candidatePath;
144145
return true;

src/shared/Core/FileSystem.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ public interface IFileSystem
3434
/// <returns>True if a file exists, false otherwise.</returns>
3535
bool FileExists(string path);
3636

37+
/// <summary>
38+
/// Check if a file has execute permissions.
39+
/// On Windows this always returns true. On POSIX it checks for any execute bit.
40+
/// </summary>
41+
/// <param name="path">Full path to file to test.</param>
42+
/// <returns>True if the file is executable, false otherwise.</returns>
43+
bool FileIsExecutable(string path);
44+
3745
/// <summary>
3846
/// Check if a directory exists at the specified path.
3947
/// </summary>
@@ -122,6 +130,23 @@ public abstract class FileSystem : IFileSystem
122130

123131
public bool FileExists(string path) => File.Exists(path);
124132

133+
#if NETFRAMEWORK
134+
public bool FileIsExecutable(string path) => true;
135+
#else
136+
public bool FileIsExecutable(string path)
137+
{
138+
if (!PlatformUtils.IsPosix())
139+
return true;
140+
141+
#pragma warning disable CA1416 // Platform guard via PlatformUtils.IsPosix()
142+
var mode = File.GetUnixFileMode(path);
143+
return (mode & (UnixFileMode.UserExecute |
144+
UnixFileMode.GroupExecute |
145+
UnixFileMode.OtherExecute)) != 0;
146+
#pragma warning restore CA1416
147+
}
148+
#endif
149+
125150
public bool DirectoryExists(string path) => Directory.Exists(path);
126151

127152
public string GetCurrentDirectory() => Directory.GetCurrentDirectory();

src/shared/Core/Git.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,15 @@ private string GetCurrentRepositoryInternal(bool suppressStreams)
146146
}
147147

148148
git.Start(Trace2ProcessClass.Git);
149+
150+
// Drain and throw away stderr asynchronously to avoid a deadlock
151+
// if the child process fills the stderr pipe buffer.
152+
if (suppressStreams)
153+
{
154+
git.Process.ErrorDataReceived += (_, _) => { };
155+
git.Process.BeginErrorReadLine();
156+
}
157+
149158
string data = git.StandardOutput.ReadToEnd();
150159
git.WaitForExit();
151160

@@ -167,6 +176,8 @@ public IEnumerable<GitRemote> GetRemotes()
167176
{
168177
using (var git = CreateProcess("remote -v show"))
169178
{
179+
// Redirect stderr so we can check for 'not a git repository' errors
180+
git.StartInfo.RedirectStandardError = true;
170181
git.Start(Trace2ProcessClass.Git);
171182
// To avoid deadlocks, always read the output stream first and then wait
172183
// TODO: don't read in all the data at once; stream it
@@ -267,7 +278,9 @@ public async Task<IDictionary<string, string>> InvokeHelperAsync(string args, ID
267278

268279
public static GitException CreateGitException(ChildProcess git, string message, ITrace2 trace2 = null)
269280
{
270-
var gitMessage = git.StandardError.ReadToEnd();
281+
var gitMessage = git.StartInfo.RedirectStandardError
282+
? git.StandardError.ReadToEnd()
283+
: null;
271284

272285
if (trace2 != null)
273286
throw new Trace2GitException(trace2, message, git.ExitCode, gitMessage);

0 commit comments

Comments
 (0)