diff --git a/Octokit.Tests/Helpers/StringExtensionsTests.cs b/Octokit.Tests/Helpers/StringExtensionsTests.cs index 8a3fc36f..c90d9468 100644 --- a/Octokit.Tests/Helpers/StringExtensionsTests.cs +++ b/Octokit.Tests/Helpers/StringExtensionsTests.cs @@ -64,5 +64,30 @@ namespace Octokit.Tests.Helpers Assert.Equal(expected, template.ExpandUriTemplate(new { name = "example name.txt", label = "labeltext" }).ToString()); } } + + public class EscapeDoubleQuotesMethod + { + [Fact] + public void EscapeDoubleQuotesReturnsNullForNullInput() + { + Assert.Equal(null, (null as string).EscapeDoubleQuotes()); + } + + [Fact] + public void EscapeDoubleQuotesReturnsInputWithoutDoubleQuotes() + { + string input = "some test input without double quotes in it"; + + Assert.Equal(input, input.EscapeDoubleQuotes()); + } + + [Fact] + public void EscapeDoubleQuotesEscapesAllDoubleQuotes() + { + string input = "\"test milestone\""; + + Assert.Equal("\\\"test milestone\\\"", input.EscapeDoubleQuotes()); + } + } } } diff --git a/Octokit.Tests/Models/SearchIssuesRequestExclusionsTests.cs b/Octokit.Tests/Models/SearchIssuesRequestExclusionsTests.cs index 7b6d9815..2b31db6f 100644 --- a/Octokit.Tests/Models/SearchIssuesRequestExclusionsTests.cs +++ b/Octokit.Tests/Models/SearchIssuesRequestExclusionsTests.cs @@ -19,7 +19,8 @@ public class SearchIssuesRequestExclusionsTests { "commenter:", (x,value) => x.Commenter = value }, { "involves:", (x,value) => x.Involves = value }, { "head:", (x,value) => x.Head = value }, - { "base:", (x,value) => x.Base = value } + { "base:", (x,value) => x.Base = value }, + { "milestone:", (x,value) => x.Milestone = value } }; foreach (var property in stringProperties) @@ -80,5 +81,25 @@ public class SearchIssuesRequestExclusionsTests Assert.True(request.MergedQualifiers().Contains("-status:error")); } + + [Fact] + public void HandlesMilestoneAttributeWithoutQuotes() + { + var request = new SearchIssuesRequestExclusions(); + Assert.False(request.MergedQualifiers().Any(x => x.Contains("-milestone:"))); + + request.Milestone = "testMilestone"; + Assert.True(request.MergedQualifiers().Contains("-milestone:\"testMilestone\"")); + } + + [Fact] + public void DoesntWrapMilestoneWithDoubleQuotesForQuotedMilestone() + { + var request = new SearchIssuesRequestExclusions(); + Assert.False(request.MergedQualifiers().Any(x => x.Contains("-milestone:"))); + + request.Milestone = "\"testMilestone\""; + Assert.Contains("-milestone:\"\\\"testMilestone\\\"\"", request.MergedQualifiers()); + } } } \ No newline at end of file diff --git a/Octokit.Tests/Models/SearchIssuesRequestTests.cs b/Octokit.Tests/Models/SearchIssuesRequestTests.cs index 889081d2..13bcbb32 100644 --- a/Octokit.Tests/Models/SearchIssuesRequestTests.cs +++ b/Octokit.Tests/Models/SearchIssuesRequestTests.cs @@ -41,7 +41,8 @@ public class SearchIssuesRequestTests { "team:", (x,value) => x.Team = value }, { "head:", (x,value) => x.Head = value }, { "base:", (x,value) => x.Base = value }, - { "user:", (x,value) => x.User = value } + { "user:", (x,value) => x.User = value }, + { "milestone:", (x,value) => x.Milestone = value } }; foreach (var property in stringProperties) @@ -116,6 +117,26 @@ public class SearchIssuesRequestTests Assert.True(request.MergedQualifiers().Contains("label:label2")); } + [Fact] + public void HandlesMilestoneAttributeWithoutQuotes() + { + var request = new SearchIssuesRequest("text"); + Assert.False(request.MergedQualifiers().Any(x => x.Contains("milestone:"))); + + request.Milestone = "testMilestone"; + Assert.True(request.MergedQualifiers().Contains("milestone:\"testMilestone\"")); + } + + [Fact] + public void DoesntWrapMilestoneWithDoubleQuotesForQuotedMilestone() + { + var request = new SearchIssuesRequest("text"); + Assert.False(request.MergedQualifiers().Any(x => x.Contains("milestone:"))); + + request.Milestone = "\"testMilestone\""; + Assert.Contains("milestone:\"\\\"testMilestone\\\"\"", request.MergedQualifiers()); + } + [Fact] public void HandlesNoMetadataAttributeCorrectly() { diff --git a/Octokit/Helpers/StringExtensions.cs b/Octokit/Helpers/StringExtensions.cs index a1cd0580..30526f83 100644 --- a/Octokit/Helpers/StringExtensions.cs +++ b/Octokit/Helpers/StringExtensions.cs @@ -87,6 +87,17 @@ namespace Octokit Ensure.ArgumentNotNullOrEmptyString(value, nameof(value)); return string.Concat(value[0].ToString().ToUpperInvariant(), value.Substring(1)); } + + internal static string EscapeDoubleQuotes(this string value) + { + if (value != null) + { + return value.Replace("\"", "\\\""); + } + + return value; + } + static IEnumerable SplitUpperCase(this string source) { Ensure.ArgumentNotNullOrEmptyString(source, nameof(source)); diff --git a/Octokit/Models/Request/SearchIssuesRequest.cs b/Octokit/Models/Request/SearchIssuesRequest.cs index 299111b1..f0c66287 100644 --- a/Octokit/Models/Request/SearchIssuesRequest.cs +++ b/Octokit/Models/Request/SearchIssuesRequest.cs @@ -1,11 +1,11 @@ -using System; +using Octokit.Internal; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using Octokit.Internal; namespace Octokit { @@ -168,6 +168,7 @@ namespace Octokit public Language? Language { get; set; } private IEnumerable _is; + /// /// Searches for issues using a more human syntax covering options like state, type, merged status, private/public repository /// @@ -258,6 +259,11 @@ namespace Octokit /// public string User { get; set; } + /// + /// Gets or sets the milestone to filter issues based on + /// + public string Milestone { get; set; } + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public RepositoryCollection Repos { get; set; } @@ -391,6 +397,11 @@ namespace Octokit parameters.AddRange(Repos.Select(x => string.Format(CultureInfo.InvariantCulture, "repo:{0}", x))); } + if (Milestone.IsNotBlank()) + { + parameters.Add(string.Format(CultureInfo.InvariantCulture, "milestone:\"{0}\"", Milestone.EscapeDoubleQuotes())); + } + // Add any exclusion parameters if (Exclusions != null) { diff --git a/Octokit/Models/Request/SearchIssuesRequestExclusions.cs b/Octokit/Models/Request/SearchIssuesRequestExclusions.cs index 9b0ecc6b..38815a2a 100644 --- a/Octokit/Models/Request/SearchIssuesRequestExclusions.cs +++ b/Octokit/Models/Request/SearchIssuesRequestExclusions.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using Octokit.Internal; namespace Octokit { @@ -122,6 +120,14 @@ namespace Octokit /// public string Base { get; set; } + /// + /// Excludes issues which target the specified milestone. + /// + /// + /// https://help.github.com/articles/searching-issues-and-pull-requests/#search-by-milestone-on-an-issue-or-pull-request + /// + public string Milestone { get; set; } + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] public IReadOnlyList MergedQualifiers() { @@ -182,6 +188,11 @@ namespace Octokit parameters.Add(string.Format(CultureInfo.InvariantCulture, "-base:{0}", Base)); } + if (Milestone.IsNotBlank()) + { + parameters.Add(string.Format(CultureInfo.InvariantCulture, "-milestone:\"{0}\"", Milestone.EscapeDoubleQuotes())); + } + return new ReadOnlyCollection(parameters); }