Skip to content

Commit

Permalink
Merge pull request #835 from octokit/refine-search-api
Browse files Browse the repository at this point in the history
search across multiple repositories
  • Loading branch information
haacked committed Jul 24, 2015
2 parents c64e397 + b763985 commit 6aa323c
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 36 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 28 additions & 6 deletions Octokit.Tests.Integration/Clients/SearchClientTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,8 +36,8 @@ public async Task SearchForGitHub()
[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);
Expand All @@ -45,6 +47,11 @@ public async Task SearchForFunctionInCode()
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;

Expand All @@ -57,7 +64,7 @@ public async Task SearchForWordInCode()
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);
Expand All @@ -66,10 +73,25 @@ public async Task SearchForOpenIssues()
}

[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);

Expand Down
54 changes: 44 additions & 10 deletions Octokit.Tests/Clients/SearchClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using NSubstitute;
using Xunit;
using System.Threading.Tasks;
Expand Down Expand Up @@ -1150,13 +1151,31 @@ public void TestingTheRepoQualifier()
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]
Expand All @@ -1165,7 +1184,7 @@ public void TestingTheRepoAndUserAndLabelQualifier()
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" };

Expand All @@ -1174,7 +1193,7 @@ public void TestingTheRepoAndUserAndLabelQualifier()
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"));
}
}

Expand Down Expand Up @@ -1445,14 +1464,13 @@ public void TestingTheRepoQualifier()
{
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]
Expand All @@ -1475,8 +1493,7 @@ public void TestingTheRepoAndPathAndExtensionQualifiers()
{
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";

Expand All @@ -1485,7 +1502,24 @@ public void TestingTheRepoAndPathAndExtensionQualifiers()
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));
}
}
}
Expand Down
65 changes: 65 additions & 0 deletions Octokit/Exceptions/RepositoryFormatException.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions Octokit/Helpers/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,19 @@ static IEnumerable<string> SplitUpperCase(this string source)
//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);
}
}
}
21 changes: 16 additions & 5 deletions Octokit/Models/Request/SearchCodeRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ 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)
{
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "name");

this.Repo = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", owner, name);
Repos.Add(owner, name);
}

/// <summary>
Expand Down Expand Up @@ -117,7 +120,8 @@ public IEnumerable<CodeInQualifier> In
/// <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()
Expand Down Expand Up @@ -162,9 +166,16 @@ public override IReadOnlyList<string> MergedQualifiers()
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);
Expand Down
Loading

0 comments on commit 6aa323c

Please sign in to comment.