-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Refresh PullRequestSessionManager.CurrentSession when PR branch changes #1422
Conversation
This service is used to access IGitExt from Visual Studio 2015 and 2017.
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
left a comment
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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(); | ||
| }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"?> | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
UIContextevents, first withIsActive=falsethen withIsActive=true - When changing back to a Git repo there are (often?) no events fired
- The
IGitExtservice will only be null when directly opening a TFS solution - Once set the returned
IGitExtservice 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 👍

What's in this PR
TeamExplorerContextservice for repo and branch change eventsPullRequestSessionManagerto use theITeamExplorerContextserviceGitHubPaneViewModelto useITeamExplorerContextIVSGitExthack fromPullRequestDetailViewModelIsPullRequestFromRepositorywhen repo doesn't have a remotePullRequestSessionManagerto refresh when PROwnerorNumberchangesGitHub.App.UnitTestsforTeamExplorerContexttestsThe TeamExplorerContext service
PropertyChangedevent is fired when the repo changesStatusChangedevent is fired when the branch name, head SHA or tracking SHA changesIVSGitExtare ignoredStatusChangedevent is firedActiveRepositorywould be set to null and then again to the original repoActiveRepositorywill be set to nullRelated
Notes
Re: issue #23, it looks like the current implementation of Team Explorer won't return null.
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 201514.0.25431.01Update 3. This implementation ofTeamExplorerContextwouldn't crash ifActiveRepositoriesdid come back null.@meaghanlewis Did some tests using VS 2015
14.0.23107.0RTM. It appears that theIGitExtservice 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 theIServiceProviderused 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
IsPullRequestFromRepositoryis called on a local repository with no remote URL.