Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upRefresh PullRequestSessionManager.CurrentSession when PR branch changes #1422
Conversation
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.
|
Looking great in general! A few things to fix up however. |
| this.log = log; | ||
| this.testing = testing; | ||
|
|
||
| gitExtType = gitExtType ?? FindGitExtType(); |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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?
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 19, 2018
Author
Collaborator
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.
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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) |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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; |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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); | ||
| } | ||
| }; |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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.
| @@ -322,7 +320,8 @@ public Uri WebUrl | |||
| Number = number; | |||
| WebUrl = LocalRepository.CloneUrl.ToRepositoryUrl().Append("pull/" + number); | |||
| modelService = await modelServiceFactory.CreateAsync(connection); | |||
| vsGitExt.ActiveRepositoriesChanged += ActiveRepositoriesChanged; | |||
| sessionManager.PropertyChanged += ActiveRepositoriesChanged; | |||
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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. |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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) => |
This comment has been minimized.
This comment has been minimized.
| teamExplorerContext.StatusChanged += (s, e) => | ||
| { | ||
| RepoChanged(teamExplorerContext.ActiveRepository).Forget(); | ||
| }; |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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.
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 19, 2018
Author
Collaborator
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"?> | |||
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
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.
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 19, 2018
Author
Collaborator
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); |
This comment has been minimized.
This comment has been minimized.
grokys
Jan 19, 2018
Contributor
Now that we're using a properly mockable class for the team explorer stuff, we can probably simplify these tests...
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
grokys
Jan 23, 2018
Contributor
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?
| { | ||
| try | ||
| { | ||
| var repo = gitExt.ActiveRepositories?.FirstOrDefault(); |
This comment has been minimized.
This comment has been minimized.
shana
Jan 26, 2018
Collaborator
This needs to be done on a background thread, see TeamExplorerServiceHolder for how it does this.
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 26, 2018
Author
Collaborator
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. |
This comment has been minimized.
This comment has been minimized.
shana
Jan 26, 2018
Collaborator
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.
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 26, 2018
•
Author
Collaborator
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.
| syncContext = SynchronizationContext.Current; | ||
|
|
||
| UpdateActiveRepo(); |
This comment has been minimized.
This comment has been minimized.
shana
Jan 26, 2018
Collaborator
This cannot happen here, it will call ActiveRepositories which is a potentially blocking call. It needs to be shuffled to a background thread.
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 26, 2018
Author
Collaborator
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. |
This comment has been minimized.
This comment has been minimized.
shana
Jan 26, 2018
Collaborator
We need to continually listen for events so we stop using GitExt if the scc provider becomes deactivated.
This comment has been minimized.
This comment has been minimized.
grokys
Jan 26, 2018
•
Contributor
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]; |
This comment has been minimized.
This comment has been minimized.
shana
Jan 29, 2018
Collaborator
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?
This comment has been minimized.
This comment has been minimized.
jcansdale
Jan 29, 2018
Author
Collaborator
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.
| @@ -25,7 +25,7 @@ public class VSGitExt : IVSGitExt | |||
| readonly ILocalRepositoryModelFactory repositoryFactory; | |||
|
|
|||
| IGitExt gitService; | |||
| IEnumerable<ILocalRepositoryModel> activeRepositories; | |||
| IReadOnlyList<ILocalRepositoryModel> activeRepositories; | |||

jcansdale commentedJan 17, 2018
•
edited
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.