Skip to content

Add FileProvider for Repository contents #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
15 changes: 15 additions & 0 deletions TH-NETII Octokit Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
LICENSE = LICENSE
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "THNETII.Octokit.FileProviders", "src\THNETII.Octokit.FileProviders\THNETII.Octokit.FileProviders.csproj", "{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{675627EF-4A75-4472-8770-70554C6DBDC1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "THNETII.Octokit.Test", "test\THNETII.Octokit.Test\THNETII.Octokit.Test.csproj", "{69CA99F9-5112-4E8D-BBC9-5530EDAD5F71}"
Expand All @@ -45,6 +47,18 @@ Global
{398AAFB6-5B6B-41F7-9FB3-9DAD64F71150}.Release|x64.Build.0 = Release|Any CPU
{398AAFB6-5B6B-41F7-9FB3-9DAD64F71150}.Release|x86.ActiveCfg = Release|Any CPU
{398AAFB6-5B6B-41F7-9FB3-9DAD64F71150}.Release|x86.Build.0 = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|x64.ActiveCfg = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|x64.Build.0 = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|x86.ActiveCfg = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Debug|x86.Build.0 = Debug|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|Any CPU.Build.0 = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|x64.ActiveCfg = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|x64.Build.0 = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|x86.ActiveCfg = Release|Any CPU
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C}.Release|x86.Build.0 = Release|Any CPU
{69CA99F9-5112-4E8D-BBC9-5530EDAD5F71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69CA99F9-5112-4E8D-BBC9-5530EDAD5F71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69CA99F9-5112-4E8D-BBC9-5530EDAD5F71}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand All @@ -63,6 +77,7 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{398AAFB6-5B6B-41F7-9FB3-9DAD64F71150} = {E0168284-A2AE-4A68-A586-1AA693585A53}
{DD7790D0-661D-4C16-AFC0-DC2D13769F2C} = {E0168284-A2AE-4A68-A586-1AA693585A53}
{69CA99F9-5112-4E8D-BBC9-5530EDAD5F71} = {675627EF-4A75-4472-8770-70554C6DBDC1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
36 changes: 36 additions & 0 deletions http/github_v3_repo_content.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@api_url = https://api.github.com
@oauth_token = {{$dotenv GITHUB_PAT_AUTH}}

@owner = thnetii
@repo_name = octokit.net-extensions

###
# @name GitHubRepositoryContentRoot
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentRootByRef
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/
?ref=91a54a7bf93463db48748a4f5b1e646d7a4bc4c3
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentWithDotPath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/.
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentWithNamedPath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/src
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentWithDotDotPath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/src/../
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}
23 changes: 23 additions & 0 deletions http/github_v3_repo_content_submodule.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@api_url = https://api.github.com
@oauth_token = {{$dotenv GITHUB_PAT_AUTH}}

@owner = thnetii
@repo_name = dotnet-data

###
# @name GitHubRepositoryContentWithSubmodulePath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentToSubmodulePath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/dotnet-common
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}

###
# @name GitHubRepositoryContentInsideSubmodulePath
GET {{api_url}}/repos/{{owner}}/{{repo_name}}/contents/dotnet-common/LICENSE
Accept: application/vnd.github.v3+json
Authorization: Basic pat:{{oauth_token}}
107 changes: 107 additions & 0 deletions src/THNETII.Octokit.FileProviders/GitHubRepositoryFileProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

using Octokit;

namespace THNETII.Octokit.FileProviders
{
public class GitHubRepositoryFileProvider : IFileProvider
{
private readonly IRepositoryCommitsClient repoCommitClient;
private readonly IRepositoryContentsClient repoContentClient;

public string Owner { get; }
public string Repository { get; }
public string? Reference { get; }

internal GitHubRepositoryFileProvider(IGitHubClient client,
Uri repositoryUri, string? reference = null)
: this(client,
GetOwnerAndRepositoryFromUri(repositoryUri ?? throw new ArgumentNullException(nameof(repositoryUri))),
reference)
{ }

[SuppressMessage("Globalization", "CA1303: Do not pass literals as localized parameters")]
private static (string owner, string name) GetOwnerAndRepositoryFromUri(
Uri uri)
{
try
{
string owner = uri.Segments[1].TrimEnd('/').TrimEnd();
string name = uri.Segments[2].TrimEnd('/').TrimEnd();
const string dotGitSuffix = ".git";
if (name.EndsWith(dotGitSuffix, StringComparison.OrdinalIgnoreCase))
name = name.Substring(0, name.Length - dotGitSuffix.Length);

return (owner, name);
}
catch (IndexOutOfRangeException idxExcept)
{
throw new ArgumentException("Repository URL must contain at least three path segments.",
nameof(uri), idxExcept);
}
}

[DebuggerStepThrough]
private GitHubRepositoryFileProvider(IGitHubClient client,
(string owner, string name) repo, string? reference = null)
: this(client, repo.owner, repo.name, reference)
{ }

[SuppressMessage("Globalization", "CA1303: Do not pass literals as localized parameters")]
public GitHubRepositoryFileProvider(IGitHubClient client,
string owner, string repository, string? reference = null)
: base()
{
_ = client ?? throw new ArgumentNullException(nameof(client));
repoContentClient = client.Repository.Content;
repoCommitClient = client.Repository.Commit;
Owner = owner switch
{
string _ when string.IsNullOrWhiteSpace(owner) =>
throw new ArgumentException("Repository owner must contain at least one non-whitespace character", nameof(owner)),
null => throw new ArgumentNullException(nameof(owner)),
_ => owner,
};
Repository = repository switch
{
string _ when string.IsNullOrWhiteSpace(repository) =>
throw new ArgumentException("Repository owner must contain at least one non-whitespace character", nameof(repository)),
null => throw new ArgumentNullException(nameof(repository)),
_ => repository,
};
Reference = reference;
}

public async Task<IDirectoryContents> GetDirectoryContentsAsync(
string subpath, CancellationToken cancelToken = default)
{
throw new NotImplementedException();
}

public async Task<IFileInfo> GetFileInfoAsync(string subpath,
CancellationToken cancelToken = default)
{
throw new NotImplementedException();
}

public IDirectoryContents GetDirectoryContents(string subpath) =>
GetDirectoryContentsAsync(subpath)
.ConfigureAwait(false).GetAwaiter().GetResult();

public IFileInfo GetFileInfo(string subpath) =>
GetFileInfoAsync(subpath)
.ConfigureAwait(false).GetAwaiter().GetResult();

public IChangeToken Watch(string filter)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="3.1.4" />
<PackageReference Include="Octokit" Version="0.47.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

using Octokit.Authentication.Test;
using Octokit.GitHubSourceLink.Test;
using Octokit.Test;

using THNETII.Octokit.FileProviders;

using Xunit;

namespace Octokit.FileProviders.Test
{
public class GitHubRepositoryFileProviderTest
{
private readonly IServiceProvider provider;
private readonly GitHubClient client;
private readonly GitHubSourceLinkOptions sourceLink;

public GitHubRepositoryFileProviderTest()
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
.AddGitHubSourceLinkInfo()
.Build());
services.AddOctokitCredentials();
services.AddGitHubSourceLinkOptions();

provider = services.BuildServiceProvider();

client = new GitHubClient(
AssemblyProductHeaderValue.Instance,
provider.GetRequiredService<ICredentialStore>());

sourceLink = provider.GetRequiredService<IOptions<GitHubSourceLinkOptions>>()
.Value;
}

[Fact]
public void CtorThrowsForNullClientWithOwnerAndName()
{
IGitHubClient client = null!;
Assert.Throws<ArgumentNullException>(nameof(client), () =>
{
_ = new GitHubRepositoryFileProvider(client, sourceLink.Owner, sourceLink.Repository);
});
}

[Fact]
public void CtorThrowsForNullOwner()
{
var client = this.client;
string owner = null!;
string repo = sourceLink.Repository;

Assert.Throws<ArgumentNullException>(nameof(owner),
() => new GitHubRepositoryFileProvider(client,
owner, repo));
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
[InlineData("\r\n")]
public void CtorThrowsForInvalidNonNullOwner(string owner)
{
string repo = sourceLink.Repository;

Assert.Throws<ArgumentException>(nameof(owner),
() => new GitHubRepositoryFileProvider(client,
owner, repo));
}

[Fact]
public void CtorThrowsForNullRepositoryName()
{
string owner = sourceLink.Owner;
string repository = null!;

Assert.Throws<ArgumentNullException>(nameof(repository),
() => new GitHubRepositoryFileProvider(client,
owner, repository));
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
[InlineData("\r\n")]
public void CtorThrowsForInvalidNonNullRepositoryName(string repository)
{
string owner = sourceLink.Owner;

Assert.Throws<ArgumentException>(nameof(repository),
() => new GitHubRepositoryFileProvider(client,
owner, repository));
}

[Fact]
public void CtorAcceptsNullReferenceWithOwnerAndName()
{
string owner = sourceLink.Owner;
string repository = sourceLink.Repository;

_ = new GitHubRepositoryFileProvider(client,
owner, repository, null);
}

[SkippableFact]
public void GetFileInfoForRepositoryLicense()
{
RepositoryContentLicense? licsenseContent;
try
{
licsenseContent = client.Repository.GetLicenseContents(
sourceLink.Owner, sourceLink.Repository
)
.ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (NotFoundException) { licsenseContent = null; }

Skip.If(licsenseContent is null, "Repository does not contain a LICENSE file");
Debug.Assert(!(licsenseContent is null));

var fileProvider = new GitHubRepositoryFileProvider(
client, sourceLink.Owner, sourceLink.Repository,
sourceLink.Reference);

var licenseFileInfo = fileProvider.GetFileInfo(licsenseContent.Path);

Assert.NotNull(licenseFileInfo);
Assert.True(licenseFileInfo.Exists);
Assert.Equal(licsenseContent.Name, licenseFileInfo.Name);
Assert.False(licenseFileInfo.IsDirectory);
}

[Fact]
public void GetDirectoryContentOfRootMatchClientContents()
{
var fileProvider = new GitHubRepositoryFileProvider(
client, sourceLink.Owner, sourceLink.Repository,
sourceLink.Reference);

var directoryContents = fileProvider.GetDirectoryContents("");

Task<IReadOnlyList<RepositoryContent>> clientContentsTask =
sourceLink.Reference is null
? client.Repository.Content.GetAllContents(
sourceLink.Owner,
sourceLink.Repository)
: client.Repository.Content.GetAllContentsByRef(
sourceLink.Owner,
sourceLink.Repository,
sourceLink.Reference);
var clientContents = clientContentsTask.ConfigureAwait(false)
.GetAwaiter().GetResult();

Assert.True(directoryContents.Exists);
Assert.Equal(
clientContents.Select(c => c.Path),
directoryContents.Select(fi => fi.Name),
StringComparer.OrdinalIgnoreCase
);
}
}
}
Loading