diff --git a/SourceGit/Git/Remote.cs b/SourceGit/Git/Remote.cs index f7f4ccb2..ed32e810 100644 --- a/SourceGit/Git/Remote.cs +++ b/SourceGit/Git/Remote.cs @@ -1,97 +1,97 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Git remote - /// - public class Remote { - private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); - - /// - /// Name of this remote - /// - public string Name { get; set; } - - /// - /// URL - /// - public string URL { get; set; } - - /// - /// Parsing remote - /// - /// Repository - /// - public static List Load(Repository repo) { - var remotes = new List(); - var added = new List(); - - repo.RunCommand("remote -v", data => { - var match = FORMAT.Match(data); - if (!match.Success) return; - - var remote = new Remote() { - Name = match.Groups[1].Value, - URL = match.Groups[2].Value, - }; - - if (added.Contains(remote.Name)) return; - - added.Add(remote.Name); - remotes.Add(remote); - }); - - return remotes; - } - - /// - /// Add new remote - /// - /// - /// - /// - public static void Add(Repository repo, string name, string url) { - var errs = repo.RunCommand($"remote add {name} {url}", null); - if (errs != null) { - App.RaiseError(errs); - } else { - repo.Fetch(null, true, null); - } - } - - /// - /// Delete remote. - /// - /// - /// - public static void Delete(Repository repo, string remote) { - var errs = repo.RunCommand($"remote remove {remote}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Edit remote. - /// - /// - /// - /// - public void Edit(Repository repo, string name, string url) { - string errs = null; - - if (name != Name) { - errs = repo.RunCommand($"remote rename {Name} {name}", null); - if (errs != null) { - App.RaiseError(errs); - return; - } - } - - if (url != URL) { - errs = repo.RunCommand($"remote set-url {name} {url}", null); - if (errs != null) App.RaiseError(errs); - } - } - } -} +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git remote + /// + public class Remote { + private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); + + /// + /// Name of this remote + /// + public string Name { get; set; } + + /// + /// URL + /// + public string URL { get; set; } + + /// + /// Parsing remote + /// + /// Repository + /// + public static List Load(Repository repo) { + var remotes = new List(); + var added = new List(); + + repo.RunCommand("remote -v", data => { + var match = FORMAT.Match(data); + if (!match.Success) return; + + var remote = new Remote() { + Name = match.Groups[1].Value, + URL = match.Groups[2].Value, + }; + + if (added.Contains(remote.Name)) return; + + added.Add(remote.Name); + remotes.Add(remote); + }); + + return remotes; + } + + /// + /// Add new remote + /// + /// + /// + /// + public static void Add(Repository repo, string name, string url) { + var errs = repo.RunCommand($"remote add {name} {url}", null); + if (errs != null) { + App.RaiseError(errs); + } else { + repo.Fetch(new Remote() { Name = name }, true, null); + } + } + + /// + /// Delete remote. + /// + /// + /// + public static void Delete(Repository repo, string remote) { + var errs = repo.RunCommand($"remote remove {remote}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Edit remote. + /// + /// + /// + /// + public void Edit(Repository repo, string name, string url) { + string errs = null; + + if (name != Name) { + errs = repo.RunCommand($"remote rename {Name} {name}", null); + if (errs != null) { + App.RaiseError(errs); + return; + } + } + + if (url != URL) { + errs = repo.RunCommand($"remote set-url {name} {url}", null); + if (errs != null) App.RaiseError(errs); + } + } + } +} diff --git a/SourceGit/UI/Dashboard.xaml.cs b/SourceGit/UI/Dashboard.xaml.cs index 45804eda..7833c681 100644 --- a/SourceGit/UI/Dashboard.xaml.cs +++ b/SourceGit/UI/Dashboard.xaml.cs @@ -1,984 +1,996 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Threading; - -namespace SourceGit.UI { - - /// - /// Branch node in tree. - /// - public class BranchNode { - public string Name { get; set; } - public Git.Branch Branch { get; set; } - public bool IsExpanded { get; set; } - public bool IsCurrent => Branch != null ? Branch.IsCurrent : false; - public bool IsFiltered => Branch != null ? Branch.IsFiltered : false; - public string Track => Branch != null ? Branch.UpstreamTrack : ""; - public Visibility FilterVisibility => Branch == null ? Visibility.Collapsed : Visibility.Visible; - public Visibility TrackVisibility => (Branch != null && !Branch.IsSameWithUpstream) ? Visibility.Visible : Visibility.Collapsed; - public List Children { get; set; } - } - - /// - /// Remote node in tree. - /// - public class RemoteNode { - public string Name { get; set; } - public bool IsExpanded { get; set; } - public List Children { get; set; } - } - - /// - /// Dashboard for opened repository. - /// - public partial class Dashboard : UserControl { - private Git.Repository repo = null; - private List cachedLocalBranches = new List(); - private List cachedRemotes = new List(); - private string abortCommand = null; - - /// - /// Constructor. - /// - /// Opened repository. - public Dashboard(Git.Repository opened) { - opened.OnWorkingCopyChanged = UpdateLocalChanges; - opened.OnTagChanged = UpdateTags; - opened.OnStashChanged = UpdateStashes; - opened.OnBranchChanged = () => UpdateBranches(false); - opened.OnCommitsChanged = UpdateHistories; - opened.OnNavigateCommit = commit => { - Dispatcher.Invoke(() => { - workspace.SelectedItem = historiesSwitch; - histories.Navigate(commit); - }); - }; - - InitializeComponent(); - - repo = opened; - repoName.Content = repo.Name; - histories.Repo = opened; - commits.Repo = opened; - - UpdateBranches(); - UpdateHistories(); - UpdateLocalChanges(); - UpdateStashes(); - UpdateTags(); - } - - #region DATA_UPDATE - private void UpdateHistories() { - Dispatcher.Invoke(() => { - histories.SetLoadingEnabled(true); - }); - - Task.Run(() => { - var args = "-5000 "; - if (repo.LogFilters.Count > 0) { - args = args + string.Join(" ", repo.LogFilters); - } else { - args = args + "--branches --remotes --tags"; - } - - var commits = repo.Commits(args); - histories.SetCommits(commits); - }); - } - - private void UpdateLocalChanges() { - Task.Run(() => { - var changes = repo.LocalChanges(); - var conflicts = commits.SetData(changes); - - Dispatcher.Invoke(() => { - localChangesBadge.Visibility = changes.Count == 0 ? Visibility.Collapsed : Visibility.Visible; - localChangesCount.Content = changes.Count; - btnContinue.Visibility = conflicts ? Visibility.Collapsed : Visibility.Visible; - DetectMergeState(); - }); - }); - } - - private void UpdateStashes() { - Task.Run(() => { - var data = repo.Stashes(); - Dispatcher.Invoke(() => { - stashBadge.Visibility = data.Count > 0 ? Visibility.Visible : Visibility.Collapsed; - stashCount.Content = data.Count; - stashes.SetData(repo, data); - }); - }); - } - - private void BackupBranchNodeExpandState(Dictionary states, List nodes, string prefix) { - foreach (var node in nodes) { - var path = prefix + "/" + node.Name; - states.Add(path, node.IsExpanded); - BackupBranchNodeExpandState(states, node.Children, path); - } - } - - private void MakeBranchNode(Git.Branch branch, List collection, Dictionary folders, Dictionary expandStates, string prefix) { - var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (!branch.IsLocal) { - if (subs.Length < 2) return; - subs = subs.Skip(1).ToArray(); - } - - branch.IsFiltered = repo.LogFilters.Contains(branch.FullName); - - if (subs.Length == 1) { - var node = new BranchNode() { - Name = subs[0], - Branch = branch, - Children = new List(), - }; - collection.Add(node); - } else { - BranchNode lastFolder = null; - string path = prefix; - for (int i = 0; i < subs.Length - 1; i++) { - path = path + "/" + subs[i]; - if (folders.ContainsKey(path)) { - lastFolder = folders[path]; - } else if (lastFolder == null) { - lastFolder = new BranchNode() { - Name = subs[i], - IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false, - Children = new List(), - }; - collection.Add(lastFolder); - folders.Add(path, lastFolder); - } else { - var folder = new BranchNode() { - Name = subs[i], - IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false, - Children = new List(), - }; - lastFolder.Children.Add(folder); - folders.Add(path, folder); - lastFolder = folder; - } - } - - BranchNode node = new BranchNode(); - node.Name = subs[subs.Length - 1]; - node.Branch = branch; - node.Children = new List(); - lastFolder.Children.Add(node); - } - } - - private void SortBranchNodes(List collection) { - collection.Sort((l, r) => { - if (l.Branch != null) { - return r.Branch != null ? l.Branch.Name.CompareTo(r.Branch.Name) : -1; - } else { - return r.Branch == null ? l.Name.CompareTo(r.Name) : 1; - } - }); - - foreach (var sub in collection) { - if (sub.Children.Count > 0) SortBranchNodes(sub.Children); - } - } - - private void UpdateBranches(bool force = true) { - Task.Run(() => { - var branches = repo.Branches(force); - var localBranchNodes = new List(); - var remoteNodes = new List(); - var remoteMap = new Dictionary(); - var folders = new Dictionary(); - var states = new Dictionary(); - - BackupBranchNodeExpandState(states, cachedLocalBranches, "locals"); - foreach (var r in cachedRemotes) { - var prefix = $"remotes/{r.Name}"; - states.Add(prefix, r.IsExpanded); - BackupBranchNodeExpandState(states, r.Children, prefix); - } - - foreach (var b in branches) { - if (b.IsLocal) { - MakeBranchNode(b, localBranchNodes, folders, states, "locals"); - } else { - RemoteNode remote = null; - - if (!remoteMap.ContainsKey(b.Remote)) { - var key = "remotes/" + b.Remote; - remote = new RemoteNode() { - Name = b.Remote, - IsExpanded = states.ContainsKey(key) ? states[key] : false, - Children = new List(), - }; - remoteNodes.Add(remote); - remoteMap.Add(b.Remote, remote); - } else { - remote = remoteMap[b.Remote]; - } - - MakeBranchNode(b, remote.Children, folders, states, "remotes"); - } - } - - SortBranchNodes(localBranchNodes); - foreach (var r in remoteNodes) SortBranchNodes(r.Children); - - cachedLocalBranches = localBranchNodes; - cachedRemotes = remoteNodes; - - Dispatcher.Invoke(() => { - localBranchTree.ItemsSource = localBranchNodes; - remoteBranchTree.ItemsSource = remoteNodes; - }); - }); - } - - private void UpdateTags() { - Task.Run(() => { - var tags = repo.Tags(true); - foreach (var t in tags) t.IsFiltered = repo.LogFilters.Contains(t.Name); - - Dispatcher.Invoke(() => { - tagCount.Content = $"TAGS ({tags.Count})"; - tagList.ItemsSource = tags; - }); - }); - } - - private void Cleanup(object sender, RoutedEventArgs e) { - localBranchTree.ItemsSource = null; - remoteBranchTree.ItemsSource = null; - tagList.ItemsSource = null; - cachedLocalBranches.Clear(); - cachedRemotes.Clear(); - } - #endregion - - #region TOOLBAR - private void Close(object sender, RoutedEventArgs e) { - if (PopupManager.IsLocked()) return; - PopupManager.Close(); - - cachedLocalBranches.Clear(); - cachedRemotes.Clear(); - - repo.Close(); - } - - private void OpenFetch(object sender, RoutedEventArgs e) { - Fetch.Show(repo); - } - - private void OpenPull(object sender, RoutedEventArgs e) { - Pull.Show(repo); - } - - private void OpenPush(object sender, RoutedEventArgs e) { - Push.Show(repo); - } - - private void OpenStash(object sender, RoutedEventArgs e) { - Stash.Show(repo, new List()); - } - - private void OpenApply(object sender, RoutedEventArgs e) { - Apply.Show(repo); - } - - private void OpenSearch(object sender, RoutedEventArgs e) { - workspace.SelectedItem = historiesSwitch; - if (histories.searchBar.Margin.Top == 0) { - histories.HideSearchBar(); - } else { - histories.OpenSearchBar(); - } - } - - private void OpenExplorer(object sender, RoutedEventArgs e) { - Process.Start(repo.Path); - } - - private void OpenTerminal(object sender, RoutedEventArgs e) { - var bash = Path.Combine(App.Preference.GitExecutable, "..", "bash.exe"); - if (!File.Exists(bash)) { - App.RaiseError("Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe"); - return; - } - - var start = new ProcessStartInfo(); - start.WorkingDirectory = repo.Path; - start.FileName = bash; - Process.Start(start); - } - #endregion - - #region HOT_KEYS - public void OpenSearchBar(object sender, ExecutedRoutedEventArgs e) { - workspace.SelectedItem = historiesSwitch; - histories.OpenSearchBar(); - } - - public void HideSearchBar(object sender, ExecutedRoutedEventArgs e) { - if (histories.Visibility == Visibility.Visible) { - histories.HideSearchBar(); - } - } - #endregion - - #region MERGE_ABORTS - public void DetectMergeState() { - var gitDir = Path.Combine(repo.Path, ".git"); - var cherryPickMerge = Path.Combine(gitDir, "CHERRY_PICK_HEAD"); - var rebaseMerge = Path.Combine(gitDir, "REBASE_HEAD"); - var revertMerge = Path.Combine(gitDir, "REVERT_HEAD"); - var otherMerge = Path.Combine(gitDir, "MERGE_HEAD"); - - if (File.Exists(cherryPickMerge)) { - abortCommand = "cherry-pick"; - txtMergeProcessing.Content = "Cherry-Pick merge request detected! Press 'Abort' to restore original HEAD"; - } else if (File.Exists(rebaseMerge)) { - abortCommand = "rebase"; - txtMergeProcessing.Content = "Rebase merge request detected! Press 'Abort' to restore original HEAD"; - } else if (File.Exists(revertMerge)) { - abortCommand = "revert"; - txtMergeProcessing.Content = "Revert merge request detected! Press 'Abort' to restore original HEAD"; - } else if (File.Exists(otherMerge)) { - abortCommand = "merge"; - txtMergeProcessing.Content = "Merge request detected! Press 'Abort' to restore original HEAD"; - } else { - abortCommand = null; - } - - if (abortCommand != null) { - abortPanel.Visibility = Visibility.Visible; - if (commits.Visibility == Visibility.Visible) { - btnResolve.Visibility = Visibility.Collapsed; - } else { - btnResolve.Visibility = Visibility.Visible; - } - - commits.LoadMergeMessage(); - } else { - abortPanel.Visibility = Visibility.Collapsed; - } - } - - private void Resolve(object sender, RoutedEventArgs e) { - workspace.SelectedItem = workingCopySwitch; - } - - private async void Continue(object sender, RoutedEventArgs e) { - if (abortCommand == null) return; - - await Task.Run(() => { - repo.SetWatcherEnabled(false); - var errs = repo.RunCommand($"-c core.editor=true {abortCommand} --continue", null); - repo.AssertCommand(errs); - }); - - commits.ClearMessage(); - } - - private async void Abort(object sender, RoutedEventArgs e) { - if (abortCommand == null) return; - - await Task.Run(() => { - repo.SetWatcherEnabled(false); - var errs = repo.RunCommand($"{abortCommand} --abort", null); - repo.AssertCommand(errs); - }); - - commits.ClearMessage(); - } - #endregion - - #region WORKSPACE - private void SwitchWorkingCopy(object sender, RoutedEventArgs e) { - if (commits == null || histories == null || stashes == null) return; - - commits.Visibility = Visibility.Visible; - histories.Visibility = Visibility.Collapsed; - stashes.Visibility = Visibility.Collapsed; - - if (abortPanel.Visibility == Visibility.Visible) { - btnResolve.Visibility = Visibility.Collapsed; - } - } - - private void SwitchHistories(object sender, RoutedEventArgs e) { - if (commits == null || histories == null || stashes == null) return; - - commits.Visibility = Visibility.Collapsed; - histories.Visibility = Visibility.Visible; - stashes.Visibility = Visibility.Collapsed; - - if (abortPanel.Visibility == Visibility.Visible) { - btnResolve.Visibility = Visibility.Visible; - } - } - - private void SwitchStashes(object sender, RoutedEventArgs e) { - if (commits == null || histories == null || stashes == null) return; - - commits.Visibility = Visibility.Collapsed; - histories.Visibility = Visibility.Collapsed; - stashes.Visibility = Visibility.Visible; - - if (abortPanel.Visibility == Visibility.Visible) { - btnResolve.Visibility = Visibility.Visible; - } - } - #endregion - - #region LOCAL_BRANCHES - private void OpenNewBranch(object sender, RoutedEventArgs e) { - CreateBranch.Show(repo); - } - - private void OpenGitFlow(object sender, RoutedEventArgs ev) { - var button = sender as Button; - if (button.ContextMenu == null) { - button.ContextMenu = new ContextMenu(); - button.ContextMenu.PlacementTarget = button; - button.ContextMenu.Placement = PlacementMode.Bottom; - button.ContextMenu.StaysOpen = false; - button.ContextMenu.Focusable = true; - } else { - button.ContextMenu.Items.Clear(); - } - - if (repo.IsGitFlowEnabled()) { - var startFeature = new MenuItem(); - startFeature.Header = "Start Feature ..."; - startFeature.Click += (o, e) => { - GitFlowStartBranch.Show(repo, Git.Branch.Type.Feature); - e.Handled = true; - }; - - var startRelease = new MenuItem(); - startRelease.Header = "Start Release ..."; - startRelease.Click += (o, e) => { - GitFlowStartBranch.Show(repo, Git.Branch.Type.Release); - e.Handled = true; - }; - - var startHotfix = new MenuItem(); - startHotfix.Header = "Start Hotfix ..."; - startHotfix.Click += (o, e) => { - GitFlowStartBranch.Show(repo, Git.Branch.Type.Hotfix); - e.Handled = true; - }; - - button.ContextMenu.Items.Add(startFeature); - button.ContextMenu.Items.Add(startRelease); - button.ContextMenu.Items.Add(startHotfix); - } else { - var init = new MenuItem(); - init.Header = "Initialize Git-Flow"; - init.Click += (o, e) => { - GitFlowSetup.Show(repo); - e.Handled = true; - }; - button.ContextMenu.Items.Add(init); - } - - button.ContextMenu.IsOpen = true; - ev.Handled = true; - } - - private void LocalBranchSelected(object sender, RoutedPropertyChangedEventArgs e) { - var node = e.NewValue as BranchNode; - if (node == null || node.Branch == null) return; - repo.OnNavigateCommit?.Invoke(node.Branch.Head); - } - - private void LocalBranchMouseDoubleClick(object sender, MouseButtonEventArgs e) { - var node = (sender as TreeViewItem).DataContext as BranchNode; - if (node == null || node.Branch == null) return; - Task.Run(() => repo.Checkout(node.Branch.Name)); - } - - private void LocalBranchContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var node = (sender as TreeViewItem).DataContext as BranchNode; - if (node == null || node.Branch == null) return; - - var menu = new ContextMenu(); - var branch = node.Branch; - - var push = new MenuItem(); - push.Header = $"Push '{branch.Name}'"; - push.Click += (o, e) => { - Push.Show(repo, branch); - e.Handled = true; - }; - - if (branch.IsCurrent) { - var discard = new MenuItem(); - discard.Header = "Discard all changes"; - discard.Click += (o, e) => { - Discard.Show(repo, null); - e.Handled = true; - }; - menu.Items.Add(discard); - menu.Items.Add(new Separator()); - - if (!string.IsNullOrEmpty(branch.Upstream)) { - var upstream = branch.Upstream.Substring(13); - var fastForward = new MenuItem(); - fastForward.Header = $"Fast-Forward to '{upstream}'"; - fastForward.Click += (o, e) => { - Merge.StartDirectly(repo, upstream, branch.Name); - e.Handled = true; - }; - - var pull = new MenuItem(); - pull.Header = $"Pull '{upstream}'"; - pull.Click += (o, e) => { - Pull.Show(repo); - e.Handled = true; - }; - - menu.Items.Add(fastForward); - menu.Items.Add(pull); - } - - menu.Items.Add(push); - } else { - var current = repo.CurrentBranch(); - - var checkout = new MenuItem(); - checkout.Header = $"Checkout {branch.Name}"; - checkout.Click += (o, e) => { - Task.Run(() => repo.Checkout(node.Branch.Name)); - e.Handled = true; - }; - menu.Items.Add(checkout); - menu.Items.Add(new Separator()); - menu.Items.Add(push); - - var merge = new MenuItem(); - merge.Header = $"Merge '{branch.Name}' into '{current.Name}'"; - merge.Click += (o, e) => { - Merge.Show(repo, branch.Name, current.Name); - e.Handled = true; - }; - menu.Items.Add(merge); - - var rebase = new MenuItem(); - rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'"; - rebase.Click += (o, e) => { - Rebase.Show(repo, branch); - e.Handled = true; - }; - menu.Items.Add(rebase); - } - - if (branch.Kind != Git.Branch.Type.Normal) { - menu.Items.Add(new Separator()); - - var icon = new System.Windows.Shapes.Path(); - icon.Style = FindResource("Style.Icon") as Style; - icon.Data = FindResource("Icon.Flow") as Geometry; - icon.Width = 10; - - var finish = new MenuItem(); - finish.Header = $"Git Flow - Finish '{branch.Name}'"; - finish.Icon = icon; - finish.Click += (o, e) => { - GitFlowFinishBranch.Show(repo, branch); - e.Handled = true; - }; - - menu.Items.Add(finish); - } - - var rename = new MenuItem(); - rename.Header = $"Rename '{branch.Name}'"; - rename.Click += (o, e) => { - RenameBranch.Show(repo, branch); - e.Handled = true; - }; - menu.Items.Add(new Separator()); - menu.Items.Add(rename); - - var delete = new MenuItem(); - delete.Header = $"Delete '{branch.Name}'"; - delete.IsEnabled = !branch.IsCurrent; - delete.Click += (o, e) => { - DeleteBranch.Show(repo, branch); - e.Handled = true; - }; - menu.Items.Add(delete); - menu.Items.Add(new Separator()); - - var createBranch = new MenuItem(); - createBranch.Header = "Create Branch"; - createBranch.Click += (o, e) => { - CreateBranch.Show(repo, branch); - e.Handled = true; - }; - menu.Items.Add(createBranch); - - var createTag = new MenuItem(); - createTag.Header = "Create Tag"; - createTag.Click += (o, e) => { - CreateTag.Show(repo, branch); - e.Handled = true; - }; - menu.Items.Add(createTag); - menu.Items.Add(new Separator()); - - var copy = new MenuItem(); - copy.Header = "Copy Branch Name"; - copy.Click += (o, e) => { - Clipboard.SetText(branch.Name); - e.Handled = true; - }; - menu.Items.Add(copy); - - menu.IsOpen = true; - ev.Handled = true; - } - #endregion - - #region REMOTE_BRANCHES - private void OpenRemote(object sender, RoutedEventArgs e) { - Remote.Show(repo); - } - - private void OpenRemoteContextMenu(RemoteNode node) { - var fetch = new MenuItem(); - fetch.Header = $"Fetch '{node.Name}'"; - fetch.Click += (o, e) => { - Fetch.Show(repo, node.Name); - e.Handled = true; - }; - - var edit = new MenuItem(); - edit.Header = $"Edit '{node.Name}'"; - edit.Click += (o, e) => { - var remotes = repo.Remotes(); - var found = remotes.Find(r => r.Name == node.Name); - if (found != null) Remote.Show(repo, found); - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = $"Delete '{node.Name}'"; - delete.Click += (o, e) => { - DeleteRemote.Show(repo, node.Name); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = "Copy Remote URL"; - copy.Click += (o, e) => { - var remotes = repo.Remotes(); - var found = remotes.Find(r => r.Name == node.Name); - if (found != null) Clipboard.SetText(found.URL); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(fetch); - menu.Items.Add(new Separator()); - menu.Items.Add(edit); - menu.Items.Add(delete); - menu.Items.Add(new Separator()); - menu.Items.Add(copy); - menu.IsOpen = true; - } - - private void OpenRemoteBranchContextMenu(BranchNode node) { - var branch = node.Branch; - var current = repo.CurrentBranch(); - if (current == null) return; - - var checkout = new MenuItem(); - checkout.Header = $"Checkout '{branch.Name}'"; - checkout.Click += (o, e) => { - var branches = repo.Branches(); - var tracked = null as Git.Branch; - var upstream = $"refs/remotes/{branch.Name}"; - - foreach (var b in branches) { - if (b.IsLocal && b.Upstream == upstream) { - tracked = b; - break; - } - } - - if (tracked == null) { - CreateBranch.Show(repo, branch); - } else if (!tracked.IsCurrent) { - Task.Run(() => repo.Checkout(tracked.Name)); - } - - e.Handled = true; - }; - - var pull = new MenuItem(); - pull.Header = $"Pull '{branch.Name}' into '{current.Name}'"; - pull.Click += (o, e) => { - Pull.Show(repo, branch.Name); - e.Handled = true; - }; - - var merge = new MenuItem(); - merge.Header = $"Merge '{branch.Name}' into '{current.Name}'"; - merge.Click += (o, e) => { - Merge.Show(repo, branch.Name, current.Name); - e.Handled = true; - }; - - var rebase = new MenuItem(); - rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'"; - rebase.Click += (o, e) => { - Rebase.Show(repo, branch); - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = $"Delete '{branch.Name}'"; - delete.Click += (o, e) => { - DeleteBranch.Show(repo, branch); - e.Handled = true; - }; - - var createBranch = new MenuItem(); - createBranch.Header = "Create New Branch"; - createBranch.Click += (o, e) => { - CreateBranch.Show(repo, branch); - e.Handled = true; - }; - - var createTag = new MenuItem(); - createTag.Header = "Create New Tag"; - createTag.Click += (o, e) => { - CreateTag.Show(repo, branch); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = "Copy Branch Name"; - copy.Click += (o, e) => { - Clipboard.SetText(branch.Name); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(checkout); - menu.Items.Add(new Separator()); - menu.Items.Add(pull); - menu.Items.Add(merge); - menu.Items.Add(rebase); - menu.Items.Add(new Separator()); - menu.Items.Add(delete); - menu.Items.Add(new Separator()); - menu.Items.Add(createBranch); - menu.Items.Add(createTag); - menu.Items.Add(new Separator()); - menu.Items.Add(copy); - menu.IsOpen = true; - } - - private void RemoteBranchSelected(object sender, RoutedPropertyChangedEventArgs e) { - var node = e.NewValue as BranchNode; - if (node == null || node.Branch == null) return; - repo.OnNavigateCommit?.Invoke(node.Branch.Head); - } - - private void RemoteContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var remoteNode = (sender as TreeViewItem).DataContext as RemoteNode; - if (remoteNode != null) { - OpenRemoteContextMenu(remoteNode); - ev.Handled = true; - return; - } - - var branchNode = (sender as TreeViewItem).DataContext as BranchNode; - if (branchNode != null && branchNode.Branch != null) { - OpenRemoteBranchContextMenu(branchNode); - ev.Handled = true; - return; - } - } - #endregion - - #region TAGS - private void OpenNewTag(object sender, RoutedEventArgs e) { - CreateTag.Show(repo); - } - - private void TagLostFocus(object sender, RoutedEventArgs e) { - (sender as ListView).UnselectAll(); - } - - private void TagSelectionChanged(object sender, SelectionChangedEventArgs e) { - if (e.AddedItems.Count == 1) { - var item = e.AddedItems[0] as Git.Tag; - repo.OnNavigateCommit?.Invoke(item.SHA); - } - } - - private void TagContextMenuOpening(object sender, ContextMenuEventArgs e) { - var tag = (sender as ListView).SelectedItem as Git.Tag; - if (tag == null) return; - - var createBranch = new MenuItem(); - createBranch.Header = "Create New Branch"; - createBranch.Click += (o, ev) => { - CreateBranch.Show(repo, tag); - ev.Handled = true; - }; - - var pushTag = new MenuItem(); - pushTag.Header = $"Push '{tag.Name}'"; - pushTag.Click += (o, ev) => { - PushTag.Show(repo, tag); - ev.Handled = true; - }; - - var deleteTag = new MenuItem(); - deleteTag.Header = $"Delete '{tag.Name}'"; - deleteTag.Click += (o, ev) => { - DeleteTag.Show(repo, tag); - ev.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = "Copy Name"; - copy.Click += (o, ev) => { - Clipboard.SetText(tag.Name); - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(createBranch); - menu.Items.Add(new Separator()); - menu.Items.Add(pushTag); - menu.Items.Add(deleteTag); - menu.Items.Add(new Separator()); - menu.Items.Add(copy); - menu.IsOpen = true; - - e.Handled = true; - } - #endregion - - #region TREES - private TreeViewItem FindTreeViewItem(ItemsControl item, BranchNode node) { - if (item == null) return null; - - var data = item.DataContext as BranchNode; - if (data == node) return item as TreeViewItem; - - for (int i = 0; i < item.Items.Count; i++) { - var childContainer = item.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl; - var child = FindTreeViewItem(childContainer, node); - if (child != null) return child; - } - - return null; - } - - private void TreeLostFocus(object sender, RoutedEventArgs e) { - var tree = sender as TreeView; - var remote = tree.SelectedItem as RemoteNode; - if (remote != null) { - var remoteItem = tree.ItemContainerGenerator.ContainerFromItem(remote) as TreeViewItem; - if (remoteItem != null) remoteItem.IsSelected = false; - return; - } - - var node = tree.SelectedItem as BranchNode; - if (node == null) return; - - var item = FindTreeViewItem(tree, node); - if (item != null) item.IsSelected = false; - } - - private ScrollViewer GetScrollViewer(FrameworkElement owner) { - if (owner == null) return null; - if (owner is ScrollViewer) return owner as ScrollViewer; - - int n = VisualTreeHelper.GetChildrenCount(owner); - for (int i = 0; i < n; i++) { - var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement; - var deep = GetScrollViewer(child); - if (deep != null) return deep; - } - - return null; - } - - private void TreeMouseWheel(object sender, MouseWheelEventArgs e) { - var scroll = GetScrollViewer(sender as TreeView); - if (scroll == null) return; - - if (e.Delta > 0) { - scroll.LineUp(); - } else { - scroll.LineDown(); - } - - e.Handled = true; - } - #endregion - - #region FILETER - private void FilterChanged(object sender, RoutedEventArgs e) { - var toggle = sender as ToggleButton; - if (toggle == null) return; - - if (toggle.DataContext is BranchNode) { - var branch = (toggle.DataContext as BranchNode).Branch; - if (branch == null) return; - - if (toggle.IsChecked == true) { - if (!repo.LogFilters.Contains(branch.FullName)) { - repo.LogFilters.Add(branch.FullName); - } - if (!string.IsNullOrEmpty(branch.Upstream) && !repo.LogFilters.Contains(branch.Upstream)) { - repo.LogFilters.Add(branch.Upstream); - UpdateBranches(false); - } - } else { - repo.LogFilters.Remove(branch.FullName); - if (!string.IsNullOrEmpty(branch.Upstream)) { - repo.LogFilters.Remove(branch.Upstream); - UpdateBranches(false); - } - } - } - - if (toggle.DataContext is Git.Tag) { - var tag = toggle.DataContext as Git.Tag; - - if (toggle.IsChecked == true) { - if (!repo.LogFilters.Contains(tag.Name)) { - repo.LogFilters.Add(tag.Name); - } - } else { - repo.LogFilters.Remove(tag.Name); - } - } - - UpdateHistories(); - } - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace SourceGit.UI { + + /// + /// Branch node in tree. + /// + public class BranchNode { + public string Name { get; set; } + public Git.Branch Branch { get; set; } + public bool IsExpanded { get; set; } + public bool IsCurrent => Branch != null ? Branch.IsCurrent : false; + public bool IsFiltered => Branch != null ? Branch.IsFiltered : false; + public string Track => Branch != null ? Branch.UpstreamTrack : ""; + public Visibility FilterVisibility => Branch == null ? Visibility.Collapsed : Visibility.Visible; + public Visibility TrackVisibility => (Branch != null && !Branch.IsSameWithUpstream) ? Visibility.Visible : Visibility.Collapsed; + public List Children { get; set; } + } + + /// + /// Remote node in tree. + /// + public class RemoteNode { + public string Name { get; set; } + public bool IsExpanded { get; set; } + public List Children { get; set; } + } + + /// + /// Dashboard for opened repository. + /// + public partial class Dashboard : UserControl { + private Git.Repository repo = null; + private List cachedLocalBranches = new List(); + private List cachedRemotes = new List(); + private string abortCommand = null; + + /// + /// Constructor. + /// + /// Opened repository. + public Dashboard(Git.Repository opened) { + opened.OnWorkingCopyChanged = UpdateLocalChanges; + opened.OnTagChanged = UpdateTags; + opened.OnStashChanged = UpdateStashes; + opened.OnBranchChanged = () => UpdateBranches(false); + opened.OnCommitsChanged = UpdateHistories; + opened.OnNavigateCommit = commit => { + Dispatcher.Invoke(() => { + workspace.SelectedItem = historiesSwitch; + histories.Navigate(commit); + }); + }; + + InitializeComponent(); + + repo = opened; + repoName.Content = repo.Name; + histories.Repo = opened; + commits.Repo = opened; + + UpdateBranches(); + UpdateHistories(); + UpdateLocalChanges(); + UpdateStashes(); + UpdateTags(); + } + + #region DATA_UPDATE + private void UpdateHistories() { + Dispatcher.Invoke(() => { + histories.SetLoadingEnabled(true); + }); + + Task.Run(() => { + var args = "-5000 "; + if (repo.LogFilters.Count > 0) { + args = args + string.Join(" ", repo.LogFilters); + } else { + args = args + "--branches --remotes --tags"; + } + + var commits = repo.Commits(args); + histories.SetCommits(commits); + }); + } + + private void UpdateLocalChanges() { + Task.Run(() => { + var changes = repo.LocalChanges(); + var conflicts = commits.SetData(changes); + + Dispatcher.Invoke(() => { + localChangesBadge.Visibility = changes.Count == 0 ? Visibility.Collapsed : Visibility.Visible; + localChangesCount.Content = changes.Count; + btnContinue.Visibility = conflicts ? Visibility.Collapsed : Visibility.Visible; + DetectMergeState(); + }); + }); + } + + private void UpdateStashes() { + Task.Run(() => { + var data = repo.Stashes(); + Dispatcher.Invoke(() => { + stashBadge.Visibility = data.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + stashCount.Content = data.Count; + stashes.SetData(repo, data); + }); + }); + } + + private void BackupBranchNodeExpandState(Dictionary states, List nodes, string prefix) { + foreach (var node in nodes) { + var path = prefix + "/" + node.Name; + states.Add(path, node.IsExpanded); + BackupBranchNodeExpandState(states, node.Children, path); + } + } + + private void MakeBranchNode(Git.Branch branch, List collection, Dictionary folders, Dictionary expandStates, string prefix) { + var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (!branch.IsLocal) { + if (subs.Length < 2) return; + subs = subs.Skip(1).ToArray(); + } + + branch.IsFiltered = repo.LogFilters.Contains(branch.FullName); + + if (subs.Length == 1) { + var node = new BranchNode() { + Name = subs[0], + Branch = branch, + Children = new List(), + }; + collection.Add(node); + } else { + BranchNode lastFolder = null; + string path = prefix; + for (int i = 0; i < subs.Length - 1; i++) { + path = path + "/" + subs[i]; + if (folders.ContainsKey(path)) { + lastFolder = folders[path]; + } else if (lastFolder == null) { + lastFolder = new BranchNode() { + Name = subs[i], + IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false, + Children = new List(), + }; + collection.Add(lastFolder); + folders.Add(path, lastFolder); + } else { + var folder = new BranchNode() { + Name = subs[i], + IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false, + Children = new List(), + }; + lastFolder.Children.Add(folder); + folders.Add(path, folder); + lastFolder = folder; + } + } + + BranchNode node = new BranchNode(); + node.Name = subs[subs.Length - 1]; + node.Branch = branch; + node.Children = new List(); + lastFolder.Children.Add(node); + } + } + + private void SortBranchNodes(List collection) { + collection.Sort((l, r) => { + if (l.Branch != null) { + return r.Branch != null ? l.Branch.Name.CompareTo(r.Branch.Name) : -1; + } else { + return r.Branch == null ? l.Name.CompareTo(r.Name) : 1; + } + }); + + foreach (var sub in collection) { + if (sub.Children.Count > 0) SortBranchNodes(sub.Children); + } + } + + private void UpdateBranches(bool force = true) { + Task.Run(() => { + var branches = repo.Branches(force); + var remotes = repo.Remotes(true); + var localBranchNodes = new List(); + var remoteNodes = new List(); + var remoteMap = new Dictionary(); + var folders = new Dictionary(); + var states = new Dictionary(); + + BackupBranchNodeExpandState(states, cachedLocalBranches, "locals"); + foreach (var r in cachedRemotes) { + var prefix = $"remotes/{r.Name}"; + states.Add(prefix, r.IsExpanded); + BackupBranchNodeExpandState(states, r.Children, prefix); + } + + foreach (var b in branches) { + if (b.IsLocal) { + MakeBranchNode(b, localBranchNodes, folders, states, "locals"); + } else { + RemoteNode remote = null; + + if (!remoteMap.ContainsKey(b.Remote)) { + var key = "remotes/" + b.Remote; + remote = new RemoteNode() { + Name = b.Remote, + IsExpanded = states.ContainsKey(key) ? states[key] : false, + Children = new List(), + }; + remoteNodes.Add(remote); + remoteMap.Add(b.Remote, remote); + } else { + remote = remoteMap[b.Remote]; + } + + MakeBranchNode(b, remote.Children, folders, states, "remotes"); + } + } + + foreach (var r in remotes) { + if (!remoteMap.ContainsKey(r.Name)) { + var remote = new RemoteNode() { + Name = r.Name, + IsExpanded = false, + Children = new List(), + }; + remoteNodes.Add(remote); + } + } + + SortBranchNodes(localBranchNodes); + foreach (var r in remoteNodes) SortBranchNodes(r.Children); + + cachedLocalBranches = localBranchNodes; + cachedRemotes = remoteNodes; + + Dispatcher.Invoke(() => { + localBranchTree.ItemsSource = localBranchNodes; + remoteBranchTree.ItemsSource = remoteNodes; + }); + }); + } + + private void UpdateTags() { + Task.Run(() => { + var tags = repo.Tags(true); + foreach (var t in tags) t.IsFiltered = repo.LogFilters.Contains(t.Name); + + Dispatcher.Invoke(() => { + tagCount.Content = $"TAGS ({tags.Count})"; + tagList.ItemsSource = tags; + }); + }); + } + + private void Cleanup(object sender, RoutedEventArgs e) { + localBranchTree.ItemsSource = null; + remoteBranchTree.ItemsSource = null; + tagList.ItemsSource = null; + cachedLocalBranches.Clear(); + cachedRemotes.Clear(); + } + #endregion + + #region TOOLBAR + private void Close(object sender, RoutedEventArgs e) { + if (PopupManager.IsLocked()) return; + PopupManager.Close(); + + cachedLocalBranches.Clear(); + cachedRemotes.Clear(); + + repo.Close(); + } + + private void OpenFetch(object sender, RoutedEventArgs e) { + Fetch.Show(repo); + } + + private void OpenPull(object sender, RoutedEventArgs e) { + Pull.Show(repo); + } + + private void OpenPush(object sender, RoutedEventArgs e) { + Push.Show(repo); + } + + private void OpenStash(object sender, RoutedEventArgs e) { + Stash.Show(repo, new List()); + } + + private void OpenApply(object sender, RoutedEventArgs e) { + Apply.Show(repo); + } + + private void OpenSearch(object sender, RoutedEventArgs e) { + workspace.SelectedItem = historiesSwitch; + if (histories.searchBar.Margin.Top == 0) { + histories.HideSearchBar(); + } else { + histories.OpenSearchBar(); + } + } + + private void OpenExplorer(object sender, RoutedEventArgs e) { + Process.Start(repo.Path); + } + + private void OpenTerminal(object sender, RoutedEventArgs e) { + var bash = Path.Combine(App.Preference.GitExecutable, "..", "bash.exe"); + if (!File.Exists(bash)) { + App.RaiseError("Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe"); + return; + } + + var start = new ProcessStartInfo(); + start.WorkingDirectory = repo.Path; + start.FileName = bash; + Process.Start(start); + } + #endregion + + #region HOT_KEYS + public void OpenSearchBar(object sender, ExecutedRoutedEventArgs e) { + workspace.SelectedItem = historiesSwitch; + histories.OpenSearchBar(); + } + + public void HideSearchBar(object sender, ExecutedRoutedEventArgs e) { + if (histories.Visibility == Visibility.Visible) { + histories.HideSearchBar(); + } + } + #endregion + + #region MERGE_ABORTS + public void DetectMergeState() { + var gitDir = Path.Combine(repo.Path, ".git"); + var cherryPickMerge = Path.Combine(gitDir, "CHERRY_PICK_HEAD"); + var rebaseMerge = Path.Combine(gitDir, "REBASE_HEAD"); + var revertMerge = Path.Combine(gitDir, "REVERT_HEAD"); + var otherMerge = Path.Combine(gitDir, "MERGE_HEAD"); + + if (File.Exists(cherryPickMerge)) { + abortCommand = "cherry-pick"; + txtMergeProcessing.Content = "Cherry-Pick merge request detected! Press 'Abort' to restore original HEAD"; + } else if (File.Exists(rebaseMerge)) { + abortCommand = "rebase"; + txtMergeProcessing.Content = "Rebase merge request detected! Press 'Abort' to restore original HEAD"; + } else if (File.Exists(revertMerge)) { + abortCommand = "revert"; + txtMergeProcessing.Content = "Revert merge request detected! Press 'Abort' to restore original HEAD"; + } else if (File.Exists(otherMerge)) { + abortCommand = "merge"; + txtMergeProcessing.Content = "Merge request detected! Press 'Abort' to restore original HEAD"; + } else { + abortCommand = null; + } + + if (abortCommand != null) { + abortPanel.Visibility = Visibility.Visible; + if (commits.Visibility == Visibility.Visible) { + btnResolve.Visibility = Visibility.Collapsed; + } else { + btnResolve.Visibility = Visibility.Visible; + } + + commits.LoadMergeMessage(); + } else { + abortPanel.Visibility = Visibility.Collapsed; + } + } + + private void Resolve(object sender, RoutedEventArgs e) { + workspace.SelectedItem = workingCopySwitch; + } + + private async void Continue(object sender, RoutedEventArgs e) { + if (abortCommand == null) return; + + await Task.Run(() => { + repo.SetWatcherEnabled(false); + var errs = repo.RunCommand($"-c core.editor=true {abortCommand} --continue", null); + repo.AssertCommand(errs); + }); + + commits.ClearMessage(); + } + + private async void Abort(object sender, RoutedEventArgs e) { + if (abortCommand == null) return; + + await Task.Run(() => { + repo.SetWatcherEnabled(false); + var errs = repo.RunCommand($"{abortCommand} --abort", null); + repo.AssertCommand(errs); + }); + + commits.ClearMessage(); + } + #endregion + + #region WORKSPACE + private void SwitchWorkingCopy(object sender, RoutedEventArgs e) { + if (commits == null || histories == null || stashes == null) return; + + commits.Visibility = Visibility.Visible; + histories.Visibility = Visibility.Collapsed; + stashes.Visibility = Visibility.Collapsed; + + if (abortPanel.Visibility == Visibility.Visible) { + btnResolve.Visibility = Visibility.Collapsed; + } + } + + private void SwitchHistories(object sender, RoutedEventArgs e) { + if (commits == null || histories == null || stashes == null) return; + + commits.Visibility = Visibility.Collapsed; + histories.Visibility = Visibility.Visible; + stashes.Visibility = Visibility.Collapsed; + + if (abortPanel.Visibility == Visibility.Visible) { + btnResolve.Visibility = Visibility.Visible; + } + } + + private void SwitchStashes(object sender, RoutedEventArgs e) { + if (commits == null || histories == null || stashes == null) return; + + commits.Visibility = Visibility.Collapsed; + histories.Visibility = Visibility.Collapsed; + stashes.Visibility = Visibility.Visible; + + if (abortPanel.Visibility == Visibility.Visible) { + btnResolve.Visibility = Visibility.Visible; + } + } + #endregion + + #region LOCAL_BRANCHES + private void OpenNewBranch(object sender, RoutedEventArgs e) { + CreateBranch.Show(repo); + } + + private void OpenGitFlow(object sender, RoutedEventArgs ev) { + var button = sender as Button; + if (button.ContextMenu == null) { + button.ContextMenu = new ContextMenu(); + button.ContextMenu.PlacementTarget = button; + button.ContextMenu.Placement = PlacementMode.Bottom; + button.ContextMenu.StaysOpen = false; + button.ContextMenu.Focusable = true; + } else { + button.ContextMenu.Items.Clear(); + } + + if (repo.IsGitFlowEnabled()) { + var startFeature = new MenuItem(); + startFeature.Header = "Start Feature ..."; + startFeature.Click += (o, e) => { + GitFlowStartBranch.Show(repo, Git.Branch.Type.Feature); + e.Handled = true; + }; + + var startRelease = new MenuItem(); + startRelease.Header = "Start Release ..."; + startRelease.Click += (o, e) => { + GitFlowStartBranch.Show(repo, Git.Branch.Type.Release); + e.Handled = true; + }; + + var startHotfix = new MenuItem(); + startHotfix.Header = "Start Hotfix ..."; + startHotfix.Click += (o, e) => { + GitFlowStartBranch.Show(repo, Git.Branch.Type.Hotfix); + e.Handled = true; + }; + + button.ContextMenu.Items.Add(startFeature); + button.ContextMenu.Items.Add(startRelease); + button.ContextMenu.Items.Add(startHotfix); + } else { + var init = new MenuItem(); + init.Header = "Initialize Git-Flow"; + init.Click += (o, e) => { + GitFlowSetup.Show(repo); + e.Handled = true; + }; + button.ContextMenu.Items.Add(init); + } + + button.ContextMenu.IsOpen = true; + ev.Handled = true; + } + + private void LocalBranchSelected(object sender, RoutedPropertyChangedEventArgs e) { + var node = e.NewValue as BranchNode; + if (node == null || node.Branch == null) return; + repo.OnNavigateCommit?.Invoke(node.Branch.Head); + } + + private void LocalBranchMouseDoubleClick(object sender, MouseButtonEventArgs e) { + var node = (sender as TreeViewItem).DataContext as BranchNode; + if (node == null || node.Branch == null) return; + Task.Run(() => repo.Checkout(node.Branch.Name)); + } + + private void LocalBranchContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var node = (sender as TreeViewItem).DataContext as BranchNode; + if (node == null || node.Branch == null) return; + + var menu = new ContextMenu(); + var branch = node.Branch; + + var push = new MenuItem(); + push.Header = $"Push '{branch.Name}'"; + push.Click += (o, e) => { + Push.Show(repo, branch); + e.Handled = true; + }; + + if (branch.IsCurrent) { + var discard = new MenuItem(); + discard.Header = "Discard all changes"; + discard.Click += (o, e) => { + Discard.Show(repo, null); + e.Handled = true; + }; + menu.Items.Add(discard); + menu.Items.Add(new Separator()); + + if (!string.IsNullOrEmpty(branch.Upstream)) { + var upstream = branch.Upstream.Substring(13); + var fastForward = new MenuItem(); + fastForward.Header = $"Fast-Forward to '{upstream}'"; + fastForward.Click += (o, e) => { + Merge.StartDirectly(repo, upstream, branch.Name); + e.Handled = true; + }; + + var pull = new MenuItem(); + pull.Header = $"Pull '{upstream}'"; + pull.Click += (o, e) => { + Pull.Show(repo); + e.Handled = true; + }; + + menu.Items.Add(fastForward); + menu.Items.Add(pull); + } + + menu.Items.Add(push); + } else { + var current = repo.CurrentBranch(); + + var checkout = new MenuItem(); + checkout.Header = $"Checkout {branch.Name}"; + checkout.Click += (o, e) => { + Task.Run(() => repo.Checkout(node.Branch.Name)); + e.Handled = true; + }; + menu.Items.Add(checkout); + menu.Items.Add(new Separator()); + menu.Items.Add(push); + + var merge = new MenuItem(); + merge.Header = $"Merge '{branch.Name}' into '{current.Name}'"; + merge.Click += (o, e) => { + Merge.Show(repo, branch.Name, current.Name); + e.Handled = true; + }; + menu.Items.Add(merge); + + var rebase = new MenuItem(); + rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'"; + rebase.Click += (o, e) => { + Rebase.Show(repo, branch); + e.Handled = true; + }; + menu.Items.Add(rebase); + } + + if (branch.Kind != Git.Branch.Type.Normal) { + menu.Items.Add(new Separator()); + + var icon = new System.Windows.Shapes.Path(); + icon.Style = FindResource("Style.Icon") as Style; + icon.Data = FindResource("Icon.Flow") as Geometry; + icon.Width = 10; + + var finish = new MenuItem(); + finish.Header = $"Git Flow - Finish '{branch.Name}'"; + finish.Icon = icon; + finish.Click += (o, e) => { + GitFlowFinishBranch.Show(repo, branch); + e.Handled = true; + }; + + menu.Items.Add(finish); + } + + var rename = new MenuItem(); + rename.Header = $"Rename '{branch.Name}'"; + rename.Click += (o, e) => { + RenameBranch.Show(repo, branch); + e.Handled = true; + }; + menu.Items.Add(new Separator()); + menu.Items.Add(rename); + + var delete = new MenuItem(); + delete.Header = $"Delete '{branch.Name}'"; + delete.IsEnabled = !branch.IsCurrent; + delete.Click += (o, e) => { + DeleteBranch.Show(repo, branch); + e.Handled = true; + }; + menu.Items.Add(delete); + menu.Items.Add(new Separator()); + + var createBranch = new MenuItem(); + createBranch.Header = "Create Branch"; + createBranch.Click += (o, e) => { + CreateBranch.Show(repo, branch); + e.Handled = true; + }; + menu.Items.Add(createBranch); + + var createTag = new MenuItem(); + createTag.Header = "Create Tag"; + createTag.Click += (o, e) => { + CreateTag.Show(repo, branch); + e.Handled = true; + }; + menu.Items.Add(createTag); + menu.Items.Add(new Separator()); + + var copy = new MenuItem(); + copy.Header = "Copy Branch Name"; + copy.Click += (o, e) => { + Clipboard.SetText(branch.Name); + e.Handled = true; + }; + menu.Items.Add(copy); + + menu.IsOpen = true; + ev.Handled = true; + } + #endregion + + #region REMOTE_BRANCHES + private void OpenRemote(object sender, RoutedEventArgs e) { + Remote.Show(repo); + } + + private void OpenRemoteContextMenu(RemoteNode node) { + var fetch = new MenuItem(); + fetch.Header = $"Fetch '{node.Name}'"; + fetch.Click += (o, e) => { + Fetch.Show(repo, node.Name); + e.Handled = true; + }; + + var edit = new MenuItem(); + edit.Header = $"Edit '{node.Name}'"; + edit.Click += (o, e) => { + var remotes = repo.Remotes(); + var found = remotes.Find(r => r.Name == node.Name); + if (found != null) Remote.Show(repo, found); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = $"Delete '{node.Name}'"; + delete.Click += (o, e) => { + DeleteRemote.Show(repo, node.Name); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = "Copy Remote URL"; + copy.Click += (o, e) => { + var remotes = repo.Remotes(); + var found = remotes.Find(r => r.Name == node.Name); + if (found != null) Clipboard.SetText(found.URL); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(fetch); + menu.Items.Add(new Separator()); + menu.Items.Add(edit); + menu.Items.Add(delete); + menu.Items.Add(new Separator()); + menu.Items.Add(copy); + menu.IsOpen = true; + } + + private void OpenRemoteBranchContextMenu(BranchNode node) { + var branch = node.Branch; + var current = repo.CurrentBranch(); + if (current == null) return; + + var checkout = new MenuItem(); + checkout.Header = $"Checkout '{branch.Name}'"; + checkout.Click += (o, e) => { + var branches = repo.Branches(); + var tracked = null as Git.Branch; + var upstream = $"refs/remotes/{branch.Name}"; + + foreach (var b in branches) { + if (b.IsLocal && b.Upstream == upstream) { + tracked = b; + break; + } + } + + if (tracked == null) { + CreateBranch.Show(repo, branch); + } else if (!tracked.IsCurrent) { + Task.Run(() => repo.Checkout(tracked.Name)); + } + + e.Handled = true; + }; + + var pull = new MenuItem(); + pull.Header = $"Pull '{branch.Name}' into '{current.Name}'"; + pull.Click += (o, e) => { + Pull.Show(repo, branch.Name); + e.Handled = true; + }; + + var merge = new MenuItem(); + merge.Header = $"Merge '{branch.Name}' into '{current.Name}'"; + merge.Click += (o, e) => { + Merge.Show(repo, branch.Name, current.Name); + e.Handled = true; + }; + + var rebase = new MenuItem(); + rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'"; + rebase.Click += (o, e) => { + Rebase.Show(repo, branch); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = $"Delete '{branch.Name}'"; + delete.Click += (o, e) => { + DeleteBranch.Show(repo, branch); + e.Handled = true; + }; + + var createBranch = new MenuItem(); + createBranch.Header = "Create New Branch"; + createBranch.Click += (o, e) => { + CreateBranch.Show(repo, branch); + e.Handled = true; + }; + + var createTag = new MenuItem(); + createTag.Header = "Create New Tag"; + createTag.Click += (o, e) => { + CreateTag.Show(repo, branch); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = "Copy Branch Name"; + copy.Click += (o, e) => { + Clipboard.SetText(branch.Name); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(checkout); + menu.Items.Add(new Separator()); + menu.Items.Add(pull); + menu.Items.Add(merge); + menu.Items.Add(rebase); + menu.Items.Add(new Separator()); + menu.Items.Add(delete); + menu.Items.Add(new Separator()); + menu.Items.Add(createBranch); + menu.Items.Add(createTag); + menu.Items.Add(new Separator()); + menu.Items.Add(copy); + menu.IsOpen = true; + } + + private void RemoteBranchSelected(object sender, RoutedPropertyChangedEventArgs e) { + var node = e.NewValue as BranchNode; + if (node == null || node.Branch == null) return; + repo.OnNavigateCommit?.Invoke(node.Branch.Head); + } + + private void RemoteContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var remoteNode = (sender as TreeViewItem).DataContext as RemoteNode; + if (remoteNode != null) { + OpenRemoteContextMenu(remoteNode); + ev.Handled = true; + return; + } + + var branchNode = (sender as TreeViewItem).DataContext as BranchNode; + if (branchNode != null && branchNode.Branch != null) { + OpenRemoteBranchContextMenu(branchNode); + ev.Handled = true; + return; + } + } + #endregion + + #region TAGS + private void OpenNewTag(object sender, RoutedEventArgs e) { + CreateTag.Show(repo); + } + + private void TagLostFocus(object sender, RoutedEventArgs e) { + (sender as ListView).UnselectAll(); + } + + private void TagSelectionChanged(object sender, SelectionChangedEventArgs e) { + if (e.AddedItems.Count == 1) { + var item = e.AddedItems[0] as Git.Tag; + repo.OnNavigateCommit?.Invoke(item.SHA); + } + } + + private void TagContextMenuOpening(object sender, ContextMenuEventArgs e) { + var tag = (sender as ListView).SelectedItem as Git.Tag; + if (tag == null) return; + + var createBranch = new MenuItem(); + createBranch.Header = "Create New Branch"; + createBranch.Click += (o, ev) => { + CreateBranch.Show(repo, tag); + ev.Handled = true; + }; + + var pushTag = new MenuItem(); + pushTag.Header = $"Push '{tag.Name}'"; + pushTag.Click += (o, ev) => { + PushTag.Show(repo, tag); + ev.Handled = true; + }; + + var deleteTag = new MenuItem(); + deleteTag.Header = $"Delete '{tag.Name}'"; + deleteTag.Click += (o, ev) => { + DeleteTag.Show(repo, tag); + ev.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = "Copy Name"; + copy.Click += (o, ev) => { + Clipboard.SetText(tag.Name); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(createBranch); + menu.Items.Add(new Separator()); + menu.Items.Add(pushTag); + menu.Items.Add(deleteTag); + menu.Items.Add(new Separator()); + menu.Items.Add(copy); + menu.IsOpen = true; + + e.Handled = true; + } + #endregion + + #region TREES + private TreeViewItem FindTreeViewItem(ItemsControl item, BranchNode node) { + if (item == null) return null; + + var data = item.DataContext as BranchNode; + if (data == node) return item as TreeViewItem; + + for (int i = 0; i < item.Items.Count; i++) { + var childContainer = item.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl; + var child = FindTreeViewItem(childContainer, node); + if (child != null) return child; + } + + return null; + } + + private void TreeLostFocus(object sender, RoutedEventArgs e) { + var tree = sender as TreeView; + var remote = tree.SelectedItem as RemoteNode; + if (remote != null) { + var remoteItem = tree.ItemContainerGenerator.ContainerFromItem(remote) as TreeViewItem; + if (remoteItem != null) remoteItem.IsSelected = false; + return; + } + + var node = tree.SelectedItem as BranchNode; + if (node == null) return; + + var item = FindTreeViewItem(tree, node); + if (item != null) item.IsSelected = false; + } + + private ScrollViewer GetScrollViewer(FrameworkElement owner) { + if (owner == null) return null; + if (owner is ScrollViewer) return owner as ScrollViewer; + + int n = VisualTreeHelper.GetChildrenCount(owner); + for (int i = 0; i < n; i++) { + var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement; + var deep = GetScrollViewer(child); + if (deep != null) return deep; + } + + return null; + } + + private void TreeMouseWheel(object sender, MouseWheelEventArgs e) { + var scroll = GetScrollViewer(sender as TreeView); + if (scroll == null) return; + + if (e.Delta > 0) { + scroll.LineUp(); + } else { + scroll.LineDown(); + } + + e.Handled = true; + } + #endregion + + #region FILETER + private void FilterChanged(object sender, RoutedEventArgs e) { + var toggle = sender as ToggleButton; + if (toggle == null) return; + + if (toggle.DataContext is BranchNode) { + var branch = (toggle.DataContext as BranchNode).Branch; + if (branch == null) return; + + if (toggle.IsChecked == true) { + if (!repo.LogFilters.Contains(branch.FullName)) { + repo.LogFilters.Add(branch.FullName); + } + if (!string.IsNullOrEmpty(branch.Upstream) && !repo.LogFilters.Contains(branch.Upstream)) { + repo.LogFilters.Add(branch.Upstream); + UpdateBranches(false); + } + } else { + repo.LogFilters.Remove(branch.FullName); + if (!string.IsNullOrEmpty(branch.Upstream)) { + repo.LogFilters.Remove(branch.Upstream); + UpdateBranches(false); + } + } + } + + if (toggle.DataContext is Git.Tag) { + var tag = toggle.DataContext as Git.Tag; + + if (toggle.IsChecked == true) { + if (!repo.LogFilters.Contains(tag.Name)) { + repo.LogFilters.Add(tag.Name); + } + } else { + repo.LogFilters.Remove(tag.Name); + } + } + + UpdateHistories(); + } + #endregion + } +}