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

Conversation

@jcansdale
Copy link
Collaborator

@jcansdale jcansdale commented Jan 17, 2018

What's in this PR

  • Add TeamExplorerContext service for repo and branch change events
  • Change PullRequestSessionManager to use the ITeamExplorerContext service
  • Change GitHubPaneViewModel to use ITeamExplorerContext
    • This prevents GitHub pane from changing to PR list when PR checkout touches .sln file
  • Revert IVSGitExt hack from PullRequestDetailViewModel
    • This stops it from receiving multiple events when nothing of interest has changed
  • Fix null exception in IsPullRequestFromRepository when repo doesn't have a remote
  • Fix PullRequestSessionManager to refresh when PR Owner or Number changes
  • Add NUnit tests project GitHub.App.UnitTests for TeamExplorerContext tests
    • This is to avoid stepping on @meaghanlewis's unit test refactorings

The TeamExplorerContext service

namespace GitHub.Services
{
    public interface ITeamExplorerContext : INotifyPropertyChanged
    {
        ILocalRepositoryModel ActiveRepository { get; }
        event EventHandler StatusChanged;
    }
}
  • The PropertyChanged event is fired when the repo changes
  • The StatusChanged event is fired when the branch name, head SHA or tracking SHA changes
    • Duplicate events from IVSGitExt are ignored
  • When a PR touches the .sln file only the StatusChanged event is fired
    • Previously ActiveRepository would be set to null and then again to the original repo
  • When a solution that isn't in Git is loaded ActiveRepository will be set to null

Related

Notes

Re: issue #23, it looks like the current implementation of Team Explorer won't return null.

image

The original report was from a user running VS 2015 14.0.23107.0 (which is pre-Update 1). The above code is from VS 2015 14.0.25431.01 Update 3. This implementation of TeamExplorerContext wouldn't crash if ActiveRepositories did come back null.

@meaghanlewis Did some tests using VS 2015 14.0.23107.0 RTM. It appears that the IGitExt service is available as a global service even on this early version. I'm confused about how the service could come up null. I wonder if the IServiceProvider used by the Team Explorer UI might have had a bug that made it sometimes erroneously return null? 😕

Fixes #1408
Fixes #1421

Also fixes issue when IsPullRequestFromRepository is called on a local repository with no remote URL.

@jcansdale jcansdale changed the title [WIP] Refresh PullRequestSessionManager.CurrentSession when PR branch changes Refresh PullRequestSessionManager.CurrentSession when PR branch changes Jan 17, 2018
@jcansdale jcansdale requested a review from grokys January 17, 2018 19:17
ActiveRepository should only be set to null when moving to a solution that isn't in Git.
Avoid issue with multiple loads and unloads.
Converted to use IPullRequestSessionManager property change event.
Create a unit test project for GitHub.App.
This fixes an issue where a PR is checked out causing the PR list to reset.
Reduce logging verbosity.
grokys
grokys previously requested changes Jan 19, 2018
Copy link
Contributor

@grokys grokys left a comment

Choose a reason for hiding this comment

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

Looking great in general! A few things to fix up however.

this.log = log;
this.testing = testing;

gitExtType = gitExtType ?? FindGitExtType();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why interface with IGitExt via reflection? Why not just import the type? It would mean having a dependency from GitHub.App to Microsoft.TeamFoundation.Git.Provider but I don't see that being a problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem is there are two versions of this assembly with no binding redirections between them. I wanted to avoid dependencies on GitHub.TeamFoundation.14/15.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahhh, ok yeah that makes more sense then. Maybe add a comment to this effect to the source.

bool testing;

[ImportingConstructor]
public TeamExplorerContext([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider)
Copy link
Contributor

Choose a reason for hiding this comment

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

Most other services depend on IGitHubServiceProvider to get their services - this gives you typed GetService/TryGetService methods .

Type gitExtType, bool testing)
{
this.log = log;
this.testing = testing;
Copy link
Contributor

Choose a reason for hiding this comment

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

Other parts of the code use splat for detecting if we're in a unit test runner, either via Splat.ModeDetector.InUnitTestRunner() or Guard.InUnitTestRunner.

await ThreadingHelper.SwitchToMainThreadAsync();
await UpdateContent(teamExplorerContext.ActiveRepository);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

You can subscribe to property changed subscriptions using .WhenAnyValue, so the above block could be reduced to:

await UpdateContent(teamExplorerContext.ActiveRepository);
teamExplorerContext.WhenAnyValue(x => x.ActiveRepository)
   .Skip(1)
   .ObserveOn(RxApp.MainThreadScheduler)
   .Subscribe(x => UpdateContent(x).Forget());

WhenAnyValue immediately fires with the current value of the property, but we ignore this value, instead calling UpdateContent manually so we can await it. For subsequent changes, we fire and forget.

WebUrl = LocalRepository.CloneUrl.ToRepositoryUrl().Append("pull/" + number);
modelService = await modelServiceFactory.CreateAsync(connection);
vsGitExt.ActiveRepositoriesChanged += ActiveRepositoriesChanged;
sessionManager.PropertyChanged += ActiveRepositoriesChanged;
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly use sessionManager.WhenAnyValue(x => x.CurrentSession) to listen to the current session being changed.

namespace GitHub.Services
{
/// <summary>
/// Responsible for watching the active repository in Team Explorer.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: the summary should typically be a single sentence summary of what the class does - the details should be moved to <remarks>, e.g.:

    /// <summary>
    /// Tracks the active repository in Team Explorer.
    /// </summary>
    /// <remarks>
    /// The <see cref="PropertyChanged"/> event is fired for <see cref="ActiveRepository"/> when
    /// moving to a new repository. The <see cref="StatusChanged"/> event is fired when the
    /// CurrentBranch or HeadSha changes.
    /// </remarks>

{
RepoChanged(teamExplorerContext.ActiveRepository).Forget();
};
teamExplorerContext.PropertyChanged += (s, e) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, you can use WhenAnyValue here.

teamExplorerContext.StatusChanged += (s, e) =>
{
RepoChanged(teamExplorerContext.ActiveRepository).Forget();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

If you wanted to be reactive here you could use:

            Observable.FromEventPattern(teamExplorerContext, nameof(teamExplorerContext.StatusChanged))
                .StartWith((EventPattern<object>)null)
                .Subscribe(_ => RepoChanged(teamExplorerContext.ActiveRepository).Forget());

The StartWith operator makes sure that the observable is fired immediately, making the initial RepoChanged(teamExplorerContext.ActiveRepository).Forget(); unnecessary. It's not particularly more readable or succinct to do it like this though, so I'm ambivalent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's good to know what's possible. :-)

I'm going with Rx for the property change and regular event for this one.

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about creating this test assembly now with a single test class in it... I know we plan to move everything over at some point, but if that doesn't happen for some reason we've now got this outlier... I think I'd prefer to put this test with the others for now.

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 wanted to avoid treading on the work @meaghanlewis has been doing when we merge. Maybe we could merge the unit test refactoring first and then move this test into the new NUnit UnitTests project?

var target = CreateTarget(teServiceHolder: te);
var te = Substitute.For<ITeamExplorerContext>();
te.ActiveRepository.Returns(default(ILocalRepositoryModel));
var target = CreateTarget(teamExplorerContext: te);
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we're using a properly mockable class for the team explorer stuff, we can probably simplify these tests...

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 did you have in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of tests are creating a mock ITeamExplorerContext that returns an ActiveRepository of null. Could this could be made the default in CreateTarget if no mock is passed in?

The VSUIContextChangedEventArgs class can be used directly and doesn't need an interface.
We're exporting the IVSUIContextFactory service not IVSUIContext.
TeamExplorerServiceHolder no longer watches for UIContextChanged events and refreshes VSGitExt.
{
try
{
var repo = gitExt.ActiveRepositories?.FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be done on a background thread, see TeamExplorerServiceHolder for how it does this.

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've fixed it so that ActiveRepositories is initialized by VSGitExt on a thread pool thread.


void ContextChanged(object sender, VSUIContextChangedEventArgs e)
{
// If we're in the GitSccProvider context and TryInitialize succeeds, we can stop listening for events.
Copy link
Contributor

Choose a reason for hiding this comment

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

We also need to listen for the context to be deactivated, and throw away the instance of GitExt if that happens, and get it back when it activates again.

Copy link
Collaborator Author

@jcansdale jcansdale Jan 26, 2018

Choose a reason for hiding this comment

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

I've done a bunch of experiments changing to and from solutions in TFS repos. Here are some findings:

  • When changing to a TFS repo, there can be two UIContext events, first with IsActive=false then with IsActive=true
  • When changing back to a Git repo there are (often?) no events fired
  • The IGitExt service will only be null when directly opening a TFS solution
  • Once set the returned IGitExt service object will remain constant not return

This is consistent with the Microsoft.TeamFoundation.Git.Provider.SccExtensibilityService being delay loaded but never unloaded.

Given that the context is deactivated and then activated again when opening a TFS based solution, I'm reluctant to do anything to the IGitExt object when this happens. We'd still be listening when in a TFS solution and continue to use the same object when returning to a Git based solution.

Does that make sense? I should update the comments to reflect this.

this.gitService = gitService;
syncContext = SynchronizationContext.Current;

UpdateActiveRepo();
Copy link
Contributor

Choose a reason for hiding this comment

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

This cannot happen here, it will call ActiveRepositories which is a potentially blocking call. It needs to be shuffled to a background thread.

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've shuffled the initial fetching of ActiveRepositories to a thread pool thread so this is now safe to call.

@jcansdale
Copy link
Collaborator Author

jcansdale commented Jan 26, 2018

Should make sure this can't happen:

image

There's no reason for ActiveRepositories to return null rather than an empty collection.

Make sure ActiveRepositories isn't read during the construction of our service.
ActiveRepositories is first initialized on ThreadPoolThread.
It is later updated when the property changed event is fired.

void ContextChanged(object sender, VSUIContextChangedEventArgs e)
{
// If we're in the GitSccProvider context and TryInitialize succeeds, we can stop listening for events.
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to continually listen for events so we stop using GitExt if the scc provider becomes deactivated.

Copy link
Contributor

@grokys grokys Jan 26, 2018

Choose a reason for hiding this comment

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

Could you explain why this is? Isn't @jcansdale's analysis correct? Looking at the code in ILSpy I don't see how IGitExt can become null once we have an instance unless the package is unloaded (which I think only happens when VS closes?).

Ensure that ActiveRepositories is only read on thread pool thread or its own property change event.
Added LocalRepositoryModelFactory service (and interface) so we can check the correct repositorys are being created.
Create repositories on thread pool rather than calling thread.
Return empty list if there's an exception creating repositories.
catch (Exception e)
{
log.Error(e, "Error refreshing repositories");
ActiveRepositories = new ILocalRepositoryModel[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be raising the ActiveRepositoriesChanged event as well? And if so, shouldn't this event be raised in the setter of the property directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops! Good catch. This is why I should have done it on the property itself!

Clear the list and fire repo changes event if there is an exception when reading or creating repositories (there shouldn't be).
There's now no reason not to return a IReadOnlyList like IGitExt.ActiveRepositories.
Keep the property change event pattern consistent.

IGitExt gitService;
IEnumerable<ILocalRepositoryModel> activeRepositories;
IReadOnlyList<ILocalRepositoryModel> activeRepositories;
Copy link
Contributor

Choose a reason for hiding this comment

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

nice! was going to mention that and then forgot 👍

@grokys grokys dismissed their stale review January 29, 2018 13:45

out of date

@grokys grokys merged commit 97e4570 into master Jan 29, 2018
@grokys grokys deleted the fixes/1408-change-branch-event-squashed branch March 5, 2018 17:28
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActiveRepository is sometimes set to null when changing PR branches PullRequestSessionManager.CurrentSession not updated when changing branches

4 participants