Skip to content
Merged
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
75 changes: 75 additions & 0 deletions ProjectDirector.Test/GitHubRemoteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.ProjectDirector.Test;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests the parsing of a GitHub remote into its owner and repository name.
/// </summary>
/// <remarks>
/// "Scan Dev Dir" keeps a working tree only when its origin parses here, so a form left out drops
/// every repository cloned that way without a word. SSH clones were exactly that case.
/// </remarks>
[TestClass]
public sealed class GitHubRemoteTests
{
[TestMethod]
[DataRow("https://github.com/ktsu-dev/ProjectDirector")]
[DataRow("https://github.com/ktsu-dev/ProjectDirector.git")]
[DataRow("https://github.com/ktsu-dev/ProjectDirector/")]
[DataRow("https://github.com/ktsu-dev/ProjectDirector.git/")]
[DataRow("HTTPS://GitHub.com/ktsu-dev/ProjectDirector.git")]
[DataRow("[email protected]:ktsu-dev/ProjectDirector")]
[DataRow("[email protected]:ktsu-dev/ProjectDirector.git")]
[DataRow("[email protected]:ktsu-dev/ProjectDirector/")]
[DataRow("[email protected]:ktsu-dev/ProjectDirector.git")]
[DataRow("ssh://[email protected]/ktsu-dev/ProjectDirector")]
[DataRow("ssh://[email protected]/ktsu-dev/ProjectDirector.git")]
[DataRow("ssh://[email protected]/ktsu-dev/ProjectDirector.git/")]
[DataRow("SSH://[email protected]/ktsu-dev/ProjectDirector.git")]
public void EveryGitHubRemoteFormYieldsItsOwnerAndRepository(string remote)
{
// Act
bool parsed = GitHubRepository.TryParseRemote(remote, out string owner, out string repo);

// Assert
Assert.IsTrue(parsed, $"{remote} should be recognised as a GitHub remote.");
Assert.AreEqual("ktsu-dev", owner);
Assert.AreEqual("ProjectDirector", repo);
Assert.IsTrue(GitHubRepository.IsRemotePathValid(GitRemotePath.Create<GitRemotePath>(remote)));
}

[TestMethod]
[DataRow("")]
[DataRow("https://gitlab.com/ktsu-dev/ProjectDirector.git")]
[DataRow("https://github.com.evil.example/ktsu-dev/ProjectDirector.git")]
[DataRow("[email protected]:ktsu-dev/ProjectDirector.git")]
[DataRow("https://github.com/ktsu-dev")]
[DataRow("https://github.com/ktsu-dev/")]
[DataRow("https://github.com//ProjectDirector")]
[DataRow("https://github.com/ktsu-dev/ProjectDirector/tree/main")]
[DataRow("C:/dev/ProjectDirector")]
public void AnythingElseIsNotAGitHubRemote(string remote)
{
// Act
bool parsed = GitHubRepository.TryParseRemote(remote, out string owner, out string repo);

// Assert
Assert.IsFalse(parsed, $"{remote} should not be recognised as a GitHub remote.");
Assert.AreEqual(string.Empty, owner);
Assert.AreEqual(string.Empty, repo);
}

[TestMethod]
public void AnSshCloneCreatesAGitHubRepository()
{
// Act
GitRepository? repo = GitRepository.Create(
GitRemotePath.Create<GitRemotePath>("[email protected]:ktsu-dev/ProjectDirector.git"),
FullyQualifiedLocalRepoPath.Create<FullyQualifiedLocalRepoPath>("/dev/ktsu-dev/ProjectDirector"));

// Assert
Assert.IsInstanceOfType<GitHubRepository>(repo);
}
}
61 changes: 60 additions & 1 deletion ProjectDirector/GitHubRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,68 @@
public GitHubOwnerName OwnerName { get; set; } = new();
public GitHubRepoName RepoName { get; set; } = new();

private static readonly string[] RemotePrefixes = ["https://github.com/", "ssh://[email protected]/", "[email protected]:"];

internal static bool IsRemotePathValid(GitRemotePath remotePath)
{
Ensure.NotNull(remotePath);
return remotePath.StartsWith("https://github.com/", StringComparison.Ordinal);
return TryParseRemote(remotePath, out _, out _);
}

/// <summary>
/// Reads the owner and repository name out of a GitHub remote.
/// </summary>
/// <param name="remote">The remote URL, as <c>git remote get-url</c> reports it.</param>
/// <param name="owner">The owner, empty when the remote is not a GitHub remote.</param>
/// <param name="repo">The repository name without any <c>.git</c> suffix, empty when the remote is not a GitHub remote.</param>
/// <returns>Whether <paramref name="remote"/> names a repository on GitHub.</returns>
/// <remarks>
/// Accepts the three forms git itself clones from: <c>https://github.com/owner/repo</c>,
/// the scp-style <c>[email protected]:owner/repo</c> and <c>ssh://[email protected]/owner/repo</c>,
/// each with or without a <c>.git</c> suffix and a trailing <c>/</c>. The host is compared
/// ignoring case, as DNS does, and the owner and repository name are kept as given. Git's
/// credential helper and SSH agent are what make the SSH forms work, so turning them away here
/// would drop every repository cloned over SSH.
/// </remarks>
internal static bool TryParseRemote(string remote, out string owner, out string repo)
{
owner = string.Empty;
repo = string.Empty;

if (string.IsNullOrWhiteSpace(remote))
{
return false;
}

string? path = null;
foreach (string prefix in RemotePrefixes)

Check warning on line 54 in ProjectDirector/GitHubRepository.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Loops should be simplified using the "Where" LINQ method

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ProjectDirector&issues=AaEQyeZY7w-d_53ObwWS&open=AaEQyeZY7w-d_53ObwWS&pullRequest=471
{
if (remote.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
path = remote[prefix.Length..];
break;
}
}

if (path is null)
{
return false;
}

path = path.TrimEnd('/');
if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
{
path = path[..^".git".Length];
}

string[] parts = path.Split('/');
if (parts.Length != 2 || parts[0].Length == 0 || parts[1].Length == 0)
{
return false;
}

owner = parts[0];
repo = parts[1];
return true;
}
}
29 changes: 13 additions & 16 deletions ProjectDirector/ProjectDirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
using ktsu.ImGui.Widgets;
using ktsu.ImGui.Styler;
using Octokit;
// using OpenAI.Chat;

Check warning on line 21 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

Check warning on line 21 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.
using Semantics.Paths;

#pragma warning disable CA1506
Expand Down Expand Up @@ -56,7 +56,7 @@
/// </summary>
private GitHubOwnerName? OwnerPendingTokenPopup { get; set; }

// private ChatClient ChatClient { get; init; }

Check warning on line 59 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

Check warning on line 59 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

private static void Main(string[] _)
{
Expand All @@ -79,7 +79,7 @@
_ = MakeLoadedOptionsSafe(Options, QueueLog);

Options.Save();
// ChatClient = new(model: "gpt-4o", new ApiKeyCredential(Options.OpenAIToken));

Check warning on line 82 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

Check warning on line 82 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.
DividerDiff = new("DiffDivider", DividerResized, ImGuiWidgets.DividerLayout.Columns);
DividerContainerCols = new("VerticalDivider", DividerResized, ImGuiWidgets.DividerLayout.Columns);
DividerContainerRows = new("HorizontalDivider", DividerResized, ImGuiWidgets.DividerLayout.Rows);
Expand Down Expand Up @@ -738,7 +738,7 @@
});
}

//int fetchInterval = repo.MinFetchIntervalSeconds;

Check warning on line 741 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

Check warning on line 741 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.
//if (ImGuiWidgets.Knob("Min Fetch Interval", ref fetchInterval, 0, 300, 150))
//{
// repo.MinFetchIntervalSeconds = fetchInterval;
Expand Down Expand Up @@ -1171,27 +1171,24 @@
FullyQualifiedLocalRepoPath localPath = MakeFullyQualifyLocalRepoPath(AbsoluteDirectoryPath.Create<AbsoluteDirectoryPath>(workingDirectory));
GitRemotePath remoteUrl = GitRemotePath.Create<GitRemotePath>(originUrl);

if (!GitHubRepository.TryParseRemote(originUrl, out string owner, out string name))
{
QueueLog($"[{DateTimeOffset.Now}] Skipped {workingDirectory}: origin {originUrl} is not a GitHub repository");
continue;
}

try
{
GitRepository? repo = GitRepository.Create(remoteUrl, localPath);
if (repo is GitHubRepository gitHubRepo)
{
List<string> remoteUrlParts = [];
foreach (ReadOnlySpan<char> part in remoteUrl.Split('/'))
{
remoteUrlParts.Add(part.ToString());
}
remoteUrlParts.Reverse();
if (remoteUrlParts.Count >= 2)
{
GitHubRepoName repoName = GitHubRepoName.Create<GitHubRepoName>(remoteUrlParts[0].RemoveSuffix(".git"));
GitHubOwnerName ownerName = GitHubOwnerName.Create<GitHubOwnerName>(remoteUrlParts[1]);
FullyQualifiedGitHubRepoName repoFullName = GetFullyQualifiedRepoName(ownerName, repoName);
Options.Repos[repoFullName] = gitHubRepo;
gitHubRepo.OwnerName = ownerName;
gitHubRepo.RepoName = repoName;
_ = Options.GitHubOwners.Add(gitHubRepo.OwnerName);
}
GitHubRepoName repoName = GitHubRepoName.Create<GitHubRepoName>(name);
GitHubOwnerName ownerName = GitHubOwnerName.Create<GitHubOwnerName>(owner);
FullyQualifiedGitHubRepoName repoFullName = GetFullyQualifiedRepoName(ownerName, repoName);
Options.Repos[repoFullName] = gitHubRepo;
gitHubRepo.OwnerName = ownerName;
gitHubRepo.RepoName = repoName;
_ = Options.GitHubOwners.Add(gitHubRepo.OwnerName);
}
}
catch (NotSupportedException)
Expand Down Expand Up @@ -2111,7 +2108,7 @@

if (ImGui.TableNextColumn())
{
//if (ImGui.Button($"Propagate Directory###Propagate{path.Replace(Path.DirectorySeparatorChar, '.').Replace(Path.AltDirectorySeparatorChar, '.')}"))

Check warning on line 2111 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.

Check warning on line 2111 in ProjectDirector/ProjectDirector.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this commented out code.
//{
// shouldOpenPopup |= true;
// Options.PropagatePath = path;
Expand Down
Loading