diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b1378917..e79d0bbe7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1 +1,2 @@ +- Added validation to detect and return clear error messages when a URL is provided instead of a name for organization, repository, or enterprise arguments (e.g., `--github-org`, `--github-target-org`, `--source-repo`, `--github-target-enterprise`) diff --git a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs index bf2d6697f..0008c1dcf 100644 --- a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs +++ b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs @@ -1,4 +1,7 @@ -namespace OctoshiftCLI.Commands.CreateTeam; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.Commands.CreateTeam; public class CreateTeamCommandArgs : CommandArgs { @@ -8,4 +11,12 @@ public class CreateTeamCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs index 8795ca52d..c7cb03bc7 100644 --- a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs +++ b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs @@ -1,4 +1,6 @@ - +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + namespace OctoshiftCLI.Commands.DownloadLogs; public class DownloadLogsCommandArgs : CommandArgs @@ -11,4 +13,17 @@ public class DownloadLogsCommandArgs : CommandArgs public string GithubPat { get; set; } public string MigrationLogFile { get; set; } public bool Overwrite { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } diff --git a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs index 7b92a6f0c..95a3968c7 100644 --- a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs +++ b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs @@ -1,4 +1,6 @@ using System.IO; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.GenerateMannequinCsv; @@ -10,4 +12,12 @@ public class GenerateMannequinCsvCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs b/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs index c85a52004..cb71c94df 100644 --- a/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs +++ b/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs @@ -15,6 +15,11 @@ public class GrantMigratorRoleCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + ActorType = ActorType?.ToUpper(); if (ActorType is "TEAM" or "USER") diff --git a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs index 71db554cb..701c8ad59 100644 --- a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs +++ b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs @@ -1,4 +1,5 @@ -using OctoshiftCLI.Services; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.ReclaimMannequin; @@ -17,6 +18,11 @@ public class ReclaimMannequinCommandArgs : CommandArgs public string TargetApiUrl { get; set; } public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (string.IsNullOrEmpty(Csv) && (string.IsNullOrEmpty(MannequinUser) || string.IsNullOrEmpty(TargetUser))) { throw new OctoshiftCliException($"Either --csv or --mannequin-user and --target-user must be specified"); diff --git a/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs b/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs index 1d9db9ef7..64bf2899c 100644 --- a/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs +++ b/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs @@ -15,6 +15,11 @@ public class RevokeMigratorRoleCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + ActorType = ActorType?.ToUpper(); if (ActorType is "TEAM" or "USER") diff --git a/src/Octoshift/Extensions/StringExtensions.cs b/src/Octoshift/Extensions/StringExtensions.cs index 8d323ff92..39d6cc8fd 100644 --- a/src/Octoshift/Extensions/StringExtensions.cs +++ b/src/Octoshift/Extensions/StringExtensions.cs @@ -26,5 +26,29 @@ public static class StringExtensions public static string EscapeDataString(this string value) => Uri.EscapeDataString(value); public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s); + + public static bool IsUrl(this string s) + { + if (s.IsNullOrWhiteSpace()) + { + return false; + } + + // Check if string starts with http:// or https:// + if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if string contains common URL patterns like domain.com/path or www. + if (s.Contains("://") || s.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if it looks like a URL path (contains / and .) + return s.Contains('/') && s.Contains('.'); + } } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs new file mode 100644 index 000000000..19fcfc8c9 --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs @@ -0,0 +1,44 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.CreateTeam; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.CreateTeam; + +public class CreateTeamCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + private const string TEAM_NAME = "my-team"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new CreateTeamCommandArgs + { + GithubOrg = "http://github.com/my-org", + TeamName = TEAM_NAME + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Name() + { + var args = new CreateTeamCommandArgs + { + GithubOrg = GITHUB_ORG, + TeamName = TEAM_NAME + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs new file mode 100644 index 000000000..f23079966 --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.DownloadLogs; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.DownloadLogs; + +public class DownloadLogsCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + private const string GITHUB_REPO = "foo-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = "https://github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = GITHUB_ORG, + GithubRepo = "github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs new file mode 100644 index 000000000..e92370e3e --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.GenerateMannequinCsv; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.GenerateMannequinCsv; + +public class GenerateMannequinCsvCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new GenerateMannequinCsvCommandArgs + { + GithubOrg = "www.github.com" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Name() + { + var args = new GenerateMannequinCsvCommandArgs + { + GithubOrg = GITHUB_ORG + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs index f429438c4..3183a41ce 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs @@ -42,4 +42,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl() FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) .Should().Throw(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new GrantMigratorRoleCommandArgs + { + GithubOrg = "https://github.com/my-org", + Actor = ACTOR, + ActorType = "USER" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs index 5592afc86..5640c6d25 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs @@ -21,4 +21,19 @@ public void No_Parameters_Provided_Throws_OctoshiftCliException() .Invoking(() => args.Validate(_mockOctoLogger.Object)) .Should().Throw(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new ReclaimMannequinCommandArgs + { + GithubOrg = "www.github.com/my-org", + Csv = "mannequins.csv" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs index 03d2ee051..ea4fc30f2 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs @@ -45,4 +45,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl() .Should() .ThrowExactly(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new RevokeMigratorRoleCommandArgs + { + GithubOrg = "github.com/my-org", + Actor = ACTOR, + ActorType = "USER" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs index 077de2cf8..3941be7fd 100644 --- a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs +++ b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs @@ -17,5 +17,27 @@ public void ReplaceInvalidCharactersWithDash_Returns_Valid_String(string value, normalizedValue.Should().Be(expectedValue); } + + [Theory] + [InlineData("https://github.com/my-org", true)] + [InlineData("http://github.com/my-org", true)] + [InlineData("https://github.com/my-org/my-repo", true)] + [InlineData("http://example.com", true)] + [InlineData("www.github.com", true)] + [InlineData("github.com/my-org", true)] + [InlineData("my-org", false)] + [InlineData("my-repo", false)] + [InlineData("my-org-123", false)] + [InlineData("my_repo", false)] + [InlineData("MyOrganization", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData(" ", false)] + public void IsUrl_Detects_URLs_Correctly(string value, bool expectedResult) + { + var result = value.IsUrl(); + + result.Should().Be(expectedResult); + } } } diff --git a/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs new file mode 100644 index 000000000..fbe52c145 --- /dev/null +++ b/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.AdoToGithub.Commands.IntegrateBoards; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.AdoToGithub.Commands.IntegrateBoards; + +public class IntegrateBoardsCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string ADO_ORG = "ado-org"; + private const string ADO_TEAM_PROJECT = "ado-project"; + private const string GITHUB_ORG = "github-org"; + private const string GITHUB_REPO = "github-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = "github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = GITHUB_ORG, + GithubRepo = "http://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs new file mode 100644 index 000000000..161617093 --- /dev/null +++ b/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.AdoToGithub.Commands.MigrateRepo; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.AdoToGithub.Commands.MigrateRepo; + +public class MigrateRepoCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string ADO_ORG = "ado-org"; + private const string ADO_TEAM_PROJECT = "ado-project"; + private const string ADO_REPO = "ado-repo"; + private const string GITHUB_ORG = "github-org"; + private const string GITHUB_REPO = "github-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = "https://github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = GITHUB_ORG, + GithubRepo = "www.github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs index 981b0ce5c..3ad43a6ab 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs @@ -107,5 +107,37 @@ public void UseGithubStorage_And_Aws_Bucket_Name_Throws() .ThrowExactly() .WithMessage("*--use-github-storage flag was provided with an AWS S3 Bucket name*"); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new GenerateScriptCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + GithubTargetOrg = TARGET_ORG, + Output = new FileInfo("unit-test-output") + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new GenerateScriptCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = "https://github.com/my-org", + Output = new FileInfo("unit-test-output") + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs index a90226961..2d1c24a5f 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs @@ -28,4 +28,69 @@ public void Target_Repo_Defaults_To_Source_Repo() args.TargetRepo.Should().Be(SOURCE_REPO); } + + [Fact] + public void Validate_Throws_When_SourceOrg_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = "http://github.com/my-org", + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_TargetOrg_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = "www.github.com", + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = "github.com/org/repo", + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + TargetRepo = "https://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs index e5508275b..596d3a3e0 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs @@ -30,5 +30,56 @@ public void Source_Pat_Defaults_To_Target_Pat() args.GithubSourcePat.Should().Be(TARGET_PAT); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + GithubTargetOrg = TARGET_ORG, + GithubTargetEnterprise = TARGET_ENTERPRISE, + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = "https://github.com/my-org", + GithubTargetEnterprise = TARGET_ENTERPRISE, + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetEnterprise_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = TARGET_ORG, + GithubTargetEnterprise = "https://github.com/enterprises/my-enterprise", + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-enterprise option expects an enterprise name, not a URL. Please provide just the enterprise name (e.g., 'my-enterprise' instead of 'https://github.com/enterprises/my-enterprise')."); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs index ae8b994b4..2cdbcab6a 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -308,5 +308,73 @@ public void MetadataArchiveUrl_With_MetadataArchivePath_Throws() .ThrowExactly() .WithMessage("*--metadata-archive-url and --metadata-archive-path may not be used together*"); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = "https://github.com/my-org", + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = "https://github.com/my-org/my-repo", + GithubTargetOrg = TARGET_ORG, + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + TargetRepo = "https://github.com/my-org/my-repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs index cde4810c7..0887124a2 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs @@ -28,4 +28,69 @@ public void Target_Repo_Defaults_To_Source_Repo() args.TargetRepo.Should().Be(SOURCE_REPO); } + + [Fact] + public void Validate_Throws_When_SourceOrg_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = "https://github.com/my-org", + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_TargetOrg_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = "github.com/my-org", + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = "www.github.com/org/repo", + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + TargetRepo = "http://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } diff --git a/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs b/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs index 0057d5266..426a9487b 100644 --- a/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs +++ b/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs @@ -1,4 +1,6 @@ using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.AdoToGithub.Commands.IntegrateBoards { @@ -12,5 +14,18 @@ public class IntegrateBoardsCommandArgs : CommandArgs public string AdoPat { get; set; } [Secret] public string GithubPat { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } } diff --git a/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs b/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs index a704883cc..379be827e 100644 --- a/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs +++ b/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs @@ -1,4 +1,6 @@ using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.AdoToGithub.Commands.MigrateRepo { @@ -17,5 +19,18 @@ public class MigrateRepoCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs index 52bfd7f52..26aa90416 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs @@ -28,6 +28,16 @@ public class GenerateScriptCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (AwsBucketName.HasValue()) { if (GhesApiUrl.IsNullOrWhiteSpace()) diff --git a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs index d472097d1..a3215a8e9 100644 --- a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs @@ -21,6 +21,26 @@ public class MigrateCodeScanningAlertsCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (SourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (TargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + if (SourceRepo.HasValue() && TargetRepo.IsNullOrWhiteSpace()) { TargetRepo = SourceRepo; diff --git a/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs b/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs index 4b9a85ae8..f0e11f20c 100644 --- a/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs +++ b/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs @@ -19,6 +19,21 @@ public class MigrateOrgCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetEnterprise.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-enterprise option expects an enterprise name, not a URL. Please provide just the enterprise name (e.g., 'my-enterprise' instead of 'https://github.com/enterprises/my-enterprise')."); + } + if (GithubTargetPat.HasValue() && GithubSourcePat.IsNullOrWhiteSpace()) { GithubSourcePat = GithubTargetPat; diff --git a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs index 58b24171a..076979476 100644 --- a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs +++ b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs @@ -41,6 +41,26 @@ public class MigrateRepoCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + DefaultSourcePat(log); DefaultTargetRepo(log); diff --git a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs index 890044fef..23a1fbf00 100644 --- a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs @@ -21,6 +21,26 @@ public class MigrateSecretAlertsCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (SourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (TargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + if (SourceRepo.HasValue() && TargetRepo.IsNullOrWhiteSpace()) { TargetRepo = SourceRepo;