diff --git a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs index 6084e1c7..1511259c 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs @@ -15,6 +15,20 @@ namespace Octokit.Reactive.Clients _client = client; } + /// + /// Creates a new repository for the current user. + /// + /// A instance describing the new repository to create + /// An instance for the created repository + public IObservable Create(NewRepository newRepository) + { + Ensure.ArgumentNotNull(newRepository, "newRepository"); + if (string.IsNullOrEmpty(newRepository.Name)) + throw new ArgumentException("The new repository's name must not be null."); + + return _client.Create(newRepository).ToObservable(); + } + public IObservable Get(string owner, string name) { Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); diff --git a/Octokit.Reactive/IObservableRepositoriesClient.cs b/Octokit.Reactive/IObservableRepositoriesClient.cs index bc8a1a32..2dc6bb33 100644 --- a/Octokit.Reactive/IObservableRepositoriesClient.cs +++ b/Octokit.Reactive/IObservableRepositoriesClient.cs @@ -6,6 +6,13 @@ namespace Octokit.Reactive { public interface IObservableRepositoriesClient { + /// + /// Creates a new repository for the current user. + /// + /// A instance describing the new repository to create + /// An instance for the created repository + IObservable Create(NewRepository newRepository); + /// /// Retrieves the for the specified owner and name. /// diff --git a/Octokit.Tests.Integration/AutomationSettings.cs b/Octokit.Tests.Integration/AutomationSettings.cs index fca5ac97..ea7ae1df 100644 --- a/Octokit.Tests.Integration/AutomationSettings.cs +++ b/Octokit.Tests.Integration/AutomationSettings.cs @@ -57,5 +57,15 @@ namespace Octokit.Tests.Integration /// Username of a GitHub test account (DO NOT USE A "REAL" ACCOUNT). /// public string GitHubUsername { get; private set; } + + /// + /// Makes a name with an appended timestamp so that it's safe for testing (i.e., won't collide with existing names). + /// + /// The name to use as a base, to which a timestamp will be appended + /// The name with a timestamp appended + public static string MakeNameWithTimestamp(string name) + { + return string.Concat(name, "-", DateTime.UtcNow.ToString("yyyyMMddhhmmssfff")); + } } } diff --git a/Octokit.Tests.Integration/RepositoriesClientTests.cs b/Octokit.Tests.Integration/RepositoriesClientTests.cs index d53d27c2..67f5ece5 100644 --- a/Octokit.Tests.Integration/RepositoriesClientTests.cs +++ b/Octokit.Tests.Integration/RepositoriesClientTests.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Xunit; @@ -6,6 +7,199 @@ namespace Octokit.Tests.Integration { public class RepositoriesClientTests { + public class TheCreateMethod + { + [IntegrationTest] + public async Task CreatesANewPublicRepository() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("public-repo"); + + var createdRepository = await github.Repository.Create(new NewRepository { Name = repoName }); + + var cloneUrl = string.Format("https://github.com/{0}/{1}.git", github.Credentials.Login, repoName); + Assert.Equal(repoName, createdRepository.Name); + Assert.False(createdRepository.Private); + Assert.Equal(cloneUrl, createdRepository.CloneUrl); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.Equal(repoName, repository.Name); + Assert.Null(repository.Description); + Assert.False(repository.Private); + Assert.True(repository.HasDownloads); + Assert.True(repository.HasIssues); + Assert.True(repository.HasWiki); + Assert.Null(repository.Homepage); + } + + [IntegrationTest] + public async Task CreatesANewPrivateRepository() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("private-repo"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + Private = true + }); + + Assert.True(createdRepository.Private); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.True(repository.Private); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithoutDownloads() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-without-downloads"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + HasDownloads = false + }); + + Assert.False(createdRepository.HasDownloads); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.False(repository.HasDownloads); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithoutIssues() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-without-issues"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + HasIssues = false + }); + + Assert.False(createdRepository.HasIssues); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.False(repository.HasIssues); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithoutAWiki() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-without-wiki"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + HasWiki = false + }); + + Assert.False(createdRepository.HasWiki); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.False(repository.HasWiki); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithADescription() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-with-description"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + Description = "theDescription" + }); + + Assert.Equal("theDescription", createdRepository.Description); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.Equal("theDescription", repository.Description); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithAHomepage() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-with-homepage"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + Homepage = "http://aUrl.to/nowhere" + }); + + Assert.Equal("http://aUrl.to/nowhere", createdRepository.Homepage); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.Equal("http://aUrl.to/nowhere", repository.Homepage); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithAutoInit() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-with-autoinit"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + AutoInit = true + }); + + // TODO: Once the contents API has been added, check the actual files in the created repo + Assert.Equal(repoName, createdRepository.Name); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.Equal(repoName, repository.Name); + } + + [IntegrationTest] + public async Task CreatesARepositoryWithAGitignoreTemplate() + { + var github = new GitHubClient("Test Runner User Agent") + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + var repoName = AutomationSettings.MakeNameWithTimestamp("repo-with-gitignore"); + + var createdRepository = await github.Repository.Create(new NewRepository + { + Name = repoName, + AutoInit = true, + GitignoreTemplate = "visualstudio" + }); + + // TODO: Once the contents API has been added, check the actual files in the created repo + Assert.Equal(repoName, createdRepository.Name); + var repository = await github.Repository.Get(github.Credentials.Login, repoName); + Assert.Equal(repoName, repository.Name); + } + + // TODO: Add a test for the team_id param once an overload that takes an oranization is added + } + public class TheGetAsyncMethod { [IntegrationTest] diff --git a/Octokit.Tests/Clients/RepositoriesClientTests.cs b/Octokit.Tests/Clients/RepositoriesClientTests.cs index 08a0ec5b..2c9a698d 100644 --- a/Octokit.Tests/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests/Clients/RepositoriesClientTests.cs @@ -24,6 +24,41 @@ namespace Octokit.Tests.Clients } } + public class TheCreateMethod + { + [Fact] + public async Task EnsuresNonNullArguments() + { + var repositoriesClient = new RepositoriesClient(Substitute.For>()); + + await AssertEx.Throws(async () => await repositoriesClient.Create(null)); + await AssertEx.Throws(async () => await repositoriesClient.Create(new NewRepository { Name = null })); + } + + [Fact] + public void UsesTheUserReposUrl() + { + var client = Substitute.For>(); + var repositoriesClient = new RepositoriesClient(client); + + repositoriesClient.Create(new NewRepository { Name = "aName" }); + + client.Received().Create(Arg.Is(u => u.ToString() == "user/repos"), Arg.Any()); + } + + [Fact] + public void TheNewRepositoryDescription() + { + var client = Substitute.For>(); + var repositoriesClient = new RepositoriesClient(client); + var newRepository = new NewRepository { Name = "aName" }; + + repositoriesClient.Create(newRepository); + + client.Received().Create(Arg.Any(), newRepository); + } + } + public class TheGetMethod { [Fact] diff --git a/Octokit.Tests/SimpleJsonSerializerTests.cs b/Octokit.Tests/SimpleJsonSerializerTests.cs index 0c69c8ba..cf523cfa 100644 --- a/Octokit.Tests/SimpleJsonSerializerTests.cs +++ b/Octokit.Tests/SimpleJsonSerializerTests.cs @@ -1,4 +1,5 @@ -using Octokit.Http; +using System; +using Octokit.Http; using Xunit; namespace Octokit.Tests @@ -16,6 +17,52 @@ namespace Octokit.Tests Assert.Equal("{\"id\":42,\"first_name\":\"Phil\",\"is_something\":true,\"private\":true}", json); } + + [Fact] + public void OmitsPropertiesWithNullValue() + { + var item = new + { + Object = (object)null, + NullableInt = (int?)null, + NullableBool = (bool?)null + }; + + var json = new SimpleJsonSerializer().Serialize(item); + + Assert.Equal("{}", json); + } + + [Fact] + public void DoesNotOmitsNullablePropertiesWithAValue() + { + var item = new + { + Object = new { Id = 42 }, + NullableInt = (int?)1066, + NullableBool = (bool?)true + }; + + var json = new SimpleJsonSerializer().Serialize(item); + + Assert.Equal("{\"object\":{\"id\":42},\"nullable_int\":1066,\"nullable_bool\":true}", json); + } + + [Fact] + public void HandlesMixingNullAndNotNullData() + { + var item = new + { + Int = 42, + Bool = true, + NullableInt = (int?)null, + NullableBool = (bool?)null + }; + + var json = new SimpleJsonSerializer().Serialize(item); + + Assert.Equal("{\"int\":42,\"bool\":true}", json); + } } public class TheDeserializeMethod diff --git a/Octokit/Clients/RepositoriesClient.cs b/Octokit/Clients/RepositoriesClient.cs index 7cf0025c..4480c465 100644 --- a/Octokit/Clients/RepositoriesClient.cs +++ b/Octokit/Clients/RepositoriesClient.cs @@ -11,6 +11,21 @@ namespace Octokit.Clients { } + /// + /// Creates a new repository for the current user. + /// + /// A instance describing the new repository to create + /// A instance for the created repository + public async Task Create(NewRepository newRepository) + { + Ensure.ArgumentNotNull(newRepository, "newRepository"); + if (string.IsNullOrEmpty(newRepository.Name)) + throw new ArgumentException("The new repository's name must not be null."); + + var endpoint = new Uri("user/repos", UriKind.Relative); + return await Client.Create(endpoint, newRepository); + } + public async Task Get(string owner, string name) { Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); diff --git a/Octokit/GitHubModels.cs b/Octokit/GitHubModels.cs index 9562494b..230e2d5f 100644 --- a/Octokit/GitHubModels.cs +++ b/Octokit/GitHubModels.cs @@ -530,4 +530,61 @@ namespace Octokit public string Resource { get; set; } } + + /// + /// Describes a new repository to create via the method. + /// + public class NewRepository + { + /// + /// Optional. Gets or sets whether to create an initial commit with empty README. The default is false. + /// + public bool? AutoInit { get; set; } + + /// + /// Required. Gets or sets the new repository's description + /// + public string Description { get; set; } + + /// s + /// Optional. Gets or sets whether to the enable downloads for the new repository. The default is true. + /// + public bool? HasDownloads { get; set; } + + /// s + /// Optional. Gets or sets whether to the enable issues for the new repository. The default is true. + /// + public bool? HasIssues { get; set; } + + /// s + /// Optional. Gets or sets whether to the enable the wiki for the new repository. The default is true. + /// + public bool? HasWiki { get; set; } + + /// + /// Optional. Gets or sets the new repository's optional website. + /// + public string Homepage { get; set; } + + /// + /// Optional. Gets or sets the desired language's or platform's .gitignore template to apply. Use the name of the template without the extension; "Haskell", for example. Ignored if is null or false. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gitignore", Justification = "It needs to be this way for proper serialization.")] + public string GitignoreTemplate { get; set; } + + /// + /// Required. Gets or sets the new repository's name. + /// + public string Name { get; set; } + + /// + /// Optional. Gets or sets whether the new repository is private; the default is false. + /// + public bool? Private { get; set; } + + /// + /// Optional. Gets or sets the ID of the team to grant access to this repository. This is only valid when creating a repository for an organization. + /// + public int? TeamId { get; set; } + } } diff --git a/Octokit/Http/SimpleJsonSerializer.cs b/Octokit/Http/SimpleJsonSerializer.cs index 8c846cf4..8876b95b 100644 --- a/Octokit/Http/SimpleJsonSerializer.cs +++ b/Octokit/Http/SimpleJsonSerializer.cs @@ -1,4 +1,10 @@ -namespace Octokit.Http +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Octokit.Reflection; + +namespace Octokit.Http { public class SimpleJsonSerializer : IJsonSerializer { @@ -20,6 +26,32 @@ { return clrPropertyName.ToRubyCase(); } + + // This is overridden so that null values are omitted from serialized objects. + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "Need to support .NET 2")] + protected override bool TrySerializeUnknownTypes(object input, out object output) + { + if (input == null) throw new ArgumentNullException("input"); + output = null; + Type type = input.GetType(); + if (type.FullName == null) + return false; + IDictionary obj = new JsonObject(); + IDictionary getters = GetCache[type]; + foreach (KeyValuePair getter in getters) + { + if (getter.Value != null) + { + var value = getter.Value(input); + if (value == null) + continue; + + obj.Add(MapClrMemberNameToJsonFieldName(getter.Key), value); + } + } + output = obj; + return true; + } } } } diff --git a/Octokit/IRepositoriesClient.cs b/Octokit/IRepositoriesClient.cs index 47c7a44d..e6028c21 100644 --- a/Octokit/IRepositoriesClient.cs +++ b/Octokit/IRepositoriesClient.cs @@ -7,6 +7,13 @@ namespace Octokit { public interface IRepositoriesClient { + /// + /// Creates a new repository for the current user. + /// + /// A instance describing the new repository to create + /// A instance for the created repository + Task Create(NewRepository newRepository); + /// /// Retrieves the for the specified owner and name. /// diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index d4227ada..258ac06c 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -72,6 +72,7 @@ ..\packages\Microsoft.Bcl.1.1.3\lib\net40\System.Runtime.dll + ..\packages\Microsoft.Bcl.1.1.3\lib\net40\System.Threading.Tasks.dll