Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

search across multiple repositories #835

Merged
merged 27 commits into from
Jul 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cf1b99d
define a custom Repos property when searching issues
shiftkey Jul 13, 2015
12e9155
deprecate Repo property and update test usages
shiftkey Jul 14, 2015
b191ebc
extract specific exception for when invalid paths raised
shiftkey Jul 16, 2015
c93eeaa
extract method for reuse as extension
shiftkey Jul 16, 2015
8596019
update SearchCodeRequest to use collection for Repos
shiftkey Jul 16, 2015
e4ec54d
rework the rules
shiftkey Jul 16, 2015
b19b6b8
added some docs for these changes
shiftkey Jul 17, 2015
07b8dd2
tidy up ctors and deprecate usage of ctor with owner and repo name
shiftkey Jul 18, 2015
cb41c58
:lipstick: cleanup
shiftkey Jul 18, 2015
d1bd50c
wrote some examples
shiftkey Jul 18, 2015
f3fab08
tweaks
shiftkey Jul 18, 2015
3b21502
some more tweaks
shiftkey Jul 18, 2015
af2db5a
HACK: disable xml-doc checks so i can any possible msbuild issues
shiftkey Jul 19, 2015
4ca3aa6
bump fake.core
shiftkey Jul 19, 2015
fae9fc0
update gitignore to skip FAKE's temporary folder
shiftkey Jul 19, 2015
cf86cb1
tie things to version 12.0 of MSBuild
shiftkey Jul 19, 2015
2cd0a37
trying a different approach to get this compiling using the right ver…
shiftkey Jul 19, 2015
7bd0447
Changed repos to a specialized collection
khellang Jul 19, 2015
f98ff15
Added overloads with a single string
khellang Jul 19, 2015
43feb74
Changed collection to inherit from Collection<string>
khellang Jul 20, 2015
c8bee54
Merge pull request #842 from khellang/refine-search-api
shiftkey Jul 20, 2015
78796fb
Updated search docs with new collection type
khellang Jul 20, 2015
bb438bf
Updated comment :lipstick:
khellang Jul 20, 2015
1fe03e8
Merge pull request #844 from khellang/patch-1
shiftkey Jul 20, 2015
cf4c7c4
Merge branch 'master' into refine-search-api
shiftkey Jul 23, 2015
ee1c9b9
oops, this test should fail
shiftkey Jul 23, 2015
b763985
one convention test needing review
shiftkey Jul 23, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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