mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-11-01 13:13:21 -07:00
1070 lines
40 KiB
C#
1070 lines
40 KiB
C#
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 {
|
||
|
||
/// <summary>
|
||
/// Branch node in tree.
|
||
/// </summary>
|
||
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<BranchNode> Children { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Remote node in tree.
|
||
/// </summary>
|
||
public class RemoteNode {
|
||
public string Name { get; set; }
|
||
public bool IsExpanded { get; set; }
|
||
public List<BranchNode> Children { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Dashboard for opened repository.
|
||
/// </summary>
|
||
public partial class Dashboard : UserControl {
|
||
private Git.Repository repo = null;
|
||
private List<BranchNode> cachedLocalBranches = new List<BranchNode>();
|
||
private List<RemoteNode> cachedRemotes = new List<RemoteNode>();
|
||
private string abortCommand = null;
|
||
|
||
/// <summary>
|
||
/// Constructor.
|
||
/// </summary>
|
||
/// <param name="repo">Opened repository.</param>
|
||
public Dashboard(Git.Repository opened) {
|
||
opened.OnWorkingCopyChanged = UpdateLocalChanges;
|
||
opened.OnTagChanged = UpdateTags;
|
||
opened.OnStashChanged = UpdateStashes;
|
||
opened.OnBranchChanged = () => UpdateBranches(false);
|
||
opened.OnCommitsChanged = UpdateHistories;
|
||
opened.OnSubmoduleChanged = UpdateSubmodules;
|
||
opened.OnNavigateCommit = commit => {
|
||
Dispatcher.Invoke(() => {
|
||
workspace.SelectedItem = historiesSwitch;
|
||
histories.Navigate(commit);
|
||
});
|
||
};
|
||
|
||
InitializeComponent();
|
||
|
||
repo = opened;
|
||
histories.Repo = opened;
|
||
commits.Repo = opened;
|
||
|
||
UpdateBranches();
|
||
UpdateHistories();
|
||
UpdateLocalChanges();
|
||
UpdateStashes();
|
||
UpdateTags();
|
||
UpdateSubmodules();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Cleanup all items
|
||
/// </summary>
|
||
public void Cleanup() {
|
||
localBranchTree.ItemsSource = null;
|
||
remoteBranchTree.ItemsSource = null;
|
||
tagList.ItemsSource = null;
|
||
cachedLocalBranches.Clear();
|
||
cachedRemotes.Clear();
|
||
}
|
||
|
||
#region DATA_UPDATE
|
||
private void UpdateHistories() {
|
||
Dispatcher.Invoke(() => {
|
||
histories.SetLoadingEnabled(true);
|
||
});
|
||
|
||
Task.Run(() => {
|
||
var args = "-8000 ";
|
||
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<string, bool> states, List<BranchNode> 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<BranchNode> collection, Dictionary<string, BranchNode> folders, Dictionary<string, bool> 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<BranchNode>(),
|
||
};
|
||
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<BranchNode>(),
|
||
};
|
||
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<BranchNode>(),
|
||
};
|
||
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<BranchNode>();
|
||
lastFolder.Children.Add(node);
|
||
}
|
||
}
|
||
|
||
private void SortBranchNodes(List<BranchNode> 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) {
|
||
bool IsDetached = false;
|
||
Git.Branch branch = null;
|
||
Task.Run(() => {
|
||
var branches = repo.Branches(force);
|
||
var remotes = repo.Remotes(true);
|
||
var localBranchNodes = new List<BranchNode>();
|
||
var remoteNodes = new List<RemoteNode>();
|
||
var remoteMap = new Dictionary<string, RemoteNode>();
|
||
var folders = new Dictionary<string, BranchNode>();
|
||
var states = new Dictionary<string, bool>();
|
||
|
||
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");
|
||
branch = b;
|
||
} else if (!string.IsNullOrEmpty(b.Remote)) {
|
||
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<BranchNode>(),
|
||
};
|
||
remoteNodes.Add(remote);
|
||
remoteMap.Add(b.Remote, remote);
|
||
} else {
|
||
remote = remoteMap[b.Remote];
|
||
}
|
||
|
||
MakeBranchNode(b, remote.Children, folders, states, "remotes");
|
||
} else {
|
||
/// 对于 SUBMODULE HEAD 出于游离状态(detached on commit id)
|
||
/// 此时,分支既不是 本地分支,也不是远程分支
|
||
IsDetached = b.IsCurrent;
|
||
}
|
||
}
|
||
|
||
foreach (var r in remotes) {
|
||
if (!remoteMap.ContainsKey(r.Name)) {
|
||
var remote = new RemoteNode() {
|
||
Name = r.Name,
|
||
IsExpanded = false,
|
||
Children = new List<BranchNode>(),
|
||
};
|
||
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;
|
||
});
|
||
|
||
if (IsDetached && branch != null) repo.Checkout(branch.Name);
|
||
});
|
||
}
|
||
|
||
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 UpdateSubmodules() {
|
||
Task.Run(() => {
|
||
var submodules = repo.Submodules();
|
||
Dispatcher.Invoke(() => {
|
||
submoduleCount.Content = $"SUBMODULES ({submodules.Count})";
|
||
submoduleList.ItemsSource = submodules;
|
||
});
|
||
});
|
||
}
|
||
#endregion
|
||
|
||
#region TOOLBAR
|
||
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<string>());
|
||
}
|
||
|
||
private void OpenApply(object sender, RoutedEventArgs e) {
|
||
Apply.Show(repo);
|
||
}
|
||
|
||
private void OpenSearch(object sender, RoutedEventArgs e) {
|
||
if (popupManager.IsLocked()) return;
|
||
|
||
workspace.SelectedItem = historiesSwitch;
|
||
if (histories.searchBar.Margin.Top == 0) {
|
||
histories.HideSearchBar();
|
||
} else {
|
||
histories.OpenSearchBar();
|
||
}
|
||
}
|
||
|
||
private void OpenConfigure(object sender, RoutedEventArgs e) {
|
||
Configure.Show(repo);
|
||
}
|
||
|
||
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 cherryPickMerge = Path.Combine(repo.GitDir, "CHERRY_PICK_HEAD");
|
||
var rebaseMerge = Path.Combine(repo.GitDir, "REBASE_HEAD");
|
||
var revertMerge = Path.Combine(repo.GitDir, "REVERT_HEAD");
|
||
var otherMerge = Path.Combine(repo.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<object> 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<object> 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 DataGrid).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 DataGrid).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 SUBMODULES
|
||
private void OpenAddSubmodule(object sender, RoutedEventArgs e) {
|
||
AddSubmodule.Show(repo);
|
||
}
|
||
|
||
private void UpdateSubmodule(object sender, RoutedEventArgs e) {
|
||
Waiting.Show(repo, () => repo.UpdateSubmodule());
|
||
}
|
||
|
||
private void SubmoduleLostFocus(object sender, RoutedEventArgs e) {
|
||
(sender as DataGrid).UnselectAll();
|
||
}
|
||
|
||
private void SubmoduleContextMenuOpening(object sender, ContextMenuEventArgs e) {
|
||
var path = (sender as DataGrid).SelectedItem as string;
|
||
if (path == null) return;
|
||
|
||
var open = new MenuItem();
|
||
open.Header = "Open Submodule Repository";
|
||
open.Click += (o, ev) => {
|
||
var sub = new Git.Repository();
|
||
sub.Path = Path.Combine(repo.Path, path);
|
||
sub.Name = Path.GetFileName(path);
|
||
sub.Parent = repo;
|
||
sub.Open();
|
||
|
||
ev.Handled = true;
|
||
};
|
||
|
||
var copy = new MenuItem();
|
||
copy.Header = "Copy Relative Path";
|
||
copy.Click += (o, ev) => {
|
||
Clipboard.SetText(path);
|
||
ev.Handled = true;
|
||
};
|
||
|
||
var menu = new ContextMenu();
|
||
menu.Items.Add(open);
|
||
menu.Items.Add(copy);
|
||
menu.IsOpen = true;
|
||
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void SubmoduleMouseDoubleClick(object sender, MouseButtonEventArgs e) {
|
||
var path = (sender as DataGridRow).DataContext as string;
|
||
if (path == null) return;
|
||
|
||
var sub = new Git.Repository();
|
||
sub.Path = Path.Combine(repo.Path, path);
|
||
sub.Name = Path.GetFileName(path);
|
||
sub.Parent = repo;
|
||
sub.Open();
|
||
}
|
||
#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
|
||
}
|
||
}
|