Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ public void BuildConfig_ShouldUseProvidedValues_WhenValuesAreProvided()
config.FileSystem.Should().Be(fileSystem);
}

[Fact]
public void BuildConfig_ShouldUseDecodedRepositoryName_ForAzureDevOpsUrls()
{
// Arrange
const string gitUrl = "https://dev.azure.com/example/example%20web%20site/_git/example%20web%20site";

RepositoryConfigBuilder builder = new RepositoryConfigBuilder()
.WithGitUrl(gitUrl);

// Act
RepositoryConfig config = builder.Build();

// Assert
config.RepositoryPath.Should().Be(Path.Combine(Path.GetFullPath("."), "example web site"));
}

[Fact]
public void WithCloneTargetPath_ShouldSetCloneTargetPath()
{
Expand Down
28 changes: 28 additions & 0 deletions src/Dutchskull.Aspire.PolyRepo.Tests.Unit/GitUrlUtilitiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ public class GitUrlUtilitiesTests
{
private const string GitUrl = "https://github.com/example/repo.git";
private const string ExpectedProjectName = "repo";
private const string AzureDevOpsGitUrl = "https://dev.azure.com/example/example%20web%20site/_git/example%20web%20site";

[Theory]
[InlineData("https://github.com/example/repo.git", true)]
[InlineData("https://github.com/example/repo", false)]
[InlineData("https://example@dev.azure.com/example/example%20web%20site/_git/example%20web%20site", true)]
Comment thread
RobGibbens marked this conversation as resolved.
[InlineData("https://dev.azure.com/example/example%20web%20site/_git/example%20web%20site", true)]
[InlineData("https://dev.azure.com/example/example%20web%20site/_git/example%20web%20site/", true)]
[InlineData("git@ssh.dev.azure.com:v3/example/example%20web%20site/example%20web%20site", true)]
[InlineData("git@github.com:example/repo", false)]
[InlineData("git://github.com/example/repo", false)]
[InlineData("https://example.com", false)]
Expand Down Expand Up @@ -56,4 +61,27 @@ public void GetProjectNameFromGitUrl_ShouldReturnLastSegment_WhenUrlDoesNotEndWi
// Assert
projectName.Should().Be(ExpectedProjectName);
}

[Theory]
[InlineData(AzureDevOpsGitUrl)]
[InlineData("https://dev.azure.com/example/example%20web%20site/_git/example%20web%20site/")]
[InlineData("git@ssh.dev.azure.com:v3/example/example%20web%20site/example%20web%20site")]
public void GetProjectNameFromGitUrl_ShouldDecodePercentEncodedRepositoryNames(string gitUrl)
{
// Act
string projectName = GitUrlUtilities.GetProjectNameFromGitUrl(gitUrl);

// Assert
projectName.Should().Be("example web site");
}

[Fact]
public void GetProjectNameFromGitUrl_ShouldThrow_WhenDecodedRepositoryNameContainsPathSeparators()
{
// Act
Action act = () => GitUrlUtilities.GetProjectNameFromGitUrl("https://dev.azure.com/example/project/_git/repository%2Fchild");

// Assert
act.Should().Throw<ArgumentException>();
}
}
36 changes: 33 additions & 3 deletions src/Dutchskull.Aspire.PolyRepo/GitUrlUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,42 @@ namespace Dutchskull.Aspire.PolyRepo;

internal static partial class GitUrlUtilities
{
internal static string GetProjectNameFromGitUrl(string gitUrl) =>
gitUrl.RemovePostfix(".git").Split('/')[^1];
internal static string GetProjectNameFromGitUrl(string gitUrl)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it maybe an idea to create a Uri object and get the segment you want by index? The reason I did some string math before is because it was a simple Uri. Now that it is more complex, a more simple solution that would scale would be nice.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you looking for "simpler", or more performant? I just pushed some changes with some optimizations for parsing and validation the URI.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like it simpler. Because I can image gitea and gitlab also having different Uri's and then it is interesting to just have a index to change of which segment of the uri you want to have.

I am moving, so it could be that I will react after Monday.

{
ReadOnlySpan<char> gitUrlSpan = gitUrl.AsSpan().TrimEnd('/');
int projectNameStart = gitUrlSpan.LastIndexOf('/');

if (projectNameStart < 0 || projectNameStart == gitUrlSpan.Length - 1)
{
throw new ArgumentException("Git URL could not be parsed.", nameof(gitUrl));
}

ReadOnlySpan<char> encodedProjectName = gitUrlSpan[(projectNameStart + 1)..];

if (encodedProjectName.EndsWith(".git", StringComparison.Ordinal))
{
encodedProjectName = encodedProjectName[..^4];
}

string projectName = encodedProjectName.Contains('%')
? Uri.UnescapeDataString(encodedProjectName.ToString())
: encodedProjectName.ToString();

if (string.IsNullOrWhiteSpace(projectName) ||
projectName is "." or ".." ||
projectName.Contains('/') ||
projectName.Contains('\\') ||
projectName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
throw new ArgumentException("Git URL resolved to an unsafe repository name.", nameof(gitUrl));
}

return projectName;
}

internal static bool IsValidGitUrl(string url) =>
!string.IsNullOrEmpty(url) && GitUrlRegex().IsMatch(url);

[GeneratedRegex(@"^(?:git|https?|git@[\w\.]+):\/\/[\w\.@\:\/\-~]+\.git(?:\/)?$")]
[GeneratedRegex(@"^(?:(?:git|https?):\/\/[^\s]+\.git(?:\/)?|https:\/\/(?:[\w.-]+@)?dev\.azure\.com\/[^\/\s]+\/[^\/\s]+\/_git\/[^\/\s]+(?:\/)?|git@ssh\.dev\.azure\.com:v3\/[^\/\s]+\/[^\/\s]+\/[^\/\s]+(?:\/)?)$")]

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Azure DevOps patterns allow percent-encoded names (e.g., %20), but GetProjectNameFromGitUrl later derives the local folder name by taking the last / segment verbatim. With Azure DevOps URLs this will produce a repository path containing %20 sequences instead of spaces (and other escaped characters), which is likely not the intended behavior given the goal of supporting spaces in project names. Consider URL-decoding the extracted segment(s) before using them as a directory name (and ensure decoding is safe for filesystem paths).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

private static partial Regex GitUrlRegex();
}