diff --git a/src/GitHub.App/SampleData/SampleViewModels.cs b/src/GitHub.App/SampleData/SampleViewModels.cs index 161a5767e2..c93d26acd2 100644 --- a/src/GitHub.App/SampleData/SampleViewModels.cs +++ b/src/GitHub.App/SampleData/SampleViewModels.cs @@ -388,7 +388,7 @@ public long PrivateReposInPlan } [ExcludeFromCodeCoverage] - public class RepositoryModelDesigner : IRepositoryModel + public class RepositoryModelDesigner : NotificationAwareObject, IRepositoryModel { public RepositoryModelDesigner() : this("repo") { @@ -416,6 +416,8 @@ public void SetIcon(bool isPrivate, bool isFork) public Octicon Icon { get; set; } public IAccount Owner { get; set; } + + public void Refresh() { } } public class RepositoryCloneViewModelDesigner : BaseViewModelDesigner, IRepositoryCloneViewModel diff --git a/src/GitHub.Exports/Extensions/SimpleRepositoryModelExtensions.cs b/src/GitHub.Exports/Extensions/SimpleRepositoryModelExtensions.cs index 08ce5bcf7d..918d747218 100644 --- a/src/GitHub.Exports/Extensions/SimpleRepositoryModelExtensions.cs +++ b/src/GitHub.Exports/Extensions/SimpleRepositoryModelExtensions.cs @@ -15,9 +15,7 @@ public static ISimpleRepositoryModel ToModel(this IGitRepositoryInfo repo) { if (repo == null) return null; - var uri = repo.GetUriFromRepository(); - var name = uri?.NameWithOwner; - return name != null ? new SimpleRepositoryModel(name, uri, repo.RepositoryPath) : null; + return SimpleRepositoryModel.Create(repo.RepositoryPath); } public static bool HasCommits(this ISimpleRepositoryModel repository) diff --git a/src/GitHub.Exports/Models/ISimpleRepositoryModel.cs b/src/GitHub.Exports/Models/ISimpleRepositoryModel.cs index 5b15b3d179..197a09378f 100644 --- a/src/GitHub.Exports/Models/ISimpleRepositoryModel.cs +++ b/src/GitHub.Exports/Models/ISimpleRepositoryModel.cs @@ -1,9 +1,10 @@ using GitHub.Primitives; using GitHub.UI; +using System.ComponentModel; namespace GitHub.Models { - public interface ISimpleRepositoryModel + public interface ISimpleRepositoryModel : INotifyPropertyChanged { string Name { get; } UriString CloneUrl { get; } @@ -11,5 +12,11 @@ public interface ISimpleRepositoryModel Octicon Icon { get; } void SetIcon(bool isPrivate, bool isFork); + + + /// + /// Updates the url information based on the local path + /// + void Refresh(); } } diff --git a/src/GitHub.Exports/Models/SimpleRepositoryModel.cs b/src/GitHub.Exports/Models/SimpleRepositoryModel.cs index 7ca976a1d1..4ec5ddbe83 100644 --- a/src/GitHub.Exports/Models/SimpleRepositoryModel.cs +++ b/src/GitHub.Exports/Models/SimpleRepositoryModel.cs @@ -1,9 +1,12 @@ -using GitHub.Primitives; +using GitHub.Extensions; +using GitHub.Primitives; using GitHub.UI; +using GitHub.VisualStudio; using GitHub.VisualStudio.Helpers; using System; using System.Diagnostics; using System.Globalization; +using System.IO; namespace GitHub.Models { @@ -18,6 +21,32 @@ public SimpleRepositoryModel(string name, UriString cloneUrl, string localPath = Icon = Octicon.repo; } + public SimpleRepositoryModel(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + var dir = new DirectoryInfo(path); + if (!dir.Exists) + throw new ArgumentException("Path does not exist", nameof(path)); + var uri = GitHelpers.GetRepoFromPath(path)?.GetUri(); + var name = uri?.NameWithOwner; + if (name == null) + name = dir.Name; + Name = name; + LocalPath = path; + CloneUrl = uri; + Icon = Octicon.repo; + } + + public static ISimpleRepositoryModel Create(string path) + { + if (path == null) + return null; + if (!Directory.Exists(path)) + return null; + return new SimpleRepositoryModel(path); + } + public void SetIcon(bool isPrivate, bool isFork) { Icon = isPrivate @@ -27,15 +56,30 @@ public void SetIcon(bool isPrivate, bool isFork) : Octicon.repo; } + public void Refresh() + { + if (LocalPath == null) + return; + var uri = GitHelpers.GetRepoFromPath(LocalPath)?.GetUri(); + if (CloneUrl != uri) + CloneUrl = uri; + } + public string Name { get; private set; } - public UriString CloneUrl { get; private set; } + UriString cloneUrl; + public UriString CloneUrl { get { return cloneUrl; } set { cloneUrl = value; this.RaisePropertyChange(); } } public string LocalPath { get; private set; } Octicon icon; public Octicon Icon { get { return icon; } set { icon = value; this.RaisePropertyChange(); } } + /// + /// Note: We don't consider CloneUrl a part of the hash code because it can change during the lifetime + /// of a repository. Equals takes care of any hash collisions because of this + /// + /// public override int GetHashCode() { - return (Name?.GetHashCode() ?? 0) ^ (CloneUrl?.GetHashCode() ?? 0) ^ (LocalPath?.TrimEnd('\\').ToUpperInvariant().GetHashCode() ?? 0); + return (Name?.GetHashCode() ?? 0) ^ (LocalPath?.TrimEnd('\\').ToUpperInvariant().GetHashCode() ?? 0); } public override bool Equals(object obj) diff --git a/src/GitHub.Exports/Services/ITeamExplorerServiceHolder.cs b/src/GitHub.Exports/Services/ITeamExplorerServiceHolder.cs index 7356dc1b7f..eab5345aef 100644 --- a/src/GitHub.Exports/Services/ITeamExplorerServiceHolder.cs +++ b/src/GitHub.Exports/Services/ITeamExplorerServiceHolder.cs @@ -1,6 +1,7 @@ using System; using GitHub.Primitives; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; +using GitHub.Models; namespace GitHub.Services { @@ -28,13 +29,13 @@ public interface ITeamExplorerServiceHolder /// /// A IGitRepositoryInfo representing the currently active repository /// - IGitRepositoryInfo ActiveRepo { get; } + ISimpleRepositoryModel ActiveRepo { get; } /// /// Subscribe to be notified when the active repository is set and Notify is called. /// /// The instance that is interested in being called (or a unique key/object for that instance) /// The handler to call when ActiveRepo is set - void Subscribe(object who, Action handler); + void Subscribe(object who, Action handler); /// /// Unsubscribe from notifications /// @@ -42,11 +43,16 @@ public interface ITeamExplorerServiceHolder void Unsubscribe(object who); IGitAwareItem HomeSection { get; } + + /// + /// Refresh the information on the active repo (in case of remote url changes or other such things) + /// + void Refresh(); } public interface IGitAwareItem { - IGitRepositoryInfo ActiveRepo { get; } + ISimpleRepositoryModel ActiveRepo { get; } /// /// Represents the web URL of the repository on GitHub.com, even if the origin is an SSH address. diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs b/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs index 9a75cb4efa..2bc47af60c 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs +++ b/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs @@ -1,4 +1,5 @@ -using GitHub.Primitives; +using GitHub.Models; +using GitHub.Primitives; using GitHub.Services; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; using NullGuard; @@ -12,9 +13,9 @@ public TeamExplorerGitRepoInfo() ActiveRepo = null; } - IGitRepositoryInfo activeRepo; + ISimpleRepositoryModel activeRepo; [AllowNull] - public IGitRepositoryInfo ActiveRepo + public ISimpleRepositoryModel ActiveRepo { [return: AllowNull] get { return activeRepo; } diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerItemBase.cs b/src/GitHub.VisualStudio/Base/TeamExplorerItemBase.cs index 27893aaf6a..b181f9ea0e 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerItemBase.cs +++ b/src/GitHub.VisualStudio/Base/TeamExplorerItemBase.cs @@ -50,7 +50,7 @@ protected virtual void RepoChanged() var repo = ActiveRepo; if (repo != null) { - var uri = repo.GetUriFromRepository(); + var uri = repo.CloneUrl; if (uri?.RepositoryName != null) { ActiveRepoUri = uri; diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerNavigationItemBase.cs b/src/GitHub.VisualStudio/Base/TeamExplorerNavigationItemBase.cs index dd3abd0aad..e60fc892f0 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerNavigationItemBase.cs +++ b/src/GitHub.VisualStudio/Base/TeamExplorerNavigationItemBase.cs @@ -10,6 +10,7 @@ using Microsoft.VisualStudio.PlatformUI; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; using NullGuard; +using GitHub.Models; namespace GitHub.VisualStudio.Base { @@ -56,7 +57,7 @@ void OnThemeChanged() } } - void UpdateRepo(IGitRepositoryInfo repo) + void UpdateRepo(ISimpleRepositoryModel repo) { ActiveRepo = repo; RepoChanged(); diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerSectionBase.cs b/src/GitHub.VisualStudio/Base/TeamExplorerSectionBase.cs index 7f6b0833d5..6b234ba36c 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerSectionBase.cs +++ b/src/GitHub.VisualStudio/Base/TeamExplorerSectionBase.cs @@ -100,7 +100,7 @@ public virtual void SaveContext(object sender, SectionSaveContextEventArgs e) void SubscribeToRepoChanges() { - holder.Subscribe(this, (IGitRepositoryInfo repo) => + holder.Subscribe(this, (ISimpleRepositoryModel repo) => { ActiveRepo = repo; RepoChanged(); diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerServiceHolder.cs b/src/GitHub.VisualStudio/Base/TeamExplorerServiceHolder.cs index 931494e2d3..046b0171a7 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerServiceHolder.cs +++ b/src/GitHub.VisualStudio/Base/TeamExplorerServiceHolder.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using System.Globalization; +using GitHub.Models; namespace GitHub.VisualStudio.Base { @@ -18,8 +19,8 @@ namespace GitHub.VisualStudio.Base [PartCreationPolicy(CreationPolicy.Shared)] public class TeamExplorerServiceHolder : ITeamExplorerServiceHolder { - readonly Dictionary> activeRepoHandlers = new Dictionary>(); - IGitRepositoryInfo activeRepo; + readonly Dictionary> activeRepoHandlers = new Dictionary>(); + ISimpleRepositoryModel activeRepo; bool activeRepoNotified = false; IServiceProvider serviceProvider; @@ -48,35 +49,50 @@ public IServiceProvider ServiceProvider if (serviceProvider == null) return; GitUIContext = GitUIContext ?? UIContext.FromUIContextGuid(new Guid("11B8E6D7-C08B-4385-B321-321078CDD1F8")); - UIContextChanged(GitUIContext?.IsActive ?? false); + UIContextChanged(GitUIContext?.IsActive ?? false, false); } } [AllowNull] - public IGitRepositoryInfo ActiveRepo + public ISimpleRepositoryModel ActiveRepo { [return: AllowNull] get { return activeRepo; } private set { - if (activeRepo.Compare(value)) + if (activeRepo == value) return; + if (activeRepo != null) + activeRepo.PropertyChanged -= ActiveRepoPropertyChanged; activeRepo = value; + if (activeRepo != null) + activeRepo.PropertyChanged += ActiveRepoPropertyChanged; NotifyActiveRepo(); } } - public void Subscribe(object who, Action handler) + public void Subscribe(object who, Action handler) { + bool notificationsExist; + ISimpleRepositoryModel repo; lock(activeRepoHandlers) { - var repo = ActiveRepo; + repo = ActiveRepo; + notificationsExist = activeRepoNotified; if (!activeRepoHandlers.ContainsKey(who)) activeRepoHandlers.Add(who, handler); else activeRepoHandlers[who] = handler; - if (activeRepoNotified) - handler(repo); } + + // the repo url might have changed and we don't get notifications + // for that, so this is a good place to refresh it in case that happened + repo?.Refresh(); + + // if the active repo hasn't changed and there's notifications queued up, + // notify the subscriber. If the repo has changed, the set above will trigger + // notifications so we don't have to do it here. + if (repo == ActiveRepo && notificationsExist) + handler(repo); } public void Unsubscribe(object who) @@ -100,6 +116,12 @@ public void ClearServiceProvider(IServiceProvider provider) ServiceProvider = null; } + public void Refresh() + { + GitUIContext = GitUIContext ?? UIContext.FromUIContextGuid(new Guid("11B8E6D7-C08B-4385-B321-321078CDD1F8")); + UIContextChanged(GitUIContext?.IsActive ?? false, true); + } + void NotifyActiveRepo() { lock (activeRepoHandlers) @@ -113,10 +135,10 @@ void NotifyActiveRepo() void UIContextChanged(object sender, UIContextChangedEventArgs e) { ActiveRepo = null; - UIContextChanged(e.Activated); + UIContextChanged(e.Activated, false); } - async void UIContextChanged(bool active) + async void UIContextChanged(bool active, bool refresh) { Debug.Assert(ServiceProvider != null, "UIContextChanged called before service provider is set"); if (ServiceProvider == null) @@ -125,7 +147,7 @@ async void UIContextChanged(bool active) if (active) { GitService = GitService ?? ServiceProvider.GetService(); - if (ActiveRepo == null) + if (ActiveRepo == null || refresh) ActiveRepo = await System.Threading.Tasks.Task.Run(() => { var repos = GitService?.ActiveRepositories; @@ -140,7 +162,7 @@ async void UIContextChanged(bool active) if (repos == null) VsOutputLogger.WriteLine(string.Format(CultureInfo.CurrentCulture, "Error 2002: ActiveRepositories is null. GitService: '{0}'", GitService)); } - return repos?.FirstOrDefault(); + return repos?.FirstOrDefault()?.ToModel(); }); } else @@ -156,11 +178,16 @@ void CheckAndUpdate(object sender, System.ComponentModel.PropertyChangedEventArg if (service == null) return; - var repo = service.ActiveRepositories.FirstOrDefault(); - // this comparison is safe, the extension method supports null instances - if (!repo.Compare(ActiveRepo)) + var repo = service.ActiveRepositories.FirstOrDefault()?.ToModel(); + if (repo != ActiveRepo) // so annoying that this is on the wrong thread - syncContext.Post(r => ActiveRepo = r as IGitRepositoryInfo, repo); + syncContext.Post(r => ActiveRepo = r as ISimpleRepositoryModel, repo); + } + + void ActiveRepoPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "CloneUrl") + ActiveRepo = sender as ISimpleRepositoryModel; } public IGitAwareItem HomeSection diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index 00d7bd1dfc..d482c453fd 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -8,7 +8,7 @@ using GitHub.UI; using GitHub.Services; using GitHub.Models; -using GitHub.VisualStudio.UI; +using GitHub.Extensions; namespace GitHub.VisualStudio { diff --git a/src/GitHub.VisualStudio/Services/ConnectionManager.cs b/src/GitHub.VisualStudio/Services/ConnectionManager.cs index 9cbfd6598e..41cc18c7d2 100644 --- a/src/GitHub.VisualStudio/Services/ConnectionManager.cs +++ b/src/GitHub.VisualStudio/Services/ConnectionManager.cs @@ -129,7 +129,7 @@ public void RequestLogout(IConnection connection) public void RefreshRepositories() { var list = vsServices.GetKnownRepositories(); - list.GroupBy(r => Connections.FirstOrDefault(c => c.HostAddress.Equals(HostAddress.Create(r.CloneUrl)))) + list.GroupBy(r => Connections.FirstOrDefault(c => r.CloneUrl != null && c.HostAddress.Equals(HostAddress.Create(r.CloneUrl)))) .Where(g => g.Key != null) .ForEach(g => { diff --git a/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubConnectSection.cs b/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubConnectSection.cs index 59ba33e00a..95982b14df 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubConnectSection.cs +++ b/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubConnectSection.cs @@ -209,7 +209,7 @@ async void UpdateRepositoryList(object sender, NotifyCollectionChangedEventArgs .Cast() .ForEach(async r => { - if (String.Equals(Holder.ActiveRepo?.RepositoryPath, r.LocalPath, StringComparison.CurrentCultureIgnoreCase)) + if (Equals(Holder.ActiveRepo, r)) SelectedRepository = r; var repo = await ApiFactory.Create(r.CloneUrl).GetRepository(); r.SetIcon(repo.Private, repo.Fork); @@ -298,7 +298,7 @@ public void Login() public bool OpenRepository() { - var old = Repositories.FirstOrDefault(x => String.Equals(Holder.ActiveRepo?.RepositoryPath, x.LocalPath, StringComparison.CurrentCultureIgnoreCase)); + var old = Repositories.FirstOrDefault(x => x.Equals(Holder.ActiveRepo)); // open the solution selection dialog when the user wants to switch to a different repo // since there's no other way of changing the source control context in VS if (!Equals(SelectedRepository, old)) diff --git a/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubInvitationSection.cs b/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubInvitationSection.cs index 366029b945..2b49accd51 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubInvitationSection.cs +++ b/src/GitHub.VisualStudio/TeamExplorer/Connect/GitHubInvitationSection.cs @@ -3,7 +3,7 @@ using GitHub.Services; using GitHub.UI; using GitHub.VisualStudio.Base; -using GitHub.VisualStudio.UI; +using GitHub.Extensions; using Microsoft.TeamFoundation.Controls; using Microsoft.VisualStudio.PlatformUI; using System;