Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Write to log when error is shown in PR detail view #1199

Merged
merged 14 commits into from
Aug 25, 2017
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
10 changes: 5 additions & 5 deletions src/GitHub.App/Services/GitClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public Task Fetch(IRepository repository, string remoteName, params string[] ref
var remote = repository.Network.Remotes[remoteName];
repository.Network.Fetch(remote, refspecs, fetchOptions);
}
catch(Exception ex)
catch (Exception ex)
{
log.Error("Failed to fetch", ex);
#if DEBUG
Expand Down Expand Up @@ -394,7 +394,7 @@ public async Task<string> GetPullRequestMergeBase(IRepository repo,
baseCommit = repo.Lookup<Commit>(baseSha);
if (baseCommit == null)
{
return null;
throw new NotFoundException($"Couldn't find {baseSha} after fetching from {baseCloneUrl}:{baseRef}.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I understand why you'd want the details of the shas in the log, but this error message is incredibly cryptic, there is no way anyone is going to know what is going on with an error like this, and this is going to be displayed directly to a user where it's going to be completely useless.

We should have a message for the user, and another with more details for the log. If the log is only showing the message and stacktrace, we can set InnerException with the log details and have the outer exception be for the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, are all the callers of this method expecting an exception to be thrown and handling it properly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callers of this method previously turned the null into an exception themselves. I've also tested it by making this method always throw to check that nothing bad happens. The exception message is displayed as expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what the user facing message should say? I suspect that for some reason, fetch doesn't always work or there is some race condition. I guess we could suggest thry try again or maybe refresh. I'll investagate...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, an indication of what the user action should be in the case of this error would be great!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we go:
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So "merge base" may be meaningless to a user, and try again might not be helpful either if the action that the user performed to get here isn't available from this screen.

What is the action that failed here? Opening a file? Showing the PR details? The first part of the message should say what was the action that failed.

What is try again? Refresh? Click on the PR button in the toolbar and then clicking on the PR again? The second part should say what the user can do on this screen to attempt to make it work.

Assuming it was the PR that failed to load, then we should probably say something like

The Pull Request failed to load. Please check your network connection and click refresh to try again.

Adjust accordingly for the failed action.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've chained it to variants of your suggestion above.

My concern was that we hide the real issue behind a workaround. I'm still keen to get to the bottom of what's going wrong. My hunch is that it's a race condition in gitlib2sharp. I'll see if I can reproduce the issue with some standalone code... 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we don't want to hide the issue in a workaround. If you're concerned about people not reporting it, you can add "If this issue persists, please let us know at support@github.com"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also confirmed that nested exceptions appear in logs.

}
}

Expand All @@ -405,14 +405,14 @@ public async Task<string> GetPullRequestMergeBase(IRepository repo,
headCommit = repo.Lookup<Commit>(headSha);
if (headCommit == null)
{
return null;
throw new NotFoundException($"Couldn't find {headSha} after fetching from {headCloneUrl}:{headRef}.");
}
}

var mergeBaseCommit = repo.ObjectDatabase.FindMergeBase(baseCommit, headCommit);
if(mergeBaseCommit == null)
if (mergeBaseCommit == null)
{
return null;
throw new NotFoundException($"Couldn't find merge base between {baseCommit} and {headCommit}.");
}

return mergeBaseCommit.Sha;
Expand Down
30 changes: 16 additions & 14 deletions src/GitHub.App/Services/PullRequestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,18 +308,20 @@ public IObservable<string> ExtractFile(
}
else
{
sha = await gitClient.GetPullRequestMergeBase(
repo,
pullRequest.Base.RepositoryCloneUrl,
pullRequest.Head.RepositoryCloneUrl,
pullRequest.Base.Sha,
pullRequest.Head.Sha,
pullRequest.Base.Ref,
pullRequest.Head.Ref);

if (sha == null)
try
{
throw new NotFoundException($"Couldn't find merge base between {pullRequest.Base.Sha} and {pullRequest.Head.Sha}.");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception is now thrown from GetPullRequestMergeBase (with more info about what went wrong).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if an exception is thrown from a Observable.Defer(async () =>?

sha = await gitClient.GetPullRequestMergeBase(
repo,
pullRequest.Base.RepositoryCloneUrl,
pullRequest.Head.RepositoryCloneUrl,
pullRequest.Base.Sha,
pullRequest.Head.Sha,
pullRequest.Base.Ref,
pullRequest.Head.Ref);
}
catch (NotFoundException ex)
{
throw new NotFoundException($"The Pull Request file failed to load. Please check your network connection and click refresh to try again. If this issue persists, please let us know at support@github.com", ex);
}
}

Expand Down Expand Up @@ -350,7 +352,7 @@ static bool HasPreamble(string file, Encoding encoding)
{
foreach (var b in encoding.GetPreamble())
{
if(b != stream.ReadByte())
if (b != stream.ReadByte())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look @shana, a space! ;-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rejoice

{
return false;
}
Expand Down Expand Up @@ -473,7 +475,7 @@ async Task<bool> IsBranchMarkedAsPullRequest(IRepository repo, string branchName
{
var prConfigKey = $"branch.{branchName}.{SettingGHfVSPullRequest}";
var value = ParseGHfVSConfigKeyValue(await gitClient.GetConfig<string>(repo, prConfigKey));
return value != null &&
return value != null &&
value.Item1 == pullRequest.Base.RepositoryCloneUrl.Owner &&
value.Item2 == pullRequest.Number;
}
Expand Down Expand Up @@ -550,4 +552,4 @@ static Tuple<string, int> ParseGHfVSConfigKeyValue(string value)
return null;
}
}
}
}
7 changes: 6 additions & 1 deletion src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using GitHub.UI;
using LibGit2Sharp;
using ReactiveUI;
using NLog;

namespace GitHub.ViewModels
{
Expand All @@ -26,6 +27,8 @@ namespace GitHub.ViewModels
[PartCreationPolicy(CreationPolicy.NonShared)]
public class PullRequestDetailViewModel : PanePageViewModelBase, IPullRequestDetailViewModel
{
static readonly Logger log = LogManager.GetCurrentClassLogger();

readonly IModelService modelService;
readonly IPullRequestService pullRequestsService;
readonly IPullRequestSessionManager sessionManager;
Expand Down Expand Up @@ -93,7 +96,7 @@ public PullRequestDetailViewModel(
Checkout = ReactiveCommand.CreateAsyncObservable(
this.WhenAnyValue(x => x.CheckoutState)
.Cast<CheckoutCommandState>()
.Select(x => x != null && x.IsEnabled),
.Select(x => x != null && x.IsEnabled),
DoCheckout);
Checkout.IsExecuting.Subscribe(x => isInCheckout = x);
SubscribeOperationError(Checkout);
Expand Down Expand Up @@ -351,6 +354,7 @@ public override void Initialize(ViewWithData data)
.ObserveOn(RxApp.MainThreadScheduler)
.Catch<IPullRequestModel, Exception>(ex =>
{
log.Error("Error observing GetPullRequest", ex);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add logs when exception is displayed on UI.

ErrorMessage = ex.Message.Trim();
IsLoading = IsBusy = false;
return Observable.Empty<IPullRequestModel>();
Expand Down Expand Up @@ -460,6 +464,7 @@ public async Task Load(IRemoteRepositoryModel remoteRepository, IPullRequestMode
}
catch (Exception ex)
{
log.Error("Error loading PullRequestModel", ex);
ErrorMessage = ex.Message.Trim();
}
finally
Expand Down
1 change: 1 addition & 0 deletions src/GitHub.Exports.Reactive/Services/IGitClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ public interface IGitClient
/// <returns>
/// The merge base SHA or null.
/// </returns>
/// <exception cref="LibGit2Sharp.NotFoundException">Thrown when the merge base can't be found.</exception>
Task<string> GetPullRequestMergeBase(IRepository repo, UriString baseCloneUrl, UriString headCloneUrl, string baseSha, string headSha, string baseRef, string headRef);

/// Checks whether the current head is pushed to its remote tracking branch.
Expand Down
14 changes: 8 additions & 6 deletions src/GitHub.InlineReviews/Services/PullRequestSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,26 @@ public async Task<string> GetPullRequestMergeBase(ILocalRepositoryModel reposito
var key = new Tuple<string, string>(baseSha, headSha);

string mergeBase;
if(mergeBaseCache.TryGetValue(key, out mergeBase))
if (mergeBaseCache.TryGetValue(key, out mergeBase))
{
return mergeBase;
}

var repo = await GetRepository(repository);
var baseUrl = pullRequest.Base.RepositoryCloneUrl;
var headUrl = pullRequest.Head.RepositoryCloneUrl;
var headCloneUrl = pullRequest.Head.RepositoryCloneUrl;
var baseRef = pullRequest.Base.Ref;
var headRef = pullRequest.Head.Ref;
mergeBase = await gitClient.GetPullRequestMergeBase(repo, baseUrl, headUrl, baseSha, headSha, baseRef, headRef);
if (mergeBase != null)
try
{
mergeBase = await gitClient.GetPullRequestMergeBase(repo, baseUrl, headUrl, baseSha, headSha, baseRef, headRef);
}
catch (NotFoundException ex)
{
return mergeBaseCache[key] = mergeBase;
throw new NotFoundException("The Pull Request failed to load. Please check your network connection and click refresh to try again. If this issue persists, please let us know at support@github.com", ex);
}

throw new FileNotFoundException($"Couldn't find merge base between {baseSha} and {headSha}.");
return mergeBaseCache[key] = mergeBase;
}

/// <inheritdoc/>
Expand Down
14 changes: 11 additions & 3 deletions src/UnitTests/GitHub.App/Services/GitClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task PushesToDefaultOrigin()

await gitClient.Push(repository, "master", "origin");

repository.Network.Received().Push(origin,"HEAD", @"refs/heads/master", Arg.Any<PushOptions>());
repository.Network.Received().Push(origin, "HEAD", @"refs/heads/master", Arg.Any<PushOptions>());
}

[Fact]
Expand Down Expand Up @@ -194,7 +194,11 @@ public async Task WhenToFetch(string baseUrl, string headUrl, string baseSha, st
repo.Network.Remotes.Add(null, null).ReturnsForAnyArgs(remote);
var gitClient = new GitClient(Substitute.For<IGitHubCredentialProvider>());

await gitClient.GetPullRequestMergeBase(repo, baseUri, headUri, baseSha, headSha, baseRef, headRef);
try
{
await gitClient.GetPullRequestMergeBase(repo, baseUri, headUri, baseSha, headSha, baseRef, headRef);
}
catch (NotFoundException) { /* We're interested in calls to Fetch even if it throws */ }

repo.Network.Received(receivedFetch).Fetch(Arg.Any<Remote>(), Arg.Any<string[]>(), Arg.Any<FetchOptions>());
}
Expand All @@ -210,7 +214,11 @@ public async Task WhatToFetch(string baseSha, string headSha, string mergeBaseSh
var headUrl = new UriString("https://github.com/owner/repo");
var gitClient = new GitClient(Substitute.For<IGitHubCredentialProvider>());

await gitClient.GetPullRequestMergeBase(repo, baseUrl, headUrl, baseSha, headSha, baseRef, headRef);
try
{
await gitClient.GetPullRequestMergeBase(repo, baseUrl, headUrl, baseSha, headSha, baseRef, headRef);
}
catch (NotFoundException) { /* We're interested in calls to Fetch even if it throws */ }

repo.Network.Received(1).Fetch(Arg.Any<Remote>(), Arg.Is<IEnumerable<string>>(x => x.Contains(expectRefSpec)), Arg.Any<FetchOptions>());
}
Expand Down
22 changes: 15 additions & 7 deletions src/UnitTests/GitHub.App/Services/PullRequestServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task ExtractBase_MergeBaseAvailable_UseMergeBaseSha()
}

[Fact]
public async void MergeBaseNotAvailable_ThrowsFileNotFoundException()
public async void MergeBaseNotAvailable_ThrowsNotFoundException()
{
var baseFileContent = "baseFileContent";
var headFileContent = "headFileContent";
Expand All @@ -64,11 +64,10 @@ public async void MergeBaseNotAvailable_ThrowsFileNotFoundException()
var headSha = "headSha";
var mergeBaseSha = null as string;
var head = false;
var expectMessage = $"Couldn't find merge base between {baseSha} and {headSha}.";
var mergeBaseException = new NotFoundException();

var ex = await Assert.ThrowsAsync<NotFoundException>(() => ExtractFile(baseSha, baseFileContent, headSha, headFileContent, mergeBaseSha, mergeBaseFileContent,
fileName, head, Encoding.UTF8));
Assert.Equal(expectMessage, ex.Message);
fileName, head, Encoding.UTF8, mergeBaseException: mergeBaseException));
}

[Fact]
Expand Down Expand Up @@ -150,7 +149,8 @@ static bool HasPreamble(string file, Encoding encoding)

static async Task<string> ExtractFile(
string baseSha, object baseFileContent, string headSha, object headFileContent, string mergeBaseSha, object mergeBaseFileContent,
string fileName, bool head, Encoding encoding, string repoDir = "repoDir", int pullNumber = 666, string baseRef = "baseRef", string headRef = "headRef")
string fileName, bool head, Encoding encoding, string repoDir = "repoDir", int pullNumber = 666, string baseRef = "baseRef", string headRef = "headRef",
Exception mergeBaseException = null)
{
var repositoryModel = Substitute.For<ILocalRepositoryModel>();
repositoryModel.LocalPath.Returns(repoDir);
Expand All @@ -166,7 +166,15 @@ static async Task<string> ExtractFile(
var gitService = serviceProvider.GetGitService();
var service = new PullRequestService(gitClient, gitService, serviceProvider.GetOperatingSystem(), Substitute.For<IUsageTracker>());

gitClient.GetPullRequestMergeBase(Arg.Any<IRepository>(), Arg.Any<UriString>(), Arg.Any<UriString>(), baseSha, headSha, baseRef, headRef).ReturnsForAnyArgs(Task.FromResult(mergeBaseSha));
if (mergeBaseException == null)
{
gitClient.GetPullRequestMergeBase(Arg.Any<IRepository>(), Arg.Any<UriString>(), Arg.Any<UriString>(), baseSha, headSha, baseRef, headRef).ReturnsForAnyArgs(Task.FromResult(mergeBaseSha));
}
else
{
gitClient.GetPullRequestMergeBase(Arg.Any<IRepository>(), Arg.Any<UriString>(), Arg.Any<UriString>(), baseSha, headSha, baseRef, headRef).ReturnsForAnyArgs(Task.FromException<string>(mergeBaseException));
}

gitClient.ExtractFile(Arg.Any<IRepository>(), mergeBaseSha, fileName).Returns(GetFileTask(mergeBaseFileContent));
gitClient.ExtractFile(Arg.Any<IRepository>(), baseSha, fileName).Returns(GetFileTask(baseFileContent));
gitClient.ExtractFile(Arg.Any<IRepository>(), headSha, fileName).Returns(GetFileTask(headFileContent));
Expand Down Expand Up @@ -561,7 +569,7 @@ static IGitService MockGitService()
var branches = MockBranches("pr/123-foo1", "pr/123-foo1-2");
repository.Branches.Returns(branches);

var result = Substitute.For<IGitService>();
var result = Substitute.For<IGitService>();
result.GetRepository(Arg.Any<string>()).Returns(repository);
return result;
}
Expand Down