diff --git a/Octokit.Tests.Integration/IssuesClientTests.cs b/Octokit.Tests.Integration/IssuesClientTests.cs index ac39ca1d..273f2708 100644 --- a/Octokit.Tests.Integration/IssuesClientTests.cs +++ b/Octokit.Tests.Integration/IssuesClientTests.cs @@ -1,5 +1,7 @@ using System; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Octokit; using Octokit.Tests.Integration; @@ -9,6 +11,7 @@ public class IssuesClientTests : IDisposable { readonly IGitHubClient _gitHubClient; readonly Repository _repository; + readonly IIssuesClient _issuesClient; public IssuesClientTests() { @@ -17,49 +20,138 @@ public class IssuesClientTests : IDisposable Credentials = Helper.Credentials }; var repoName = Helper.MakeNameWithTimestamp("public-repo"); - + _issuesClient = _gitHubClient.Issue; _repository = _gitHubClient.Repository.Create(new NewRepository { Name = repoName }).Result; } [IntegrationTest] - public async Task CanCreateRetrieveAndDeleteIssue() + public async Task CanCreateRetrieveAndCloseIssue() { string owner = _repository.Owner.Login; var newIssue = new NewIssue("a test issue") { Body = "A new unassigned issue" }; - var issue = await _gitHubClient.Issue.Create(owner, _repository.Name, newIssue); + var issue = await _issuesClient.Create(owner, _repository.Name, newIssue); try { Assert.NotNull(issue); - var retrieved = await _gitHubClient.Issue.Get(owner, _repository.Name, issue.Number); - var all = await _gitHubClient.Issue.GetForRepository(owner, _repository.Name); + var retrieved = await _issuesClient.Get(owner, _repository.Name, issue.Number); + var all = await _issuesClient.GetForRepository(owner, _repository.Name); Assert.NotNull(retrieved); Assert.True(all.Any(i => i.Number == retrieved.Number)); } finally { - var closed = _gitHubClient.Issue.Update(owner, _repository.Name, issue.Number, + var closed = _issuesClient.Update(owner, _repository.Name, issue.Number, new IssueUpdate { State = ItemState.Closed }) .Result; Assert.NotNull(closed); } } + [IntegrationTest] + public async Task CanListOpenIssuesWithDefaultSort() + { + string owner = _repository.Owner.Login; + var newIssue1 = new NewIssue("A test issue1") { Body = "A new unassigned issue" }; + var newIssue2 = new NewIssue("A test issue2") { Body = "A new unassigned issue" }; + var newIssue3 = new NewIssue("A test issue3") { Body = "A new unassigned issue" }; + var newIssue4 = new NewIssue("A test issue4") { Body = "A new unassigned issue" }; + await _issuesClient.Create(owner, _repository.Name, newIssue1); + Thread.Sleep(1000); + await _issuesClient.Create(owner, _repository.Name, newIssue2); + Thread.Sleep(1000); + await _issuesClient.Create(owner, _repository.Name, newIssue3); + var closed = await _issuesClient.Create(owner, _repository.Name, newIssue4); + await _issuesClient.Update(owner, _repository.Name, closed.Number, + new IssueUpdate { State = ItemState.Closed }); + + var issues = await _issuesClient.GetForRepository(owner, _repository.Name); + + Assert.Equal(3, issues.Count); + Assert.Equal("A test issue3", issues[0].Title); + Assert.Equal("A test issue2", issues[1].Title); + Assert.Equal("A test issue1", issues[2].Title); + } + + [IntegrationTest] + public async Task CanListIssuesWithAscendingSort() + { + string owner = _repository.Owner.Login; + + var newIssue1 = new NewIssue("A test issue1") { Body = "A new unassigned issue" }; + var newIssue2 = new NewIssue("A test issue2") { Body = "A new unassigned issue" }; + var newIssue3 = new NewIssue("A test issue3") { Body = "A new unassigned issue" }; + var newIssue4 = new NewIssue("A test issue4") { Body = "A new unassigned issue" }; + await _issuesClient.Create(owner, _repository.Name, newIssue1); + Thread.Sleep(1000); + await _issuesClient.Create(owner, _repository.Name, newIssue2); + Thread.Sleep(1000); + await _issuesClient.Create(owner, _repository.Name, newIssue3); + var closed = await _issuesClient.Create(owner, _repository.Name, newIssue4); + await _issuesClient.Update(owner, _repository.Name, closed.Number, + new IssueUpdate { State = ItemState.Closed }); + + var issues = await _issuesClient.GetForRepository(owner, _repository.Name, + new RepositoryIssueRequest {SortDirection = SortDirection.Ascending}); + + Assert.Equal(3, issues.Count); + Assert.Equal("A test issue1", issues[0].Title); + Assert.Equal("A test issue2", issues[1].Title); + Assert.Equal("A test issue3", issues[2].Title); + } + + [IntegrationTest] + public async Task CanListClosedIssues() + { + string owner = _repository.Owner.Login; + + var newIssue1 = new NewIssue("A test issue1") { Body = "A new unassigned issue" }; + var newIssue2 = new NewIssue("A closed issue") { Body = "A new unassigned issue" }; + await _issuesClient.Create(owner, _repository.Name, newIssue1); + await _issuesClient.Create(owner, _repository.Name, newIssue2); + var closed = await _issuesClient.Create(owner, _repository.Name, newIssue2); + await _issuesClient.Update(owner, _repository.Name, closed.Number, + new IssueUpdate { State = ItemState.Closed }); + + var issues = await _issuesClient.GetForRepository(owner, _repository.Name, + new RepositoryIssueRequest { State = ItemState.Closed }); + + Assert.Equal(1, issues.Count); + Assert.Equal("A closed issue", issues[0].Title); + } + + [IntegrationTest] + public async Task CanListMilestoneIssues() + { + string owner = _repository.Owner.Login; + var milestone = await _issuesClient.Milestone.Create(owner, _repository.Name, new NewMilestone("milestone")); + var newIssue1 = new NewIssue("A test issue1") { Body = "A new unassigned issue" }; + var newIssue2 = new NewIssue("A milestone issue") { Body = "A new unassigned issue", Milestone = milestone.Number }; + await _issuesClient.Create(owner, _repository.Name, newIssue1); + await _issuesClient.Create(owner, _repository.Name, newIssue2); + + var issues = await _issuesClient.GetForRepository(owner, _repository.Name, + new RepositoryIssueRequest { Milestone = milestone.Number.ToString(CultureInfo.InvariantCulture) }); + + Assert.Equal(1, issues.Count); + Assert.Equal("A milestone issue", issues[0].Title); + } + [IntegrationTest] public async Task CanRetrieveClosedIssues() { string owner = _repository.Owner.Login; var newIssue = new NewIssue("A test issue") { Body = "A new unassigned issue" }; - var issue1 = await _gitHubClient.Issue.Create(owner, _repository.Name, newIssue); - var issue2 = await _gitHubClient.Issue.Create(owner, _repository.Name, newIssue); - await _gitHubClient.Issue.Update(owner, _repository.Name, issue1.Number, + var issue1 = await _issuesClient.Create(owner, _repository.Name, newIssue); + var issue2 = await _issuesClient.Create(owner, _repository.Name, newIssue); + await _issuesClient.Update(owner, _repository.Name, issue1.Number, new IssueUpdate { State = ItemState.Closed }); - await _gitHubClient.Issue.Update(owner, _repository.Name, issue2.Number, + await _issuesClient.Update(owner, _repository.Name, issue2.Number, new IssueUpdate { State = ItemState.Closed }); - var retrieved = await _gitHubClient.Issue.GetForRepository(owner, _repository.Name, + var retrieved = await _issuesClient.GetForRepository(owner, _repository.Name, new RepositoryIssueRequest { State = ItemState.Closed }); Assert.True(retrieved.Count >= 2); diff --git a/Octokit.Tests.Integration/MilestonesClientTests.cs b/Octokit.Tests.Integration/MilestonesClientTests.cs new file mode 100644 index 00000000..2876ac9f --- /dev/null +++ b/Octokit.Tests.Integration/MilestonesClientTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Octokit; +using Octokit.Tests.Integration; +using Xunit; + +public class MilestonesClientTests : IDisposable +{ + readonly IGitHubClient _gitHubClient; + readonly IMilestonesClient _milestonesClient; + readonly Repository _repository; + readonly string _repositoryOwner; + readonly string _repositoryName; + + public MilestonesClientTests() + { + _gitHubClient = new GitHubClient("Test Runner User Agent") + { + Credentials = Helper.Credentials + }; + _milestonesClient = _gitHubClient.Issue.Milestone; + var repoName = Helper.MakeNameWithTimestamp("public-repo"); + + _repository = _gitHubClient.Repository.Create(new NewRepository { Name = repoName }).Result; + _repositoryOwner = _repository.Owner.Login; + _repositoryName = _repository.Name; + } + + [IntegrationTest] + public async Task CanRetrieveOneMilestone() + { + var newMilestone = new NewMilestone("a milestone") { DueOn = DateTime.Now }; + var created = await _milestonesClient.Create(_repositoryOwner, _repositoryName, newMilestone); + + var result = await _milestonesClient.Get(_repositoryOwner, _repositoryName, created.Number); + + Assert.Equal("a milestone", result.Title); + } + + [IntegrationTest] + public async Task CanListEmptyMilestones() + { + var milestones = await _milestonesClient.GetForRepository(_repositoryOwner, _repositoryName); + + Assert.Empty(milestones); + } + + [IntegrationTest] + public async Task CanListMilestonesWithDefaultSortByDueDateAsc() + { + var milestone1 = new NewMilestone("milestone 1") { DueOn = DateTime.Now }; + var milestone2 = new NewMilestone("milestone 2") { DueOn = DateTime.Now.AddDays(1) }; + var milestone3 = new NewMilestone("milestone 3") { DueOn = DateTime.Now.AddDays(3), State = ItemState.Closed }; + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone1); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone2); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone3); + + var milestones = await _milestonesClient.GetForRepository(_repositoryOwner, _repositoryName); + Assert.Equal(2, milestones.Count); + Assert.Equal("milestone 1", milestones[0].Title); + Assert.Equal("milestone 2", milestones[1].Title); + } + + [IntegrationTest] + public async Task CanListMilestonesWithSortByDueDateDesc() + { + var milestone1 = new NewMilestone("milestone 1") { DueOn = DateTime.Now }; + var milestone2 = new NewMilestone("milestone 2") { DueOn = DateTime.Now.AddDays(1) }; + var milestone3 = new NewMilestone("milestone 3") { DueOn = DateTime.Now.AddDays(3), State = ItemState.Closed }; + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone1); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone2); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone3); + + var milestones = await _milestonesClient.GetForRepository(_repositoryOwner, _repositoryName, + new MilestoneRequest { SortDirection = SortDirection.Descending }); + Assert.Equal(2, milestones.Count); + Assert.Equal("milestone 2", milestones[0].Title); + Assert.Equal("milestone 1", milestones[1].Title); + } + + [IntegrationTest] + public async Task CanListClosedMilestones() + { + var milestone1 = new NewMilestone("milestone 1") { DueOn = DateTime.Now }; + var milestone2 = new NewMilestone("milestone 2") { DueOn = DateTime.Now.AddDays(1) }; + var milestone3 = new NewMilestone("milestone 3") { DueOn = DateTime.Now.AddDays(3), State = ItemState.Closed }; + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone1); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone2); + await _milestonesClient.Create(_repositoryOwner, _repositoryName, milestone3); + + var milestones = await _milestonesClient.GetForRepository(_repositoryOwner, _repositoryName, + new MilestoneRequest { State = ItemState.Closed }); + + Assert.Equal(1, milestones.Count); + Assert.Equal("milestone 3", milestones[0].Title); + } + + [IntegrationTest] + public async Task CanRetrieveClosedIssues() + { + string owner = _repository.Owner.Login; + + var newIssue = new NewIssue("A test issue") { Body = "A new unassigned issue" }; + var issue1 = await _gitHubClient.Issue.Create(owner, _repository.Name, newIssue); + var issue2 = await _gitHubClient.Issue.Create(owner, _repository.Name, newIssue); + await _gitHubClient.Issue.Update(owner, _repository.Name, issue1.Number, + new IssueUpdate { State = ItemState.Closed }); + await _gitHubClient.Issue.Update(owner, _repository.Name, issue2.Number, + new IssueUpdate { State = ItemState.Closed }); + + var retrieved = await _gitHubClient.Issue.GetForRepository(owner, _repository.Name, + new RepositoryIssueRequest { State = ItemState.Closed }); + + Assert.True(retrieved.Count >= 2); + Assert.True(retrieved.Any(i => i.Number == issue1.Number)); + Assert.True(retrieved.Any(i => i.Number == issue2.Number)); + } + + public void Dispose() + { + Helper.DeleteRepo(_repository); + } +} diff --git a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj index 6e331027..2f4ddd09 100644 --- a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj +++ b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj @@ -56,6 +56,7 @@ + diff --git a/Octokit.Tests/Clients/MilestonesClientTests.cs b/Octokit.Tests/Clients/MilestonesClientTests.cs new file mode 100644 index 00000000..c0bc4ced --- /dev/null +++ b/Octokit.Tests/Clients/MilestonesClientTests.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NSubstitute; +using Octokit; +using Octokit.Tests; +using Octokit.Tests.Helpers; +using Xunit; + +public class MilestonesClientTests +{ + public class TheGetMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + client.Get("fake", "repo", 42); + + connection.Received().Get(Arg.Is(u => u.ToString() == "/repos/fake/repo/milestones/42"), + null); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new MilestonesClient(Substitute.For()); + + await AssertEx.Throws(async () => await client.Get(null, "name", 1)); + await AssertEx.Throws(async () => await client.Get("owner", null, 1)); + await AssertEx.Throws(async () => await client.Get(null, "", 1)); + await AssertEx.Throws(async () => await client.Get("", null, 1)); + } + } + + public class TheGetForRepositoryMethod + { + [Fact] + public async Task RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + await client.GetForRepository("fake", "repo"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "/repos/fake/repo/milestones"), + Args.EmptyDictionary); + } + + [Fact] + public void SendsAppropriateParameters() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + client.GetForRepository("fake", "repo", new MilestoneRequest { SortDirection = SortDirection.Descending }); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "/repos/fake/repo/milestones"), + Arg.Is>(d => d["direction"] == "desc" && d.Count == 1)); + } + } + + public class TheCreateMethod + { + [Fact] + public void PostsToCorrectUrl() + { + var newIssue = new NewIssue("some title"); + var connection = Substitute.For(); + var client = new IssuesClient(connection); + + client.Create("fake", "repo", newIssue); + + connection.Received().Post(Arg.Is(u => u.ToString() == "/repos/fake/repo/issues"), + newIssue); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var connection = Substitute.For(); + var client = new IssuesClient(connection); + + AssertEx.Throws(async () => await + client.Create(null, "name", new NewIssue("title"))); + AssertEx.Throws(async () => await + client.Create("", "name", new NewIssue("x"))); + AssertEx.Throws(async () => await + client.Create("owner", null, new NewIssue("x"))); + AssertEx.Throws(async () => await + client.Create("owner", "", new NewIssue("x"))); + AssertEx.Throws(async () => await + client.Create("owner", "name", null)); + } + } + + public class TheUpdateMethod + { + [Fact] + public void PostsToCorrectUrl() + { + var milestoneUpdate = new MilestoneUpdate(); + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + client.Update("fake", "repo", 42, milestoneUpdate); + + connection.Received().Patch(Arg.Is(u => u.ToString() == "/repos/fake/repo/milestones/42"), + milestoneUpdate); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + AssertEx.Throws(async () => await + client.Create(null, "name", new NewMilestone("title"))); + AssertEx.Throws(async () => await + client.Create("", "name", new NewMilestone("x"))); + AssertEx.Throws(async () => await + client.Create("owner", null, new NewMilestone("x"))); + AssertEx.Throws(async () => await + client.Create("owner", "", new NewMilestone("x"))); + AssertEx.Throws(async () => await + client.Create("owner", "name", null)); + } + } + + public class TheDeleteMethod + { + [Fact] + public void PostsToCorrectUrl() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + client.Delete("fake", "repo", 42); + + connection.Received().Delete(Arg.Is(u => u.ToString() == "/repos/fake/repo/milestones/42")); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var connection = Substitute.For(); + var client = new MilestonesClient(connection); + + AssertEx.Throws(async () => await + client.Delete(null, "name", 42)); + AssertEx.Throws(async () => await + client.Delete("", "name", 42)); + AssertEx.Throws(async () => await + client.Delete("owner", null, 42)); + AssertEx.Throws(async () => await + client.Delete("owner", "", 42)); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresArgument() + { + Assert.Throws(() => new MilestonesClient(null)); + } + } +} diff --git a/Octokit.Tests/Models/MilestoneRequestTests.cs b/Octokit.Tests/Models/MilestoneRequestTests.cs new file mode 100644 index 00000000..b25375fb --- /dev/null +++ b/Octokit.Tests/Models/MilestoneRequestTests.cs @@ -0,0 +1,59 @@ +using Octokit; +using Xunit; + +public class MilestoneRequestTests +{ + public class TheToParametersDictionaryMethod + { + [Fact] + public void OnlyContainsChangedValues() + { + var request = new MilestoneRequest { SortDirection = SortDirection.Descending }; + + var parameters = request.ToParametersDictionary(); + + Assert.Equal(1, parameters.Count); + Assert.Equal("desc", parameters["direction"]); + } + + [Fact] + public void ContainsSetValues() + { + var request = new MilestoneRequest + { + State = ItemState.Closed, + SortProperty = MilestoneSort.Completeness, + SortDirection = SortDirection.Descending, + }; + + var parameters = request.ToParametersDictionary(); + + Assert.Equal("closed", parameters["state"]); + Assert.Equal("completeness", parameters["sort"]); + Assert.Equal("desc", parameters["direction"]); + } + + [Fact] + public void DoesNotAddDefaultAscendingSort() + { + var request = new MilestoneRequest + { + SortDirection = SortDirection.Ascending, + }; + + var parameters = request.ToParametersDictionary(); + + Assert.Empty(parameters); + } + + [Fact] + public void ReturnsEmptyDictionaryForDefaultRequest() + { + var request = new MilestoneRequest(); + + var parameters = request.ToParametersDictionary(); + + Assert.Empty(parameters); + } + } +} diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index d71b72bc..8bee7a41 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -61,6 +61,7 @@ + @@ -93,6 +94,7 @@ + diff --git a/Octokit.Tests/OctokitRT.Tests.csproj b/Octokit.Tests/OctokitRT.Tests.csproj index d9352ac2..70f33577 100644 --- a/Octokit.Tests/OctokitRT.Tests.csproj +++ b/Octokit.Tests/OctokitRT.Tests.csproj @@ -53,6 +53,7 @@ + @@ -85,6 +86,7 @@ + diff --git a/Octokit/Clients/IssuesClient.cs b/Octokit/Clients/IssuesClient.cs index 7f3cc63d..0f732a40 100644 --- a/Octokit/Clients/IssuesClient.cs +++ b/Octokit/Clients/IssuesClient.cs @@ -8,9 +8,11 @@ namespace Octokit public IssuesClient(IApiConnection apiConnection) : base(apiConnection) { Assignee = new AssigneesClient(apiConnection); + Milestone = new MilestonesClient(apiConnection); } public IAssigneesClient Assignee { get; private set; } + public IMilestonesClient Milestone { get; private set; } /// /// Gets a single Issue by number./// diff --git a/Octokit/Clients/MilestonesClient.cs b/Octokit/Clients/MilestonesClient.cs new file mode 100644 index 00000000..efcea301 --- /dev/null +++ b/Octokit/Clients/MilestonesClient.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Octokit +{ + public class MilestonesClient : ApiClient, IMilestonesClient + { + public MilestonesClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// Gets all Milestones across all the authenticated user’s visible repositories including owned repositories, + /// member repositories, and organization repositories. + /// + /// + /// http://developer.github.com/v3/Milestones/#get-a-single-Milestone + /// + /// + public async Task Get(string owner, string name, int number) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + + return await ApiConnection.Get(ApiUrls.Milestone(owner, name, number)); + + } + + /// + /// Gets all open milestones for the repository. + /// + /// + /// http://developer.github.com/v3/Milestones/#list-Milestones-for-a-repository + /// + /// The owner of the repository + /// The name of the repository + /// + public async Task> GetForRepository(string owner, string name) + { + return await GetForRepository(owner, name, new MilestoneRequest()); + } + + /// + /// Gets all open milestones for the repository. + /// + /// + /// http://developer.github.com/v3/Milestones/#list-Milestones-for-a-repository + /// + /// The owner of the repository + /// The name of the repository + /// Used to filter and sort the list of Milestones returned + /// + public async Task> GetForRepository(string owner, string name, MilestoneRequest request) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(request, "request"); + + return await ApiConnection.GetAll(ApiUrls.Milestones(owner, name), + request.ToParametersDictionary()); + } + + /// + /// Creates an Milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#create-an-Milestone + /// The owner of the repository + /// The name of the repository + /// A instance describing the new Milestone to create + /// + public async Task Create(string owner, string name, NewMilestone newMilestone) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(newMilestone, "newMilestone"); + + return await ApiConnection.Post(ApiUrls.Milestones(owner, name), newMilestone); + } + + /// + /// Creates an Milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#create-an-Milestone + /// The owner of the repository + /// The name of the repository + /// The Milestone number + /// An instance describing the changes to make to the Milestone + /// + /// + public async Task Update(string owner, string name, int number, MilestoneUpdate milestoneUpdate) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(milestoneUpdate, "milestoneUpdate"); + + return await ApiConnection.Patch(ApiUrls.Milestone(owner, name, number), milestoneUpdate); + } + + /// + /// Deletes a milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#delete-a-milestone + /// The owner of the repository + /// The name of the repository + /// The milestone number + /// + public async Task Delete(string owner, string name, int number) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + + await ApiConnection.Delete(ApiUrls.Milestone(owner, name, number)); + } + } +} diff --git a/Octokit/Clients/NotificationsClient.cs b/Octokit/Clients/NotificationsClient.cs index 0e4429bb..0898a29f 100644 --- a/Octokit/Clients/NotificationsClient.cs +++ b/Octokit/Clients/NotificationsClient.cs @@ -6,7 +6,7 @@ namespace Octokit { public class NotificationsClient : ApiClient, INotificationsClient { - public NotificationsClient(IApiConnection client) : base(client) + public NotificationsClient(IApiConnection apiConnection) : base(apiConnection) { } diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 482cebe9..551084ce 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -211,5 +211,28 @@ namespace Octokit { return "/repos/{0}/{1}/assignees/{2}".FormatUri(owner, name, login); } + + /// + /// Returns the that returns the specified milestone. + /// + /// The owner of the repository + /// The name of the repository + /// /// The milestone number + /// + public static Uri Milestone(string owner, string name, int number) + { + return "/repos/{0}/{1}/milestones/{2}".FormatUri(owner, name, number); + } + + /// + /// Returns the that returns all of the milestones for the specified repository. + /// + /// The owner of the repository + /// The name of the repository + /// + public static Uri Milestones(string owner, string name) + { + return "/repos/{0}/{1}/milestones".FormatUri(owner, name); + } } } diff --git a/Octokit/IIssuesClient.cs b/Octokit/IIssuesClient.cs index 9841931a..b47b4c36 100644 --- a/Octokit/IIssuesClient.cs +++ b/Octokit/IIssuesClient.cs @@ -8,6 +8,11 @@ namespace Octokit { IAssigneesClient Assignee { get; } + /// + /// Client for managing milestones. + /// + IMilestonesClient Milestone { get; } + /// /// Gets a single Issue by number. /// diff --git a/Octokit/IMilestonesClient.cs b/Octokit/IMilestonesClient.cs new file mode 100644 index 00000000..d73c643c --- /dev/null +++ b/Octokit/IMilestonesClient.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Octokit +{ + public interface IMilestonesClient + { + /// + /// Gets all Milestones across all the authenticated user’s visible repositories including owned repositories, + /// member repositories, and organization repositories. + /// + /// + /// http://developer.github.com/v3/Milestones/#get-a-single-Milestone + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", + Justification = "Method makes a network request")] + Task Get(string owner, string name, int number); + + /// + /// Gets all open milestones for the repository. + /// + /// + /// http://developer.github.com/v3/Milestones/#list-Milestones-for-a-repository + /// + /// The owner of the repository + /// The name of the repository + /// + Task> GetForRepository(string owner, string name); + + /// + /// Gets all open milestones for the repository. + /// + /// + /// http://developer.github.com/v3/Milestones/#list-Milestones-for-a-repository + /// + /// The owner of the repository + /// The name of the repository + /// Used to filter and sort the list of Milestones returned + /// + Task> GetForRepository(string owner, string name, MilestoneRequest request); + + /// + /// Creates a milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#create-an-Milestone + /// The owner of the repository + /// The name of the repository + /// A instance describing the new Milestone to create + /// + Task Create(string owner, string name, NewMilestone newMilestone); + + /// + /// Creates a milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#update-a-milestone + /// The owner of the repository + /// The name of the repository + /// The Milestone number + /// An instance describing the changes to make to the Milestone + /// + /// + Task Update(string owner, string name, int number, MilestoneUpdate milestoneUpdate); + + /// + /// Deletes a milestone for the specified repository. Any user with pull access to a repository can create an + /// Milestone. + /// + /// http://developer.github.com/v3/Milestones/#delete-a-milestone + /// The owner of the repository + /// The name of the repository + /// The milestone number + /// + Task Delete(string owner, string name, int number); + } +} diff --git a/Octokit/Models/IssueRequest.cs b/Octokit/Models/IssueRequest.cs index 5bb001a4..bf392fb3 100644 --- a/Octokit/Models/IssueRequest.cs +++ b/Octokit/Models/IssueRequest.cs @@ -38,17 +38,19 @@ namespace Octokit if (Filter != _defaultParameterValues.Filter) { - parameters.Add("filter", Enum.GetName(typeof(IssueFilter), Filter).ToLowerInvariant()); + var filter = Enum.GetName(typeof(IssueFilter), Filter) ?? "filter"; + parameters.Add("filter", filter.ToLowerInvariant()); } if (State != _defaultParameterValues.State) { - parameters.Add("state", Enum.GetName(typeof(ItemState), State).ToLowerInvariant()); + parameters.Add("state", "closed"); } if (SortProperty != _defaultParameterValues.SortProperty) { - parameters.Add("sort", Enum.GetName(typeof(IssueSort), SortProperty).ToLowerInvariant()); + var sort = Enum.GetName(typeof(IssueSort), SortProperty) ?? "created"; + parameters.Add("sort", sort.ToLowerInvariant()); } if (SortDirection != _defaultParameterValues.SortDirection) diff --git a/Octokit/Models/Milestone.cs b/Octokit/Models/Milestone.cs index 8e681b15..5416a62a 100644 --- a/Octokit/Models/Milestone.cs +++ b/Octokit/Models/Milestone.cs @@ -4,15 +4,54 @@ namespace Octokit { public class Milestone { + /// + /// The URL for this milestone. + /// public Uri Url { get; set; } + + /// + /// The milestone number. + /// public int Number { get; set; } + + /// + /// Whether the milestone is open or closed. + /// public ItemState State { get; set; } + + /// + /// Title of the milestone + /// public string Title { get; set; } + + /// + /// Optional description for the milestone. + /// public string Description { get; set; } + + /// + /// The user that created this milestone. + /// public User Creator { get; set; } + + /// + /// The number of open issues in this milestone. + /// public int OpenIssues { get; set; } + + /// + /// The number of closed issues in this milestone. + /// public int ClosedIssues { get; set; } + + /// + /// The date this milestone was created + /// public DateTimeOffset CreatedAt { get; set; } + + /// + /// The date, if any, when this milestone is due. + /// public DateTimeOffset? DueOn { get; set; } } } \ No newline at end of file diff --git a/Octokit/Models/MilestoneRequest.cs b/Octokit/Models/MilestoneRequest.cs new file mode 100644 index 00000000..696b8a9f --- /dev/null +++ b/Octokit/Models/MilestoneRequest.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Octokit +{ + public class MilestoneRequest + { + static readonly MilestoneRequest _defaultParameterValues = new MilestoneRequest(); + + public MilestoneRequest() + { + State = ItemState.Open; + SortProperty = MilestoneSort.DueDate; + SortDirection = SortDirection.Ascending; + } + + public ItemState State { get; set; } + public MilestoneSort SortProperty { get; set; } + public SortDirection SortDirection { get; set; } + + /// + /// Returns a dictionary of query string parameters that represent this request. Only values that + /// do not have default values are in the dictionary. If everything is default, this returns an + /// empty dictionary. + /// + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "The API expects lowercase")] + public virtual IDictionary ToParametersDictionary() + { + var parameters = new Dictionary(); + + if (State != _defaultParameterValues.State) + { + parameters.Add("state", "closed"); + } + + if (SortProperty != _defaultParameterValues.SortProperty) + { + parameters.Add("sort", "completeness"); + } + + if (SortDirection != _defaultParameterValues.SortDirection) + { + parameters.Add("direction", "desc"); + } + + return parameters; + } + } + + public enum MilestoneSort + { + DueDate, + Completeness + } +} diff --git a/Octokit/Models/MilestoneUpdate.cs b/Octokit/Models/MilestoneUpdate.cs new file mode 100644 index 00000000..af883b09 --- /dev/null +++ b/Octokit/Models/MilestoneUpdate.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit +{ + public class MilestoneUpdate + { + /// + /// The milestone number. + /// + public int Number { get; set; } + + /// + /// Title of the milestone (required) + /// + public string Title { get; set; } + + /// + /// Whether the milestone is open or closed. The default is . + /// + public ItemState State { get; set; } + + /// + /// Optional description for the milestone. + /// + public string Description { get; set; } + + /// + /// An optional date when the milestone is due. + /// + public DateTimeOffset? DueOn { get; set; } + } +} diff --git a/Octokit/Models/NewMilestone.cs b/Octokit/Models/NewMilestone.cs new file mode 100644 index 00000000..d9823177 --- /dev/null +++ b/Octokit/Models/NewMilestone.cs @@ -0,0 +1,38 @@ +using System; + +namespace Octokit +{ + /// + /// Describes a new milestone to create via the method. + /// + public class NewMilestone + { + public NewMilestone(string title) + { + Ensure.ArgumentNotNull(title, "title"); + + Title = title; + State = ItemState.Open; + } + + /// + /// Title of the milestone (required) + /// + public string Title { get; private set; } + + /// + /// Whether the milestone is open or closed. The default is . + /// + public ItemState State { get; set; } + + /// + /// Optional description for the milestone. + /// + public string Description { get; set; } + + /// + /// An optional date when the milestone is due. + /// + public DateTimeOffset? DueOn { get; set; } + } +} diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index 9382c47f..bd874dd2 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -83,9 +83,13 @@ + + + + @@ -96,6 +100,7 @@ + diff --git a/Octokit/OctokitRT.csproj b/Octokit/OctokitRT.csproj index d7fb2e80..6f90671f 100644 --- a/Octokit/OctokitRT.csproj +++ b/Octokit/OctokitRT.csproj @@ -113,6 +113,7 @@ + @@ -151,6 +152,7 @@ + @@ -190,8 +192,11 @@ + + +