Merge pull request #835 from octokit/refine-search-api

search across multiple repositories
This commit is contained in:
Phil Haack
2015-07-24 15:14:48 -07:00
16 changed files with 387 additions and 36 deletions
+3
View File
@@ -77,6 +77,9 @@ tools/xunit.runner.console
*.ncrunch*
*.GhostDoc.xml
# FAKE temporary files
.fake/
# New VS Test Runner creates arbitrary folders with PDBs
*.pdb
pingme.txt
@@ -1,4 +1,6 @@
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Octokit;
using Octokit.Tests.Integration;
@@ -34,8 +36,8 @@ public class SearchClientTests
[Fact]
public async Task SearchForFunctionInCode()
{
var request = new SearchCodeRequest("addClass");
request.Repo = "jquery/jquery";
var request = new SearchCodeRequest("addClass", "jquery", "jquery");
var repos = await _gitHubClient.Search.SearchCode(request);
Assert.NotEmpty(repos.Items);
@@ -45,6 +47,11 @@ public class SearchClientTests
public async Task SearchForWordInCode()
{
var request = new SearchIssuesRequest("windows");
request.Repos = new RepositoryCollection {
{ "aspnet", "dnx" },
{ "aspnet", "dnvm" }
};
request.SortField = IssueSearchSort.Created;
request.Order = SortDirection.Descending;
@@ -57,7 +64,7 @@ public class SearchClientTests
public async Task SearchForOpenIssues()
{
var request = new SearchIssuesRequest("phone");
request.Repo = "caliburn-micro/caliburn.micro";
request.Repos.Add("caliburn-micro", "caliburn.micro");
request.State = ItemState.Open;
var issues = await _gitHubClient.Search.SearchIssues(request);
@@ -66,10 +73,25 @@ public class SearchClientTests
}
[Fact]
public async Task SearchForAllIssues()
public async Task SearchForAllIssuesWithouTaskUsingTerm()
{
var request = new SearchIssuesRequest();
request.Repos.Add("caliburn-micro/caliburn.micro");
var issues = await _gitHubClient.Search.SearchIssues(request);
var closedIssues = issues.Items.Where(x => x.State == ItemState.Closed);
var openedIssues = issues.Items.Where(x => x.State == ItemState.Open);
Assert.NotEmpty(closedIssues);
Assert.NotEmpty(openedIssues);
}
[Fact]
public async Task SearchForAllIssuesUsingTerm()
{
var request = new SearchIssuesRequest("phone");
request.Repo = "caliburn-micro/caliburn.micro";
request.Repos.Add("caliburn-micro", "caliburn.micro");
var issues = await _gitHubClient.Search.SearchIssues(request);
+44 -10
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using NSubstitute;
using Xunit;
using System.Threading.Tasks;
@@ -1150,13 +1151,31 @@ namespace Octokit.Tests.Clients
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchIssuesRequest("something");
request.Repo = "octokit.net";
request.Repos.Add("octokit", "octokit.net");
client.SearchIssues(request);
connection.Received().Get<SearchIssuesResult>(
Arg.Is<Uri>(u => u.ToString() == "search/issues"),
Arg.Is<Dictionary<string, string>>(d => d["q"] == "something+repo:octokit.net"));
Arg.Is<Dictionary<string, string>>(d => d["q"] == "something+repo:octokit/octokit.net"));
}
[Fact]
public async Task ErrorOccursWhenSpecifyingInvalidFormatForRepos()
{
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchIssuesRequest("windows");
request.Repos = new RepositoryCollection {
"haha-business"
};
request.SortField = IssueSearchSort.Created;
request.Order = SortDirection.Descending;
await Assert.ThrowsAsync<RepositoryFormatException>(
async () => await client.SearchIssues(request));
}
[Fact]
@@ -1165,7 +1184,7 @@ namespace Octokit.Tests.Clients
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchIssuesRequest("something");
request.Repo = "octokit.net";
request.Repos.Add("octokit/octokit.net");
request.User = "alfhenrik";
request.Labels = new[] { "bug" };
@@ -1174,7 +1193,7 @@ namespace Octokit.Tests.Clients
connection.Received().Get<SearchIssuesResult>(
Arg.Is<Uri>(u => u.ToString() == "search/issues"),
Arg.Is<Dictionary<string, string>>(d => d["q"] ==
"something+label:bug+user:alfhenrik+repo:octokit.net"));
"something+label:bug+user:alfhenrik+repo:octokit/octokit.net"));
}
}
@@ -1445,14 +1464,13 @@ namespace Octokit.Tests.Clients
{
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchCodeRequest("something");
request.Repo = "octokit.net";
var request = new SearchCodeRequest("something", "octokit", "octokit.net");
client.SearchCode(request);
connection.Received().Get<SearchCodeResult>(
Arg.Is<Uri>(u => u.ToString() == "search/code"),
Arg.Is<Dictionary<string, string>>(d => d["q"] == "something+repo:octokit.net"));
Arg.Is<Dictionary<string, string>>(d => d["q"] == "something+repo:octokit/octokit.net"));
}
[Fact]
@@ -1475,8 +1493,7 @@ namespace Octokit.Tests.Clients
{
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchCodeRequest("something");
request.Repo = "octokit.net";
var request = new SearchCodeRequest("something", "octokit", "octokit.net");
request.Path = "tools/FAKE.core";
request.Extension = "fs";
@@ -1485,7 +1502,24 @@ namespace Octokit.Tests.Clients
connection.Received().Get<SearchCodeResult>(
Arg.Is<Uri>(u => u.ToString() == "search/code"),
Arg.Is<Dictionary<string, string>>(d =>
d["q"] == "something+path:tools/FAKE.core+extension:fs+repo:octokit.net"));
d["q"] == "something+path:tools/FAKE.core+extension:fs+repo:octokit/octokit.net"));
}
[Fact]
public async Task ErrorOccursWhenSpecifyingInvalidFormatForRepos()
{
var connection = Substitute.For<IApiConnection>();
var client = new SearchClient(connection);
var request = new SearchCodeRequest("windows");
request.Repos = new RepositoryCollection {
"haha-business"
};
request.Order = SortDirection.Descending;
await Assert.ThrowsAsync<RepositoryFormatException>(
async () => await client.SearchCode(request));
}
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Octokit
{
#if !NETFX_CORE
[Serializable]
#endif
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")]
public class RepositoryFormatException : Exception
{
readonly string message;
public RepositoryFormatException(IEnumerable<string> invalidRepositories)
{
var parameterList = string.Join(", ", invalidRepositories);
message = string.Format(
CultureInfo.InvariantCulture,
"The list of repositories must be formatted as 'owner/name' - these values don't match this rule: {0}",
parameterList);
}
public override string Message
{
get
{
return message;
}
}
#if !NETFX_CORE
/// <summary>
/// Constructs an instance of LoginAttemptsExceededException
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the
/// serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains
/// contextual information about the source or destination.
/// </param>
protected RepositoryFormatException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
if (info == null) return;
message = info.GetString("Message");
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Message", Message);
}
#endif
}
}
+14
View File
@@ -101,5 +101,19 @@ namespace Octokit
//We need to have the last word.
yield return new String(letters, wordStartIndex, letters.Length - wordStartIndex);
}
// the rule:
// Username may only contain alphanumeric characters or single hyphens
// and cannot begin or end with a hyphen
static readonly Regex nameWithOwner = new Regex("[a-z0-9.-]{1,}/[a-z0-9.-]{1,}",
#if (!PORTABLE && !NETFX_CORE)
RegexOptions.Compiled |
#endif
RegexOptions.IgnoreCase);
internal static bool IsNameWithOwnerFormat(this string input)
{
return nameWithOwner.IsMatch(input);
}
}
}
+16 -5
View File
@@ -16,7 +16,10 @@ namespace Octokit
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class SearchCodeRequest : BaseSearchRequest
{
public SearchCodeRequest(string term) : base(term) { }
public SearchCodeRequest(string term) : base(term)
{
Repos = new RepositoryCollection();
}
public SearchCodeRequest(string term, string owner, string name)
: this(term)
@@ -24,7 +27,7 @@ namespace Octokit
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "name");
this.Repo = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", owner, name);
Repos.Add(owner, name);
}
/// <summary>
@@ -117,7 +120,8 @@ namespace Octokit
/// <remarks>
/// https://help.github.com/articles/searching-code#users-organizations-and-repositories
/// </remarks>
public string Repo { get; set; }
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public RepositoryCollection Repos { get; set; }
[SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.String.ToLower")]
public override IReadOnlyList<string> MergedQualifiers()
@@ -162,9 +166,16 @@ namespace Octokit
parameters.Add(String.Format(CultureInfo.InvariantCulture, "user:{0}", User));
}
if (Repo.IsNotBlank())
if (Repos.Any())
{
parameters.Add(String.Format(CultureInfo.InvariantCulture, "repo:{0}", Repo));
var invalidFormatRepos = Repos.Where(x => !x.IsNameWithOwnerFormat());
if (invalidFormatRepos.Any())
{
throw new RepositoryFormatException(invalidFormatRepos);
}
parameters.Add(
string.Join("+", Repos.Select(x => "repo:" + x)));
}
return new ReadOnlyCollection<string>(parameters);
+64 -11
View File
@@ -15,15 +15,31 @@ namespace Octokit
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class SearchIssuesRequest : BaseSearchRequest
{
public SearchIssuesRequest(string term) : base(term) { }
/// <summary>
/// Search without specifying a keyword
/// </summary>
public SearchIssuesRequest()
{
Repos = new RepositoryCollection();
}
/// <summary>
/// Search using a specify keyword
/// </summary>
/// <param name="term">The term to filter on</param>
public SearchIssuesRequest(string term) : base(term)
{
Repos = new RepositoryCollection();
}
[Obsolete("this will be deprecated in a future version")]
public SearchIssuesRequest(string term, string owner, string name)
: this(term)
{
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "name");
this.Repo = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", owner, name);
Repos.Add(owner, name);
}
/// <summary>
@@ -177,13 +193,8 @@ namespace Octokit
/// </remarks>
public string User { get; set; }
/// <summary>
/// Limits searches to a specific repository.
/// </summary>
/// <remarks>
/// https://help.github.com/articles/searching-issues#users-organizations-and-repositories
/// </remarks>
public string Repo { get; set; }
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public RepositoryCollection Repos { get; set; }
public override IReadOnlyList<string> MergedQualifiers()
{
@@ -264,9 +275,16 @@ namespace Octokit
parameters.Add(String.Format(CultureInfo.InvariantCulture, "user:{0}", User));
}
if (Repo.IsNotBlank())
if (Repos.Any())
{
parameters.Add(String.Format(CultureInfo.InvariantCulture, "repo:{0}", Repo));
var invalidFormatRepos = Repos.Where(x => !x.IsNameWithOwnerFormat());
if (invalidFormatRepos.Any())
{
throw new RepositoryFormatException(invalidFormatRepos);
}
parameters.Add(
string.Join("+", Repos.Select(x => "repo:" + x)));
}
return new ReadOnlyCollection<string>(parameters);
@@ -317,4 +335,39 @@ namespace Octokit
[Parameter(Value = "issue")]
Issue
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class RepositoryCollection : Collection<string>
{
public void Add(string owner, string name)
{
Add(GetRepositoryName(owner, name));
}
public bool Contains(string owner, string name)
{
return Contains(GetRepositoryName(owner, name));
}
public bool Remove(string owner, string name)
{
return Remove(GetRepositoryName(owner, name));
}
static string GetRepositoryName(string owner, string name)
{
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "name");
return string.Format(CultureInfo.InvariantCulture, "{0}/{1}", owner, name);
}
internal string DebuggerDisplay
{
get
{
return String.Format(CultureInfo.InvariantCulture, "Repositories: {0}", Count);
}
}
}
}
+1
View File
@@ -396,6 +396,7 @@
<Compile Include="Exceptions\TwoFactorAuthorizationException.cs" />
<Compile Include="Http\HttpMessageHandlerFactory.cs" />
<Compile Include="Models\Response\TeamMembership.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
+1
View File
@@ -412,6 +412,7 @@
<Compile Include="Models\Request\RepositoryHookTestRequest.cs" />
<Compile Include="Http\HttpMessageHandlerFactory.cs" />
<Compile Include="Models\Response\TeamMembership.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" />
</Project>
+1
View File
@@ -405,6 +405,7 @@
<Compile Include="Models\Request\RepositoryHookTestRequest.cs" />
<Compile Include="Http\HttpMessageHandlerFactory.cs" />
<Compile Include="Models\Response\TeamMembership.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.MonoTouch.CSharp.targets" />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+1
View File
@@ -395,6 +395,7 @@
<Compile Include="Exceptions\TwoFactorAuthorizationException.cs" />
<Compile Include="Http\HttpMessageHandlerFactory.cs" />
<Compile Include="Models\Response\TeamMembership.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="..\CustomDictionary.xml">
+1
View File
@@ -399,6 +399,7 @@
<Compile Include="Exceptions\TwoFactorAuthorizationException.cs" />
<Compile Include="Http\HttpMessageHandlerFactory.cs" />
<Compile Include="Models\Response\TeamMembership.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="..\CustomDictionary.xml">
+1
View File
@@ -74,6 +74,7 @@
<Compile Include="Clients\RepositoryContentsClient.cs" />
<Compile Include="Exceptions\PrivateRepositoryQuotaExceededException.cs" />
<Compile Include="Exceptions\RepositoryExistsException.cs" />
<Compile Include="Exceptions\RepositoryFormatException.cs" />
<Compile Include="Exceptions\TwoFactorAuthorizationException.cs" />
<Compile Include="Helpers\ApiErrorExtensions.cs" />
<Compile Include="Helpers\ApiUrls.Authorizations.cs" />
+1 -1
View File
@@ -1,7 +1,7 @@
@echo off
"tools\nuget\nuget.exe" "install" "xunit.runner.console" "-OutputDirectory" "tools" "-ExcludeVersion" "-version" "2.0.0"
"tools\nuget\nuget.exe" "install" "FAKE.Core" "-OutputDirectory" "tools" "-ExcludeVersion" "-version" "3.12.2"
"tools\nuget\nuget.exe" "install" "FAKE.Core" "-OutputDirectory" "tools" "-ExcludeVersion" "-version" "3.37.1"
"tools\nuget\nuget.exe" "install" "SourceLink.Fake" "-OutputDirectory" "tools" "-ExcludeVersion" "-version" "1.0.0"
:Build
+16 -3
View File
@@ -29,7 +29,10 @@ let releaseNotes =
let buildMode = getBuildParamOrDefault "buildMode" "Release"
MSBuildDefaults <- { MSBuildDefaults with Verbosity = Some MSBuildVerbosity.Minimal }
MSBuildDefaults <- {
MSBuildDefaults with
ToolsVersion = Some "12.0"
Verbosity = Some MSBuildVerbosity.Minimal }
Target "Clean" (fun _ ->
CleanDirs [buildDir; reactiveBuildDir; testResultsDir; packagingRoot; packagingDir; reactivePackagingDir]
@@ -62,9 +65,19 @@ Target "FixProjects" (fun _ ->
|> Fake.MSBuild.ProjectSystem.FixProjectFiles "./Octokit.Reactive/Octokit.Reactive.csproj"
)
let setParams defaults = {
defaults with
ToolsVersion = Some("12.0")
Targets = ["Build"]
Properties =
[
"Configuration", buildMode
]
}
Target "BuildApp" (fun _ ->
MSBuild null "Build" ["Configuration", buildMode] ["./Octokit.sln"]
|> Log "AppBuild-Output: "
build setParams "./Octokit.sln"
|> DoNothing
)
Target "ConventionTests" (fun _ ->
+130
View File
@@ -0,0 +1,130 @@
# Search
You can use Octokit to search for different sorts of data available
on the GitHub or GitHub Enterprise server:
- issues
- repositories
- code
- users
## Search Issues
A common scenario is to search for issues to triage:
```csharp
// you can also specify a search term here
var request = new SearchIssuesRequest();
// you can add individual repos to focus your search
request.Repos.Add("aspnet/dnx");
request.Repos.Add("aspnet", "dnvm");
// or use a series of repositories
request.Repos = new RepositoryCollection {
"aspnet/dnx",
"aspnet/dnvm"
};
request.Repos = new RepositoryCollection {
{ "aspnet", "dnx" },
{ "aspnet", "dnvm" }
};
```
There's many other options available here to tweak
your search criteria:
```csharp
// if you're searching for a specific term, you can
// focus your search on specific criteria
request.In = new[] {
IssueInQualifier.Title,
IssueInQualifier.Body
};
// you can restrict your search to issues or pull requests
request.Type = IssueTypeQualifier.Issue;
// you can filter on when the issue was created or updated
var aWeekAgo = DateTime.Now.Subtract(TimeSpan.FromDays(7));
request.Created = new DateRange(aWeekAgo, SearchQualifierOperator.GreaterThan)
// you can search for issues created by, assigned to
// or mentioning a specific user
request.Author = "davidfowl";
request.Assignee = "damianedwards";
request.Mentions = "shiftkey";
request.Commenter = "haacked";
// rather than setting all these, you can use this to find
// all the above for a specific user with this one-liner
request.Involves = "davidfowl";
// by default this will search on open issues, set this if
// you want to get all issues
request.State = ItemState.All;
// or to just search closed issues
request.State = ItemState.Closed;
```
There's other options available to control how the results are returned:
```csharp
request.SortField = IssueSearchSort.Created;
request.Order = SortDirection.Descending;
// 100 results per page as default
request.PerPage = 30;
// set this when you want to fetch subsequent pages
request.Page = 2;
```
Once you've set the right parameters, execute the request:
```csharp
var repos = await client.Search.SearchIssues(request);
Console.WriteLine("Query has {0} matches.", repos.TotalCount);
Console.WriteLine("Response has {0} items.", repos.Items.Count);
```
## Search Pull Requests
Another scenario to consider is how to search broadly:
```csharp
var threeMonthsAgoIsh = DateTime.Now.Subtract(TimeSpan.FromDays(90));
// search for a specific term
var request = new SearchIssuesRequest("linux")
{
// only search pull requests
Type = IssueTypeQualifier.PR,
// search across open and closed PRs
State = ItemState.All,
// search repositories which contain code
// matching a given language
Language = Language.CSharp,
// focus on pull requests updated recently
Updated = new DateRange(threeMonthsAgoIsh, SearchQualifierOperator.GreaterThan)
};
```
## Search Repositories
**TODO**
## Search Code
**TODO**
## Search Users
**TODO**