Skip to content

Commit 5b92ef6

Browse files
authored
GitHub features (#152)
* Cache repositories. * Take path depth into account when sorting search results. * Added support for creating new GitHub items. * Localization.
1 parent 404cf2a commit 5b92ef6

19 files changed

Lines changed: 469 additions & 57 deletions

src/Tql.App/Search/SearchContext.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
using Tql.Abstractions;
33
using Tql.App.Services.Database;
44
using Tql.App.Support;
5+
using Tql.Utilities;
56

67
namespace Tql.App.Search;
78

8-
internal partial class SearchContext : ISearchContext, IDisposable
9+
internal class SearchContext : ISearchContext, IDisposable
910
{
1011
private readonly ConcurrentDictionary<IMatch, SearchResult> _results =
1112
new(ReferenceEqualityComparer<IMatch>.Instance);
@@ -244,6 +245,13 @@ public int Compare(SearchResult? a, SearchResult? b)
244245
return result;
245246
}
246247

248+
var aPathDepth = MatchText.GetPathDepth(a.Text);
249+
var bPathDepth = MatchText.GetPathDepth(b.Text);
250+
251+
result = aPathDepth.CompareTo(bPathDepth);
252+
if (result != 0)
253+
return result;
254+
247255
result = string.Compare(
248256
a.SimpleText,
249257
b.SimpleText,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System.Windows.Forms;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Tql.Abstractions;
4+
using Tql.Utilities;
5+
6+
namespace Tql.Plugins.GitHub.Categories;
7+
8+
internal class NewMatch(NewMatchDto dto) : IRunnableMatch, ISerializableMatch, ICopyableMatch
9+
{
10+
public string Text =>
11+
dto.Type switch
12+
{
13+
NewMatchType.Issue
14+
=> MatchText.Path($"{dto.Owner}/{dto.Repository}", Labels.NewMatch_NewIssue),
15+
NewMatchType.PullRequest
16+
=> MatchText.Path($"{dto.Owner}/{dto.Repository}", Labels.NewMatch_NewPullRequest),
17+
NewMatchType.Repository => Labels.NewMatch_NewRepository,
18+
NewMatchType.Gist => Labels.NewMatch_NewGist,
19+
NewMatchType.Organization => Labels.NewMatch_NewOrganization,
20+
NewMatchType.ImportRepository => Labels.NewMatch_ImportRepository,
21+
NewMatchType.Codespace => Labels.NewMatch_NewCodespace,
22+
_ => throw new ArgumentOutOfRangeException()
23+
};
24+
25+
public ImageSource Icon =>
26+
dto.Type switch
27+
{
28+
NewMatchType.Issue => Images.Issue,
29+
NewMatchType.PullRequest => Images.PullRequest,
30+
NewMatchType.Repository => Images.Repository,
31+
NewMatchType.Gist => Images.Gist,
32+
NewMatchType.Organization => Images.Organization,
33+
NewMatchType.ImportRepository => Images.ImportRepository,
34+
NewMatchType.Codespace => Images.Codespace,
35+
_ => throw new ArgumentOutOfRangeException()
36+
};
37+
38+
public MatchTypeId TypeId => TypeIds.New;
39+
40+
public Task Run(IServiceProvider serviceProvider, IWin32Window owner)
41+
{
42+
serviceProvider.GetRequiredService<IUI>().OpenUrl(GetUrl());
43+
44+
return Task.CompletedTask;
45+
}
46+
47+
public string Serialize()
48+
{
49+
return JsonSerializer.Serialize(dto);
50+
}
51+
52+
public Task Copy(IServiceProvider serviceProvider)
53+
{
54+
serviceProvider.GetRequiredService<IClipboard>().CopyUri(Text, GetUrl());
55+
56+
return Task.CompletedTask;
57+
}
58+
59+
private string GetUrl() =>
60+
dto.Type switch
61+
{
62+
NewMatchType.Issue
63+
=> $"https://github.com/{dto.Owner}/{dto.Repository}/issues/new/choose",
64+
NewMatchType.PullRequest => $"https://github.com/{dto.Owner}/{dto.Repository}/compare",
65+
NewMatchType.Repository => "https://github.com/new",
66+
NewMatchType.Gist => "https://gist.github.com/",
67+
NewMatchType.Organization => "https://github.com/account/organizations/new",
68+
NewMatchType.ImportRepository => "https://github.com/new/import",
69+
NewMatchType.Codespace => "https://github.com/codespaces/new",
70+
_ => throw new ArgumentOutOfRangeException()
71+
};
72+
}
73+
74+
internal record NewMatchDto(Guid? Id, string? Owner, string? Repository, NewMatchType Type);
75+
76+
internal enum NewMatchType
77+
{
78+
Issue,
79+
PullRequest,
80+
Repository,
81+
Gist,
82+
Organization,
83+
ImportRepository,
84+
Codespace
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Tql.Abstractions;
2+
using Tql.Plugins.GitHub.Services;
3+
using Tql.Utilities;
4+
5+
namespace Tql.Plugins.GitHub.Categories;
6+
7+
internal class NewType(
8+
IMatchFactory<NewMatch, NewMatchDto> factory,
9+
ConfigurationManager configurationManager
10+
) : MatchType<NewMatch, NewMatchDto>(factory)
11+
{
12+
public override Guid Id => TypeIds.New.Id;
13+
14+
protected override bool IsValid(NewMatchDto dto) =>
15+
!dto.Id.HasValue || configurationManager.Configuration.HasConnection(dto.Id.Value);
16+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Tql.Abstractions;
2+
using Tql.Plugins.GitHub.Data;
3+
using Tql.Plugins.GitHub.Services;
4+
using Tql.Plugins.GitHub.Support;
5+
using Tql.Utilities;
6+
7+
namespace Tql.Plugins.GitHub.Categories;
8+
9+
internal class NewsMatch(
10+
RootItemDto dto,
11+
ICache<GitHubData> cache,
12+
ConfigurationManager configurationManager,
13+
IMatchFactory<NewMatch, NewMatchDto> factory
14+
) : CachedMatch<GitHubData>(cache), ISerializableMatch
15+
{
16+
public override string Text =>
17+
MatchUtils.GetMatchLabel(Labels.NewsMatch_Label, configurationManager.Configuration, dto);
18+
19+
public override ImageSource Icon => Images.New;
20+
public override MatchTypeId TypeId => TypeIds.News;
21+
public override string SearchHint => Labels.NewsMatch_SearchHint;
22+
23+
protected override IEnumerable<IMatch> Create(GitHubData data)
24+
{
25+
yield return factory.Create(new NewMatchDto(null, null, null, NewMatchType.Repository));
26+
yield return factory.Create(
27+
new NewMatchDto(null, null, null, NewMatchType.ImportRepository)
28+
);
29+
yield return factory.Create(new NewMatchDto(null, null, null, NewMatchType.Codespace));
30+
yield return factory.Create(new NewMatchDto(null, null, null, NewMatchType.Gist));
31+
yield return factory.Create(new NewMatchDto(null, null, null, NewMatchType.Organization));
32+
33+
foreach (var repository in data.GetConnection(dto.Id).Repositories)
34+
{
35+
yield return factory.Create(
36+
new NewMatchDto(dto.Id, repository.Owner, repository.Name, NewMatchType.Issue)
37+
);
38+
yield return factory.Create(
39+
new NewMatchDto(dto.Id, repository.Owner, repository.Name, NewMatchType.PullRequest)
40+
);
41+
}
42+
}
43+
44+
public string Serialize()
45+
{
46+
return JsonSerializer.Serialize(dto);
47+
}
48+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Tql.Abstractions;
2+
using Tql.Plugins.GitHub.Services;
3+
using Tql.Plugins.GitHub.Support;
4+
using Tql.Utilities;
5+
6+
namespace Tql.Plugins.GitHub.Categories;
7+
8+
[RootMatchType]
9+
internal class NewsType(
10+
IMatchFactory<NewsMatch, RootItemDto> factory,
11+
ConfigurationManager configurationManager
12+
) : MatchType<NewsMatch, RootItemDto>(factory)
13+
{
14+
public override Guid Id => TypeIds.News.Id;
15+
16+
protected override bool IsValid(RootItemDto dto) =>
17+
configurationManager.Configuration.HasConnection(dto.Id);
18+
}

src/Tql.Plugins.GitHub/Categories/RepositoriesMatch.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,34 @@ public async Task<IEnumerable<IMatch>> Search(
3333
CancellationToken cancellationToken
3434
)
3535
{
36-
if (dto.Scope == RootItemScope.Global && text.IsWhiteSpace())
36+
if (dto.Scope == RootItemScope.User)
37+
{
38+
var data = await cache.Get();
39+
var connection = data.GetConnection(dto.Id);
40+
41+
if (text.IsWhiteSpace())
42+
{
43+
return connection
44+
.Repositories
45+
.OrderByDescending(p => p.UpdatedAt)
46+
.Select(CreateMatch);
47+
}
48+
49+
return context.Filter(connection.Repositories.Select(CreateMatch));
50+
51+
RepositoryMatch CreateMatch(GitHubRepository repository)
52+
{
53+
return factory.Create(
54+
new RepositoryMatchDto(
55+
dto.Id,
56+
$"{repository.Owner}/{repository.Name}",
57+
repository.HtmlUrl
58+
)
59+
);
60+
}
61+
}
62+
63+
if (text.IsWhiteSpace())
3764
return Array.Empty<IMatch>();
3865

3966
await context.DebounceDelay(cancellationToken);
@@ -44,12 +71,6 @@ CancellationToken cancellationToken
4471
await GitHubUtils.GetSearchPrefix(dto, cache) + text
4572
);
4673

47-
if (text.IsWhiteSpace())
48-
{
49-
request.SortField = RepoSearchSort.Updated;
50-
request.Order = SortDirection.Descending;
51-
}
52-
5374
var response = await client.Search.SearchRepo(request);
5475

5576
cancellationToken.ThrowIfCancellationRequested();

src/Tql.Plugins.GitHub/Categories/TypeIds.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ internal static class TypeIds
2828

2929
public static readonly MatchTypeId Gists = CreateId("660f3739-c1fb-4e59-af2d-608b743dcc7c");
3030
public static readonly MatchTypeId Gist = CreateId("948f5282-6bb8-4122-8c5f-ff2a1929d2c1");
31+
32+
public static readonly MatchTypeId News = CreateId("d5561f08-6bbf-4133-8ab4-d55cd1c97d57");
33+
public static readonly MatchTypeId New = CreateId("0141215a-4947-4d12-9e45-ba929314178d");
3134
}

src/Tql.Plugins.GitHub/Data/GitHubCacheManager.cs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using Tql.Abstractions;
1+
using Octokit;
2+
using Tql.Abstractions;
23
using Tql.Plugins.GitHub.Services;
4+
using Tql.Plugins.GitHub.Support;
35

46
namespace Tql.Plugins.GitHub.Data;
57

@@ -8,7 +10,7 @@ internal class GitHubCacheManager : ICacheManager<GitHubData>
810
private readonly ConfigurationManager _configurationManager;
911
private readonly GitHubApi _api;
1012

11-
public int Version => 1;
13+
public int Version => 2;
1214

1315
public event EventHandler<CacheInvalidationRequiredEventArgs>? CacheInvalidationRequired;
1416

@@ -30,20 +32,54 @@ public async Task<GitHubData> Create()
3032
var client = await _api.GetClient(connection.Id);
3133

3234
var user = await client.User.Current();
33-
var organizations = await client.Organization.GetAllForCurrent();
35+
var organizations = (
36+
from organization in await client.Organization.GetAllForCurrent()
37+
select organization.Login
38+
).ToImmutableArray();
39+
40+
var repositories = await GetRepositories(user, organizations, client);
3441

3542
connections.Add(
36-
new GitHubConnectionData(
37-
connection.Id,
38-
user.Login,
39-
organizations.Select(p => p.Login).ToImmutableArray()
40-
)
43+
new GitHubConnectionData(connection.Id, user.Login, organizations, repositories)
4144
);
4245
}
4346

4447
return new GitHubData(connections.ToImmutableArray());
4548
}
4649

50+
private static async Task<ImmutableArray<GitHubRepository>> GetRepositories(
51+
User user,
52+
ImmutableArray<string> organizations,
53+
GitHubClient client
54+
)
55+
{
56+
var repositories = ImmutableArray.CreateBuilder<GitHubRepository>();
57+
58+
var request = new SearchRepositoriesRequest(
59+
GitHubUtils.GetSearchPrefix(user.Login, organizations)
60+
);
61+
62+
for (var page = 1; ; page++)
63+
{
64+
request.Page = page;
65+
66+
var response = await client.Search.SearchRepo(request);
67+
68+
repositories.AddRange(
69+
response
70+
.Items
71+
.Select(
72+
p => new GitHubRepository(p.Owner.Login, p.Name, p.HtmlUrl, p.UpdatedAt)
73+
)
74+
);
75+
76+
if (response.Items.Count == 0)
77+
break;
78+
}
79+
80+
return repositories.ToImmutable();
81+
}
82+
4783
protected virtual void OnCacheInvalidationRequired(CacheInvalidationRequiredEventArgs e) =>
4884
CacheInvalidationRequired?.Invoke(this, e);
4985
}
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
namespace Tql.Plugins.GitHub.Data;
22

3-
internal record GitHubData(ImmutableArray<GitHubConnectionData> Connections);
3+
internal record GitHubData(ImmutableArray<GitHubConnectionData> Connections)
4+
{
5+
public GitHubConnectionData GetConnection(Guid id) => Connections.Single(p => p.Id == id);
6+
}
47

58
internal record GitHubConnectionData(
69
Guid Id,
710
string UserName,
8-
ImmutableArray<string> Organizations
11+
ImmutableArray<string> Organizations,
12+
ImmutableArray<GitHubRepository> Repositories
13+
);
14+
15+
internal record GitHubRepository(
16+
string Owner,
17+
string Name,
18+
string HtmlUrl,
19+
DateTimeOffset UpdatedAt
920
);

0 commit comments

Comments
 (0)