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.OnSubmoduleChanged = UpdateSubmodules;
opened.OnNavigateCommit = commit => {
Dispatcher.Invoke(() => {
workspace.SelectedItem = historiesSwitch;
histories.Navigate(commit);
});
};
InitializeComponent();
repo = opened;
repoName.Content = repo.Name;
histories.Repo = opened;
commits.Repo = opened;
if (repo.Parent != null) {
btnParent.Visibility = Visibility.Visible;
txtParent.Content = repo.Parent.Name;
} else {
btnParent.Visibility = Visibility.Collapsed;
}
UpdateBranches();
UpdateHistories();
UpdateLocalChanges();
UpdateStashes();
UpdateTags();
UpdateSubmodules();
}
#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 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) {
bool IsDetached = false;
Git.Branch branch = null;
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");
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(),
};
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(),
};
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;
});
});
}
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 GotoParent(object sender, RoutedEventArgs e) {
if (repo.Parent == null) return;
repo.Parent.Open();
e.Handled = true;
}
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) {
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