diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f94276e3a..15e2f4ad6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,16 +13,16 @@ This project adheres to the [Open Code of Conduct][code-of-conduct]. By particip ## Submitting a pull request -0. [Fork][] and clone the repository (see Build Instructions in the [README][readme]) -0. Create a new branch: `git checkout -b my-branch-name` -0. Make your change, add tests, and make sure the tests still pass -0. Push to your fork and [submit a pull request][pr] -0. Pat your self on the back and wait for your pull request to be reviewed and merged. +1. [Fork][] and clone the repository (see Build Instructions in the [README][readme]) +2. Create a new branch: `git checkout -b my-branch-name` +3. Make your change, add tests, and make sure the tests still pass +4. Push to your fork and [submit a pull request][pr] +5. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: -- Follow the existing code's style. -- Write tests. +- Follow the style/format of the existing code. +- Write tests for your changes. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). @@ -38,7 +38,7 @@ There are certain areas of the extension that are restricted in what they can do ### Bug Reporting Here are a few helpful tips when reporting a bug: -- Verify the bug resides in the GitHub for Visual Studio extension +- Verify that the bug resides in the GitHub for Visual Studio extension - A lot of functionality provided by this extension resides in the Team Explorer pane, alongside other non-GitHub tools to manage and collaborate on source code, including Visual Studio's Git support, which is owned by Microsoft. - If this bug not is related to the GitHub extension, visit the [Visual Studio support page](https://www.visualstudio.com/support/support-overview-vs) for help - Screenshots are very helpful in diagnosing bugs and understanding the state of the extension when it's experiencing problems. Please include them whenever possible. diff --git a/appveyor.yml b/appveyor.yml index af53bbc14..3dd584456 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ os: Visual Studio 2017 -version: '2.5.4.{build}' +version: '2.5.5.{build}' skip_tags: true install: - ps: | diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj index 21f5ee78e..9e4bd472f 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -197,6 +197,7 @@ ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + @@ -257,6 +258,7 @@ + @@ -267,6 +269,7 @@ + diff --git a/src/GitHub.App/Resources.Designer.cs b/src/GitHub.App/Resources.Designer.cs index cd34a343c..ac56bb6e1 100644 --- a/src/GitHub.App/Resources.Designer.cs +++ b/src/GitHub.App/Resources.Designer.cs @@ -13,12 +13,11 @@ namespace GitHub.App { /// - /// 一个强类型的资源类,用于查找本地化的字符串等。 /// - // 此类是由 StronglyTypedResourceBuilder - // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 - // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen - // (以 /str 作为命令选项),或重新生成 VS 项目。 + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -88,7 +87,25 @@ namespace GitHub.App { } /// - /// 查找类似 Changes Requested 的本地化字符串。 + /// Looks up a localized string similar to Are you sure you want to cancel this review? You will lose all your pending comments.. + /// + public static string CancelPendingReviewConfirmation { + get { + return ResourceManager.GetString("CancelPendingReviewConfirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel Review. + /// + public static string CancelPendingReviewConfirmationCaption { + get { + return ResourceManager.GetString("CancelPendingReviewConfirmationCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Changes Requested. /// public static string ChangesRequested { get { diff --git a/src/GitHub.App/Resources.resx b/src/GitHub.App/Resources.resx index 8d9d9aabc..124f51230 100644 --- a/src/GitHub.App/Resources.resx +++ b/src/GitHub.App/Resources.resx @@ -321,4 +321,10 @@ https://git-scm.com/download/win Switch Origin + + Are you sure you want to cancel this review? You will lose all your pending comments. + + + Cancel Review + \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs new file mode 100644 index 000000000..7b7cc105f --- /dev/null +++ b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs @@ -0,0 +1,25 @@ +using System; +using System.Windows.Media.Imaging; +using GitHub.ViewModels; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.SampleData +{ + public sealed class PullRequestCheckViewModelDesigner : ViewModelBase, IPullRequestCheckViewModel + { + public string Title { get; set; } = "continuous-integration/appveyor/pr"; + + public string Description { get; set; } = "AppVeyor build failed"; + + public PullRequestCheckStatus Status { get; set; } = PullRequestCheckStatus.Failure; + + public Uri DetailsUrl { get; set; } = new Uri("http://github.com"); + + public string AvatarUrl { get; set; } = "https://avatars1.githubusercontent.com/u/417571?s=88&v=4"; + + public BitmapImage Avatar { get; set; } = null; + + public ReactiveCommand OpenDetailsUrl { get; set; } = null; + } +} \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs index a174c3e29..bb1ac8a3c 100644 --- a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reactive; -using System.Text; -using System.Threading.Tasks; using GitHub.Models; using GitHub.Services; using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using System.Threading.Tasks; +using GitHub.SampleData; namespace GitHub.SampleData { @@ -95,6 +95,8 @@ This requires that errors be propagated from the viewmodel to the view and from }; Files = new PullRequestFilesViewModelDesigner(); + + Checks = new PullRequestCheckViewModelDesigner[0]; } public PullRequestDetailModel Model { get; } @@ -123,6 +125,8 @@ This requires that errors be propagated from the viewmodel to the view and from public ReactiveCommand OpenOnGitHub { get; } public ReactiveCommand ShowReview { get; } + public IReadOnlyList Checks { get; } + public Task InitializeAsync(ILocalRepositoryModel localRepository, IConnection connection, string owner, string repo, int number) => Task.CompletedTask; public string GetLocalFilePath(IPullRequestFileNode file) diff --git a/src/GitHub.App/SampleData/PullRequestListItemViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestListItemViewModelDesigner.cs index 4c00b0ba9..500089fb8 100644 --- a/src/GitHub.App/SampleData/PullRequestListItemViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestListItemViewModelDesigner.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using GitHub.Models; using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; @@ -16,5 +17,6 @@ namespace GitHub.SampleData public int Number { get; set; } public string Title { get; set; } public DateTimeOffset UpdatedAt { get; set; } + public PullRequestChecksState Checks { get; set; } } } diff --git a/src/GitHub.App/Services/GitHubContextService.cs b/src/GitHub.App/Services/GitHubContextService.cs index 20dce6933..640e85a65 100644 --- a/src/GitHub.App/Services/GitHubContextService.cs +++ b/src/GitHub.App/Services/GitHubContextService.cs @@ -58,6 +58,8 @@ namespace GitHub.Services static readonly Regex treeishCommitRegex = new Regex($"(?[a-z0-9]{{40}})(/(?.+))?", RegexOptions.Compiled); static readonly Regex treeishBranchRegex = new Regex($"(?master)(/(?.+))?", RegexOptions.Compiled); + static readonly Regex tempFileObjectishRegex = new Regex(@"\\TFSTemp\\[^\\]*[.](?[a-z0-9]{8})[.][^.\\]*$", RegexOptions.Compiled); + [ImportingConstructor] public GitHubContextService(IGitHubServiceProvider serviceProvider, IGitService gitService) { @@ -305,6 +307,55 @@ namespace GitHub.Services } } + /// + public string FindObjectishForTFSTempFile(string tempFile) + { + var match = tempFileObjectishRegex.Match(tempFile); + if (match.Success) + { + return match.Groups["objectish"].Value; + } + + return null; + } + + /// + public (string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish) + { + using (var repo = gitService.GetRepository(repositoryDir)) + { + var blob = repo.Lookup(objectish); + if (blob == null) + { + return (null, null); + } + + foreach (var commit in repo.Commits) + { + var trees = new Stack(); + trees.Push(commit.Tree); + + while (trees.Count > 0) + { + foreach (var treeEntry in trees.Pop()) + { + if (treeEntry.Target == blob) + { + return (commit.Sha, treeEntry.Path); + } + + if (treeEntry.TargetType == TreeEntryTargetType.Tree) + { + trees.Push((Tree)treeEntry.Target); + } + } + } + } + + return (null, null); + } + } + /// public bool HasChangesInWorkingDirectory(string repositoryDir, string commitish, string path) { diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index 8180cd255..47a7f947f 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -11,6 +11,7 @@ using System.Reactive.Threading.Tasks; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Windows.Forms; using GitHub.Api; using GitHub.Extensions; using GitHub.Logging; @@ -22,6 +23,7 @@ using Octokit.GraphQL.Model; using Rothko; using static System.FormattableString; using static Octokit.GraphQL.Variable; +using StatusState = GitHub.Models.StatusState; namespace GitHub.Services { @@ -93,6 +95,17 @@ namespace GitHub.Services Items = page.Nodes.Select(pr => new ListItemAdapter { Id = pr.Id.Value, + LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit => + new LastCommitSummaryModel + { + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusSummaryModel + { + State = (StatusState)statusContext.State, + }).ToList() + ).SingleOrDefault() + }).ToList().FirstOrDefault(), Author = new ActorModel { Login = pr.Author.Login, @@ -123,10 +136,46 @@ namespace GitHub.Services var result = await graphql.Run(readPullRequests, vars); - foreach (ListItemAdapter item in result.Items) + foreach (var item in result.Items.Cast()) { item.CommentCount += item.Reviews.Sum(x => x.Count); item.Reviews = null; + + var hasStatuses = item.LastCommit.Statuses != null + && item.LastCommit.Statuses.Any(); + + if (!hasStatuses) + { + item.Checks = PullRequestChecksState.None; + } + else + { + var statusHasFailure = item.LastCommit + .Statuses + .Any(status => status.State == StatusState.Failure); + + var statusHasCompleteSuccess = true; + if (!statusHasFailure) + { + statusHasCompleteSuccess = + item.LastCommit.Statuses.All(status => status.State == StatusState.Success); + } + + if (statusHasFailure) + { + item.Checks = PullRequestChecksState.Failure; + } + else if (statusHasCompleteSuccess) + { + item.Checks = PullRequestChecksState.Success; + } + else + { + item.Checks = PullRequestChecksState.Pending; + } + } + + item.LastCommit = null; } return result; @@ -392,7 +441,7 @@ namespace GitHub.Services { await gitClient.Checkout(repo, localBranchName); } - else if (repository.CloneUrl.Owner == pullRequest.HeadRepositoryOwner) + else if (string.Equals(repository.CloneUrl.Owner, pullRequest.HeadRepositoryOwner, StringComparison.OrdinalIgnoreCase)) { var remote = await gitClient.GetHttpRemote(repo, "origin"); await gitClient.Fetch(repo, remote.Name); @@ -524,7 +573,7 @@ namespace GitHub.Services public bool IsPullRequestFromRepository(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { - return pullRequest.HeadRepositoryOwner == repository.CloneUrl.Owner; + return string.Equals(repository.CloneUrl?.Owner, pullRequest.HeadRepositoryOwner, StringComparison.OrdinalIgnoreCase); } public IObservable SwitchToBranch(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) @@ -666,6 +715,16 @@ namespace GitHub.Services }); } + /// + public bool ConfirmCancelPendingReview() + { + return MessageBox.Show( + GitHub.App.Resources.CancelPendingReviewConfirmation, + GitHub.App.Resources.CancelPendingReviewConfirmationCaption, + MessageBoxButtons.YesNo, + MessageBoxIcon.Question) == DialogResult.Yes; + } + async Task CreateRemote(IRepository repo, UriString cloneUri) { foreach (var remote in repo.Network.Remotes) @@ -840,6 +899,8 @@ namespace GitHub.Services class ListItemAdapter : PullRequestListItemModel { public IList Reviews { get; set; } + + public LastCommitSummaryModel LastCommit { get; set; } } class ReviewAdapter @@ -848,5 +909,15 @@ namespace GitHub.Services public int CommentCount { get; set; } public int Count => CommentCount + (!string.IsNullOrWhiteSpace(Body) ? 1 : 0); } + + class StatusSummaryModel + { + public StatusState State { get; set; } + } + + class LastCommitSummaryModel + { + public List Statuses { get; set; } + } } } diff --git a/src/GitHub.App/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModel.cs b/src/GitHub.App/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModel.cs index 0adae2581..acba78ca1 100644 --- a/src/GitHub.App/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModel.cs +++ b/src/GitHub.App/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModel.cs @@ -65,7 +65,7 @@ namespace GitHub.ViewModels.Dialog this.WhenAnyValue(x => x.EnterpriseUrl, x => x.EnterpriseUrlValidator.ValidationResult) .Throttle(TimeSpan.FromMilliseconds(500), scheduler) .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(x => EnterpriseUrlChanged(x.Item1, x.Item2?.IsValid ?? false)); + .Subscribe(x => UpdatingProbeStatus = EnterpriseUrlChanged(x.Item1, x.Item2?.IsValid ?? false)); NavigateLearnMore = ReactiveCommand.CreateAsyncObservable(_ => { @@ -122,13 +122,19 @@ namespace GitHub.ViewModels.Dialog get; } + public Task UpdatingProbeStatus + { + get; + private set; + } + protected override async Task ResetValidation() { EnterpriseUrl = null; await EnterpriseUrlValidator.ResetAsync(); } - async void EnterpriseUrlChanged(string url, bool valid) + async Task EnterpriseUrlChanged(string url, bool valid) { if (!valid) { diff --git a/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModelBase.cs b/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModelBase.cs index 796341ed6..472029c3a 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModelBase.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModelBase.cs @@ -7,6 +7,7 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; +using System.Windows.Threading; using GitHub.Collections; using GitHub.Extensions; using GitHub.Extensions.Reactive; @@ -209,6 +210,7 @@ namespace GitHub.ViewModels.GitHubPane this.WhenAnyValue(x => x.SelectedState), this.WhenAnyValue(x => x.AuthorFilter.Selected), (loading, count, _, __, ___) => Tuple.Create(loading, count)) + .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => UpdateState(x.Item1, x.Item2))); dispose.Add( Observable.FromEventPattern( @@ -246,10 +248,7 @@ namespace GitHub.ViewModels.GitHubPane { numberFilter = 0; - if (SearchQuery.StartsWith('#')) - { - int.TryParse(SearchQuery.Substring(1), out numberFilter); - } + int.TryParse(SearchQuery.Substring(SearchQuery.StartsWith('#') ? 1 : 0), out numberFilter); if (numberFilter == 0) { diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs new file mode 100644 index 000000000..e705a5860 --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Windows.Media.Imaging; +using GitHub.Extensions; +using GitHub.Factories; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + [Export(typeof(IPullRequestCheckViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class PullRequestCheckViewModel: ViewModelBase, IPullRequestCheckViewModel + { + private readonly IUsageTracker usageTracker; + const string DefaultAvatar = "pack://application:,,,/GitHub.App;component/Images/default_user_avatar.png"; + + public static IEnumerable Build(IViewViewModelFactory viewViewModelFactory, PullRequestDetailModel pullRequest) + { + return pullRequest.Statuses?.Select(model => + { + PullRequestCheckStatus checkStatus; + switch (model.State) + { + case StatusState.Expected: + case StatusState.Error: + case StatusState.Failure: + checkStatus = PullRequestCheckStatus.Failure; + break; + case StatusState.Pending: + checkStatus = PullRequestCheckStatus.Pending; + break; + case StatusState.Success: + checkStatus = PullRequestCheckStatus.Success; + break; + default: + throw new InvalidOperationException("Unkown PullRequestCheckStatusEnum"); + } + + var pullRequestCheckViewModel = (PullRequestCheckViewModel) viewViewModelFactory.CreateViewModel(); + pullRequestCheckViewModel.Title = model.Context; + pullRequestCheckViewModel.Description = model.Description; + pullRequestCheckViewModel.Status = checkStatus; + pullRequestCheckViewModel.DetailsUrl = new Uri(model.TargetUrl); + pullRequestCheckViewModel.AvatarUrl = model.AvatarUrl ?? DefaultAvatar; + pullRequestCheckViewModel.Avatar = model.AvatarUrl != null + ? new BitmapImage(new Uri(model.AvatarUrl)) + : AvatarProvider.CreateBitmapImage(DefaultAvatar); + + return pullRequestCheckViewModel; + + }) ?? new PullRequestCheckViewModel[0]; + } + + [ImportingConstructor] + public PullRequestCheckViewModel(IUsageTracker usageTracker) + { + this.usageTracker = usageTracker; + OpenDetailsUrl = ReactiveCommand.Create().OnExecuteCompleted(DoOpenDetailsUrl); + } + + private void DoOpenDetailsUrl(object obj) + { + usageTracker.IncrementCounter(x => x.NumberOfPRCheckStatusesOpenInGitHub).Forget(); + } + + public string Title { get; private set; } + + public string Description { get; private set; } + + public PullRequestCheckStatus Status{ get; private set; } + + public Uri DetailsUrl { get; private set; } + + public string AvatarUrl { get; private set; } + + public BitmapImage Avatar { get; private set; } + + public ReactiveCommand OpenDetailsUrl { get; } + } +} diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs index 38a1d4b47..643d38ab6 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs @@ -38,6 +38,7 @@ namespace GitHub.ViewModels.GitHubPane readonly IUsageTracker usageTracker; readonly ITeamExplorerContext teamExplorerContext; readonly ISyncSubmodulesCommand syncSubmodulesCommand; + readonly IViewViewModelFactory viewViewModelFactory; IModelService modelService; PullRequestDetailModel model; IActorViewModel author; @@ -55,6 +56,7 @@ namespace GitHub.ViewModels.GitHubPane bool refreshOnActivate; Uri webUrl; IDisposable sessionSubscription; + IReadOnlyList checks; /// /// Initializes a new instance of the class. @@ -73,7 +75,8 @@ namespace GitHub.ViewModels.GitHubPane IUsageTracker usageTracker, ITeamExplorerContext teamExplorerContext, IPullRequestFilesViewModel files, - ISyncSubmodulesCommand syncSubmodulesCommand) + ISyncSubmodulesCommand syncSubmodulesCommand, + IViewViewModelFactory viewViewModelFactory) { Guard.ArgumentNotNull(pullRequestsService, nameof(pullRequestsService)); Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); @@ -81,6 +84,7 @@ namespace GitHub.ViewModels.GitHubPane Guard.ArgumentNotNull(usageTracker, nameof(usageTracker)); Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext)); Guard.ArgumentNotNull(syncSubmodulesCommand, nameof(syncSubmodulesCommand)); + Guard.ArgumentNotNull(viewViewModelFactory, nameof(viewViewModelFactory)); this.pullRequestsService = pullRequestsService; this.sessionManager = sessionManager; @@ -88,6 +92,7 @@ namespace GitHub.ViewModels.GitHubPane this.usageTracker = usageTracker; this.teamExplorerContext = teamExplorerContext; this.syncSubmodulesCommand = syncSubmodulesCommand; + this.viewViewModelFactory = viewViewModelFactory; Files = files; Checkout = ReactiveCommand.CreateAsyncObservable( @@ -302,6 +307,12 @@ namespace GitHub.ViewModels.GitHubPane /// public ReactiveCommand ShowReview { get; } + public IReadOnlyList Checks + { + get { return checks; } + private set { this.RaiseAndSetIfChanged(ref checks, value); } + } + /// /// Initializes the view model. /// @@ -377,6 +388,8 @@ namespace GitHub.ViewModels.GitHubPane Body = !string.IsNullOrWhiteSpace(pullRequest.Body) ? pullRequest.Body : Resources.NoDescriptionProvidedMarkdown; Reviews = PullRequestReviewSummaryViewModel.BuildByUser(Session.User, pullRequest).ToList(); + Checks = PullRequestCheckViewModel.Build(viewViewModelFactory, pullRequest)?.ToList(); + await Files.InitializeAsync(Session); var localBranches = await pullRequestsService.GetLocalBranches(LocalRepository, pullRequest).ToList(); diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs index 0588b8a1e..f512c0f57 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs @@ -7,7 +7,9 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; +using System.Windows.Input; using GitHub.Extensions; using GitHub.Models; using GitHub.Services; @@ -50,7 +52,7 @@ namespace GitHub.ViewModels.GitHubPane DiffFileWithWorkingDirectory = ReactiveCommand.CreateAsyncTask( isBranchCheckedOut, x => (Task)editorService.OpenDiff(pullRequestSession, ((IPullRequestFileNode)x).RelativePath)); - OpenFileInWorkingDirectory = ReactiveCommand.CreateAsyncTask( + OpenFileInWorkingDirectory = new NonDeletedFileCommand( isBranchCheckedOut, x => (Task)editorService.OpenFile(pullRequestSession, ((IPullRequestFileNode)x).RelativePath, true)); @@ -198,5 +200,37 @@ namespace GitHub.ViewModels.GitHubPane return threads.FirstOrDefault(); } + + /// + /// Implements the command. + /// + /// + /// We need to "Open File in Solution" when the parameter passed to the command parameter + /// represents a deleted file. ReactiveCommand doesn't allow us to change the CanExecute + /// state depending on the parameter, so we override + /// to do this ourselves. + /// + class NonDeletedFileCommand : ReactiveCommand, ICommand + { + public NonDeletedFileCommand( + IObservable canExecute, + Func executeAsync) + : base(canExecute, x => executeAsync(x).ToObservable()) + { + } + + bool ICommand.CanExecute(object parameter) + { + if (parameter is IPullRequestFileNode node) + { + if (node.Status == PullRequestFileStatus.Removed) + { + return false; + } + } + + return CanExecute(parameter); + } + } } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestListItemViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestListItemViewModel.cs index b9355c0d8..b60cf0e70 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestListItemViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestListItemViewModel.cs @@ -19,6 +19,7 @@ namespace GitHub.ViewModels.GitHubPane { Id = model.Id; Author = new ActorViewModel(model.Author); + Checks = model.Checks; CommentCount = model.CommentCount; Number = model.Number; Title = model.Title; @@ -26,13 +27,16 @@ namespace GitHub.ViewModels.GitHubPane } /// - public string Id { get; protected set; } + public string Id { get; } /// - public IActorViewModel Author { get; protected set; } + public IActorViewModel Author { get; } /// - public int CommentCount { get; protected set; } + public PullRequestChecksState Checks { get; } + + /// + public int CommentCount { get; } /// public bool IsCurrent @@ -42,12 +46,12 @@ namespace GitHub.ViewModels.GitHubPane } /// - public int Number { get; protected set; } + public int Number { get; } /// - public string Title { get; protected set; } + public string Title { get; } /// - public DateTimeOffset UpdatedAt { get; protected set; } + public DateTimeOffset UpdatedAt { get; } } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModel.cs index 607208eaa..82637c454 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModel.cs @@ -24,6 +24,7 @@ namespace GitHub.ViewModels.GitHubPane readonly IPullRequestEditorService editorService; readonly IPullRequestSessionManager sessionManager; + readonly IPullRequestService pullRequestService; IPullRequestSession session; IDisposable sessionSubscription; PullRequestReviewModel model; @@ -35,6 +36,7 @@ namespace GitHub.ViewModels.GitHubPane [ImportingConstructor] public PullRequestReviewAuthoringViewModel( + IPullRequestService pullRequestService, IPullRequestEditorService editorService, IPullRequestSessionManager sessionManager, IPullRequestFilesViewModel files) @@ -43,6 +45,7 @@ namespace GitHub.ViewModels.GitHubPane Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); Guard.ArgumentNotNull(files, nameof(files)); + this.pullRequestService = pullRequestService; this.editorService = editorService; this.sessionManager = sessionManager; @@ -68,7 +71,7 @@ namespace GitHub.ViewModels.GitHubPane _ => DoSubmit(Octokit.PullRequestReviewEvent.RequestChanges)); Cancel = ReactiveCommand.CreateAsyncTask(DoCancel); NavigateToPullRequest = ReactiveCommand.Create().OnExecuteCompleted(_ => - NavigateTo(Invariant($"{LocalRepository.Owner}/{LocalRepository.Name}/pull/{PullRequestModel.Number}"))); + NavigateTo(Invariant($"{RemoteRepositoryOwner}/{LocalRepository.Name}/pull/{PullRequestModel.Number}"))); } /// @@ -271,10 +274,17 @@ namespace GitHub.ViewModels.GitHubPane { if (Model?.Id != null) { - await session.CancelReview(); + if (pullRequestService.ConfirmCancelPendingReview()) + { + await session.CancelReview(); + Close(); + } + } + else + { + Close(); } - Close(); } catch (Exception ex) { diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs index e181ade69..3ec2fb4e7 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs @@ -41,7 +41,7 @@ namespace GitHub.ViewModels.GitHubPane { var existing = new Dictionary(); - foreach (var review in pullRequest.Reviews.OrderBy(x => x.Id)) + foreach (var review in pullRequest.Reviews.OrderBy(x => x.SubmittedAt)) { if (review.State == PullRequestReviewState.Pending && review.Author.Login != currentUser.Login) continue; @@ -90,6 +90,7 @@ namespace GitHub.ViewModels.GitHubPane { case PullRequestReviewState.Approved: case PullRequestReviewState.ChangesRequested: + case PullRequestReviewState.Dismissed: return 1; case PullRequestReviewState.Pending: return 2; diff --git a/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj b/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj index b634ab7a9..1cc6c73ee 100644 --- a/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj +++ b/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj @@ -204,6 +204,7 @@ + diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs index 25edd6bb4..dd72963ac 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs @@ -224,5 +224,11 @@ namespace GitHub.Services string baseBranch, string compareBranch, int maxCommits); + + /// + /// Displays a confirmation diaglog to ask if the user wants to cancel a pending review. + /// + /// + bool ConfirmCancelPendingReview(); } } diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs new file mode 100644 index 000000000..379346882 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs @@ -0,0 +1,56 @@ +using System; +using System.Windows.Media.Imaging; +using GitHub.Models; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// Represents a view model for displaying details of a pull request Status or Check. + /// + public interface IPullRequestCheckViewModel: IViewModel + { + /// + /// The title of the Status/Check + /// + string Title { get; } + + /// + /// The description of the Status/Check + /// + string Description { get; } + + /// + /// The status of the Status/Check + /// + PullRequestCheckStatus Status { get; } + + /// + /// The url where more information about the Status/Check can be found + /// + Uri DetailsUrl { get; } + + /// + /// The AvatarUrl of the Status/Check application + /// + string AvatarUrl { get; } + + /// + /// The BitmapImage of the AvatarUrl + /// + BitmapImage Avatar { get; } + + /// + /// A command that opens the DetailsUrl in a browser + /// + + ReactiveCommand OpenDetailsUrl { get; } + } + + public enum PullRequestCheckStatus + { + Pending, + Success, + Failure + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs index 9a11d0dd4..73ba93d39 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs @@ -175,6 +175,11 @@ namespace GitHub.ViewModels.GitHubPane /// ReactiveCommand ShowReview { get; } + /// + /// Gets the latest pull request Checks & Statuses + /// + IReadOnlyList Checks { get; } + /// /// Initializes the view model. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListItemViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListItemViewModel.cs index b252e2a60..f8fa89c35 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListItemViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListItemViewModel.cs @@ -1,4 +1,5 @@ using System; +using GitHub.Models; namespace GitHub.ViewModels.GitHubPane { @@ -27,5 +28,10 @@ namespace GitHub.ViewModels.GitHubPane /// Gets the last updated time of the pull request. /// DateTimeOffset UpdatedAt { get; } + + /// + /// Gets the pull request checks and statuses summary + /// + PullRequestChecksState Checks { get; } } } diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 36ceeaca7..ad7f0e87c 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -185,6 +185,8 @@ + + diff --git a/src/GitHub.Exports/Models/IPullRequestModel.cs b/src/GitHub.Exports/Models/IPullRequestModel.cs index 8ff71e861..4d78de08a 100644 --- a/src/GitHub.Exports/Models/IPullRequestModel.cs +++ b/src/GitHub.Exports/Models/IPullRequestModel.cs @@ -13,6 +13,14 @@ namespace GitHub.Models Merged, } + public enum PullRequestChecksState + { + None, + Pending, + Success, + Failure + } + public interface IPullRequestModel : ICopyable, IEquatable, IComparable { diff --git a/src/GitHub.Exports/Models/PullRequestDetailModel.cs b/src/GitHub.Exports/Models/PullRequestDetailModel.cs index ee6ce03a6..3cd2de8a5 100644 --- a/src/GitHub.Exports/Models/PullRequestDetailModel.cs +++ b/src/GitHub.Exports/Models/PullRequestDetailModel.cs @@ -91,5 +91,10 @@ namespace GitHub.Models /// into threads, as such each pull request review comment will appear in both collections. /// public IReadOnlyList Threads { get; set; } + + /// + /// Gets or sets a collection of pull request Checks & Statuses + /// + public List Statuses { get; set; } } } diff --git a/src/GitHub.Exports/Models/PullRequestListItemModel.cs b/src/GitHub.Exports/Models/PullRequestListItemModel.cs index 21f603042..f6a9a7fe5 100644 --- a/src/GitHub.Exports/Models/PullRequestListItemModel.cs +++ b/src/GitHub.Exports/Models/PullRequestListItemModel.cs @@ -37,6 +37,11 @@ namespace GitHub.Models /// public PullRequestStateEnum State { get; set; } + /// + /// Gets the pull request checks and statuses summary + /// + public PullRequestChecksState Checks { get; set; } + /// /// Gets or sets the date/time at which the pull request was last updated. /// diff --git a/src/GitHub.Exports/Models/StatusModel.cs b/src/GitHub.Exports/Models/StatusModel.cs new file mode 100644 index 000000000..9d4998a49 --- /dev/null +++ b/src/GitHub.Exports/Models/StatusModel.cs @@ -0,0 +1,33 @@ +namespace GitHub.Models +{ + /// + /// Holds details about a pull request Status + /// + public class StatusModel + { + /// + /// The state of the Status + /// + public StatusState State { get; set; } + + /// + /// The Status context or title + /// + public string Context { get; set; } + + /// + /// The url where more information about the Status can be found + /// + public string TargetUrl { get; set; } + + /// + /// The descritption for the Status + /// + public string Description { get; set; } + + /// + /// The Url for the avatar for the Status + /// + public string AvatarUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/StatusState.cs b/src/GitHub.Exports/Models/StatusState.cs new file mode 100644 index 000000000..57830b3dd --- /dev/null +++ b/src/GitHub.Exports/Models/StatusState.cs @@ -0,0 +1,11 @@ +namespace GitHub.Models +{ + public enum StatusState + { + Expected, + Error, + Failure, + Pending, + Success, + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/UsageModel.cs b/src/GitHub.Exports/Models/UsageModel.cs index 17dd22b1b..4a668c13e 100644 --- a/src/GitHub.Exports/Models/UsageModel.cs +++ b/src/GitHub.Exports/Models/UsageModel.cs @@ -58,6 +58,8 @@ namespace GitHub.Models public int NumberOfWelcomeDocsClicks { get; set; } public int NumberOfWelcomeTrainingClicks { get; set; } public int NumberOfGitHubPaneHelpClicks { get; set; } + public int NumberOfPRDetailsOpenInGitHub { get; set; } + public int NumberOfPRCheckStatusesOpenInGitHub { get; set; } public int NumberOfPRDetailsViewChanges { get; set; } public int NumberOfPRDetailsViewFile { get; set; } public int NumberOfPRDetailsCompareWithSolution { get; set; } diff --git a/src/GitHub.Exports/Services/IGitHubContextService.cs b/src/GitHub.Exports/Services/IGitHubContextService.cs index 9cc1767bd..b8522e93a 100644 --- a/src/GitHub.Exports/Services/IGitHubContextService.cs +++ b/src/GitHub.Exports/Services/IGitHubContextService.cs @@ -71,6 +71,30 @@ namespace GitHub.Services /// The resolved commit-ish, blob path and commit SHA for the blob. Path will be null if the commit-ish can be resolved but not the blob. (string commitish, string path, string commitSha) ResolveBlob(string repositoryDir, GitHubContext context, string remoteName = "origin"); + /// + /// Find the object-ish (first 8 chars of a blob SHA) from the path to historical blob created by Team Explorer. + /// + /// + /// Team Explorer creates temporary blob files in the following format: + /// C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs + /// The object-ish appears immediately before the file extension and the path contains the folder "TFSTemp". + /// + /// The path to a possible Team Explorer temporary blob file. + /// The target file's object-ish (blob SHA fragment) or null if the path isn't recognized as a Team Explorer blob file. + string FindObjectishForTFSTempFile(string tempFile); + + /// + /// Find a tree entry in the commit log where a blob appears and return its commit SHA and path. + /// + /// + /// Search back through the commit log for the first tree entry where a blob appears. This operation only takes + /// a fraction of a seond on the `github/VisualStudio` repository even if a tree entry casn't be found. + /// + /// The target repository directory. + /// The fragment of a blob SHA to find. + /// The commit SHA and blob path or null if the blob can't be found. + (string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish); + /// /// Check if a file in the working directory has changed since a specified commit-ish. /// diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index e016c9cb1..8e46e3a6b 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -85,6 +85,8 @@ + + diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs b/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs index 460d7b164..36cbda2c6 100644 --- a/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs +++ b/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs @@ -1,11 +1,13 @@ using System; using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Text.Classification; -using GitHub.InlineReviews.Services; using GitHub.Services; +using GitHub.VisualStudio; +using GitHub.InlineReviews.Services; namespace GitHub.InlineReviews.Margins { @@ -17,28 +19,41 @@ namespace GitHub.InlineReviews.Margins [TextViewRole(PredefinedTextViewRoles.Interactive)] internal sealed class InlineCommentMarginProvider : IWpfTextViewMarginProvider { - readonly IEditorFormatMapService editorFormatMapService; - readonly IViewTagAggregatorFactoryService tagAggregatorFactory; - readonly IInlineCommentPeekService peekService; + readonly Lazy editorFormatMapService; + readonly Lazy tagAggregatorFactory; + readonly Lazy peekService; readonly Lazy sessionManager; + readonly UIContext uiContext; [ImportingConstructor] public InlineCommentMarginProvider( - IGitHubServiceProvider serviceProvider, - IEditorFormatMapService editorFormatMapService, - IViewTagAggregatorFactoryService tagAggregatorFactory, - IInlineCommentPeekService peekService) + Lazy sessionManager, + Lazy editorFormatMapService, + Lazy tagAggregatorFactory, + Lazy peekService) { + this.sessionManager = sessionManager; this.editorFormatMapService = editorFormatMapService; this.tagAggregatorFactory = tagAggregatorFactory; this.peekService = peekService; - sessionManager = new Lazy(() => serviceProvider.GetService()); + + uiContext = UIContext.FromUIContextGuid(new Guid(Guids.UIContext_Git)); } public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin parent) { + if (!uiContext.IsActive) + { + // Only create margin when in the context of a Git repository + return null; + } + return new InlineCommentMargin( - wpfTextViewHost, peekService, editorFormatMapService, tagAggregatorFactory, sessionManager); + wpfTextViewHost, + peekService.Value, + editorFormatMapService.Value, + tagAggregatorFactory.Value, + sessionManager); } } } diff --git a/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs b/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs index 60dbe0dab..7dfafdfbc 100644 --- a/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs +++ b/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs @@ -3,6 +3,8 @@ using System.ComponentModel.Composition; using GitHub.Commands; using GitHub.Services; using GitHub.Settings; +using GitHub.VisualStudio; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Text.Editor; @@ -19,18 +21,19 @@ namespace GitHub.InlineReviews.Margins [TextViewRole(PredefinedTextViewRoles.Editable)] internal sealed class PullRequestFileMarginProvider : IWpfTextViewMarginProvider { - readonly IPullRequestSessionManager sessionManager; - readonly IToggleInlineCommentMarginCommand enableInlineCommentsCommand; - readonly IGoToSolutionOrPullRequestFileCommand goToSolutionOrPullRequestFileCommand; - readonly IPackageSettings packageSettings; + readonly Lazy sessionManager; + readonly Lazy enableInlineCommentsCommand; + readonly Lazy goToSolutionOrPullRequestFileCommand; + readonly Lazy packageSettings; readonly Lazy usageTracker; + readonly UIContext uiContext; [ImportingConstructor] public PullRequestFileMarginProvider( - IToggleInlineCommentMarginCommand enableInlineCommentsCommand, - IGoToSolutionOrPullRequestFileCommand goToSolutionOrPullRequestFileCommand, - IPullRequestSessionManager sessionManager, - IPackageSettings packageSettings, + Lazy enableInlineCommentsCommand, + Lazy goToSolutionOrPullRequestFileCommand, + Lazy sessionManager, + Lazy packageSettings, Lazy usageTracker) { this.enableInlineCommentsCommand = enableInlineCommentsCommand; @@ -38,6 +41,8 @@ namespace GitHub.InlineReviews.Margins this.sessionManager = sessionManager; this.packageSettings = packageSettings; this.usageTracker = usageTracker; + + uiContext = UIContext.FromUIContextGuid(new Guid(Guids.UIContext_Git)); } /// @@ -50,8 +55,14 @@ namespace GitHub.InlineReviews.Margins /// public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer) { + if (!uiContext.IsActive) + { + // Only create margin when in the context of a Git repository + return null; + } + // Comments in the editor feature flag - if (!packageSettings.EditorComments) + if (!packageSettings.Value.EditorComments) { return null; } @@ -63,7 +74,11 @@ namespace GitHub.InlineReviews.Margins } return new PullRequestFileMargin( - wpfTextViewHost.TextView, enableInlineCommentsCommand, goToSolutionOrPullRequestFileCommand, sessionManager, usageTracker); + wpfTextViewHost.TextView, + enableInlineCommentsCommand.Value, + goToSolutionOrPullRequestFileCommand.Value, + sessionManager.Value, + usageTracker); } bool IsDiffView(ITextView textView) => textView.Roles.Contains("DIFF"); diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs index 17bec00a2..21e63fbdd 100644 --- a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs @@ -16,17 +16,19 @@ namespace GitHub.InlineReviews.Peek readonly IPullRequestSessionManager sessionManager; readonly INextInlineCommentCommand nextCommentCommand; readonly IPreviousInlineCommentCommand previousCommentCommand; + readonly ICommentService commentService; - public InlineCommentPeekableItemSource( - IInlineCommentPeekService peekService, + public InlineCommentPeekableItemSource(IInlineCommentPeekService peekService, IPullRequestSessionManager sessionManager, INextInlineCommentCommand nextCommentCommand, - IPreviousInlineCommentCommand previousCommentCommand) + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) { this.peekService = peekService; this.sessionManager = sessionManager; this.nextCommentCommand = nextCommentCommand; this.previousCommentCommand = previousCommentCommand; + this.commentService = commentService; } public void AugmentPeekSession(IPeekSession session, IList peekableItems) @@ -38,7 +40,8 @@ namespace GitHub.InlineReviews.Peek session, sessionManager, nextCommentCommand, - previousCommentCommand); + previousCommentCommand, + commentService); viewModel.Initialize().Forget(); peekableItems.Add(new InlineCommentPeekableItem(viewModel)); } diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs index 15d59c44e..65558b3d8 100644 --- a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs @@ -20,18 +20,21 @@ namespace GitHub.InlineReviews.Peek readonly IPullRequestSessionManager sessionManager; readonly INextInlineCommentCommand nextCommentCommand; readonly IPreviousInlineCommentCommand previousCommentCommand; + readonly ICommentService commentService; [ImportingConstructor] public InlineCommentPeekableItemSourceProvider( IInlineCommentPeekService peekService, IPullRequestSessionManager sessionManager, INextInlineCommentCommand nextCommentCommand, - IPreviousInlineCommentCommand previousCommentCommand) + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) { this.peekService = peekService; this.sessionManager = sessionManager; this.nextCommentCommand = nextCommentCommand; this.previousCommentCommand = previousCommentCommand; + this.commentService = commentService; } public IPeekableItemSource TryCreatePeekableItemSource(ITextBuffer textBuffer) @@ -40,7 +43,8 @@ namespace GitHub.InlineReviews.Peek peekService, sessionManager, nextCommentCommand, - previousCommentCommand); + previousCommentCommand, + commentService); } } } diff --git a/src/GitHub.InlineReviews/Services/CommentService.cs b/src/GitHub.InlineReviews/Services/CommentService.cs new file mode 100644 index 000000000..566c4b764 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/CommentService.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.Composition; +using System.Windows.Forms; + +namespace GitHub.InlineReviews.Services +{ + [Export(typeof(ICommentService))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class CommentService:ICommentService + { + public bool ConfirmCommentDelete() + { + return MessageBox.Show( + VisualStudio.UI.Resources.DeleteCommentConfirmation, + VisualStudio.UI.Resources.DeleteCommentConfirmationCaption, + MessageBoxButtons.YesNo, + MessageBoxIcon.Question) == DialogResult.Yes; + } + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/ICommentService.cs b/src/GitHub.InlineReviews/Services/ICommentService.cs new file mode 100644 index 000000000..a206eb2ac --- /dev/null +++ b/src/GitHub.InlineReviews/Services/ICommentService.cs @@ -0,0 +1,14 @@ +namespace GitHub.InlineReviews.Services +{ + /// + /// This service allows for functionality to be injected into the chain of different peek Comment ViewModel types. + /// + public interface ICommentService + { + /// + /// This function uses MessageBox.Show to display a confirmation if a comment should be deleted. + /// + /// + bool ConfirmCommentDelete(); + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/PullRequestSession.cs b/src/GitHub.InlineReviews/Services/PullRequestSession.cs index 236063334..0759b996d 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSession.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSession.cs @@ -230,13 +230,8 @@ namespace GitHub.InlineReviews.Services throw new InvalidOperationException("There is no pending review to cancel."); } - await service.CancelPendingReview(LocalRepository, PendingReviewId); - - PullRequest.Reviews = PullRequest.Reviews - .Where(x => x.Id != PendingReviewId) - .ToList(); - - await Update(PullRequest); + var pullRequest = await service.CancelPendingReview(LocalRepository, PendingReviewId); + await Update(pullRequest); } /// diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs index ebf0b9d07..63edf306a 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs @@ -225,7 +225,7 @@ namespace GitHub.InlineReviews.Services { PullRequestSession session = null; WeakReference weakSession; - var key = Tuple.Create(owner, number); + var key = Tuple.Create(owner.ToLowerInvariant(), number); if (sessions.TryGetValue(key, out weakSession)) { diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index a23ae8b38..ccc89a6df 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -24,6 +24,7 @@ using ReactiveUI; using Serilog; using PullRequestReviewEvent = Octokit.PullRequestReviewEvent; using static Octokit.GraphQL.Variable; +using StatusState = GitHub.Models.StatusState; // GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST. #pragma warning disable CS0618 @@ -38,6 +39,7 @@ namespace GitHub.InlineReviews.Services { static readonly ILogger log = LogManager.ForContext(); static ICompiledQuery readPullRequest; + static ICompiledQuery> readCommitStatuses; static ICompiledQuery readViewer; readonly IGitService gitService; @@ -341,6 +343,9 @@ namespace GitHub.InlineReviews.Services var apiClient = await apiClientFactory.Create(address); var files = await apiClient.GetPullRequestFiles(owner, name, number).ToList(); + var lastCommitModel = await GetPullRequestLastCommitAdapter(address, owner, name, number); + + result.Statuses = lastCommitModel.Statuses; result.ChangedFiles = files.Select(file => new PullRequestFileModel { @@ -736,6 +741,42 @@ namespace GitHub.InlineReviews.Services return Task.Factory.StartNew(() => gitService.GetRepository(repository.LocalPath)); } + async Task GetPullRequestLastCommitAdapter(HostAddress address, string owner, string name, int number) + { + if (readCommitStatuses == null) + { + readCommitStatuses = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( + commit => new LastCommitAdapter + { + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusModel + { + State = (StatusState)statusContext.State, + Context = statusContext.Context, + TargetUrl = statusContext.TargetUrl, + Description = statusContext.Description, + AvatarUrl = statusContext.Creator.AvatarUrl(null) + }).ToList() + ).SingleOrDefault() + } + ).Compile(); + } + + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(number), number }, + }; + + var connection = await graphqlFactory.CreateConnection(address); + var result = await connection.Run(readCommitStatuses, vars); + return result.First(); + } + static void BuildPullRequestThreads(PullRequestDetailModel model) { var commentsByReplyId = new Dictionary>(); @@ -825,5 +866,10 @@ namespace GitHub.InlineReviews.Services public string OriginalCommitId { get; set; } public string ReplyTo { get; set; } } - } + + class LastCommitAdapter + { + public List Statuses { get; set; } + } + } } diff --git a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs index 70ac6a42f..356568299 100644 --- a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; +using System.Windows; using GitHub.Extensions; +using GitHub.InlineReviews.Services; using GitHub.Logging; using GitHub.Models; using GitHub.ViewModels; @@ -18,6 +20,7 @@ namespace GitHub.InlineReviews.ViewModels public class CommentViewModel : ReactiveObject, ICommentViewModel { static readonly ILogger log = LogManager.ForContext(); + ICommentService commentService; string body; string errorMessage; bool isReadOnly; @@ -30,6 +33,7 @@ namespace GitHub.InlineReviews.ViewModels /// /// Initializes a new instance of the class. /// + /// The comment service /// The thread that the comment is a part of. /// The current user. /// The pull request id of the comment. @@ -41,6 +45,7 @@ namespace GitHub.InlineReviews.ViewModels /// The modified date of the comment. /// protected CommentViewModel( + ICommentService commentService, ICommentThreadViewModel thread, IActorViewModel currentUser, int pullRequestId, @@ -52,6 +57,7 @@ namespace GitHub.InlineReviews.ViewModels DateTimeOffset updatedAt, Uri webUrl) { + this.commentService = commentService; Guard.ArgumentNotNull(thread, nameof(thread)); Guard.ArgumentNotNull(currentUser, nameof(currentUser)); Guard.ArgumentNotNull(author, nameof(author)); @@ -102,14 +108,17 @@ namespace GitHub.InlineReviews.ViewModels /// /// Initializes a new instance of the class. /// + /// Comment Service /// The thread that the comment is a part of. /// The current user. /// The comment model. protected CommentViewModel( + ICommentService commentService, ICommentThreadViewModel thread, ActorModel currentUser, CommentModel model) : this( + commentService, thread, new ActorViewModel(currentUser), model.PullRequestId, @@ -130,22 +139,25 @@ namespace GitHub.InlineReviews.ViewModels async Task DoDelete(object unused) { - try + if (commentService.ConfirmCommentDelete()) { - ErrorMessage = null; - IsSubmitting = true; + try + { + ErrorMessage = null; + IsSubmitting = true; - await Thread.DeleteComment.ExecuteAsyncTask(new Tuple(PullRequestId, DatabaseId)); - } - catch (Exception e) - { - var message = e.Message; - ErrorMessage = message; - log.Error(e, "Error Deleting comment"); - } - finally - { - IsSubmitting = false; + await Thread.DeleteComment.ExecuteAsyncTask(new Tuple(PullRequestId, DatabaseId)); + } + catch (Exception e) + { + var message = e.Message; + ErrorMessage = message; + log.Error(e, "Error Deleting comment"); + } + finally + { + IsSubmitting = false; + } } } diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs index d2058be47..91bef3958 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs @@ -10,6 +10,7 @@ using GitHub.Extensions; using GitHub.Extensions.Reactive; using GitHub.Factories; using GitHub.InlineReviews.Commands; +using GitHub.InlineReviews.Peek; using GitHub.InlineReviews.Services; using GitHub.Logging; using GitHub.Models; @@ -31,6 +32,7 @@ namespace GitHub.InlineReviews.ViewModels readonly IInlineCommentPeekService peekService; readonly IPeekSession peekSession; readonly IPullRequestSessionManager sessionManager; + readonly ICommentService commentService; IPullRequestSession session; IPullRequestSessionFile file; ICommentThreadViewModel thread; @@ -44,12 +46,12 @@ namespace GitHub.InlineReviews.ViewModels /// /// Initializes a new instance of the class. /// - public InlineCommentPeekViewModel( - IInlineCommentPeekService peekService, + public InlineCommentPeekViewModel(IInlineCommentPeekService peekService, IPeekSession peekSession, IPullRequestSessionManager sessionManager, INextInlineCommentCommand nextCommentCommand, - IPreviousInlineCommentCommand previousCommentCommand) + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) { Guard.ArgumentNotNull(peekService, nameof(peekService)); Guard.ArgumentNotNull(peekSession, nameof(peekSession)); @@ -60,6 +62,7 @@ namespace GitHub.InlineReviews.ViewModels this.peekService = peekService; this.peekSession = peekSession; this.sessionManager = sessionManager; + this.commentService = commentService; triggerPoint = peekSession.GetTriggerPoint(peekSession.TextView.TextBuffer); peekSession.Dismissed += (s, e) => Dispose(); @@ -180,11 +183,11 @@ namespace GitHub.InlineReviews.ViewModels if (thread != null) { - Thread = new InlineCommentThreadViewModel(session, thread.Comments); + Thread = new InlineCommentThreadViewModel(commentService, session, thread.Comments); } else { - Thread = new NewInlineCommentThreadViewModel(session, file, lineNumber, leftBuffer); + Thread = new NewInlineCommentThreadViewModel(commentService, session, file, lineNumber, leftBuffer); } if (!string.IsNullOrWhiteSpace(placeholderBody)) diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs index 1e729eb52..8c2d1567f 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Reactive.Linq; using System.Threading.Tasks; using GitHub.Extensions; +using GitHub.InlineReviews.Services; using GitHub.Models; using GitHub.Services; using ReactiveUI; @@ -18,10 +19,10 @@ namespace GitHub.InlineReviews.ViewModels /// /// Initializes a new instance of the class. /// + /// The comment service /// The current PR review session. /// The comments to display in this inline review. - public InlineCommentThreadViewModel( - IPullRequestSession session, + public InlineCommentThreadViewModel(ICommentService commentService, IPullRequestSession session, IEnumerable comments) : base(session.User) { @@ -45,13 +46,14 @@ namespace GitHub.InlineReviews.ViewModels { Comments.Add(new PullRequestReviewCommentViewModel( session, + commentService, this, CurrentUser, comment.Review, comment.Comment)); } - Comments.Add(PullRequestReviewCommentViewModel.CreatePlaceholder(session, this, CurrentUser)); + Comments.Add(PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, this, CurrentUser)); } /// diff --git a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs index 17096ae22..a9753a481 100644 --- a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs @@ -4,6 +4,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; using GitHub.Extensions; +using GitHub.InlineReviews.Services; using GitHub.Models; using GitHub.Services; using ReactiveUI; @@ -20,13 +21,14 @@ namespace GitHub.InlineReviews.ViewModels /// /// Initializes a new instance of the class. /// + /// The comment service /// The current PR review session. /// The file being commented on. /// The 0-based line number in the file. /// - /// True if the comment is being left on the left-hand-side of a diff; otherwise false. + /// True if the comment is being left on the left-hand-side of a diff; otherwise false. /// - public NewInlineCommentThreadViewModel( + public NewInlineCommentThreadViewModel(ICommentService commentService, IPullRequestSession session, IPullRequestSessionFile file, int lineNumber, @@ -53,7 +55,7 @@ namespace GitHub.InlineReviews.ViewModels Observable.Return(false), o => null); - var placeholder = PullRequestReviewCommentViewModel.CreatePlaceholder(session, this, CurrentUser); + var placeholder = PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, this, CurrentUser); placeholder.BeginEdit.Execute(null); this.WhenAnyValue(x => x.NeedsPush).Subscribe(x => placeholder.IsReadOnly = x); Comments.Add(placeholder); diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs index 947cb0d28..f762ba3da 100644 --- a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs @@ -5,6 +5,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; using GitHub.Extensions; +using GitHub.InlineReviews.Services; using GitHub.Logging; using GitHub.Models; using GitHub.Services; @@ -28,6 +29,7 @@ namespace GitHub.InlineReviews.ViewModels /// Initializes a new instance of the class. /// /// The pull request session. + /// The comment service /// The thread that the comment is a part of. /// The current user. /// The pull request id of the comment. @@ -39,7 +41,9 @@ namespace GitHub.InlineReviews.ViewModels /// The modified date of the comment. /// Whether this is a pending comment. /// - public PullRequestReviewCommentViewModel(IPullRequestSession session, + public PullRequestReviewCommentViewModel( + IPullRequestSession session, + ICommentService commentService, ICommentThreadViewModel thread, IActorViewModel currentUser, int pullRequestId, @@ -51,7 +55,7 @@ namespace GitHub.InlineReviews.ViewModels DateTimeOffset updatedAt, bool isPending, Uri webUrl) - : base(thread, currentUser, pullRequestId, commentId, databaseId, body, state, author, updatedAt, webUrl) + : base(commentService, thread, currentUser, pullRequestId, commentId, databaseId, body, state, author, updatedAt, webUrl) { Guard.ArgumentNotNull(session, nameof(session)); @@ -81,17 +85,20 @@ namespace GitHub.InlineReviews.ViewModels /// Initializes a new instance of the class. /// /// The pull request session. + /// Comment Service /// The thread that the comment is a part of. /// The current user. /// The comment model. public PullRequestReviewCommentViewModel( IPullRequestSession session, + ICommentService commentService, ICommentThreadViewModel thread, IActorViewModel currentUser, PullRequestReviewModel review, PullRequestReviewCommentModel model) : this( session, + commentService, thread, currentUser, model.PullRequestId, @@ -110,16 +117,19 @@ namespace GitHub.InlineReviews.ViewModels /// Creates a placeholder comment which can be used to add a new comment to a thread. /// /// The pull request session. + /// Comment Service /// The comment thread. /// The current user. /// THe placeholder comment. public static CommentViewModel CreatePlaceholder( IPullRequestSession session, + ICommentService commentService, ICommentThreadViewModel thread, IActorViewModel currentUser) { return new PullRequestReviewCommentViewModel( session, + commentService, thread, currentUser, 0, diff --git a/src/GitHub.InlineReviews/Views/CommentView.xaml b/src/GitHub.InlineReviews/Views/CommentView.xaml index bcc1ed483..7f4cf8c88 100644 --- a/src/GitHub.InlineReviews/Views/CommentView.xaml +++ b/src/GitHub.InlineReviews/Views/CommentView.xaml @@ -56,7 +56,7 @@ Height="16" Account="{Binding Author}"/> - + diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs index 7cd7a4a64..b2279c8b6 100644 --- a/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs +++ b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs @@ -12,6 +12,7 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; using Task = System.Threading.Tasks.Task; +using static Microsoft.VisualStudio.VSConstants; namespace GitHub.VisualStudio.Base { @@ -51,9 +52,8 @@ namespace GitHub.VisualStudio.Base // Start with empty array until we have a chance to initialize. ActiveRepositories = Array.Empty(); - // The IGitExt service isn't available when a TFS based solution is opened directly. - // It will become available when moving to a Git based solution (and cause a UIContext event to fire). - var context = factory.GetUIContext(new Guid(Guids.GitSccProviderId)); + // Initialize when we enter the context of a Git repository + var context = factory.GetUIContext(UICONTEXT.RepositoryOpen_guid); context.WhenActivated(() => JoinableTaskFactory.RunAsync(InitializeAsync).Task.Forget(log)); } diff --git a/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml b/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml index 1e08c7da6..dfbb963c8 100644 --- a/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml +++ b/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml @@ -61,13 +61,13 @@ Opacity="0" SnapsToDevicePixels="True"/> (); static Dictionary assemblyDicts = new Dictionary(); public new Uri Source @@ -31,7 +29,7 @@ namespace GitHub var assemblyName = FindAssemblyNameFromPackUri(value); if (assemblyName == null) { - log.Error("Couldn't find assembly name in '{Uri}'", value); + Trace.WriteLine($"Couldn't find assembly name in '{value}'"); return; } @@ -44,16 +42,16 @@ namespace GitHub if (!File.Exists(assemblyFile)) { - log.Error("Couldn't find assembly at '{AssemblyFile}'", assemblyFile); + Trace.WriteLine($"Couldn't find assembly at '{assemblyFile}'"); return; } var assembly = Assembly.LoadFrom(assemblyFile); assemblyDicts.Add(assemblyFile, assembly); } - catch(Exception e) + catch (Exception) { - log.Error(e, "Error loading assembly for '{Uri}'", value); + Trace.WriteLine($"Error loading assembly for '{value}"); } } diff --git a/src/GitHub.UI/Helpers/SharedDictionaryManager.cs b/src/GitHub.UI/Helpers/SharedDictionaryManager.cs index d9eb31ce5..b9a5c0fe7 100644 --- a/src/GitHub.UI/Helpers/SharedDictionaryManager.cs +++ b/src/GitHub.UI/Helpers/SharedDictionaryManager.cs @@ -1,7 +1,6 @@ using System; using System.Windows; using System.Collections.Generic; -using GitHub.Extensions; using System.Globalization; namespace GitHub.UI.Helpers @@ -17,8 +16,6 @@ namespace GitHub.UI.Helpers public SharedDictionaryManager(CachingFactory factory) { - Guard.ArgumentNotNull(factory, nameof(factory)); - this.factory = factory; } @@ -77,9 +74,6 @@ namespace GitHub.UI.Helpers public ResourceDictionary GetOrCreateResourceDictionary(ResourceDictionary owner, Uri uri) { - Guard.ArgumentNotNull(owner, nameof(owner)); - Guard.ArgumentNotNull(uri, nameof(uri)); - TryAddDisposable(owner); ResourceDictionary rd; @@ -95,8 +89,6 @@ namespace GitHub.UI.Helpers // Remember subtypes that need disposing of. public void TryAddDisposable(object owner) { - Guard.ArgumentNotNull(owner, nameof(owner)); - var disposable = owner as IDisposable; if (disposable != null) { @@ -132,8 +124,6 @@ namespace GitHub.UI.Helpers public static Uri FixDesignTimeUri(Uri inUri) { - Guard.ArgumentNotNull(inUri, nameof(inUri)); - if (inUri.Scheme != "file") { return inUri; diff --git a/src/GitHub.VisualStudio.UI/Resources.Designer.cs b/src/GitHub.VisualStudio.UI/Resources.Designer.cs index 39f106334..a291b7a1a 100644 --- a/src/GitHub.VisualStudio.UI/Resources.Designer.cs +++ b/src/GitHub.VisualStudio.UI/Resources.Designer.cs @@ -330,6 +330,24 @@ namespace GitHub.VisualStudio.UI { } } + /// + /// 查找类似 Are you sure you want to delete this comment? 的本地化字符串。 + /// + public static string DeleteCommentConfirmation { + get { + return ResourceManager.GetString("DeleteCommentConfirmation", resourceCulture); + } + } + + /// + /// 查找类似 Delete Comment 的本地化字符串。 + /// + public static string DeleteCommentConfirmationCaption { + get { + return ResourceManager.GetString("DeleteCommentConfirmationCaption", resourceCulture); + } + } + /// /// 查找类似 Description 的本地化字符串。 /// diff --git a/src/GitHub.VisualStudio.UI/Resources.resx b/src/GitHub.VisualStudio.UI/Resources.resx index 30669205b..e0085724b 100644 --- a/src/GitHub.VisualStudio.UI/Resources.resx +++ b/src/GitHub.VisualStudio.UI/Resources.resx @@ -530,6 +530,12 @@ Select Fork + + Are you sure you want to delete this comment? + + + Delete Comment + There aren't any open pull requests diff --git a/src/GitHub.VisualStudio/Commands/GoToSolutionOrPullRequestFileCommand.cs b/src/GitHub.VisualStudio/Commands/GoToSolutionOrPullRequestFileCommand.cs index 108950189..0ec4ed1bf 100644 --- a/src/GitHub.VisualStudio/Commands/GoToSolutionOrPullRequestFileCommand.cs +++ b/src/GitHub.VisualStudio/Commands/GoToSolutionOrPullRequestFileCommand.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.ComponentModel.Composition; using GitHub.Services; using GitHub.Extensions; @@ -10,6 +11,7 @@ using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.TextManager.Interop; using Task = System.Threading.Tasks.Task; namespace GitHub.Commands @@ -40,6 +42,8 @@ namespace GitHub.Commands readonly Lazy editorAdapter; readonly Lazy sessionManager; readonly Lazy pullRequestEditorService; + readonly Lazy teamExplorerContext; + readonly Lazy gitHubContextService; readonly Lazy statusBar; readonly Lazy usageTracker; @@ -49,6 +53,8 @@ namespace GitHub.Commands Lazy editorAdapter, Lazy sessionManager, Lazy pullRequestEditorService, + Lazy teamExplorerContext, + Lazy gitHubContextService, Lazy statusBar, Lazy usageTracker) : base(CommandSet, CommandId) { @@ -56,6 +62,8 @@ namespace GitHub.Commands this.editorAdapter = editorAdapter; this.sessionManager = sessionManager; this.pullRequestEditorService = pullRequestEditorService; + this.gitHubContextService = gitHubContextService; + this.teamExplorerContext = teamExplorerContext; this.statusBar = statusBar; this.usageTracker = usageTracker; @@ -112,6 +120,11 @@ namespace GitHub.Commands return; } + if (TryNavigateFromHistoryFile(sourceView)) + { + return; + } + var relativePath = sessionManager.Value.GetRelativePath(textView.TextBuffer); if (relativePath == null) { @@ -189,6 +202,11 @@ namespace GitHub.Commands return; } } + + if (TryNavigateFromHistoryFileQueryStatus(sourceView)) + { + return; + } } catch (Exception ex) { @@ -198,6 +216,65 @@ namespace GitHub.Commands Visible = false; } + // Set command Text/Visible properties and return true when active + bool TryNavigateFromHistoryFileQueryStatus(IVsTextView sourceView) + { + if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string && // Check there is an active repo + FindObjectishForTFSTempFile(sourceView) is string) // Looks like a history file + { + // Navigate from history file is active + Text = "Open File in Solution"; + Visible = true; + return true; + } + + return false; + } + + // Attempt navigation to historical file + bool TryNavigateFromHistoryFile(IVsTextView sourceView) + { + if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string repositoryDir && + FindObjectishForTFSTempFile(sourceView) is string objectish) + { + var (commitSha, blobPath) = gitHubContextService.Value.ResolveBlobFromHistory(repositoryDir, objectish); + if (blobPath is string) + { + var workingFile = Path.Combine(repositoryDir, blobPath); + VsShellUtilities.OpenDocument(serviceProvider, workingFile, VSConstants.LOGVIEWID.TextView_guid, + out IVsUIHierarchy hierarchy, out uint itemID, out IVsWindowFrame windowFrame, out IVsTextView targetView); + + pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, targetView); + return true; + } + } + + return false; + } + + // Find the blob SHA in a file name if any + string FindObjectishForTFSTempFile(IVsTextView sourceView) + { + return + FindPath(sourceView) is string path && + gitHubContextService.Value.FindObjectishForTFSTempFile(path) is string objectish ? + objectish : null; + } + + // See http://microsoft.public.vstudio.extensibility.narkive.com/agfoD1GO/full-pathname-of-file-shown-in-current-view-of-core-editor#post2 + static string FindPath(IVsTextView textView) + { + ErrorHandler.ThrowOnFailure(textView.GetBuffer(out IVsTextLines buffer)); + var userData = buffer as IVsUserData; + if (userData == null) + { + return null; + } + + ErrorHandler.ThrowOnFailure(userData.GetData(typeof(IVsUserData).GUID, out object data)); + return data as string; + } + ITextView FindActiveTextView(IDifferenceViewer diffViewer) { switch (diffViewer.ActiveViewType) diff --git a/src/GitHub.VisualStudio/GitContextPackage.cs b/src/GitHub.VisualStudio/GitContextPackage.cs index 698d5389f..c2b267609 100644 --- a/src/GitHub.VisualStudio/GitContextPackage.cs +++ b/src/GitHub.VisualStudio/GitContextPackage.cs @@ -2,31 +2,28 @@ using System.Threading; using System.Runtime.InteropServices; using GitHub.Exports; -using GitHub.Logging; using GitHub.Services; using Microsoft.VisualStudio.Shell; -using Serilog; using Task = System.Threading.Tasks.Task; +using static Microsoft.VisualStudio.VSConstants; namespace GitHub.VisualStudio { /// /// This package creates a custom UIContext that is activated when a - /// repository is active in . + /// repository is active in and the current process is Visual Studio (not Blend). /// [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [Guid(Guids.UIContext_Git)] - // this is the Git service GUID, so we load whenever it loads - [ProvideAutoLoad(Guids.GitSccProviderId, PackageAutoLoadFlags.BackgroundLoad)] + // Initialize when we enter the context of a Git repository + [ProvideAutoLoad(UICONTEXT.RepositoryOpen_string, PackageAutoLoadFlags.BackgroundLoad)] public class GitContextPackage : AsyncPackage { - static readonly ILogger log = LogManager.ForContext(); - protected async override Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { if (!ExportForVisualStudioProcessAttribute.IsVisualStudioProcess()) { - log.Warning("Don't activate 'UIContext_Git' for non-Visual Studio process"); + // Don't activate 'UIContext_Git' for non-Visual Studio process return; } diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index acb1d3d2a..2a2eff01e 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -426,6 +426,9 @@ PullRequestReviewAuthoringView.xaml + + PullRequestCheckView.xaml + PullRequestReviewSummaryView.xaml @@ -625,6 +628,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer diff --git a/src/GitHub.VisualStudio/Helpers/ActiveDocumentSnapshot.cs b/src/GitHub.VisualStudio/Helpers/ActiveDocumentSnapshot.cs index f21e3f356..437544c34 100644 --- a/src/GitHub.VisualStudio/Helpers/ActiveDocumentSnapshot.cs +++ b/src/GitHub.VisualStudio/Helpers/ActiveDocumentSnapshot.cs @@ -3,7 +3,6 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.TextManager.Interop; using System; using System.ComponentModel.Composition; -using System.Diagnostics; using GitHub.Logging; namespace GitHub.VisualStudio @@ -32,6 +31,17 @@ namespace GitHub.VisualStudio if (ErrorHandler.Succeeded(textManager.GetActiveView(0, null, out view)) && ErrorHandler.Succeeded(view.GetSelection(out anchorLine, out anchorCol, out endLine, out endCol))) { + // Ignore the bottom anchor or end line if it has zero width (starts on column 0) + // This prevents non-visible parts of the selection from being inclused in the range + if (anchorLine < endLine && endCol == 0) + { + endLine--; + } + else if (anchorLine > endLine && anchorCol == 0) + { + anchorLine--; + } + StartLine = anchorLine + 1; EndLine = endLine + 1; } diff --git a/src/GitHub.VisualStudio/Resources/icons/mark_github.xaml b/src/GitHub.VisualStudio/Resources/icons/mark_github.xaml index 44e2a18fd..c364f48ea 100644 --- a/src/GitHub.VisualStudio/Resources/icons/mark_github.xaml +++ b/src/GitHub.VisualStudio/Resources/icons/mark_github.xaml @@ -1,17 +1,19 @@ - + + + + + #424242 + + + + - - - - - - - - - - + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml new file mode 100644 index 000000000..c4dfc8477 --- /dev/null +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml.cs b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml.cs new file mode 100644 index 000000000..0bb8fe8d9 --- /dev/null +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel.Composition; +using GitHub.Exports; +using GitHub.Services; +using GitHub.UI; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.VisualStudio.Views.GitHubPane +{ + public class GenericPullRequestCheckView : ViewBase { } + + [ExportViewFor(typeof(IPullRequestCheckViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class PullRequestCheckView : GenericPullRequestCheckView + { + public PullRequestCheckView() + { + InitializeComponent(); + + this.WhenActivated(d => + { + d(ViewModel.OpenDetailsUrl.Subscribe(_ => DoOpenDetailsUrl())); + }); + } + + [Import] + IVisualStudioBrowser VisualStudioBrowser { get; set; } + + void DoOpenDetailsUrl() + { + var browser = VisualStudioBrowser; + browser.OpenUrl(ViewModel.DetailsUrl); + } + } +} diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml index 36ae95661..9a4965bfe 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml @@ -245,6 +245,20 @@ + + + + + + + + + + ()) { menuItem.CommandParameter = fileNode; } + // HACK: MenuItem doesn't re-query ICommand.CanExecute when CommandParameter changes. Force + // this to happen by resetting its DataContext to null and then to the correct value. + container.ContextMenu.DataContext = null; + container.ContextMenu.DataContext = this.DataContext; e.Handled = false; } } diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestListItemView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestListItemView.xaml index 729386579..4d7e26155 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestListItemView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestListItemView.xaml @@ -12,7 +12,8 @@ Title="Let's try doing this differently" CommentCount="4" IsCurrent="True" - UpdatedAt="2018-01-29"> + UpdatedAt="2018-01-29" + Checks="Success"> @@ -54,6 +55,7 @@ + - + @@ -78,28 +80,27 @@ - - - - - - - - - - - - - - by - - - - - - + + + - + + + + + by + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio/source.extension.vsixmanifest b/src/GitHub.VisualStudio/source.extension.vsixmanifest index 0d6f193d9..40cb99366 100644 --- a/src/GitHub.VisualStudio/source.extension.vsixmanifest +++ b/src/GitHub.VisualStudio/source.extension.vsixmanifest @@ -1,7 +1,7 @@  - + GitHub Extension for Visual Studio A Visual Studio Extension that brings the GitHub Flow into Visual Studio. GitHub.VisualStudio @@ -39,4 +39,4 @@ - \ No newline at end of file + diff --git a/src/common/SolutionInfo.cs b/src/common/SolutionInfo.cs index ad4c892ad..5930e0a50 100644 --- a/src/common/SolutionInfo.cs +++ b/src/common/SolutionInfo.cs @@ -18,6 +18,6 @@ using System.Runtime.InteropServices; namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "2.5.4.0"; + internal const string Version = "2.5.5.0"; } } diff --git a/submodules/akavache b/submodules/akavache index 1ea55dfb7..dcd51f78e 160000 --- a/submodules/akavache +++ b/submodules/akavache @@ -1 +1 @@ -Subproject commit 1ea55dfb7e36ee0737291bf013b09c895fffa624 +Subproject commit dcd51f78eaed02a87c7856f94676ee3d0d255fed diff --git a/test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs b/test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs index 8cd83d592..01fc6ba49 100644 --- a/test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs +++ b/test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using GitHub.Exports; using GitHub.Services; using NSubstitute; @@ -382,6 +383,117 @@ public class GitHubContextServiceTests } } + public class TheResolveBlobFromCommitsMethod + { + [Test] + public void FlatTree() + { + var objectish = "12345678"; + var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97"; + var expectBlobPath = "Class1.cs"; + var repositoryDir = "repositoryDir"; + var blob = Substitute.For(); + var treeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath); + var commit = CreateCommit(expectCommitSha, treeEntry); + var repository = CreateRepository(commit); + repository.Lookup(objectish).Returns(blob); + var target = CreateGitHubContextService(repositoryDir, repository); + + var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish); + + Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath))); + } + + [Test] + public void NestedTree() + { + var objectish = "12345678"; + var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97"; + var expectBlobPath = @"AnnotateFileTests\Class1.cs"; + var repositoryDir = "repositoryDir"; + var blob = Substitute.For(); + var blobTreeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath); + var childTree = CreateTree(blobTreeEntry); + var treeTreeEntry = CreateTreeEntry(TreeEntryTargetType.Tree, childTree, "AnnotateFileTests"); + var commit = CreateCommit(expectCommitSha, treeTreeEntry); + var repository = CreateRepository(commit); + repository.Lookup(objectish).Returns(blob); + var target = CreateGitHubContextService(repositoryDir, repository); + + var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish); + + Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath))); + } + + [Test] + public void MissingBlob() + { + var objectish = "12345678"; + var repositoryDir = "repositoryDir"; + var treeEntry = Substitute.For(); + var repository = CreateRepository(); + var target = CreateGitHubContextService(repositoryDir, repository); + + var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish); + + Assert.That((commitSha, blobPath), Is.EqualTo((null as string, null as string))); + } + + static IRepository CreateRepository(params Commit[] commits) + { + var repository = Substitute.For(); + var enumerator = commits.ToList().GetEnumerator(); + repository.Commits.GetEnumerator().Returns(enumerator); + return repository; + } + + static Commit CreateCommit(string sha, params TreeEntry[] treeEntries) + { + var commit = Substitute.For(); + commit.Sha.Returns(sha); + var tree = CreateTree(treeEntries); + commit.Tree.Returns(tree); + return commit; + } + + static TreeEntry CreateTreeEntry(TreeEntryTargetType targetType, GitObject target, string path) + { + var treeEntry = Substitute.For(); + treeEntry.TargetType.Returns(targetType); + treeEntry.Target.Returns(target); + treeEntry.Path.Returns(path); + return treeEntry; + } + + static Tree CreateTree(params TreeEntry[] treeEntries) + { + var tree = Substitute.For(); + var enumerator = treeEntries.ToList().GetEnumerator(); + tree.GetEnumerator().Returns(enumerator); + return tree; + } + } + + public class TheFindBlobShaForTextViewMethod + { + [TestCase(@"C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs", "783ac965")] + [TestCase(@"\TFSTemp\File.12345678.ext", "12345678")] + [TestCase(@"\TFSTemp\File.abcdefab.ext", "abcdefab")] + [TestCase(@"\TFSTemp\.12345678.", "12345678")] + [TestCase(@"\TFSTemp\File.ABCDEFAB.ext", null)] + [TestCase(@"\TFSTemp\File.1234567.ext", null)] + [TestCase(@"\TFSTemp\File.123456789.ext", null)] + [TestCase(@"\TFSTemp\File.12345678.ext\\", null)] + public void FindObjectishForTFSTempFile(string path, string expectObjectish) + { + var target = CreateGitHubContextService(); + + var objectish = target.FindObjectishForTFSTempFile(path); + + Assert.That(objectish, Is.EqualTo(expectObjectish)); + } + } + static GitHubContextService CreateGitHubContextService(string repositoryDir = null, IRepository repository = null) { var sp = Substitute.For(); diff --git a/test/GitHub.App.UnitTests/Services/PullRequestServiceTests.cs b/test/GitHub.App.UnitTests/Services/PullRequestServiceTests.cs index 909dc8525..c8e437cb8 100644 --- a/test/GitHub.App.UnitTests/Services/PullRequestServiceTests.cs +++ b/test/GitHub.App.UnitTests/Services/PullRequestServiceTests.cs @@ -777,6 +777,35 @@ public class PullRequestServiceTests : TestBaseClass Assert.That(4, Is.EqualTo(gitClient.ReceivedCalls().Count())); } + [Test] + public async Task ShouldCheckoutLocalBranchOwnerCaseMismatchAsync() + { + var gitClient = MockGitClient(); + var service = CreateTarget(gitClient, MockGitService()); + + var localRepo = Substitute.For(); + localRepo.CloneUrl.Returns(new UriString("https://foo.bar/Owner/repo")); + + var pr = new PullRequestDetailModel + { + Number = 5, + BaseRefName = "master", + BaseRefSha = "123", + BaseRepositoryOwner = "owner", + HeadRefName = "prbranch", + HeadRefSha = "123", + HeadRepositoryOwner = "owner", + }; + + await service.Checkout(localRepo, pr, "prbranch"); + + gitClient.Received().Fetch(Arg.Any(), "origin").Forget(); + gitClient.Received().Checkout(Arg.Any(), "prbranch").Forget(); + gitClient.Received().SetConfig(Arg.Any(), "branch.prbranch.ghfvs-pr-owner-number", "owner#5").Forget(); + + Assert.That(4, Is.EqualTo(gitClient.ReceivedCalls().Count())); + } + [Test] public async Task ShouldCheckoutBranchFromForkAsync() { @@ -899,6 +928,19 @@ public class PullRequestServiceTests : TestBaseClass Assert.That("source", Is.EqualTo(result.Name)); } + [Test] + public async Task ShouldReturnPullRequestBranchForPullRequestFromSameRepositoryOwnerCaseMismatchAsync() + { + var service = CreateTarget(MockGitClient(), MockGitService()); + + var localRepo = Substitute.For(); + localRepo.CloneUrl.Returns(new UriString("https://github.com/Foo/bar")); + + var result = await service.GetLocalBranches(localRepo, CreatePullRequest(fromFork: false)); + + Assert.That("source", Is.EqualTo(result.Name)); + } + [Test] public async Task ShouldReturnMarkedBranchForPullRequestFromForkAsync() { diff --git a/test/GitHub.App.UnitTests/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModelTests.cs index 30de0d6ad..53468244a 100644 --- a/test/GitHub.App.UnitTests/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/Dialog/LoginToGitHubForEnterpriseViewModelTests.cs @@ -25,7 +25,7 @@ public class LoginToGitHubForEnterpriseViewModelTests } [Test] - public void ReturnsCheckingWhenProbeNotFinished() + public async Task ReturnsCheckingWhenProbeNotFinished() { var scheduler = new TestScheduler(); var caps = Substitute.For(); @@ -37,11 +37,17 @@ public class LoginToGitHubForEnterpriseViewModelTests scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); Assert.That(EnterpriseProbeStatus.Checking, Is.EqualTo(target.ProbeStatus)); - task.SetCanceled(); + + try + { + task.SetCanceled(); + await task.Task; + } + catch (TaskCanceledException) { } } [Test] - public void ReturnsValidWhenProbeReturnsOk() + public async Task ReturnsValidWhenProbeReturnsOk() { var scheduler = new TestScheduler(); var caps = CreateCapabilties(EnterpriseProbeResult.Ok); @@ -50,12 +56,13 @@ public class LoginToGitHubForEnterpriseViewModelTests target.EnterpriseUrl = "https://foo.bar"; scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); scheduler.Stop(); + await target.UpdatingProbeStatus; Assert.That(EnterpriseProbeStatus.Valid, Is.EqualTo(target.ProbeStatus)); } [Test] - public void ReturnsInvalidWhenProbeReturnsFailed() + public async Task ReturnsInvalidWhenProbeReturnsFailed() { var scheduler = new TestScheduler(); var caps = CreateCapabilties(EnterpriseProbeResult.Failed); @@ -64,12 +71,13 @@ public class LoginToGitHubForEnterpriseViewModelTests target.EnterpriseUrl = "https://foo.bar"; scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); scheduler.Stop(); + await target.UpdatingProbeStatus; Assert.That(EnterpriseProbeStatus.Invalid, Is.EqualTo(target.ProbeStatus)); } [Test] - public void ReturnsInvalidWhenProbeReturnsNotFound() + public async Task ReturnsInvalidWhenProbeReturnsNotFound() { var scheduler = new TestScheduler(); var caps = CreateCapabilties(EnterpriseProbeResult.NotFound); @@ -78,6 +86,7 @@ public class LoginToGitHubForEnterpriseViewModelTests target.EnterpriseUrl = "https://foo.bar"; scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); scheduler.Stop(); + await target.UpdatingProbeStatus; Assert.That(EnterpriseProbeStatus.Invalid, Is.EqualTo(target.ProbeStatus)); } @@ -126,7 +135,7 @@ public class LoginToGitHubForEnterpriseViewModelTests public void GivesPrecedenceToUsernameAndPasswordOverToken() { var scheduler = new TestScheduler(); - var caps = CreateCapabilties(EnterpriseLoginMethods.Token | + var caps = CreateCapabilties(EnterpriseLoginMethods.Token | EnterpriseLoginMethods.UsernameAndPassword | EnterpriseLoginMethods.OAuth); var target = CreateTarget(scheduler, caps); diff --git a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs index 601283e5a..50df1cba0 100644 --- a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs @@ -56,12 +56,13 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane [Test] public async Task ShouldShowLatestAcceptedOrChangesRequestedReviewAsync() { + var dateTimeOffset = DateTimeOffset.Now; var target = CreateTarget(); var model = CreatePullRequestModel( - CreatePullRequestReviewModel("1", "grokys", PullRequestReviewState.ChangesRequested), - CreatePullRequestReviewModel("2", "shana", PullRequestReviewState.ChangesRequested), - CreatePullRequestReviewModel("3", "grokys", PullRequestReviewState.Approved), - CreatePullRequestReviewModel("4", "grokys", PullRequestReviewState.Commented)); + CreatePullRequestReviewModel("1", "grokys", PullRequestReviewState.ChangesRequested, dateTimeOffset.AddMinutes(1)), + CreatePullRequestReviewModel("2", "shana", PullRequestReviewState.ChangesRequested, dateTimeOffset.AddMinutes(2)), + CreatePullRequestReviewModel("3", "grokys", PullRequestReviewState.Approved, dateTimeOffset.AddMinutes(3)), + CreatePullRequestReviewModel("4", "grokys", PullRequestReviewState.Commented, dateTimeOffset.AddMinutes(4))); await target.Load(model); @@ -77,10 +78,11 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane [Test] public async Task ShouldShowLatestCommentedReviewIfNothingElsePresentAsync() { + var dateTimeOffset = DateTimeOffset.Now; var target = CreateTarget(); var model = CreatePullRequestModel( - CreatePullRequestReviewModel("1", "shana", PullRequestReviewState.Commented), - CreatePullRequestReviewModel("2", "shana", PullRequestReviewState.Commented)); + CreatePullRequestReviewModel("1", "shana", PullRequestReviewState.Commented, dateTimeOffset.AddMinutes(1)), + CreatePullRequestReviewModel("2", "shana", PullRequestReviewState.Commented, dateTimeOffset.AddMinutes(2))); await target.Load(model); @@ -107,9 +109,11 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane [Test] public async Task ShouldShowPendingReviewOverApprovedAsync() { + var dateTimeOffset = DateTimeOffset.Now; + var target = CreateTarget(); var model = CreatePullRequestModel( - CreatePullRequestReviewModel("1", "grokys", PullRequestReviewState.Approved), + CreatePullRequestReviewModel("1", "grokys", PullRequestReviewState.Approved, dateTimeOffset.AddMinutes(1)), CreatePullRequestReviewModel("2", "grokys", PullRequestReviewState.Pending)); await target.Load(model); @@ -133,16 +137,34 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane Assert.That(target.Reviews[0].Id, Is.Null); } + [Test] + public async Task ShouldNotShowChangesRequestedAfterDismissed() + { + var dateTimeOffset = DateTimeOffset.Now; + + var target = CreateTarget(); + var model = CreatePullRequestModel( + CreatePullRequestReviewModel("1", "shana", PullRequestReviewState.ChangesRequested, dateTimeOffset.AddMinutes(1)), + CreatePullRequestReviewModel("2", "shana", PullRequestReviewState.Dismissed, dateTimeOffset.AddMinutes(2))); + + await target.Load(model); + + Assert.That(target.Reviews, Has.Count.EqualTo(2)); + Assert.That(target.Reviews[0].User.Login, Is.EqualTo("shana")); + Assert.That(target.Reviews[0].State, Is.EqualTo(PullRequestReviewState.Dismissed)); + Assert.That(target.Reviews[1].User.Login, Is.EqualTo("grokys")); + } + static PullRequestDetailModel CreatePullRequestModel( params PullRequestReviewModel[] reviews) { return PullRequestDetailViewModelTests.CreatePullRequestModel(reviews: reviews); } - static PullRequestReviewModel CreatePullRequestReviewModel( - string id, + static PullRequestReviewModel CreatePullRequestReviewModel(string id, string login, - PullRequestReviewState state) + PullRequestReviewState state, + DateTimeOffset? submittedAt = null) { var account = new ActorModel { @@ -154,6 +176,7 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane Id = id, Author = account, State = state, + SubmittedAt = submittedAt }; } } @@ -581,7 +604,8 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); vm.InitializeAsync(repository, Substitute.For(), "owner", "repo", 1).Wait(); return Tuple.Create(vm, pullRequestService); diff --git a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs index 2f4bc72e2..18c708ead 100644 --- a/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestReviewAuthoringViewModelTests.cs @@ -368,7 +368,10 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane var session = CreateSession(model: model); var closed = false; - var target = CreateTarget(model, session); + var pullRequestService = Substitute.For(); + pullRequestService.ConfirmCancelPendingReview().Returns(true); + + var target = CreateTarget(model, session, pullRequestService); await InitializeAsync(target); target.CloseRequested.Subscribe(_ => closed = true); @@ -397,15 +400,18 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane static PullRequestReviewAuthoringViewModel CreateTarget( PullRequestDetailModel model, - IPullRequestSession session = null) + IPullRequestSession session = null, + IPullRequestService pullRequestService = null) { session = session ?? CreateSession(model: model); return CreateTarget( + pullRequestService: pullRequestService, sessionManager: CreateSessionManager(session)); } static PullRequestReviewAuthoringViewModel CreateTarget( + IPullRequestService pullRequestService = null, IPullRequestEditorService editorService = null, IPullRequestSessionManager sessionManager = null, IPullRequestFilesViewModel files = null) @@ -415,6 +421,7 @@ namespace UnitTests.GitHub.App.ViewModels.GitHubPane files = files ?? Substitute.For(); return new PullRequestReviewAuthoringViewModel( + pullRequestService, editorService, sessionManager, files); diff --git a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs index 6edc59a0a..fd7f7bf03 100644 --- a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs @@ -770,6 +770,19 @@ Line 4"; Assert.That(result1, Is.Not.SameAs(result3)); } + [Test] + public async Task GetSessionReturnsSameSessionForSamePullRequestOwnerCaseMismatch() + { + var target = CreateTarget(); + var newModel = CreatePullRequestModel(NotCurrentBranchPullRequestNumber); + var result1 = await target.GetSession("owner", "repo", 5); + var result2 = await target.GetSession("Owner", "repo", 5); + var result3 = await target.GetSession("owner", "repo", 6); + + Assert.That(result1, Is.SameAs(result2)); + Assert.That(result1, Is.Not.SameAs(result3)); + } + [Test] public async Task SessionCanBeCollected() { diff --git a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs index 123fe9c71..81c5f463e 100644 --- a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs @@ -143,10 +143,11 @@ namespace GitHub.InlineReviews.UnitTests.Services { var currentUser = CreateActor(); var review = CreateReview(author: currentUser, state: PullRequestReviewState.Pending); + var service = Substitute.For(); var pr = CreatePullRequest(review); var target = new PullRequestSession( - Substitute.For(), + service, currentUser, pr, Substitute.For(), @@ -155,6 +156,7 @@ namespace GitHub.InlineReviews.UnitTests.Services Assert.That(target.HasPendingReview, Is.True); + service.CancelPendingReview(null, null).ReturnsForAnyArgs(CreatePullRequest()); await target.CancelReview(); Assert.That(target.HasPendingReview, Is.False); @@ -329,6 +331,8 @@ Line 4"; var service = Substitute.For(); var target = CreateTargetWithPendingReview(service); + service.CancelPendingReview(null, null).ReturnsForAnyArgs(CreatePullRequest()); + await target.CancelReview(); await service.Received(1).CancelPendingReview( @@ -342,6 +346,8 @@ Line 4"; var service = Substitute.For(); var target = CreateTargetWithPendingReview(service); + service.CancelPendingReview(null, null).ReturnsForAnyArgs(CreatePullRequest()); + await target.CancelReview(); Assert.IsEmpty(target.PullRequest.Reviews); diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs index d30410df6..53debb849 100644 --- a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs @@ -42,7 +42,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels CreatePeekSession(), CreateSessionManager(), Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -63,7 +64,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels CreatePeekSession(), CreateSessionManager(), Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -88,7 +90,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels CreatePeekSession(), sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -110,7 +113,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels peekSession, sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); Assert.That(target.Thread, Is.InstanceOf(typeof(NewInlineCommentThreadViewModel))); @@ -151,7 +155,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels peekSession, sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -177,7 +182,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels CreatePeekSession(), sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -209,7 +215,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels peekSession, sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); @@ -245,7 +252,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels peekSession, sessionManager, Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For()); await target.Initialize(); diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs index 3d960e1b3..7704bde7c 100644 --- a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using GitHub.InlineReviews.Services; using GitHub.InlineReviews.ViewModels; using GitHub.Models; using GitHub.Services; @@ -15,6 +16,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels public void CreatesComments() { var target = new InlineCommentThreadViewModel( + Substitute.For(), CreateSession(), CreateComments("Comment 1", "Comment 2")); @@ -42,6 +44,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels public void PlaceholderCommitEnabledWhenCommentHasBody() { var target = new InlineCommentThreadViewModel( + Substitute.For(), CreateSession(), CreateComments("Comment 1")); @@ -56,6 +59,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels { var session = CreateSession(); var target = new InlineCommentThreadViewModel( + Substitute.For(), session, CreateComments("Comment 1", "Comment 2")); diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs index 771ec9cee..c1e838380 100644 --- a/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel; +using GitHub.InlineReviews.Services; using GitHub.InlineReviews.ViewModels; using GitHub.Models; using GitHub.Services; @@ -19,6 +20,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels public void CreatesReplyPlaceholder() { var target = new NewInlineCommentThreadViewModel( + Substitute.For(), CreateSession(), Substitute.For(), 10, @@ -34,6 +36,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels { var file = CreateFile(); var target = new NewInlineCommentThreadViewModel( + Substitute.For(), CreateSession(), file, 10, @@ -58,6 +61,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels { var file = CreateFile(); var target = new NewInlineCommentThreadViewModel( + Substitute.For(), CreateSession(), file, 10, @@ -80,7 +84,9 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels { var session = CreateSession(); var file = CreateFile(); - var target = new NewInlineCommentThreadViewModel(session, file, 10, false); + var target = new NewInlineCommentThreadViewModel( + Substitute.For(), + session, file, 10, false); target.Comments[0].Body = "New Comment"; target.Comments[0].CommitEdit.Execute(null); @@ -110,7 +116,9 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels } }); - var target = new NewInlineCommentThreadViewModel(session, file, 16, true); + var target = new NewInlineCommentThreadViewModel( + Substitute.For(), + session, file, 16, true); target.Comments[0].Body = "New Comment"; target.Comments[0].CommitEdit.Execute(null); diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs index 6762835d8..03bcaab45 100644 --- a/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs +++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Linq; using System.Threading.Tasks; +using GitHub.InlineReviews.Services; using GitHub.InlineReviews.ViewModels; using GitHub.Models; using GitHub.Services; @@ -52,7 +53,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var session = CreateSession(); var thread = CreateThread(); var currentUser = Substitute.For(); - var target = PullRequestReviewCommentViewModel.CreatePlaceholder(session, thread, currentUser); + var commentService = Substitute.For(); + var target = PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, thread, currentUser); Assert.That(target.BeginEdit.CanExecute(new object()), Is.True); } @@ -65,7 +67,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var currentUser = new ActorModel { Login = "CurrentUser" }; var comment = new PullRequestReviewCommentModel { Author = currentUser }; - var target = CreateTarget(session, thread, currentUser, null, comment); + var target = CreateTarget(session, null, thread, currentUser, null, comment); Assert.That(target.BeginEdit.CanExecute(new object()), Is.True); } @@ -79,7 +81,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var otherUser = new ActorModel { Login = "OtherUser" }; var comment = new PullRequestReviewCommentModel { Author = otherUser }; - var target = CreateTarget(session, thread, currentUser, null, comment); + var target = CreateTarget(session, null, thread, currentUser, null, comment); Assert.That(target.BeginEdit.CanExecute(new object()), Is.False); } } @@ -92,7 +94,8 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var session = CreateSession(); var thread = CreateThread(); var currentUser = Substitute.For(); - var target = PullRequestReviewCommentViewModel.CreatePlaceholder(session, thread, currentUser); + var commentService = Substitute.For(); + var target = PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, thread, currentUser); Assert.That(target.Delete.CanExecute(new object()), Is.False); } @@ -105,7 +108,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var currentUser = new ActorModel { Login = "CurrentUser" }; var comment = new PullRequestReviewCommentModel { Author = currentUser }; - var target = CreateTarget(session, thread, currentUser, null, comment); + var target = CreateTarget(session, null, thread, currentUser, null, comment); Assert.That(target.Delete.CanExecute(new object()), Is.True); } @@ -119,7 +122,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels var otherUser = new ActorModel { Login = "OtherUser" }; var comment = new PullRequestReviewCommentModel { Author = otherUser }; - var target = CreateTarget(session, thread, currentUser, null, comment); + var target = CreateTarget(session, null, thread, currentUser, null, comment); Assert.That(target.Delete.CanExecute(new object()), Is.False); } } @@ -206,12 +209,14 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels static PullRequestReviewCommentViewModel CreateTarget( IPullRequestSession session = null, + ICommentService commentService = null, ICommentThreadViewModel thread = null, ActorModel currentUser = null, PullRequestReviewModel review = null, PullRequestReviewCommentModel comment = null) { session = session ?? CreateSession(); + commentService = commentService ?? Substitute.For(); thread = thread ?? CreateThread(); currentUser = currentUser ?? new ActorModel { Login = "CurrentUser" }; comment = comment ?? new PullRequestReviewCommentModel(); @@ -219,6 +224,7 @@ namespace GitHub.InlineReviews.UnitTests.ViewModels return new PullRequestReviewCommentViewModel( session, + commentService, thread, new ActorViewModel(currentUser), review, diff --git a/test/GitHub.TeamFoundation.UnitTests/VSGitExtTests.cs b/test/GitHub.TeamFoundation.UnitTests/VSGitExtTests.cs index 0e26122ef..8a2ebb497 100644 --- a/test/GitHub.TeamFoundation.UnitTests/VSGitExtTests.cs +++ b/test/GitHub.TeamFoundation.UnitTests/VSGitExtTests.cs @@ -13,6 +13,7 @@ using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using Task = System.Threading.Tasks.Task; +using static Microsoft.VisualStudio.VSConstants; public class VSGitExtTests { @@ -20,7 +21,7 @@ public class VSGitExtTests { [TestCase(true, 1)] [TestCase(false, 0)] - public void GetServiceIGitExt_WhenSccProviderContextIsActive(bool isActive, int expectCalls) + public void GetServiceIGitExt_WhenRepositoryOpenIsActive(bool isActive, int expectCalls) { var context = CreateVSUIContext(isActive); var sp = Substitute.For(); @@ -135,7 +136,7 @@ public class VSGitExtTests public class TheActiveRepositoriesProperty : TestBaseClass { [Test] - public void SccProviderContextNotActive_IsEmpty() + public void RepositoryOpenContextNotActive_IsEmpty() { var context = CreateVSUIContext(false); var target = CreateVSGitExt(context); @@ -144,7 +145,7 @@ public class VSGitExtTests } [Test] - public void SccProviderContextIsActive_InitializeWithActiveRepositories() + public void RepositoryOpenIsActive_InitializeWithActiveRepositories() { var repoPath = "repoPath"; var repoFactory = Substitute.For(); @@ -217,8 +218,7 @@ public class VSGitExtTests repoFactory = repoFactory ?? Substitute.For(); joinableTaskContext = joinableTaskContext ?? new JoinableTaskContext(); var factory = Substitute.For(); - var contextGuid = new Guid(Guids.GitSccProviderId); - factory.GetUIContext(contextGuid).Returns(context); + factory.GetUIContext(UICONTEXT.RepositoryOpen_guid).Returns(context); sp.GetServiceAsync(typeof(IGitExt)).Returns(gitExt); var vsGitExt = new VSGitExt(sp, factory, repoFactory, joinableTaskContext); vsGitExt.JoinTillEmpty();