Skip to content

Commit b0d4324

Browse files
Add CLI option to filter target repositories
1 parent 3a95c5c commit b0d4324

11 files changed

Lines changed: 133 additions & 35 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Individual users are limited to GitHub's default set of issue labels, but this t
4848
### Options
4949

5050
- `-k` or `--api-key`: (*required*) GitHub API Key (Personal Access Token)
51+
- `-f` or `--filter`: only sync repositories that match the provided regular expression
5152
- `-a` or `--no-add`: do not add new labels
5253
- `-e` or `--no-edit`: do not edit existing labels
5354
- `-d` or `--no-delete`: do not delete stale labels

src/App.cs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Security.Authentication;
2+
using System.Text.RegularExpressions;
23
using Octokit;
34

45
namespace GitHubLabelSync;
@@ -22,29 +23,44 @@ public async Task Run(Settings settings)
2223
_log(settings.Name);
2324

2425
var account = await GetValidAccount(settings);
25-
var repos = await _sync.GetRepositories(account);
26+
var allRepos = await _sync.GetRepositories(account);
2627
var labels = await _sync.GetAccountLabels(account);
2728
_log(string.Empty);
2829

30+
var filteredRepos = settings.Filters.Any()
31+
? allRepos.Where(r => settings.Filters.Any(f => new Regex(f).IsMatch(r.Name)))
32+
: allRepos;
33+
34+
var repos = filteredRepos.ToArray();
35+
if (!repos.Any())
36+
{
37+
_log("(no repositories to sync)");
38+
}
39+
2940
foreach (var repo in repos)
3041
{
31-
_log(repo.Name);
32-
33-
if (repo.Archived)
34-
{
35-
_log($"(skipping: repo is archived)");
36-
}
37-
else
38-
{
39-
await _sync.SyncRepo(repo, settings, labels);
40-
}
41-
42-
_log(string.Empty);
42+
await SyncRepo(repo, settings, labels);
4343
}
4444

4545
_log("Done!");
4646
}
4747

48+
private async Task SyncRepo(Repository repo, Settings settings, IReadOnlyList<Label> labels)
49+
{
50+
_log(repo.Name);
51+
52+
if (repo.Archived)
53+
{
54+
_log($"(skipping: repo is archived)");
55+
}
56+
else
57+
{
58+
await _sync.SyncRepo(repo, settings, labels);
59+
}
60+
61+
_log(string.Empty);
62+
}
63+
4864
private async Task<Account> GetValidAccount(Settings settings)
4965
{
5066
var access = await _sync.ValidateAccess();

src/Command.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
1414
{
1515
try
1616
{
17-
await _console.Status().StartAsync("Running...", Run(settings));
17+
await _console.Status().StartAsync("Running...", async ctx => await Run(ctx, settings));
1818
return 0;
1919
}
2020
catch (Exception e)
@@ -24,9 +24,8 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
2424
}
2525
}
2626

27-
private Func<StatusContext, Task> Run(Settings settings)
28-
=> async ctx
29-
=> await Factory
30-
.App(settings.APIKey, s => ctx.Status(s), s => _console.WriteLine(s))
31-
.Run(settings);
27+
private async Task Run(StatusContext ctx, Settings settings)
28+
=> await Factory
29+
.App(settings.APIKey, s => ctx.Status(s), s => _console.WriteLine(s))
30+
.Run(settings);
3231
}

src/GitHubLabelSync.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<Version>2.0.1</Version>
3+
<Version>2.1.0</Version>
44
<OutputType>Exe</OutputType>
55
<PackAsTool>true</PackAsTool>
66
<ToolCommandName>sync-labels</ToolCommandName>

src/ISynchronizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public interface ISynchronizer
99
Task<ValidationResult> ValidateUser(Account account);
1010

1111
Task<Account> GetAccount(string name);
12-
Task<IEnumerable<Repository>> GetRepositories(Account account);
12+
Task<IReadOnlyList<Repository>> GetRepositories(Account account);
1313
Task<IReadOnlyList<Label>> GetAccountLabels(Account account);
1414

1515
Task SyncRepo(Repository repo, Settings settings, IReadOnlyList<Label> accountLabels);

src/Settings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public class Settings : CommandSettings
1010
[Description("The name of the GitHub organization or username to sync")]
1111
public string Name { get; init; } = null!;
1212

13+
[CommandOption("-f|--filter")]
14+
[Description("Only sync repositories that match the given regular expression")]
15+
public string[] Filters { get; init; } = Array.Empty<string>();
16+
1317
[CommandOption("-k|--api-key")]
1418
[Description("GitHub API Key (Personal Access Token)")]
1519
public string APIKey { get; init; } = null!;

src/Synchronizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public async Task<Account> GetAccount(string name)
5959
}
6060
}
6161

62-
public async Task<IEnumerable<Repository>> GetRepositories(Account account)
62+
public async Task<IReadOnlyList<Repository>> GetRepositories(Account account)
6363
=> account.Type == AccountType.Organization
6464
? await _gitHub.GetRepositoriesForOrganization(account)
6565
: await _gitHub.GetRepositoriesForUser(account);

test/AppTests.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using System.Text;
12
using NSubstitute;
23
using Octokit;
34
using Spectre.Console;
45
using Xunit;
6+
using Repository = GitHubLabelSync.Tests.Stubs.Repository;
57

68
namespace GitHubLabelSync.Tests;
79

@@ -16,7 +18,7 @@ public async Task SyncsAllReposFound()
1618
var sync = Substitute.For<ISynchronizer>();
1719
sync.ValidateAccess().Returns(ValidationResult.Success());
1820
sync.ValidateUser(Arg.Any<Account>()).Returns(ValidationResult.Success());
19-
sync.GetRepositories(Arg.Any<Account>()).Returns(new[] { new Repository(), new Repository(), new Repository() });
21+
sync.GetRepositories(Arg.Any<Account>()).Returns(new[] { new Repository("test1"), new Repository("test2"), new Repository("test3") });
2022
var app = new App(sync, NoOp, NoOp);
2123

2224
var settings = new Settings { Name = "ecoAPM" };
@@ -29,14 +31,50 @@ public async Task SyncsAllReposFound()
2931
await sync.Received(3).SyncRepo(Arg.Any<Repository>(), settings, Arg.Any<IReadOnlyList<Label>>());
3032
}
3133

34+
[Fact]
35+
public async Task SyncsFilteredRepos()
36+
{
37+
//arrange
38+
var sync = Substitute.For<ISynchronizer>();
39+
sync.ValidateAccess().Returns(ValidationResult.Success());
40+
sync.ValidateUser(Arg.Any<Account>()).Returns(ValidationResult.Success());
41+
sync.GetRepositories(Arg.Any<Account>()).Returns(new[]
42+
{
43+
new Repository("other1"),
44+
new Repository("test-abc1"),
45+
new Repository("test-abc2"),
46+
new Repository("def"),
47+
new Repository("other22"),
48+
});
49+
var app = new App(sync, NoOp, NoOp);
50+
51+
var settings = new Settings
52+
{
53+
Name = "ecoAPM",
54+
Filters = new[] { "abc", "def" }
55+
};
56+
57+
//act
58+
await app.Run(settings);
59+
60+
//assert
61+
await sync.Received().GetAccount("ecoAPM");
62+
await sync.Received(3).SyncRepo(Arg.Any<Repository>(), settings, Arg.Any<IReadOnlyList<Label>>());
63+
}
64+
3265
[Fact]
3366
public async Task SkipsArchivedRepos()
3467
{
3568
//arrange
3669
var sync = Substitute.For<ISynchronizer>();
3770
sync.ValidateAccess().Returns(ValidationResult.Success());
3871
sync.ValidateUser(Arg.Any<Account>()).Returns(ValidationResult.Success());
39-
sync.GetRepositories(Arg.Any<Account>()).Returns(new[] { new Repository(), new Stubs.ArchivedRepository(), new Repository() });
72+
sync.GetRepositories(Arg.Any<Account>()).Returns(new[]
73+
{
74+
new Repository("test1"),
75+
new Repository("test2", true),
76+
new Repository("test3")
77+
});
4078
var app = new App(sync, NoOp, NoOp);
4179

4280
var settings = new Settings { Name = "ecoAPM" };
@@ -48,6 +86,26 @@ public async Task SkipsArchivedRepos()
4886
await sync.Received(2).SyncRepo(Arg.Any<Repository>(), settings, Arg.Any<IReadOnlyList<Label>>());
4987
}
5088

89+
[Fact]
90+
public async Task ShowsMessageWhenNoRepos()
91+
{
92+
//arrange
93+
var sync = Substitute.For<ISynchronizer>();
94+
sync.ValidateAccess().Returns(ValidationResult.Success());
95+
sync.ValidateUser(Arg.Any<Account>()).Returns(ValidationResult.Success());
96+
sync.GetRepositories(Arg.Any<Account>()).Returns(Array.Empty<Repository>());
97+
var output = new StringBuilder();
98+
var app = new App(sync, NoOp, s => output.AppendLine(s));
99+
100+
var settings = new Settings { Name = "ecoAPM" };
101+
102+
//act
103+
await app.Run(settings);
104+
105+
//assert
106+
Assert.Contains("no repositories to sync", output.ToString());
107+
}
108+
51109
[Fact]
52110
public async Task ThrowsOnAccessValidationFailure()
53111
{

test/CommandTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,25 @@ public async Task RunPassesByDefault()
2525
//assert
2626
Assert.Equal(0, result);
2727
}
28+
29+
[Fact]
30+
public async Task RunFailsOnException()
31+
{
32+
//arrange
33+
var console = Substitute.For<IAnsiConsole>();
34+
console.ExclusivityMode.When(m => m.Run(Arg.Any<Func<Task<object>>>()))
35+
.Do(_ => throw new Exception());
36+
var command = new Command(console);
37+
38+
var remaining = Substitute.For<IRemainingArguments>();
39+
var context = new CommandContext(remaining, "test", null);
40+
41+
var settings = new Settings { APIKey = "abc123", Name = "ecoAPM" };
42+
43+
//act
44+
var result = await command.ExecuteAsync(context, settings);
45+
46+
//assert
47+
Assert.Equal(1, result);
48+
}
2849
}

test/Stubs/ArchivedRepository.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)