This commit is contained in:
leo 2020-07-03 15:24:31 +08:00
commit 38227b1d57
138 changed files with 17935 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.vs/
bin/
obj/

6
App.config Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
</configuration>

BIN
App.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

16
App.xaml Normal file
View file

@ -0,0 +1,16 @@
<Application x:Class="SourceGit.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="OnAppStartup"
Deactivated="OnAppDeactivated"
Exit="OnAppExit">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Icons.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Controls.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

134
App.xaml.cs Normal file
View file

@ -0,0 +1,134 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
namespace SourceGit {
/// <summary>
/// Application.
/// </summary>
public partial class App : Application {
/// <summary>
/// Getter/Setter for Git preference.
/// </summary>
public static Git.Preference Preference {
get { return Git.Preference.Instance; }
set { Git.Preference.Instance = value; }
}
/// <summary>
/// Check if GIT has been configured.
/// </summary>
public static bool IsGitConfigured {
get {
return !string.IsNullOrEmpty(Preference.GitExecutable)
&& File.Exists(Preference.GitExecutable);
}
}
/// <summary>
/// Interactive rebase sequence file.
/// </summary>
public static string InteractiveRebaseScript {
get;
private set;
}
/// <summary>
/// TODO file for interactive rebase.
/// </summary>
public static string InteractiveRebaseTodo {
get;
private set;
}
/// <summary>
/// Error handler.
/// </summary>
public static Action<string> OnError {
get;
set;
}
/// <summary>
/// Raise error message.
/// </summary>
/// <param name="message"></param>
public static void RaiseError(string message) {
OnError?.Invoke(message);
}
/// <summary>
/// Startup event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppStartup(object sender, StartupEventArgs e) {
// Try auto configure git via registry.
if (!IsGitConfigured) {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var git = root.OpenSubKey("SOFTWARE\\GitForWindows");
if (git != null) {
Preference.GitExecutable = Path.Combine(
git.GetValue("InstallPath") as string,
"bin",
"git.exe");
}
}
// Files for interactive rebase.
InteractiveRebaseScript = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"rebase.bat");
InteractiveRebaseTodo = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"REBASE_TODO");
if (!File.Exists(InteractiveRebaseScript)) {
var folder = Path.GetDirectoryName(InteractiveRebaseScript);
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
File.WriteAllText(InteractiveRebaseScript, $"@echo off\ntype \"{InteractiveRebaseTodo}\" > .git\\rebase-merge\\git-rebase-todo");
File.WriteAllText(InteractiveRebaseTodo, "");
}
// Apply themes
if (Preference.UIUseLightTheme) {
foreach (var rs in Current.Resources.MergedDictionaries) {
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/")) {
rs.Source = new Uri("pack://application:,,,/Resources/Themes/Light.xaml", UriKind.Absolute);
break;
}
}
}
// Show main window
var launcher = new UI.Launcher();
launcher.Show();
}
/// <summary>
/// Deactivated event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppDeactivated(object sender, EventArgs e) {
GC.Collect();
}
/// <summary>
/// Quit event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppExit(object sender, ExitEventArgs e) {
Git.Preference.Save();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Same as BoolToVisibilityConverter.
/// </summary>
public class BoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to brush
/// </summary>
public class FileStatusToColor : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return Brushes.Transparent;
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return Brushes.Yellow;
status = change.WorkTree;
} else {
status = change.Index;
}
if (App.Preference.UIUseLightTheme) {
switch (status) {
case Git.Change.Status.Modified: return Brushes.Goldenrod;
case Git.Change.Status.Added: return Brushes.Green;
case Git.Change.Status.Deleted: return Brushes.Red;
case Git.Change.Status.Renamed: return Brushes.Magenta;
case Git.Change.Status.Copied: return Brushes.Goldenrod;
case Git.Change.Status.Unmerged: return Brushes.Goldenrod;
case Git.Change.Status.Untracked: return Brushes.Green;
default: return Brushes.Transparent;
}
} else {
switch (status) {
case Git.Change.Status.Modified: return Brushes.DarkGoldenrod;
case Git.Change.Status.Added: return Brushes.DarkGreen;
case Git.Change.Status.Deleted: return Brushes.DarkRed;
case Git.Change.Status.Renamed: return Brushes.DarkMagenta;
case Git.Change.Status.Copied: return Brushes.DarkGoldenrod;
case Git.Change.Status.Unmerged: return Brushes.DarkGoldenrod;
case Git.Change.Status.Untracked: return Brushes.DarkGreen;
default: return Brushes.Transparent;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to icon.
/// </summary>
public class FileStatusToIcon : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return "";
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return "X";
status = change.WorkTree;
} else {
status = change.Index;
}
switch (status) {
case Git.Change.Status.Modified: return "M";
case Git.Change.Status.Added: return "A";
case Git.Change.Status.Deleted: return "D";
case Git.Change.Status.Renamed: return "R";
case Git.Change.Status.Copied: return "C";
case Git.Change.Status.Unmerged: return "U";
case Git.Change.Status.Untracked: return "?";
default: return "?";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert indent(horizontal offset) to Margin property
/// </summary>
public class IndentToMargin : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return new Thickness((double)value, 0, 0, 0);
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return ((Thickness)value).Left;
}
}
}

19
Converters/InverseBool.cs Normal file
View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse bool converter.
/// </summary>
public class InverseBool : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return !((bool)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse BoolToCollapsed.
/// </summary>
public class InverseBoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Collapsed : Visibility.Visible;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert percent to double.
/// </summary>
public class PercentToDouble : IValueConverter {
/// <summary>
/// Percentage.
/// </summary>
public double Percent { get; set; }
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (double)value * Percent;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert depth of a TreeViewItem to Margin property.
/// </summary>
public class TreeViewItemDepthToMargin : IValueConverter {
/// <summary>
/// Indent length
/// </summary>
public double Indent { get; set; } = 19;
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
TreeViewItem item = value as TreeViewItem;
if (item == null) return new Thickness(0);
TreeViewItem iterator = GetParent(item);
int depth = 0;
while (iterator != null) {
depth++;
iterator = GetParent(iterator);
}
return new Thickness(Indent * depth, 0, 0, 0);
}
/// <summary>
/// Implement IValueConvert.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
/// <summary>
/// Get parent item.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private TreeViewItem GetParent(TreeViewItem item) {
var parent = VisualTreeHelper.GetParent(item);
while (parent != null && !(parent is TreeView) && !(parent is TreeViewItem)) {
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
}
}

35
Git/Blame.cs Normal file
View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace SourceGit.Git {
/// <summary>
/// Blame
/// </summary>
public class Blame {
/// <summary>
/// Block content.
/// </summary>
public class Block {
public string CommitSHA { get; set; }
public string Author { get; set; }
public string Time { get; set; }
public string Content { get; set; }
}
/// <summary>
/// Blocks
/// </summary>
public List<Block> Blocks { get; set; } = new List<Block>();
/// <summary>
/// Is binary file?
/// </summary>
public bool IsBinary { get; set; } = false;
/// <summary>
/// Line count.
/// </summary>
public int LineCount { get; set; } = 0;
}
}

190
Git/Branch.cs Normal file
View file

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git branch
/// </summary>
public class Branch {
private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)";
private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
private static readonly Regex AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex BEHIND = new Regex(@"behind (\d+)");
/// <summary>
/// Branch type.
/// </summary>
public enum Type {
Normal,
Feature,
Release,
Hotfix,
}
/// <summary>
/// Branch name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Full name.
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Head ref
/// </summary>
public string Head { get; set; }
/// <summary>
/// Subject for head ref.
/// </summary>
public string HeadSubject { get; set; }
/// <summary>
/// Is local branch
/// </summary>
public bool IsLocal { get; set; }
/// <summary>
/// Branch type.
/// </summary>
public Type Kind { get; set; } = Type.Normal;
/// <summary>
/// Remote name. Only used for remote branch
/// </summary>
public string Remote { get; set; }
/// <summary>
/// Upstream. Only used for local branches.
/// </summary>
public string Upstream { get; set; }
/// <summary>
/// Track information for upstream. Only used for local branches.
/// </summary>
public string UpstreamTrack { get; set; }
/// <summary>
/// Is current branch. Only used for local branches.
/// </summary>
public bool IsCurrent { get; set; }
/// <summary>
/// Is this branch's HEAD same with upstream?
/// </summary>
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load branches.
/// </summary>
/// <param name="repo"></param>
public static List<Branch> Load(Repository repo) {
var localPrefix = "refs/heads/";
var remotePrefix = "refs/remotes/";
var branches = new List<Branch>();
var remoteBranches = new List<string>();
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
var match = PARSE.Match(line);
if (!match.Success) return;
var branch = new Branch();
var refname = match.Groups[1].Value;
if (refname.EndsWith("/HEAD")) return;
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
branch.Name = refname.Substring(localPrefix.Length);
branch.IsLocal = true;
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
var name = refname.Substring(remotePrefix.Length);
branch.Remote = name.Substring(0, name.IndexOf('/'));
branch.Name = name;
branch.IsLocal = false;
remoteBranches.Add(refname);
}
branch.FullName = refname;
branch.Head = match.Groups[2].Value;
branch.IsCurrent = match.Groups[3].Value == "*";
branch.Upstream = match.Groups[4].Value;
branch.UpstreamTrack = ParseTrack(match.Groups[5].Value);
branch.HeadSubject = match.Groups[6].Value;
branches.Add(branch);
});
// Fixed deleted remote branch
foreach (var b in branches) {
if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) {
b.Upstream = null;
}
}
return branches;
}
/// <summary>
/// Create new branch.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
public static void Create(Repository repo, string name, string startPoint) {
var errs = repo.RunCommand($"branch {name} {startPoint}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Rename branch
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
public void Rename(Repository repo, string name) {
var errs = repo.RunCommand($"branch -M {Name} {name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Delete branch.
/// </summary>
/// <param name="repo"></param>
public void Delete(Repository repo) {
string errs = null;
if (!IsLocal) {
errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null);
} else {
errs = repo.RunCommand($"branch -D {Name}", null);
}
if (errs != null) App.RaiseError(errs);
}
private static string ParseTrack(string data) {
if (string.IsNullOrEmpty(data)) return "";
string track = "";
var ahead = AHEAD.Match(data);
if (ahead.Success) {
track += ahead.Groups[1].Value + "↑ ";
}
var behind = BEHIND.Match(data);
if (behind.Success) {
track += behind.Groups[1].Value + "↓";
}
return track.Trim();
}
}
}

147
Git/Change.cs Normal file
View file

@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Changed file status.
/// </summary>
public class Change {
private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
/// <summary>
/// Status Code
/// </summary>
public enum Status {
None,
Modified,
Added,
Deleted,
Renamed,
Copied,
Unmerged,
Untracked,
}
/// <summary>
/// Index status
/// </summary>
public Status Index { get; set; }
/// <summary>
/// Work tree status.
/// </summary>
public Status WorkTree { get; set; }
/// <summary>
/// Current file path.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Original file path before this revision.
/// </summary>
public string OriginalPath { get; set; }
/// <summary>
/// Staged(added) in index?
/// </summary>
public bool IsAddedToIndex {
get {
if (Index == Status.None || Index == Status.Untracked) return false;
return true;
}
}
/// <summary>
/// Is conflict?
/// </summary>
public bool IsConflit {
get {
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
if (Index == Status.Added && WorkTree == Status.Added) return true;
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
return false;
}
}
/// <summary>
/// Parse change for `--name-status` data.
/// </summary>
/// <param name="data">Raw data.</param>
/// <param name="fromCommit">Read from commit?</param>
/// <returns>Parsed change instance.</returns>
public static Change Parse(string data, bool fromCommit = false) {
var match = FORMAT.Match(data);
if (!match.Success) return null;
var change = new Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
if (fromCommit) {
switch (status[0]) {
case 'M': change.Set(Status.Modified); break;
case 'A': change.Set(Status.Added); break;
case 'D': change.Set(Status.Deleted); break;
case 'R': change.Set(Status.Renamed); break;
case 'C': change.Set(Status.Copied); break;
default: return null;
}
} else {
switch (status) {
case " M": change.Set(Status.None, Status.Modified); break;
case " A": change.Set(Status.None, Status.Added); break;
case " D": change.Set(Status.None, Status.Deleted); break;
case " R": change.Set(Status.None, Status.Renamed); break;
case " C": change.Set(Status.None, Status.Copied); break;
case "M": change.Set(Status.Modified, Status.None); break;
case "MM": change.Set(Status.Modified, Status.Modified); break;
case "MD": change.Set(Status.Modified, Status.Deleted); break;
case "A": change.Set(Status.Added, Status.None); break;
case "AM": change.Set(Status.Added, Status.Modified); break;
case "AD": change.Set(Status.Added, Status.Deleted); break;
case "D": change.Set(Status.Deleted, Status.None); break;
case "R": change.Set(Status.Renamed, Status.None); break;
case "RM": change.Set(Status.Renamed, Status.Modified); break;
case "RD": change.Set(Status.Renamed, Status.Deleted); break;
case "C": change.Set(Status.Copied, Status.None); break;
case "CM": change.Set(Status.Copied, Status.Modified); break;
case "CD": change.Set(Status.Copied, Status.Deleted); break;
case "DR": change.Set(Status.Deleted, Status.Renamed); break;
case "DC": change.Set(Status.Deleted, Status.Copied); break;
case "DD": change.Set(Status.Deleted, Status.Deleted); break;
case "AU": change.Set(Status.Added, Status.Unmerged); break;
case "UD": change.Set(Status.Unmerged, Status.Deleted); break;
case "UA": change.Set(Status.Unmerged, Status.Added); break;
case "DU": change.Set(Status.Deleted, Status.Unmerged); break;
case "AA": change.Set(Status.Added, Status.Added); break;
case "UU": change.Set(Status.Unmerged, Status.Unmerged); break;
case "??": change.Set(Status.Untracked, Status.Untracked); break;
default: return null;
}
}
if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2);
if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2);
return change;
}
private void Set(Status index, Status workTree = Status.None) {
Index = index;
WorkTree = workTree;
if (index == Status.Renamed || workTree == Status.Renamed) {
var idx = Path.IndexOf('\t');
if (idx >= 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 1);
} else {
idx = Path.IndexOf(" -> ");
if (idx > 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 4);
}
}
}
}
}
}

262
Git/Commit.cs Normal file
View file

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git commit information.
/// </summary>
public class Commit {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Short SHA.
/// </summary>
public string ShortSHA => SHA.Substring(0, 8);
/// <summary>
/// Parent commit SHAs.
/// </summary>
public List<string> Parents { get; set; } = new List<string>();
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Committer.
/// </summary>
public User Committer { get; set; } = new User();
/// <summary>
/// Subject
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// Extra message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// HEAD commit?
/// </summary>
public bool IsHEAD { get; set; } = false;
/// <summary>
/// Merged in current branch?
/// </summary>
public bool IsMerged { get; set; } = false;
/// <summary>
/// X offset in graph
/// </summary>
public double GraphOffset { get; set; } = 0;
/// <summary>
/// Has decorators.
/// </summary>
public bool HasDecorators => Decorators.Count > 0;
/// <summary>
/// Decorators.
/// </summary>
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
/// <summary>
/// Read commits.
/// </summary>
/// <param name="repo">Repository</param>
/// <param name="limit">Limitations</param>
/// <returns>Parsed commits.</returns>
public static List<Commit> Load(Repository repo, string limit) {
List<Commit> commits = new List<Commit>();
Commit current = null;
bool bSkippingGpgsig = false;
bool findHead = false;
repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => {
if (bSkippingGpgsig) {
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false;
return;
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
bSkippingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
current = new Commit();
ParseSHA(current, line.Substring("commit ".Length));
if (!findHead) findHead = current.IsHEAD;
return;
}
if (current == null) return;
if (line.StartsWith("tree ", StringComparison.Ordinal)) {
return;
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) {
current.Parents.Add(line.Substring("parent ".Length));
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
current.Author.Parse(line);
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
current.Committer.Parse(line);
} else if (string.IsNullOrEmpty(current.Subject)) {
current.Subject = line.Trim();
} else {
current.Message += (line.Trim() + "\n");
}
});
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
if (!findHead && commits.Count > 0) {
var startInfo = new ProcessStartInfo();
startInfo.FileName = Preference.Instance.GitExecutable;
startInfo.Arguments = $"merge-base --is-ancestor {commits[0].SHA} HEAD";
startInfo.WorkingDirectory = repo.Path;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = false;
startInfo.RedirectStandardError = false;
var proc = new Process() { StartInfo = startInfo };
proc.Start();
proc.WaitForExit();
commits[0].IsMerged = proc.ExitCode == 0;
proc.Close();
}
return commits;
}
/// <summary>
/// Get changed file list.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
var changes = new List<Change>();
var regex = new Regex(@"^[MADRC]\d*\s*.*$");
var errs = repo.RunCommand($"show --name-status {SHA}", line => {
if (!regex.IsMatch(line)) return;
var change = Change.Parse(line, true);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Get revision files.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<string> GetFiles(Repository repo) {
var files = new List<string>();
var errs = repo.RunCommand($"ls-tree --name-only -r {SHA}", line => {
files.Add(line);
});
if (errs != null) App.RaiseError(errs);
return files;
}
/// <summary>
/// Get file content.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <returns></returns>
public string GetTextFileContent(Repository repo, string file) {
var data = new List<string>();
var isBinary = false;
var count = 0;
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
if (isBinary) return;
count++;
if (data.Count >= 1000) return;
if (line.IndexOf('\0') >= 0) {
isBinary = true;
data.Clear();
data.Add("BINARY FILE PREVIEW NOT SUPPORTED!");
return;
}
data.Add(line);
});
if (!isBinary && count > 1000) {
data.Add("...");
data.Add($"Total {count} lines. Hide {count-1000} lines.");
}
if (errs != null) App.RaiseError(errs);
return string.Join("\n", data);
}
private static void ParseSHA(Commit commit, string data) {
var decoratorStart = data.IndexOf('(');
if (decoratorStart < 0) {
commit.SHA = data.Trim();
return;
}
commit.SHA = data.Substring(0, decoratorStart).Trim();
var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs) {
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.Tag,
Name = d.Substring(15).Trim()
});
} else if (d.EndsWith("/HEAD")) {
continue;
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
commit.IsHEAD = true;
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim()
});
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim()
});
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim()
});
}
}
}
}
}

21
Git/Decorator.cs Normal file
View file

@ -0,0 +1,21 @@
namespace SourceGit.Git {
/// <summary>
/// Decorator type.
/// </summary>
public enum DecoratorType {
None,
CurrentBranchHead,
LocalBranchHead,
RemoteBranchHead,
Tag,
}
/// <summary>
/// Commit decorator.
/// </summary>
public class Decorator {
public DecoratorType Type { get; set; }
public string Name { get; set; }
}
}

202
Git/MergeTool.cs Normal file
View file

@ -0,0 +1,202 @@
using Microsoft.Win32;
using SourceGit.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// External merge tool
/// </summary>
public class MergeTool {
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Executable file name.
/// </summary>
public string ExecutableName { get; set; }
/// <summary>
/// Command line parameter.
/// </summary>
public string Parameter { get; set; }
/// <summary>
/// Auto finder.
/// </summary>
public Func<string> Finder { get; set; }
/// <summary>
/// Is this merge tool configured.
/// </summary>
public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName);
/// <summary>
/// Supported merge tools.
/// </summary>
public static List<MergeTool> Supported = new List<MergeTool>() {
new MergeTool("--", "", "", FindInvalid),
new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge),
new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge),
new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode),
};
/// <summary>
/// Finder for invalid merge tool.
/// </summary>
/// <returns></returns>
public static string FindInvalid() {
return "--";
}
/// <summary>
/// Find araxis merge tool install path.
/// </summary>
/// <returns></returns>
public static string FindAraxisMerge() {
var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find kdiff3.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindKDiff3() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext");
if (kdiff == null) return "";
return kdiff.GetValue("diffcommand") as string;
}
/// <summary>
/// Finder for p4merge
/// </summary>
/// <returns></returns>
public static string FindP4Merge() {
var path = @"C:\Program Files\Perforce\p4merge.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find BComp.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindBCompare() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare");
if (bc == null) return "";
var exec = bc.GetValue("ExePath") as string;
var dir = Path.GetDirectoryName(exec);
return $"{dir}\\BComp.exe";
}
/// <summary>
/// Find TortoiseMerge.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindTortoiseMerge() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
if (tortoiseSVN == null) return "";
return tortoiseSVN.GetValue("TMergePath") as string;
}
/// <summary>
/// Find vsDiffMerge.exe.
/// </summary>
/// <returns></returns>
public static string FindVSMerge() {
var dir = @"C:\Program Files (x86)\Microsoft Visual Studio";
if (Directory.Exists($"{dir}\\2019")) {
dir += "\\2019";
} else if (Directory.Exists($"{dir}\\2017")) {
dir += "\\2017";
} else {
return "";
}
if (Directory.Exists($"{dir}\\Community")) {
dir += "\\Community";
} else if (Directory.Exists($"{dir}\\Enterprise")) {
dir += "\\Enterprise";
} else if (Directory.Exists($"{dir}\\Professional")) {
dir += "\\Professional";
} else {
return "";
}
return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe";
}
/// <summary>
/// Find VSCode executable file path.
/// </summary>
/// <returns></returns>
public static string FindVSCode() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
return "";
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name"></param>
/// <param name="exe"></param>
/// <param name="param"></param>
/// <param name="finder"></param>
public MergeTool(string name, string exe, string param, Func<string> finder) {
Name = name;
ExecutableName = exe;
Parameter = param;
Finder = finder;
}
}
}

296
Git/Preference.cs Normal file
View file

@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace SourceGit.Git {
/// <summary>
/// User's preference settings. Serialized to
/// </summary>
public class Preference {
/// <summary>
/// Group(Virtual folder) for watched repositories.
/// </summary>
public class Group {
/// <summary>
/// Unique ID of this group.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Parent ID.
/// </summary>
public string ParentId { get; set; }
/// <summary>
/// Cache UI IsExpended status.
/// </summary>
public bool IsExpended { get; set; }
}
#region STATICS
/// <summary>
/// Storage path for Preference.
/// </summary>
private static readonly string SAVE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"preference.xml");
/// <summary>
/// Runtime singleton instance.
/// </summary>
private static Preference instance = null;
public static Preference Instance {
get {
if (instance == null) Load();
return instance;
}
set {
instance = value;
}
}
#endregion
#region SETTING_GIT
/// <summary>
/// Git executable file path.
/// </summary>
public string GitExecutable { get; set; }
/// <summary>
/// Default clone directory.
/// </summary>
public string GitDefaultCloneDir { get; set; }
#endregion
#region SETTING_MERGE_TOOL
/// <summary>
/// Selected merge tool.
/// </summary>
public int MergeTool { get; set; } = 0;
/// <summary>
/// Executable file path for merge tool.
/// </summary>
public string MergeExecutable { get; set; } = "--";
#endregion
#region SETTING_UI
/// <summary>
/// Main window's width
/// </summary>
public double UIMainWindowWidth { get; set; }
/// <summary>
/// Main window's height
/// </summary>
public double UIMainWindowHeight { get; set; }
/// <summary>
/// Use light color theme.
/// </summary>
public bool UIUseLightTheme { get; set; }
/// <summary>
/// Show/Hide tags' list view.
/// </summary>
public bool UIShowTags { get; set; } = true;
/// <summary>
/// Use horizontal layout for histories.
/// </summary>
public bool UIUseHorizontalLayout { get; set; }
/// <summary>
/// Use list instead of tree in unstaged view
/// </summary>
public bool UIUseListInUnstaged { get; set; }
/// <summary>
/// Use list instead of tree in staged view.
/// </summary>
public bool UIUseListInStaged { get; set; }
/// <summary>
/// Use list instead of tree in change view.
/// </summary>
public bool UIUseListInChanges { get; set; }
#endregion
#region SETTING_REPOS
/// <summary>
/// Groups for repositories.
/// </summary>
public List<Group> Groups { get; set; } = new List<Group>();
/// <summary>
/// Watched repositories.
/// </summary>
public List<Repository> Repositories { get; set; } = new List<Git.Repository>();
#endregion
#region METHODS_LOAD_SAVE
/// <summary>
/// Load preference from disk.
/// </summary>
/// <returns>Loaded preference instance.</returns>
public static void Load() {
if (!File.Exists(SAVE_PATH)) {
instance = new Preference();
return;
}
var stream = new FileStream(SAVE_PATH, FileMode.Open);
var reader = new XmlSerializer(typeof(Preference));
instance = (Preference)reader.Deserialize(stream);
stream.Close();
}
/// <summary>
/// Save current preference into disk.
/// </summary>
public static void Save() {
if (instance == null) return;
var dir = Path.GetDirectoryName(SAVE_PATH);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var stream = new FileStream(SAVE_PATH, FileMode.Create);
var writer = new XmlSerializer(typeof(Preference));
writer.Serialize(stream, instance);
stream.Flush();
stream.Close();
}
#endregion
#region METHODS_ON_GROUP
/// <summary>
/// Add new group(virtual folder).
/// </summary>
/// <param name="name">Display name.</param>
/// <param name="parentId">Parent group ID.</param>
/// <returns>Added group instance.</returns>
public Group AddGroup(string name, string parentId) {
var group = new Group() {
Name = name,
Id = Guid.NewGuid().ToString(),
ParentId = parentId,
IsExpended = false,
};
Groups.Add(group);
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
return group;
}
/// <summary>
/// Find group by ID.
/// </summary>
/// <param name="id">Unique ID</param>
/// <returns>Founded group's instance.</returns>
public Group FindGroup(string id) {
foreach (var group in Groups) {
if (group.Id == id) return group;
}
return null;
}
/// <summary>
/// Rename group.
/// </summary>
/// <param name="id">Unique ID</param>
/// <param name="newName">New name.</param>
public void RenameGroup(string id, string newName) {
foreach (var group in Groups) {
if (group.Id == id) {
group.Name = newName;
break;
}
}
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a group.
/// </summary>
/// <param name="id">Unique ID</param>
public void RemoveGroup(string id) {
int removedIdx = -1;
for (int i = 0; i < Groups.Count; i++) {
if (Groups[i].Id == id) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
}
#endregion
#region METHODS_ON_REPOS
/// <summary>
/// Add repository.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="groupId">Group's ID</param>
/// <returns>Added repository instance.</returns>
public Repository AddRepository(string path, string groupId) {
var repo = FindRepository(path);
if (repo != null) return repo;
var dir = new DirectoryInfo(path);
repo = new Repository() {
Path = dir.FullName,
Name = dir.Name,
GroupId = groupId,
LastOpenTime = 0,
};
Repositories.Add(repo);
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
return repo;
}
/// <summary>
/// Find repository by path.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <returns>Founded repository instance.</returns>
public Repository FindRepository(string path) {
var dir = new DirectoryInfo(path);
foreach (var repo in Repositories) {
if (repo.Path == dir.FullName) return repo;
}
return null;
}
/// <summary>
/// Change a repository's display name in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="newName">New name</param>
public void RenameRepository(string path, string newName) {
var repo = FindRepository(path);
if (repo == null) return;
repo.Name = newName;
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a repository in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
public void RemoveRepository(string path) {
var dir = new DirectoryInfo(path);
var removedIdx = -1;
for (int i = 0; i < Repositories.Count; i++) {
if (Repositories[i].Path == dir.FullName) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Repositories.RemoveAt(removedIdx);
}
#endregion
}
}

93
Git/Remote.cs Normal file
View file

@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git remote
/// </summary>
public class Remote {
private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
/// <summary>
/// Name of this remote
/// </summary>
public string Name { get; set; }
/// <summary>
/// URL
/// </summary>
public string URL { get; set; }
/// <summary>
/// Parsing remote
/// </summary>
/// <param name="repo">Repository</param>
/// <returns></returns>
public static List<Remote> Load(Repository repo) {
var remotes = new List<Remote>();
var added = new List<string>();
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;
}
/// <summary>
/// Add new remote
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
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);
}
/// <summary>
/// Delete remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="remote"></param>
public static void Delete(Repository repo, string remote) {
var errs = repo.RunCommand($"remote remove {remote}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Edit remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
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);
}
}
}
}

1059
Git/Repository.cs Normal file

File diff suppressed because it is too large Load diff

101
Git/Stash.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// Git stash
/// </summary>
public class Stash {
/// <summary>
/// SHA for this stash
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Stash push.
/// </summary>
/// <param name="repo"></param>
/// <param name="includeUntracked"></param>
/// <param name="message"></param>
/// <param name="files"></param>
public static void Push(Repository repo, bool includeUntracked, string message, List<string> files) {
string specialFiles = "";
if (files.Count > 0) {
specialFiles = " --";
foreach (var f in files) specialFiles += $" \"{f}\"";
}
string args = "stash push ";
if (includeUntracked) args += "-u ";
if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" ";
var errs = repo.RunCommand(args + specialFiles, null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Get changed file list in this stash.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
List<Change> changes = new List<Change>();
var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => {
var change = Change.Parse(line);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Apply stash.
/// </summary>
/// <param name="repo"></param>
public void Apply(Repository repo) {
var errs = repo.RunCommand($"stash apply -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Pop stash
/// </summary>
/// <param name="repo"></param>
public void Pop(Repository repo) {
var errs = repo.RunCommand($"stash pop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Drop stash
/// </summary>
/// <param name="repo"></param>
public void Drop(Repository repo) {
var errs = repo.RunCommand($"stash drop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

118
Git/Tag.cs Normal file
View file

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git tag.
/// </summary>
public class Tag {
private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load all tags
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public static List<Tag> Load(Repository repo) {
var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
var tags = new List<Tag>();
repo.RunCommand(args, line => {
var match = FORMAT.Match(line);
if (!match.Success) return;
var name = match.Groups[1].Value;
var commit = match.Groups[2].Value;
var dereference = match.Groups[3].Value;
if (string.IsNullOrEmpty(dereference)) {
tags.Add(new Tag() {
Name = name,
SHA = commit,
});
} else {
tags.Add(new Tag() {
Name = name,
SHA = dereference,
});
}
});
return tags;
}
/// <summary>
/// Add new tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
/// <param name="message"></param>
public static void Add(Repository repo, string name, string startPoint, string message) {
var args = $"tag -a {name} {startPoint} ";
if (!string.IsNullOrEmpty(message)) {
string temp = Path.GetTempFileName();
File.WriteAllText(temp, message);
args += $"-F \"{temp}\"";
} else {
args += $"-m {name}";
}
var errs = repo.RunCommand(args, null);
if (errs != null) App.RaiseError(errs);
else repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Delete tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="push"></param>
public static void Delete(Repository repo, string name, bool push) {
var errs = repo.RunCommand($"tag --delete {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
if (push) {
var remotes = repo.Remotes();
foreach (var r in remotes) {
repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null);
}
}
repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Push tag to remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="remote"></param>
public static void Push(Repository repo, string name, string remote) {
var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

42
Git/User.cs Normal file
View file

@ -0,0 +1,42 @@
using System;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git user.
/// </summary>
public class User {
private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+");
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Email.
/// </summary>
public string Email { get; set; } = "";
/// <summary>
/// Operation time.
/// </summary>
public string Time { get; set; } = "";
/// <summary>
/// Parse user from raw string.
/// </summary>
/// <param name="data">Raw string</param>
public void Parse(string data) {
var match = FORMAT.Match(data);
if (!match.Success) return;
var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(int.Parse(match.Groups[3].Value));
Name = match.Groups[1].Value;
Email = match.Groups[2].Value;
Time = time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
}
}

274
Helpers/CommitGraph.cs Normal file
View file

@ -0,0 +1,274 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Tools to parse commit graph.
/// </summary>
public class CommitGraphMaker {
/// <summary>
/// Sizes
/// </summary>
public static readonly double UNIT_WIDTH = 12;
public static readonly double HALF_WIDTH = 6;
public static readonly double DOUBLE_WIDTH = 24;
public static readonly double UNIT_HEIGHT = 24;
public static readonly double HALF_HEIGHT = 12;
/// <summary>
/// Colors
/// </summary>
public static Brush[] Colors = new Brush[] {
Brushes.Orange,
Brushes.ForestGreen,
Brushes.Gold,
Brushes.Magenta,
Brushes.Red,
Brushes.Gray,
Brushes.Turquoise,
Brushes.Olive,
};
/// <summary>
/// Helpers to draw lines.
/// </summary>
public class LineHelper {
private double lastX = 0;
private double lastY = 0;
/// <summary>
/// Parent commit id.
/// </summary>
public string Next { get; set; }
/// <summary>
/// Is merged into this tree.
/// </summary>
public bool IsMerged { get; set; }
/// <summary>
/// Points in line
/// </summary>
public List<Point> Points { get; set; }
/// <summary>
/// Brush to draw line
/// </summary>
public Brush Brush { get; set; }
/// <summary>
/// Current horizontal offset.
/// </summary>
public double HorizontalOffset => lastX;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="nextCommitId">Parent commit id</param>
/// <param name="isMerged">Is merged in tree</param>
/// <param name="colorIdx">Color index</param>
/// <param name="startPoint">Start point</param>
public LineHelper(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) {
Next = nextCommitId;
IsMerged = isMerged;
Points = new List<Point>() { startPoint };
Brush = Colors[colorIdx % Colors.Length];
lastX = startPoint.X;
lastY = startPoint.Y;
}
/// <summary>
/// Line to.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="isEnd"></param>
public void AddPoint(double x, double y, bool isEnd = false) {
if (x > lastX) {
Points.Add(new Point(lastX, lastY));
Points.Add(new Point(x, y - HALF_HEIGHT));
} else if (x < lastX) {
Points.Add(new Point(lastX, lastY + HALF_HEIGHT));
Points.Add(new Point(x, y));
}
lastX = x;
lastY = y;
if (isEnd) {
var last = Points.Last();
if (last.X != lastX || last.Y != lastY) Points.Add(new Point(lastX, lastY));
}
}
}
/// <summary>
/// Short link between two commits.
/// </summary>
public struct ShortLink {
public Point Start;
public Point Control;
public Point End;
public Brush Brush;
}
/// <summary>
/// Dot
/// </summary>
public struct Dot {
public double X;
public double Y;
public Brush Color;
}
/// <summary>
/// Independent lines in graph
/// </summary>
public List<LineHelper> Lines { get; set; } = new List<LineHelper>();
/// <summary>
/// Short links.
/// </summary>
public List<ShortLink> Links { get; set; } = new List<ShortLink>();
/// <summary>
/// All dots.
/// </summary>
public List<Dot> Dots { get; set; } = new List<Dot>();
/// <summary>
/// Highlight commit id.
/// </summary>
public string Highlight { get; set; }
/// <summary>
/// Parse commits.
/// </summary>
/// <param name="commits"></param>
/// <returns></returns>
public static CommitGraphMaker Parse(List<Git.Commit> commits) {
CommitGraphMaker maker = new CommitGraphMaker();
List<LineHelper> unsolved = new List<LineHelper>();
List<LineHelper> ended = new List<LineHelper>();
Dictionary<string, LineHelper> currentMap = new Dictionary<string, LineHelper>();
double offsetY = -HALF_HEIGHT;
int colorIdx = 0;
for (int i = 0; i < commits.Count; i++) {
Git.Commit commit = commits[i];
LineHelper major = null;
bool isMerged = commit.IsHEAD || commit.IsMerged;
int oldCount = unsolved.Count;
// 更新Y坐标
offsetY += UNIT_HEIGHT;
// 找到当前的分支的HEAD用于默认选中
if (maker.Highlight == null && commit.IsHEAD) {
maker.Highlight = commit.SHA;
}
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) {
if (l.Next == commit.SHA) {
if (major == null) {
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0) {
major.Next = commit.Parents[0];
if (!currentMap.ContainsKey(major.Next)) currentMap.Add(major.Next, major);
} else {
major.Next = "ENDED";
}
major.AddPoint(offsetX, offsetY);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!currentMap.ContainsKey(l.Next)) currentMap.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.AddPoint(offsetX, offsetY);
}
}
// 处理本提交为非当前分支HEAD的情况创建新依赖线路
if (major == null && commit.Parents.Count > 0) {
offsetX += UNIT_WIDTH;
major = new LineHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major);
colorIdx++;
}
// 确定本提交的点的位置
Point position = new Point(offsetX, offsetY);
if (major != null) {
major.IsMerged = isMerged;
position.X = major.HorizontalOffset;
position.Y = offsetY;
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = major.Brush });
} else {
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = Brushes.Orange });
}
// 处理本提交的其他依赖
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (currentMap.ContainsKey(parent)) {
var l = currentMap[parent];
var link = new ShortLink();
link.Start = position;
link.End = new Point(l.HorizontalOffset, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Brush = l.Brush;
maker.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
unsolved.Add(new LineHelper(commit.Parents[j], isMerged, colorIdx, position));
colorIdx++;
}
}
// 处理已终止的线
foreach (var l in ended) {
l.AddPoint(position.X, position.Y, true);
maker.Lines.Add(l);
unsolved.Remove(l);
}
// 加入本次提交
commit.IsMerged = isMerged;
commit.GraphOffset = System.Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH);
// 清理临时数据
ended.Clear();
currentMap.Clear();
}
// 处理尚未终结的线
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
path.AddPoint((i + 0.5) * UNIT_WIDTH, (commits.Count - 0.5) * UNIT_HEIGHT, true);
maker.Lines.Add(path);
}
unsolved.Clear();
// 处理默认选中异常
if (maker.Highlight == null && commits.Count > 0) {
maker.Highlight = commits[0].SHA;
}
return maker;
}
}
}

143
Helpers/TextBoxHelper.cs Normal file
View file

@ -0,0 +1,143 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Attached properties to TextBox.
/// </summary>
public static class TextBoxHelper {
/// <summary>
/// Placeholder property
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder",
typeof(string),
typeof(TextBoxHelper),
new PropertyMetadata(string.Empty, OnPlaceholderChanged));
/// <summary>
/// Vertical alignment for placeholder.
/// </summary>
public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached(
"PlaceholderBaseline",
typeof(AlignmentY),
typeof(TextBoxHelper),
new PropertyMetadata(AlignmentY.Center));
/// <summary>
/// Property to store generated placeholder brush.
/// </summary>
public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached(
"PlaceholderBrush",
typeof(Brush),
typeof(TextBoxHelper),
new PropertyMetadata(Brushes.Transparent));
/// <summary>
/// Triggered when placeholder changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var textBox = d as TextBox;
if (textBox != null) textBox.Loaded += OnTextLoaded;
}
/// <summary>
/// Setter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholder(UIElement element, string value) {
element.SetValue(PlaceholderProperty, value);
}
/// <summary>
/// Getter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static string GetPlaceholder(UIElement element) {
return (string)element.GetValue(PlaceholderProperty);
}
/// <summary>
/// Setter for PlaceholderBaseline property
/// </summary>
/// <param name="element"></param>
/// <param name="align"></param>
public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) {
element.SetValue(PlaceholderBaselineProperty, align);
}
/// <summary>
/// Setter for PlaceholderBaseline property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static AlignmentY GetPlaceholderBaseline(UIElement element) {
return (AlignmentY)element.GetValue(PlaceholderBaselineProperty);
}
/// <summary>
/// Setter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholderBrush(UIElement element, Brush value) {
element.SetValue(PlaceholderBrushProperty, value);
}
/// <summary>
/// Getter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static Brush GetPlaceholderBrush(UIElement element) {
return (Brush)element.GetValue(PlaceholderBrushProperty);
}
/// <summary>
/// Set placeholder as background when TextBox was loaded.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTextLoaded(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (textBox == null) return;
Label placeholder = new Label();
placeholder.Content = textBox.GetValue(PlaceholderProperty);
VisualBrush brush = new VisualBrush();
brush.AlignmentX = AlignmentX.Left;
brush.AlignmentY = GetPlaceholderBaseline(textBox);
brush.TileMode = TileMode.None;
brush.Stretch = Stretch.None;
brush.Opacity = 0.3;
brush.Visual = placeholder;
textBox.SetValue(PlaceholderBrushProperty, brush);
textBox.Background = brush;
textBox.TextChanged += OnTextChanged;
OnTextChanged(textBox, null);
}
/// <summary>
/// Dynamically hide/show placeholder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTextChanged(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (string.IsNullOrEmpty(textBox.Text)) {
textBox.Background = textBox.GetValue(PlaceholderBrushProperty) as Brush;
} else {
textBox.Background = Brushes.Transparent;
}
}
}
}

329
Helpers/TreeViewHelper.cs Normal file
View file

@ -0,0 +1,329 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Helper class to enable multi-selection of TreeView
/// </summary>
public static class TreeViewHelper {
/// <summary>
/// Definition of EnableMultiSelection property.
/// </summary>
public static readonly DependencyProperty EnableMultiSelectionProperty =
DependencyProperty.RegisterAttached(
"EnableMultiSelection",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged));
/// <summary>
/// Getter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetEnableMultiSelection(DependencyObject obj) {
return (bool)obj.GetValue(EnableMultiSelectionProperty);
}
/// <summary>
/// Setter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetEnableMultiSelection(DependencyObject obj, bool value) {
obj.SetValue(EnableMultiSelectionProperty, value);
}
/// <summary>
/// Definition of SelectedItems
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems",
typeof(ObservableCollection<TreeViewItem>),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Getter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static ObservableCollection<TreeViewItem> GetSelectedItems(DependencyObject obj) {
return (ObservableCollection<TreeViewItem>)obj.GetValue(SelectedItemsProperty);
}
/// <summary>
/// Setter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetSelectedItems(DependencyObject obj, ObservableCollection<TreeViewItem> value) {
obj.SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Definition of IsChecked property.
/// </summary>
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.RegisterAttached(
"IsChecked",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false));
/// <summary>
/// Getter of IsChecked Property.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetIsChecked(DependencyObject obj) {
return (bool)obj.GetValue(IsCheckedProperty);
}
/// <summary>
/// Setter of IsChecked property
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetIsChecked(DependencyObject obj, bool value) {
obj.SetValue(IsCheckedProperty, value);
}
/// <summary>
/// Definition of MultiSelectionChangedEvent
/// </summary>
public static readonly RoutedEvent MultiSelectionChangedEvent =
EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper));
/// <summary>
/// Add handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Remove handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Select all items in tree.
/// </summary>
/// <param name="tree"></param>
public static void SelectWholeTree(TreeView tree) {
var selected = GetSelectedItems(tree);
selected.Clear();
SelectAll(selected, tree);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Selected one item by DataContext
/// </summary>
/// <param name="tree"></param>
/// <param name="obj"></param>
public static void SelectOneByContext(TreeView tree, object obj) {
var item = FindTreeViewItemByDataContext(tree, obj);
if (item != null) {
var selected = GetSelectedItems(tree);
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
}
/// <summary>
/// Unselect the whole tree.
/// </summary>
/// <param name="tree"></param>
public static void UnselectTree(TreeView tree) {
var selected = GetSelectedItems(tree);
if (selected.Count == 0) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Hooks when EnableMultiSelection changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var tree = d as TreeView;
if (tree != null && (bool)e.NewValue) {
tree.SetValue(SelectedItemsProperty, new ObservableCollection<TreeViewItem>());
tree.PreviewMouseDown += OnTreeMouseDown;
}
}
/// <summary>
/// Preview mouse button select.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTreeMouseDown(object sender, MouseButtonEventArgs e) {
var tree = sender as TreeView;
if (tree == null) return;
var hit = VisualTreeHelper.HitTest(tree, e.GetPosition(tree));
if (hit == null || hit.VisualHit is null) return;
var item = FindTreeViewItem(hit.VisualHit as UIElement);
if (item == null) return;
var selected = GetSelectedItems(tree);
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
if (GetIsChecked(item)) {
selected.Remove(item);
item.SetValue(IsCheckedProperty, false);
} else {
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && selected.Count > 0) {
var last = selected.Last();
if (last == item) return;
var lastPos = last.PointToScreen(new Point(0, 0));
var curPos = item.PointToScreen(new Point(0, 0));
if (lastPos.Y > curPos.Y) {
SelectRange(selected, tree, item, last);
} else {
SelectRange(selected, tree, last, item);
}
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else if (e.RightButton == MouseButtonState.Pressed) {
if (GetIsChecked(item)) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else {
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Find TreeViewItem by child element.
/// </summary>
/// <param name="item"></param>
/// <param name="child"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItem(DependencyObject child) {
if (child == null) return null;
if (child is TreeViewItem) return child as TreeViewItem;
if (child is TreeView) return null;
return FindTreeViewItem(VisualTreeHelper.GetParent(child));
}
/// <summary>
/// Find TreeViewItem by DataContext
/// </summary>
/// <param name="control"></param>
/// <param name="obj"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl control, object obj) {
if (control == null) return null;
if (control.DataContext == obj) return control as TreeViewItem;
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
var found = FindTreeViewItemByDataContext(child, obj);
if (found != null) return found;
}
return null;
}
/// <summary>
/// Select all items.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
private static void SelectAll(ObservableCollection<TreeViewItem> selected, ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
SelectAll(selected, child);
}
}
/// <summary>
/// Select range items between given.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="started"></param>
private static int SelectRange(ObservableCollection<TreeViewItem> selected, ItemsControl control, TreeViewItem from, TreeViewItem to, int matches = 0) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (matches == 1) {
if (child == to) return 2;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
if (TryEndRangeSelection(selected, child, to)) return 2;
} else if (child == from) {
matches = 1;
if (TryEndRangeSelection(selected, child, to)) return 2;
} else {
matches = SelectRange(selected, child, from, to, matches);
if (matches == 2) return 2;
}
}
return matches;
}
private static bool TryEndRangeSelection(ObservableCollection<TreeViewItem> selected, TreeViewItem control, TreeViewItem end) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (child == end) {
return true;
} else {
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
var ended = TryEndRangeSelection(selected, child, end);
if (ended) return true;
}
}
return false;
}
}
}

124
Helpers/Validations.cs Normal file
View file

@ -0,0 +1,124 @@
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Controls;
namespace SourceGit.Helpers {
/// <summary>
/// Validate clone folder.
/// </summary>
public class CloneFolderRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badPath = "EXISTS and FULL ACCESS CONTROL needed";
var path = value as string;
return Directory.Exists(path) ? ValidationResult.ValidResult : new ValidationResult(false, badPath);
}
}
/// <summary>
/// Validate git remote URL
/// </summary>
public class RemoteUriRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badUrl = "Remote git URL not supported";
return Git.Repository.IsValidUrl(value as string) ? ValidationResult.ValidResult : new ValidationResult(false, badUrl);
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class RemoteNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var remotes = Repo.Remotes();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Remote name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for remote. Regex: ^[\\w\\-\\.]+$");
foreach (var t in remotes) {
if (t.Name == name) {
return new ValidationResult(false, $"Remote '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate branch name.
/// </summary>
public class BranchNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public string Prefix { get; set; } = "";
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-/\.]+$");
var name = value as string;
var branches = Repo.Branches();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Branch name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for branch. Regex: ^[\\w\\-/\\.]+$");
name = Prefix + name;
foreach (var b in branches) {
if (b.Name == name) {
return new ValidationResult(false, $"Branch '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class TagNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var tags = Repo.Tags();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Tag name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for tag. Regex: ^[\\w\\-\\.]+$");
foreach (var t in tags) {
if (t.Name == name) {
return new ValidationResult(false, $"Tag '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for commit subject.
/// </summary>
public class CommitSubjectRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var subject = value as string;
return string.IsNullOrWhiteSpace(subject) ? new ValidationResult(false, "Commit subject can NOT be empty") : ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for patch file.
/// </summary>
public class PatchFileRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var path = value as string;
var succ = !string.IsNullOrEmpty(path) && File.Exists(path);
return !succ ? new ValidationResult(false, "Invalid path for patch file") : ValidationResult.ValidResult;
}
}
}

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 leo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

BIN
Preview_Dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
Preview_Light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,56 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Source Git")]
[assembly: AssemblyDescription("OpenSource GIT client for Windows")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Leo")]
[assembly: AssemblyProduct("Source Git")]
[assembly: AssemblyCopyright("Copyright © longshuang@msn.cn 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
#pragma warning disable CS7035
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

62
Properties/Resources.Designer.cs generated Normal file
View file

@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SourceGit.Properties {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if ((resourceMan == null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SourceGit.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

117
Properties/Resources.resx Normal file
View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

26
Properties/Settings.Designer.cs generated Normal file
View file

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SourceGit.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View file

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# SourceGit
开源的Git客户端仅用于Windows 10。单文件无需安装< 500KB
* DarkTheme
![Preview_Dark](./Preview_Dark.png)
* LightTheme
![Preview_Light](./Preview_Light.png)

22
Resources/Controls.xaml Normal file
View file

@ -0,0 +1,22 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Border.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Button.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/CheckBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ComboBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ContextMenu.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/DataGrid.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/HyperLink.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Label.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ListView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Path.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/RadioButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollBar.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollViewer.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TabControl.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TextBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ToggleButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Tooltip.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TreeView.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

59
Resources/Icons.xaml Normal file
View file

@ -0,0 +1,59 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Geometry x:Key="Icon.Git">M1004.824 466.4L557.72 19.328c-25.728-25.76-67.488-25.76-93.28 0L360.568 123.2l78.176 78.176c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96 0 14.816-3.36 28.864-9.376 41.376l127.968 127.968c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96s-42.976 96-96 96-96-42.976-96-96c0-14.816 3.36-28.864 9.376-41.376L521.496 374.624a88.837 88.837 0 0 1-9.376 3.872v266.976c37.28 13.184 64 48.704 64 90.528 0 53.024-42.976 96-96 96s-96-42.976-96-96c0-41.792 26.72-77.344 64-90.528V378.496c-37.28-13.184-64-48.704-64-90.528 0-14.816 3.36-28.864 9.376-41.376l-78.176-78.176L19.416 464.288c-25.76 25.792-25.76 67.52 0 93.28l447.136 447.072c25.728 25.76 67.488 25.76 93.28 0l444.992-444.992c25.76-25.76 25.76-67.552 0-93.28z</Geometry>
<Geometry x:Key="Icon.Minimize">F1M0,6L0,9 9,9 9,6 0,6z</Geometry>
<Geometry x:Key="Icon.Maximize">F1M0,0L0,9 9,9 9,0 0,0 0,3 8,3 8,8 1,8 1,3z</Geometry>
<Geometry x:Key="Icon.Restore">F1M0,10L0,3 3,3 3,0 10,0 10,2 4,2 4,3 7,3 7,6 6,6 6,5 1,5 1,10z M1,10L7,10 7,7 10,7 10,2 9,2 9,6 6,6 6,9 1,9z</Geometry>
<Geometry x:Key="Icon.Close">M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z</Geometry>
<Geometry x:Key="Icon.Check">M512 597.33333332m-1.26648097 0a1.26648097 1.26648097 0 1 0 2.53296194 0 1.26648097 1.26648097 0 1 0-2.53296194 0ZM809.691429 392.777143L732.16 314.514286 447.634286 599.771429 292.571429 443.977143 214.308571 521.508571l155.794286 155.794286 77.531429 77.531429 362.057143-362.057143z</Geometry>
<Geometry x:Key="Icon.Loading">M511.680999 0C233.071131 0 6.524722 222.580887 0.12872 499.655715 6.013042 257.886821 189.834154 63.960025 415.740962 63.960025c229.61649 0 415.740162 200.450718 415.740162 447.720175 0 52.958901 42.981137 95.940037 95.940038 95.940037s95.940037-42.981137 95.940037-95.940037c0-282.57539-229.104809-511.6802-511.6802-511.6802z m0 1023.3604c278.609869 0 505.156277-222.580887 511.55228-499.655715-5.884322 241.768894-189.705434 435.69569-415.612242 435.69569-229.61649 0-415.740162-200.450718-415.740163-447.720175 0-52.958901-42.981137-95.940037-95.940037-95.940038s-95.940037 42.981137-95.940037 95.940038c0 282.57539 229.104809 511.6802 511.680199 511.6802z</Geometry>
<Geometry x:Key="Icon.Search">M701.9062029 677.41589899L589.90712068 565.41681675a148.33953321 148.33953321 0 1 0-24.97646381 26.55648342L676.07895931 703.12160261z m-346.38891409-199.50786053a114.97681148 114.97681148 0 1 1 114.85527151 114.97681148A115.09835147 115.09835147 0 0 1 355.45651882 477.90803846z</Geometry>
<Geometry x:Key="Icon.Conflict">M352 64h320L960 352v320L672 960h-320L64 672v-320L352 64z m161.28 362.688L344.128 256 259.584 341.312 428.736 512l-169.152 170.688L344.128 768 513.28 597.312 682.432 768l84.544-85.312L597.824 512l169.152-170.688L682.432 256 513.28 426.688z</Geometry>
<Geometry x:Key="Icon.List">M51.2 204.8h102.4v102.4H51.2V204.8z m204.8 0h716.8v102.4H256V204.8zM51.2 460.8h102.4v102.4H51.2V460.8z m204.8 0h716.8v102.4H256V460.8z m-204.8 256h102.4v102.4H51.2v-102.4z m204.8 0h716.8v102.4H256v-102.4z</Geometry>
<Geometry x:Key="Icon.Tree">M912 737l0 150L362 887l0-100 0-50 0-150 0-150 0-150L112 287l0-150 450 0 0 150L412 287l0 150L912 437l0 150L412 587l0 150L912 737z</Geometry>
<Geometry x:Key="Icon.MoveUp">M868 545.5L536.1 163c-12.7-14.7-35.5-14.7-48.3 0L156 545.5c-4.5 5.2-0.8 13.2 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z</Geometry>
<Geometry x:Key="Icon.MoveDown">M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861c12.7 14.7 35.5 14.7 48.3 0L868 478.5c4.5-5.2 0.8-13.2-6-13.2z</Geometry>
<Geometry x:Key="Icon.StageSelected">M509.44 546.304l270.848-270.912 90.56 90.56-347.52 349.056-0.832-0.768-13.056 13.056-362.624-361.28 91.136-91.264z</Geometry>
<Geometry x:Key="Icon.StageAll">M256 224l1e-8 115.2L512 544l255.99999999-204.8 1e-8-115.2-256 204.80000001L256 224zM512 684.8l-256-204.8L256 595.2 512 800 768 595.2l0-115.2L512 684.8z</Geometry>
<Geometry x:Key="Icon.UnstageSelected">M169.5 831l342.8-341.9L855.1 831l105.3-105.3-448.1-448.1L64.2 725.7 169.5 831z</Geometry>
<Geometry x:Key="Icon.UnstageAll">M768 800V684.8L512 480 256 684.8V800l256-204.8L768 800zM512 339.2L768 544V428.8L512 224 256 428.8V544l256-204.8z</Geometry>
<Geometry x:Key="Icon.Preference">M64.2 180.3h418.2v120.6H64.2zM64.2 461.7h358.5v120.6H64.2zM64.2 723.1h418.2v120.6H64.2zM601.9 180.3h358.5v120.6H601.9zM482.4 119.9h179.2v241.3H482.4zM303.2 401.4h179.2v241.3H303.2zM482.4 662.8h179.2v241.3H482.4zM540.3 461.7h420.1v120.6H540.3zM601.9 723.1h358.5v120.6H601.9z</Geometry>
<Geometry x:Key="Icon.Info">M 38,19C 48.4934,19 57,27.5066 57,38C 57,48.4934 48.4934,57 38,57C 27.5066,57 19,48.4934 19,38C 19,27.5066 27.5066,19 38,19 Z M 33.25,33.25L 33.25,36.4167L 36.4166,36.4167L 36.4166,47.5L 33.25,47.5L 33.25,50.6667L 44.3333,50.6667L 44.3333,47.5L 41.1666,47.5L 41.1666,36.4167L 41.1666,33.25L 33.25,33.25 Z M 38.7917,25.3333C 37.48,25.3333 36.4167,26.3967 36.4167,27.7083C 36.4167,29.02 37.48,30.0833 38.7917,30.0833C 40.1033,30.0833 41.1667,29.02 41.1667,27.7083C 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333 Z</Geometry>
<Geometry x:Key="Icon.Folder">M64 864h896V288h-396.224a64 64 0 0 1-57.242667-35.376L460.224 160H64v704z m-64 32V128a32 32 0 0 1 32-32h448a32 32 0 0 1 28.624 17.690667L563.776 224H992a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H32a32 32 0 0 1-32-32z</Geometry>
<Geometry x:Key="Icon.Folder.Fill">M448 64l128 128h448v768H0V64z</Geometry>
<Geometry x:Key="Icon.Folder.Open">M832 960l192-512H192L0 960zM128 384L0 960V128h288l128 128h416v128z</Geometry>
<Geometry x:Key="Icon.Manager">M780.512477 870.01493 780.512477 512l89.502453 0-358.013907-358.01493L153.98507 512l89.503477 0 0 358.01493 179.007977 0L422.496523 735.759203c0-49.427736 40.075741-89.503477 89.503477-89.503477s89.503477 40.075741 89.503477 89.503477l0 134.255727L780.512477 870.01493z</Geometry>
<Geometry x:Key="Icon.Clone">M928 0c53.02 0 96 42.98 96 96v576c0 53.02-42.98 96-96 96H352c-53.02 0-96-42.98-96-96V96c0-53.02 42.98-96 96-96h576M352 832c-88.224 0-160-71.776-160-160V256H96c-53.02 0-96 42.98-96 96v576c0 53.02 42.98 96 96 96h576c53.02 0 96-42.98 96-96v-96H352z</Geometry>
<Geometry x:Key="Icon.Book">M384 576H320V512h64v64z m0-192H320v64h64V384z m0-128H320v64h64V256z m0-128H320v64h64V128z m512-64v768c0 35.2-28.8 64-64 64H512v128l-96-96L320 1024v-128H192c-35.2 0-64-28.8-64-64V64c0-35.2 28.8-64 64-64h640c35.2 0 64 28.8 64 64z m-64 640H192v128h128v-64h192v64h320v-128z m0-640H256v576h576V64z</Geometry>
<Geometry x:Key="Icon.Navigator">M989.866667 512L689.493333 802.133333 614.4 729.6 839.68 512 614.4 294.4 689.493333 221.866667z</Geometry>
<Geometry x:Key="Icon.File">M958.656 320H960v639.936A64 64 0 0 1 896.128 1024H191.936A63.872 63.872 0 0 1 128 959.936V64.064A64 64 0 0 1 191.936 0H640v320.96h319.616L958.656 320zM320 544c0 17.152 14.464 32 32.192 32h383.552A32.384 32.384 0 0 0 768 544c0-17.152-14.464-32-32.256-32H352.192A32.448 32.448 0 0 0 320 544z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z</Geometry>
<Geometry x:Key="Icon.Diff">M854.2 306.6L611.3 72.9c-6-5.7-13.9-8.9-22.2-8.9H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h277l219 210.6V824c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V329.6c0-8.7-3.5-17-9.8-23zM553.4 201.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v704c0 17.7 14.3 32 32 32h512c17.7 0 32-14.3 32-32V397.3c0-8.5-3.4-16.6-9.4-22.6L553.4 201.4zM568 753c0 3.8-3.4 7-7.5 7h-225c-4.1 0-7.5-3.2-7.5-7v-42c0-3.8 3.4-7 7.5-7h225c4.1 0 7.5 3.2 7.5 7v42z m0-220c0 3.8-3.4 7-7.5 7H476v84.9c0 3.9-3.1 7.1-7 7.1h-42c-3.8 0-7-3.2-7-7.1V540h-84.5c-4.1 0-7.5-3.2-7.5-7v-42c0-3.9 3.4-7 7.5-7H420v-84.9c0-3.9 3.2-7.1 7-7.1h42c3.9 0 7 3.2 7 7.1V484h84.5c4.1 0 7.5 3.1 7.5 7v42z</Geometry>
<Geometry x:Key="Icon.Filter">M599.22969 424.769286 599.22969 657.383158 424.769286 831.844585 424.769286 424.769286 192.155415 192.155415 831.844585 192.155415Z</Geometry>
<Geometry x:Key="Icon.Vertical">M1024 1024H0V0h1024v1024z m-64-64V320H320V256h640V64H64v896h192V64h64v896z</Geometry>
<Geometry x:Key="Icon.Horizontal">M81.92 81.92v860.16h860.16V81.92H81.92z m802.304 57.856V322.56H139.776V139.776h744.448z m-744.448 240.64H322.56v503.808H139.776V380.416z m240.128 503.808V380.416h504.32v503.808H379.904z</Geometry>
<Geometry x:Key="Icon.Fetch">M1024 896v128H0V704h128v192h768V704h128v192zM576 554.688L810.688 320 896 405.312l-384 384-384-384L213.312 320 448 554.688V0h128v554.688z</Geometry>
<Geometry x:Key="Icon.Pull">M432 0h160c26.6 0 48 21.4 48 48v336h175.4c35.6 0 53.4 43 28.2 68.2L539.4 756.6c-15 15-39.6 15-54.6 0L180.2 452.2c-25.2-25.2-7.4-68.2 28.2-68.2H384V48c0-26.6 21.4-48 48-48z m592 752v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h293.4l98 98c40.2 40.2 105 40.2 145.2 0l98-98H976c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.Push">M592 768h-160c-26.6 0-48-21.4-48-48V384h-175.4c-35.6 0-53.4-43-28.2-68.2L484.6 11.4c15-15 39.6-15 54.6 0l304.4 304.4c25.2 25.2 7.4 68.2-28.2 68.2H640v336c0 26.6-21.4 48-48 48z m432-16v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h272v16c0 61.8 50.2 112 112 112h160c61.8 0 112-50.2 112-112v-16h272c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.SaveStash">M961.3 319.6L512 577.3 62.7 319.6 512 62l449.3 257.6zM512 628.4L185.4 441.6 62.7 512 512 769.6 961.3 512l-122.7-70.4L512 628.4zM512 820.8L185.4 634 62.7 704.3 512 962l449.3-257.7L838.6 634 512 820.8z</Geometry>
<Geometry x:Key="Icon.Apply">M889.259 551.125c-39.638 0-74.112 21.163-93.483 52.608v-0.426c-16.896 30.378-64.597 27.52-81.28-0.811V319.488H445.781c-41.13-6.827-49.92-66.859-15.061-86.23h-0.384c31.445-19.37 52.608-53.845 52.608-93.482 0-60.8-49.28-110.123-110.08-110.123S262.699 78.976 262.699 139.776c0 39.637 21.162 74.112 52.608 93.483h-0.384c34.858 19.37 26.069 79.402-15.062 86.229H31.104v630.23H292.48c47.744 0 59.392-69.505 22.443-81.622h0.938c-44.757-21.376-75.904-66.688-75.904-119.595 0-73.386 59.478-132.864 132.864-132.864 73.387 0 132.864 59.478 132.864 132.864 0 52.907-31.146 98.219-75.904 119.595h0.939c-36.95 12.16-25.344 81.621 22.443 81.621h261.376V719.915c16.682-28.331 64.384-31.19 81.28-0.811v-0.384c19.37 31.445 53.845 52.608 93.482 52.608 60.8 0 110.08-49.28 110.08-110.123 0-60.8-49.322-110.08-110.122-110.08z</Geometry>
<Geometry x:Key="Icon.Terminal">M89.6 806.4h844.8V217.6H89.6v588.8zM0 128h1024v768H0V128z m242.816 577.536L192 654.72l154.304-154.368L192 346.048l50.816-50.816L448 500.352 242.816 705.536z m584.32 13.248H512V640h315.072v78.72z</Geometry>
<Geometry x:Key="Icon.Flow">M508.928 556.125091l92.904727 148.759273h124.462546l-79.639273-79.173819 49.245091-49.524363 164.584727 163.700363-164.631273 163.002182-49.152-49.617454 79.36-78.568728h-162.955636l-95.650909-153.227636 41.472-65.349818z m186.973091-394.705455l164.584727 163.700364-164.631273 163.002182-49.152-49.617455L726.109091 359.936H529.687273l-135.540364 223.976727H139.636364v-69.818182h215.133091l135.586909-223.976727h235.938909l-79.639273-79.173818 49.245091-49.524364z</Geometry>
<Geometry x:Key="Icon.Commit">M795.968 471.04A291.584 291.584 0 0 0 512 256a293.376 293.376 0 0 0-283.968 215.04H0v144h228.032A292.864 292.864 0 0 0 512 832a291.136 291.136 0 0 0 283.968-216.96H1024V471.04h-228.032M512 688A145.856 145.856 0 0 1 366.016 544 144.576 144.576 0 0 1 512 400c80 0 145.984 63.104 145.984 144A145.856 145.856 0 0 1 512 688</Geometry>
<Geometry x:Key="Icon.WorkingCopy">M0 586.459429l403.968 118.784 497.517714-409.892572-385.536 441.490286-1.609143 250.587428 154.916572-204.580571 278.601143 83.456L1170.285714 36.571429z</Geometry>
<Geometry x:Key="Icon.Histories">M24.356571 512A488.155429 488.155429 0 0 1 512 24.356571 488.155429 488.155429 0 0 1 999.643429 512 488.155429 488.155429 0 0 1 512 999.643429 488.155429 488.155429 0 0 1 24.356571 512z m446.976-325.046857v326.656L242.614857 619.227429l51.126857 110.665142 299.52-138.24V186.953143H471.332571z</Geometry>
<Geometry x:Key="Icon.Stashes">M714.624 253.648h-404.8l-57.808 57.328h520.48z m-491.568 85.984v200.624h578.336V339.632z m404.8 143.296h-28.88v-28.64H425.472v28.64h-28.912v-57.312h231.328v57.312z m-404.8 295.12h578.336V559.36H223.056z m173.504-132.704h231.328v57.328h-28.912v-28.656H425.472v28.656h-28.912v-57.328z</Geometry>
<Geometry x:Key="Icon.Branch">M868.736 144.96a144.64 144.64 0 1 0-289.408 0c0 56.064 32.64 107.008 83.456 130.624-4.928 95.552-76.608 128-201.088 174.592-52.48 19.712-110.336 41.6-159.744 74.432V276.16A144.448 144.448 0 0 0 241.664 0.192a144.64 144.64 0 0 0-144.64 144.768c0 58.24 34.688 108.288 84.352 131.2v461.184a144.32 144.32 0 0 0-84.416 131.2 144.704 144.704 0 1 0 289.472 0 144.32 144.32 0 0 0-83.52-130.688c4.992-95.488 76.672-127.936 201.152-174.592 122.368-45.952 273.792-103.168 279.744-286.784a144.64 144.64 0 0 0 84.928-131.52zM241.664 61.44a83.456 83.456 0 1 1 0 166.912 83.456 83.456 0 0 1 0-166.912z m0 890.56a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04zM724.032 228.416a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04z</Geometry>
<Geometry x:Key="Icon.Branch.Add">M896 128h-64V64c0-35.2-28.8-64-64-64s-64 28.8-64 64v64h-64c-35.2 0-64 28.8-64 64s28.8 64 64 64h64v64c0 35.2 28.8 64 64 64s64-28.8 64-64V256h64c35.2 0 64-28.8 64-64s-28.8-64-64-64z m-203.52 307.2C672.64 480.64 628.48 512 576 512H448c-46.72 0-90.24 12.8-128 35.2V372.48C394.24 345.6 448 275.2 448 192c0-106.24-85.76-192-192-192S64 85.76 64 192c0 83.2 53.76 153.6 128 180.48v279.68c-74.24 25.6-128 96.64-128 179.84 0 106.24 85.76 192 192 192s192-85.76 192-192c0-66.56-33.92-124.8-84.48-159.36 22.4-19.84 51.84-32.64 84.48-32.64h128c121.6 0 223.36-85.12 248.96-199.04-18.56 4.48-37.12 7.04-56.96 7.04-26.24 0-51.2-5.12-75.52-12.8zM256 128c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0 768c-35.2 0-64-28.8-64-64s28.8-64 64-64 64 28.8 64 64-28.8 64-64 64z</Geometry>
<Geometry x:Key="Icon.Remote">M901.802667 479.232v-1.024c0-133.461333-111.616-241.664-249.514667-241.664-105.813333 0-195.925333 63.829333-232.448 153.941333-27.989333-20.138667-62.464-32.426667-100.010667-32.426666-75.776 0-139.605333 49.152-159.744 116.053333-51.882667 36.522667-86.016 96.938667-86.016 165.205333 0 111.616 90.453333 201.728 201.728 201.728h503.466667c111.616 0 201.728-90.453333 201.728-201.728 0-65.194667-31.061333-123.221333-79.189333-160.085333z</Geometry>
<Geometry x:Key="Icon.Remote.Add">M363.789474 512h67.368421v107.789474h107.789473v67.368421h-107.789473v107.789473h-67.368421v-107.789473h-107.789474v-67.368421h107.789474v-107.789474z m297.539368-64A106.671158 106.671158 0 0 1 768 554.671158C768 613.578105 719.548632 660.210526 660.210526 660.210526h-107.789473v-53.894737h-107.789474v-107.789473h-94.31579v107.789473h-94.315789c4.311579-21.194105 22.231579-46.807579 43.560421-50.755368l-0.889263-11.560421a74.671158 74.671158 0 0 1 71.248842-74.590316 128.053895 128.053895 0 0 1 238.605474-7.437473 106.172632 106.172632 0 0 1 52.816842-13.972211z</Geometry>
<Geometry x:Key="Icon.Tag">M177.311335 156.116617c-22.478967 4.729721-32.774451 17.336854-36.251645 36.893258-10.080589 56.697303-33.399691 257.604032-13.234419 277.769304l445.342858 445.341834c23.177885 23.177885 60.757782 23.178909 83.935668 0l246.019183-246.019183c23.177885-23.177885 23.177885-60.757782 0-83.935668l-445.341834-445.341834C437.419398 120.463606 231.004211 144.82034 177.311335 156.116617zM331.22375 344.221786c-26.195615 26.195615-68.667939 26.195615-94.863555 0-26.195615-26.195615-26.195615-68.666916 0-94.863555s68.667939-26.195615 94.862531 0C357.418342 275.55487 357.419366 318.02617 331.22375 344.221786z</Geometry>
<Geometry x:Key="Icon.Tag.Add">M682.666667 536.576h-143.701334v-142.336h-142.336V283.306667H238.933333a44.032 44.032 0 0 0-40.96 40.96v170.666666a55.978667 55.978667 0 0 0 14.336 34.133334l320.512 320.512a40.96 40.96 0 0 0 57.685334 0l173.738666-173.738667a40.96 40.96 0 0 0 0-57.685333z m-341.333334-108.544a40.96 40.96 0 1 1 0-57.685333 40.96 40.96 0 0 1 0 57.685333zM649.216 284.330667V141.994667h-68.608v142.336h-142.336v68.266666h142.336v142.336h68.608v-142.336h142.336v-68.266666h-142.336z</Geometry>
</ResourceDictionary>

View file

@ -0,0 +1,12 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Border.Badge" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="9"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Height" Value="18"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource Brush.Badge}"/>
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,55 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 无边框按钮(也是默认样式) -->
<Style x:Key="Style.Button" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Opacity" Value="0.9"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 修改默认样式 -->
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button}"/>
<!-- 无边框但显示Hover -->
<Style x:Key="Style.Button.HighlightHover" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#40000000"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 有边框 -->
<Style x:Key="Style.Button.Bordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
<Style x:Key="Style.Button.AccentBordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="Background" Value="{DynamicResource Brush.Accent1}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,38 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1" Background="Transparent">
<Path x:Name="Checked" Height="12" Width="12" Style="{DynamicResource Style.Icon}" Data="{DynamicResource Icon.Check}" Fill="{DynamicResource Brush.Accent1}"/>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="8,0,0,0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter TargetName="Checked" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,83 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTemplate x:Key="Template.ComboBox.ToggleButton" TargetType="{x:Type ToggleButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.ColumnSpan="2" CornerRadius="0" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{DynamicResource Brush.Border1}" Background="Transparent"/>
<Border Grid.Column="0" CornerRadius="0" Margin="1" Background="Transparent"/>
<Path x:Name="Arrow" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z" Fill="{DynamicResource Brush.Border1}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="Template.ComboBox.TextBox" TargetType="{x:Type TextBox}">
<Border x:Name="PART_ContentHost" Focusable="False" Background="{TemplateBinding Background}" />
</ControlTemplate>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="true" />
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="20" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton" BorderThickness="{TemplateBinding BorderThickness}" Template="{StaticResource Template.ComboBox.ToggleButton}" Grid.Column="2" Focusable="false" ClickMode="Press" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<ContentPresenter x:Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left" TextElement.Foreground="{DynamicResource Brush.FG}"/>
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" Template="{StaticResource Template.ComboBox.TextBox}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" />
<Popup x:Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Grid x:Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border x:Name="DropDownBorder" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Background="{DynamicResource Brush.BG2}"/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" TextElement.Foreground="{DynamicResource Brush.FG}"/>
</ScrollViewer>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
<Trigger SourceName="Popup" Property="AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder" Property="CornerRadius" Value="0" />
<Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Border" Padding="2" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,91 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border Background="{DynamicResource Brush.BG1}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
<StackPanel IsItemsHost="True" Margin="1" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuItemTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" HorizontalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" Margin="6,0" VerticalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
<Path Grid.Column="2" Width="8" Height="8" Style="{DynamicResource Style.Icon}" Data="M 0 0 L 0 7 L 4 3.5 Z"/>
<Popup Name="Popup" Placement="Right" HorizontalOffset="-2" IsOpen="{TemplateBinding IsSubmenuOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
<Border Name="SubmenuBorder" SnapsToDevicePixels="True" Background="{DynamicResource Brush.BG1}" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="{x:Type Separator}">
<Setter Property="Margin" Value="30,2,0,2"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height=".8" VerticalAlignment="Center" SnapsToDevicePixels="True" Fill="{DynamicResource Brush.Border1}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.DataGridCell" TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style x:Key="Style.DataGridRow" TargetType="{x:Type DataGridRow}">
<Setter Property="BorderThickness" Value="0"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
</Style.Resources>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="CanUserDeleteRows" Value="False"/>
<Setter Property="CanUserResizeRows" Value="False"/>
<Setter Property="CanUserReorderColumns" Value="False"/>
<Setter Property="CanUserResizeColumns" Value="False" />
<Setter Property="CanUserSortColumns" Value="False"/>
<Setter Property="AllowDrop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="TextElement.FontFamily" Value="Consolas"/>
<Setter Property="EnableColumnVirtualization" Value="True"/>
<Setter Property="EnableRowVirtualization" Value="True"/>
<Setter Property="RowBackground" Value="Transparent"/>
<Setter Property="HeadersVisibility" Value="None"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="CellStyle" Value="{StaticResource Style.DataGridCell}"/>
<Setter Property="RowStyle" Value="{StaticResource Style.DataGridRow}"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="TextDecorations" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Label" TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource Style.Label}"/>
<Style x:Key="Style.Label.GroupHeader" BasedOn="{StaticResource Style.Label}" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="Opacity" Value=".5"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,66 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ListViewItem.Borderless" TargetType="{x:Type ListViewItem}">
<Setter Property="Padding" Value="2"/>
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Border x:Name="Border" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ListView.Borderless" TargetType="{x:Type ListView}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.ListViewItem.Borderless}"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListView}">
<Border Name="Border" BorderThickness="0" Background="{TemplateBinding Background}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}">
<ItemsPresenter/>
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Icon" TargetType="{x:Type Path}">
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Stretch" Value="Uniform"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
</Style>
<Style x:Key="Style.WindowControlIcon" TargetType="{x:Type Path}">
<Setter Property="Stretch" Value="None"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
<Setter Property="RenderOptions.EdgeMode" Value="Aliased"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,52 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Path
x:Name="Border"
Width="14" Height="14"
Stretch="Uniform"
Fill="Transparent"
Stroke="{DynamicResource Brush.Border1}" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"/>
<Path
x:Name="Dot"
Width="10" Height="10"
Stretch="Uniform"
Fill="{DynamicResource Brush.Accent1}"
Stroke="Transparent" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"
Visibility="Collapsed"/>
</Grid>
<Grid Grid.Column="1" Margin="4,0">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Dot" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Stroke" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,98 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ScrollBar.RepeatPage" TargetType="{x:Type RepeatButton}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border x:Name="area" Background="Transparent" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="area" Property="Background" Value="{DynamicResource Brush.FG}" />
<Setter TargetName="area" Property="Opacity" Value=".08"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ScrollBar.Thumb" TargetType="{x:Type Thumb}">
<Setter Property="Background" Value="{DynamicResource Brush.Border1}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Focusable" Value="false" />
<Setter Property="IsTabStop" Value="false" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border x:Name="Border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Opacity=".6"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="Template.ScrollBar.Horizontal" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="Template.ScrollBar.Vertical" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track"
Grid.Row="1"
IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}"/>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Height" Value="Auto" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Vertical}" />
<Setter Property="Width" Value="8" />
</Trigger>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="8" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Horizontal}" />
<Setter Property="Width" Value="Auto" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,54 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter
Cursor="{TemplateBinding Cursor}"
Margin="{TemplateBinding Padding}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ScrollBar x:Name="PART_VerticalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Grid.Column="1" Grid.Row="0" Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Maximum="{TemplateBinding ScrollableHeight}"
Minimum="0"
Value="{TemplateBinding VerticalOffset}"
Margin="0,-1,-1,-1"/>
<ScrollBar x:Name="PART_HorizontalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Grid.Column="0" Grid.Row="1" Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Maximum="{TemplateBinding ScrollableWidth}"
Minimum="0"
Value="{TemplateBinding HorizontalOffset}"
Margin="-1,0,-1,-1"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,62 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type TabControl}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabPanel x:Name="HeaderPanel" Grid.Row="0" IsItemsHost="True" KeyboardNavigation.TabIndex="1" Background="Transparent" />
<Border
x:Name="Border"
Grid.Row="1"
Background="Transparent"
KeyboardNavigation.TabNavigation="Local"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2">
<ContentPresenter x:Name="PART_SelectedContentHost" Margin="4" ContentSource="SelectedContent" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border x:Name="Border" Margin="0" BorderThickness="0,0,0,1.1" BorderBrush="Transparent" Opacity=".7">
<ContentPresenter
x:Name="ContentSite"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextElement.Foreground="{DynamicResource Brush.FG}"
TextElement.FontWeight="Bold"
ContentSource="Header"
Margin="8,6"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
<Setter TargetName="Border" Property="Opacity" Value="1"/>
<Setter TargetName="ContentSite" Property="TextElement.Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,114 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 错误Tooltip -->
<ControlTemplate x:Key="Template.Validation.Tooltip" TargetType="{x:Type ToolTip}">
<Border x:Name="Root" Margin="5,0,0,0" Opacity="0" Padding="0,0,20,20" RenderTransformOrigin="0,0">
<Border.RenderTransform>
<TranslateTransform x:Name="xform" X="-25" />
</Border.RenderTransform>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="OpenStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" />
<VisualTransition GeneratedDuration="0:0:0.2" To="Open">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform">
<DoubleAnimation.EasingFunction>
<BackEase Amplitude=".3" EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Duration="0:0:0.2" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Closed">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
<VisualState x:Name="Open">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform" />
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<FrameworkElement.Effect>
<DropShadowEffect BlurRadius="11" ShadowDepth="6" Opacity="0.4" />
</FrameworkElement.Effect>
<Border Background="#FFDC000C" BorderThickness="1" BorderBrush="#FFBC000C">
<TextBlock Foreground="White" MaxWidth="250" Margin="8,4,8,4" TextWrapping="Wrap" Text="{Binding [0].ErrorContent}" UseLayoutRounding="false" />
</Border>
</Border>
</ControlTemplate>
<!-- 验证错误模板 -->
<ControlTemplate x:Key="Template.Validation.Error">
<AdornedElementPlaceholder x:Name="Target">
<Border BorderBrush="#FFDB000C" BorderThickness="1" x:Name="root">
<ToolTipService.ToolTip>
<ToolTip x:Name="validationTooltip"
Placement="Right"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Template="{StaticResource Template.Validation.Tooltip}"
Style="{x:Null}"/>
</ToolTipService.ToolTip>
<Grid Background="Transparent" HorizontalAlignment="Right" Height="12" Width="12" Margin="1,-4,-4,0" VerticalAlignment="Top">
<Path Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 z" Fill="#FFDC000C" Margin="1,3,0,0" />
</Grid>
</Border>
</AdornedElementPlaceholder>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.IsKeyboardFocusWithin, Mode=OneWay}" Value="True" />
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.(Validation.HasError), Mode=OneWay}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="validationTooltip" Property="IsOpen" Value="True"/>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- 修改默认 -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="CaretBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource Template.Validation.Error}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Background="{x:Null}"
BorderThickness="0"
IsTabStop="False"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<Trigger Property="AcceptsReturn" Value="True">
<Setter TargetName="PART_ContentHost" Property="VerticalAlignment" Value="Top"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,111 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ToggleButton.Expender" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<ContentPresenter/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Filter" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid>
<Path
x:Name="Icon"
Height="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG2}"
StrokeThickness="1"
Data="{DynamicResource Icon.Filter}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Orientation" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Width="18"
Height="18"
Style="{DynamicResource Style.Icon}"
Fill="{DynamicResource Brush.Border1}"
Data="{DynamicResource Icon.Horizontal}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.Vertical}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.ListOrTree" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Height="12"
Width="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG2}"
StrokeThickness="1"
Data="{DynamicResource Icon.Tree}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.List}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ToolTip}">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<Border
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border1}"
Background="{DynamicResource Brush.BG1}"
Width="Auto"
Height="Auto">
<ContentPresenter Margin="6,4" TextElement.Foreground="{DynamicResource Brush.FG}" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,201 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers">
<converters:TreeViewItemDepthToMargin x:Key="Converter.TreeViewItemIndent" Indent="19"/>
<Style x:Key="Style.TreeView.ToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="16" Height="16" Margin="1" Background="Transparent">
<Path x:Name="ExpandPath" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,1,1,1" Fill="{DynamicResource Brush.FG}" Data="M 4 0 L 8 4 L 4 8 Z"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="ExpandPath" Value="M 0 4 L 8 4 L 4 8 Z"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.ItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.MultiSelectionItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="helpers:TreeViewHelper.IsChecked" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="helpers:TreeViewHelper.IsChecked" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.TreeView.ItemContainerStyle}"/>
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Standard" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeView}">
<Border Name="Border"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="0"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ScrollViewer Padding="{TemplateBinding Padding}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,16 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FF252525"/>
<SolidColorBrush x:Key="Brush.BG2" Color="#FF1B1B1B"/>
<SolidColorBrush x:Key="Brush.BG3" Color="#FF202020"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FF303030"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FF505050"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF7C7C7C"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FF404040"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FFF1F1F1"/>
<SolidColorBrush x:Key="Brush.FG2" Color="#40F1F1F1"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF007ACC"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#33007ACC"/>
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FFEEEEF2"/>
<SolidColorBrush x:Key="Brush.BG2" Color="White"/>
<SolidColorBrush x:Key="Brush.BG3" Color="WhiteSmoke"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FFE6E7E8"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FFBDBDBD"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF898989"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FFCFCFCF"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FF1F1F1F"/>
<SolidColorBrush x:Key="Brush.FG2" Color="DarkGray"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF4295FF"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#33007ACC"/>
</ResourceDictionary>

515
SourceGit.csproj Normal file
View file

@ -0,0 +1,515 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>SourceGit</RootNamespace>
<AssemblyName>SourceGit</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<GenerateSerializationAssemblies>On</GenerateSerializationAssemblies>
</PropertyGroup>
<PropertyGroup>
<StartupObject>SourceGit.App</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>App.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup />
<PropertyGroup>
<RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Converters\FileStatusToColor.cs" />
<Compile Include="Converters\FileStatusToIcon.cs" />
<Compile Include="Converters\InverseBool.cs" />
<Compile Include="Converters\InverseBoolToCollapsed.cs" />
<Compile Include="Git\Blame.cs" />
<Compile Include="Git\Branch.cs" />
<Compile Include="Git\Decorator.cs" />
<Compile Include="Git\MergeTool.cs" />
<Compile Include="Git\Remote.cs" />
<Compile Include="Git\Stash.cs" />
<Compile Include="Git\Tag.cs" />
<Compile Include="Helpers\CommitGraph.cs" />
<Compile Include="Helpers\TreeViewHelper.cs" />
<Compile Include="Helpers\Validations.cs" />
<Compile Include="UI\Apply.xaml.cs">
<DependentUpon>Apply.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Blame.xaml.cs">
<DependentUpon>Blame.xaml</DependentUpon>
</Compile>
<Compile Include="UI\CherryPick.xaml.cs">
<DependentUpon>CherryPick.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Clone.xaml.cs">
<DependentUpon>Clone.xaml</DependentUpon>
</Compile>
<Compile Include="UI\CommitViewer.xaml.cs">
<DependentUpon>CommitViewer.xaml</DependentUpon>
</Compile>
<Compile Include="UI\CreateBranch.xaml.cs">
<DependentUpon>CreateBranch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\CreateTag.xaml.cs">
<DependentUpon>CreateTag.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Dashboard.xaml.cs">
<DependentUpon>Dashboard.xaml</DependentUpon>
</Compile>
<Compile Include="UI\DeleteBranch.xaml.cs">
<DependentUpon>DeleteBranch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\DeleteRemote.xaml.cs">
<DependentUpon>DeleteRemote.xaml</DependentUpon>
</Compile>
<Compile Include="UI\DeleteTag.xaml.cs">
<DependentUpon>DeleteTag.xaml</DependentUpon>
</Compile>
<Compile Include="UI\DiffViewer.xaml.cs">
<DependentUpon>DiffViewer.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Discard.xaml.cs">
<DependentUpon>Discard.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Fetch.xaml.cs">
<DependentUpon>Fetch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\FileHistories.xaml.cs">
<DependentUpon>FileHistories.xaml</DependentUpon>
</Compile>
<Compile Include="UI\GitFlowFinishBranch.xaml.cs">
<DependentUpon>GitFlowFinishBranch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\GitFlowSetup.xaml.cs">
<DependentUpon>GitFlowSetup.xaml</DependentUpon>
</Compile>
<Compile Include="UI\GitFlowStartBranch.xaml.cs">
<DependentUpon>GitFlowStartBranch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Histories.xaml.cs">
<DependentUpon>Histories.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Init.xaml.cs">
<DependentUpon>Init.xaml</DependentUpon>
</Compile>
<Compile Include="UI\InteractiveRebase.xaml.cs">
<DependentUpon>InteractiveRebase.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Manager.xaml.cs">
<DependentUpon>Manager.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Merge.xaml.cs">
<DependentUpon>Merge.xaml</DependentUpon>
</Compile>
<Compile Include="UI\PopupManager.xaml.cs">
<DependentUpon>PopupManager.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Preference.xaml.cs">
<DependentUpon>Preference.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Pull.xaml.cs">
<DependentUpon>Pull.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Push.xaml.cs">
<DependentUpon>Push.xaml</DependentUpon>
</Compile>
<Compile Include="UI\PushTag.xaml.cs">
<DependentUpon>PushTag.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Rebase.xaml.cs">
<DependentUpon>Rebase.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Remote.xaml.cs">
<DependentUpon>Remote.xaml</DependentUpon>
</Compile>
<Compile Include="UI\RenameBranch.xaml.cs">
<DependentUpon>RenameBranch.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Reset.xaml.cs">
<DependentUpon>Reset.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Revert.xaml.cs">
<DependentUpon>Revert.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Stash.xaml.cs">
<DependentUpon>Stash.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Stashes.xaml.cs">
<DependentUpon>Stashes.xaml</DependentUpon>
</Compile>
<Compile Include="UI\WorkingCopy.xaml.cs">
<DependentUpon>WorkingCopy.xaml</DependentUpon>
</Compile>
<Page Include="Resources\Controls.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Resources\Icons.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\Border.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\Button.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\CheckBox.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ComboBox.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ContextMenu.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\DataGrid.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\HyperLink.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\Label.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ListView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\Path.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\RadioButton.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ScrollBar.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ScrollViewer.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\TabControl.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\TextBox.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\ToggleButton.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\Tooltip.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Styles\TreeView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Themes\Dark.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Themes\Light.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\About.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Apply.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Blame.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\CherryPick.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Clone.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\CommitViewer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\CreateBranch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\CreateTag.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Dashboard.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\DeleteBranch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\DeleteRemote.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\DeleteTag.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\DiffViewer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Discard.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Fetch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\FileHistories.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\GitFlowFinishBranch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\GitFlowSetup.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\GitFlowStartBranch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Histories.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Init.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\InteractiveRebase.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Launcher.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Converters\PercentToDouble.cs" />
<Compile Include="Git\Change.cs" />
<Compile Include="Git\Commit.cs" />
<Compile Include="Git\Preference.cs" />
<Compile Include="Git\Repository.cs" />
<Compile Include="Git\User.cs" />
<Compile Include="Converters\BoolToCollapsed.cs" />
<Compile Include="Converters\IndentToMargin.cs" />
<Compile Include="Helpers\TextBoxHelper.cs" />
<Compile Include="Converters\TreeViewItemDepthToMargin.cs" />
<Compile Include="UI\About.xaml.cs">
<DependentUpon>About.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Launcher.xaml.cs">
<DependentUpon>Launcher.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="UI\Manager.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Merge.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\PopupManager.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Preference.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Pull.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Push.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\PushTag.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Rebase.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Remote.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\RenameBranch.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Reset.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Revert.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Stash.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Stashes.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\WorkingCopy.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Resource Include="App.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>@echo off &amp;setlocal
setlocal enabledelayedexpansion
set file=$(SolutionDir)Properties\AssemblyInfo.cs
set temp=$(SolutionDir)Properties\AssemblyInfo.cs.tmp
set bak=$(SolutionDir)Properties\AssemblyInfo.cs.bk
echo BACKUP AssemblyInfo
copy /Y %25file%25 %25bak%25
echo Find Version
git describe &gt; VERSION
set /P version=&lt;VERSION
set search=1.0.0.0
(for /f "delims=" %25%25i in (%25file%25) do (
set "line=%25%25i"
set "line=!line:%25search%25=%25version%25!"
echo(!line!
))&gt;"%25temp%25"
del /f %25file%25
del /f VERSION
move %25temp%25 %25file%25
endlocal</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>del /f $(SolutionDir)Properties\AssemblyInfo.cs
move $(SolutionDir)Properties\AssemblyInfo.cs.bk $(SolutionDir)Properties\AssemblyInfo.cs</PostBuildEvent>
</PropertyGroup>
</Project>

25
SourceGit.sln Normal file
View file

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30011.22
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGit", "SourceGit.csproj", "{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {01F4EC04-5B3C-4D74-BB48-1C251B2D2853}
EndGlobalSection
EndGlobal

77
UI/About.xaml Normal file
View file

@ -0,0 +1,77 @@
<Window x:Class="SourceGit.UI.About"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="280" Width="400"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<!-- Enable WindowChrome Feature -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Layout -->
<Border Background="{StaticResource Brush.BG1}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Title -->
<Label Content="ABOUT" FontWeight="Light"/>
<!-- Close Button -->
<Button Click="Quit" Width="32" Grid.Column="2" WindowChrome.IsHitTestVisibleInChrome="True">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="90"/>
<RowDefinition Height="40"/>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,6,0,0">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}" Fill="#FFF05133"/>
</StackPanel>
<Label Grid.Row="1" Content="SourceGit - OPEN SOURCE GIT CLIENT" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" FontSize="18" FontWeight="Bold"/>
<Label Grid.Row="2" Content="{Binding ElementName=me, Path=Version}" HorizontalContentAlignment="Center" FontSize="11"/>
<Label Grid.Row="3" HorizontalContentAlignment="Center" FontSize="11">
<Hyperlink RequestNavigate="OpenSource" NavigateUri="https://gitee.com/sourcegit/SourceGit.git">
<Run Text="https://gitee.com/sourcegit/SourceGit.git"/>
</Hyperlink>
</Label>
<Label Grid.Row="4" Content="Copyright © sourcegit 2020. All rights reserved." HorizontalContentAlignment="Center" FontSize="11"/>
</Grid>
</Grid>
</Border>
</Window>

46
UI/About.xaml.cs Normal file
View file

@ -0,0 +1,46 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// About dialog
/// </summary>
public partial class About : Window {
/// <summary>
/// Current app version
/// </summary>
public string Version {
get {
return FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion;
}
}
/// <summary>
/// Constructor
/// </summary>
public About() {
InitializeComponent();
}
/// <summary>
/// Open source code link
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OpenSource(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
/// <summary>
/// Close this dialog
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
}
}

85
UI/Apply.xaml Normal file
View file

@ -0,0 +1,85 @@
<UserControl x:Class="SourceGit.UI.Apply"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Apply Patch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Patch File :"/>
<Grid Grid.Row="2" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
x:Name="txtPatchFile"
Height="24"
helpers:TextBoxHelper.Placeholder="Select .patch file to apply">
<TextBox.Text>
<Binding Path="PatchFile" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:PatchFileRequiredRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Click="FindPatchFile" Padding="0" BorderThickness="1" Style="{StaticResource Style.Button.Bordered}">
<Path Width="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Whitespace :"/>
<ComboBox x:Name="combWhitespaceOptions" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="20">
<Label Content="{Binding Name}" Padding="4,0"/>
<Label Content="{Binding Desc}" Foreground="{StaticResource Brush.FG2}" FontSize="11" Padding="4,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

112
UI/Apply.xaml.cs Normal file
View file

@ -0,0 +1,112 @@
using Microsoft.Win32;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Apply patch dialog
/// </summary>
public partial class Apply : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Whitespace option.
/// </summary>
public class WhitespaceOption {
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public WhitespaceOption(string n, string d, string a) {
Name = n;
Desc = d;
Arg = a;
}
}
/// <summary>
/// Path of file to be patched.
/// </summary>
public string PatchFile { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Apply(Git.Repository opened) {
repo = opened;
InitializeComponent();
combWhitespaceOptions.ItemsSource = new WhitespaceOption[] {
new WhitespaceOption("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
new WhitespaceOption("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
new WhitespaceOption("Error", "Raise errors and refuses to apply the patch", "error"),
new WhitespaceOption("Error All", "Similar to 'error', but shows more", "error-all"),
};
combWhitespaceOptions.SelectedIndex = 0;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
public static void Show(Git.Repository opened) {
PopupManager.Show(new Apply(opened));
}
/// <summary>
/// Open file browser dialog for select a file to patch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FindPatchFile(object sender, RoutedEventArgs e) {
var dialog = new OpenFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = "Select Patch File";
dialog.InitialDirectory = repo.Path;
dialog.CheckFileExists = true;
if (dialog.ShowDialog() == true) {
PatchFile = dialog.FileName;
txtPatchFile.Text = dialog.FileName;
}
}
/// <summary>
/// Start apply selected path.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtPatchFile.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtPatchFile)) return;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
var mode = combWhitespaceOptions.SelectedItem as WhitespaceOption;
await Task.Run(() => repo.Apply(PatchFile, mode.Arg));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel options.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

201
UI/Blame.xaml Normal file
View file

@ -0,0 +1,201 @@
<Window x:Class="SourceGit.UI.Blame"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Content -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Logo & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - BLAME" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Blame file -->
<Border Grid.Row="1" Padding="2,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="blameFile" HorizontalAlignment="Left" FontSize="11" Foreground="{StaticResource Brush.FG2}" FontFamily="Consolas"/>
<Label Grid.Column="1" HorizontalAlignment="Right" Foreground="{StaticResource Brush.FG2}" FontSize="11" Content="Use right mouse button to view commit information."/>
</Grid>
</Border>
<!-- Content -->
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" ClipToBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="lineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"
FontFamily="Consolas"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="content"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="SyncScrollChanged"
PreviewMouseWheel="MouseWheelOnContent"
SizeChanged="ContentSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas">
<FlowDocument PageWidth="0"/>
</RichTextBox>
<!-- Loading tip -->
<Path x:Name="loading" Grid.ColumnSpan="5" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<!-- Popup to show commit info -->
<Popup x:Name="popup" Grid.ColumnSpan="5" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True">
<Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG1}">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="COMMIT SHA" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="commitID"/>
<Label Grid.Row="1" Grid.Column="0" Content="AUTHOR" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="authorName"/>
<Label Grid.Row="2" Grid.Column="0" Content="MODIFY TIME" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="2" Grid.Column="1" x:Name="authorTime"/>
</Grid>
</Border>
</Popup>
</Grid>
</Border>
</Grid>
</Border>
</Window>

211
UI/Blame.xaml.cs Normal file
View file

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Viewer to show git-blame
/// </summary>
public partial class Blame : Window {
/// <summary>
/// Background color for blocks.
/// </summary>
public static Brush[] BG = new Brush[] {
Brushes.Transparent,
new SolidColorBrush(Color.FromArgb(128, 0, 0, 0))
};
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <param name="revision"></param>
public Blame(Git.Repository repo, string file, string revision) {
InitializeComponent();
double minWidth = content.ActualWidth;
// Move to center.
var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading.
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible;
// Layout content
blameFile.Content = $"{file}@{revision.Substring(0, 8)}";
Task.Run(() => {
var blame = repo.BlameFile(file, revision);
Dispatcher.Invoke(() => {
content.Document.Blocks.Clear();
if (blame.IsBinary) {
lineNumber.Text = "0";
Paragraph p = new Paragraph(new Run("BINARY FILE BLAME NOT SUPPORTED!!!"));
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
content.Document.Blocks.Add(p);
} else {
List<string> numbers = new List<string>();
for (int i = 0; i < blame.LineCount; i++) numbers.Add(i.ToString());
lineNumber.Text = string.Join("\n", numbers);
numbers.Clear();
for (int i = 0; i < blame.Blocks.Count; i++) {
var frag = blame.Blocks[i];
var idx = i;
Paragraph p = new Paragraph(new Run(frag.Content));
p.DataContext = frag;
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = BG[i % 2];
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
p.MouseRightButtonDown += (sender, ev) => {
Hyperlink link = new Hyperlink(new Run(frag.CommitSHA));
link.ToolTip = "CLICK TO GO";
link.Click += (o, e) => {
repo.OnNavigateCommit?.Invoke(frag.CommitSHA);
e.Handled = true;
};
foreach (var block in content.Document.Blocks) {
var paragraph = block as Paragraph;
if ((paragraph.DataContext as Git.Blame.Block).CommitSHA == frag.CommitSHA) {
paragraph.Background = Brushes.Green;
} else {
paragraph.Background = BG[i % 2];
}
}
commitID.Content = link;
authorName.Content = frag.Author;
authorTime.Content = frag.Time;
popup.IsOpen = true;
};
var formatter = new FormattedText(
frag.Content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(content.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
content.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) {
content.Document.PageWidth = formatter.Width + 16;
minWidth = formatter.Width;
}
content.Document.Blocks.Add(p);
}
}
// Hide loading.
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
});
});
}
/// <summary>
/// Click logo
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
/// <summary>
/// Sync scroll
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
var margin = new Thickness(4, -e.VerticalOffset, 4, 0);
lineNumber.Margin = margin;
}
}
/// <summary>
/// Mouse wheel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) {
if (e.Delta > 0) {
content.LineUp();
} else {
content.LineDown();
}
e.Handled = true;
}
/// <summary>
/// Content size changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentSizeChanged(object sender, SizeChangedEventArgs e) {
if (content.Document.PageWidth < content.ActualWidth) {
content.Document.PageWidth = content.ActualWidth;
}
}
}
}

45
UI/CherryPick.xaml Normal file
View file

@ -0,0 +1,45 @@
<UserControl x:Class="SourceGit.UI.CherryPick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Cherry Pick"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Commit :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}" Margin="4,0"/>
<Label x:Name="desc"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkCommitChanges" IsChecked="True" Content="Commit the changes"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

54
UI/CherryPick.xaml.cs Normal file
View file

@ -0,0 +1,54 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Cherry pick commit dialog.
/// </summary>
public partial class CherryPick : UserControl {
private Git.Repository repo = null;
private string commitSHA = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="commit"></param>
public CherryPick(Git.Repository opened, Git.Commit commit) {
InitializeComponent();
repo = opened;
commitSHA = commit.SHA;
desc.Content = $"{commit.ShortSHA} {commit.Subject}";
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
PopupManager.Show(new CherryPick(repo, commit));
}
/// <summary>
/// Start pick.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
repo.CherryPick(commitSHA, chkCommitChanges.IsChecked != true);
PopupManager.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

98
UI/Clone.xaml Normal file
View file

@ -0,0 +1,98 @@
<UserControl x:Class="SourceGit.UI.Clone"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
Width="500" Height="192">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Clone Remote Repository"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Repository URL :"/>
<TextBox x:Name="txtUrl" Grid.Row="2" Grid.Column="1"
Height="24"
helpers:TextBoxHelper.Placeholder="Git Repository URL">
<TextBox.Text>
<Binding Path="RemoteUri" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:RemoteUriRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Parent Folder :"/>
<Grid Grid.Row="3" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
x:Name="txtParentFolder"
Height="24"
helpers:TextBoxHelper.Placeholder="Folder to contain this repository">
<TextBox.Text>
<Binding Path="ParentFolder" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:CloneFolderRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Padding="0" BorderThickness="1" Click="SelectParentFolder" Style="{StaticResource Style.Button.Bordered}">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Name :"/>
<TextBox Grid.Row="4" Grid.Column="1"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Repository name. Optional."
Text="{Binding LocalName, ElementName=me, Mode=TwoWay}">
</TextBox>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="7" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

115
UI/Clone.xaml.cs Normal file
View file

@ -0,0 +1,115 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Clone dialog.
/// </summary>
public partial class Clone : UserControl {
/// <summary>
/// Remote repository
/// </summary>
public string RemoteUri { get; set; }
/// <summary>
/// Parent folder.
/// </summary>
public string ParentFolder { get; set; }
/// <summary>
/// Local name.
/// </summary>
public string LocalName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Clone() {
ParentFolder = App.Preference.GitDefaultCloneDir;
InitializeComponent();
}
/// <summary>
/// Show clone dialog.
/// </summary>
public static void Show() {
PopupManager.Show(new Clone());
}
/// <summary>
/// Select parent folder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SelectParentFolder(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Git Repository URL";
dialog.RootFolder = Environment.SpecialFolder.MyComputer;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
txtParentFolder.Text = dialog.SelectedPath;
}
}
/// <summary>
/// Start clone
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
txtUrl.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtUrl)) return;
txtParentFolder.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtParentFolder)) return;
string repoName;
if (string.IsNullOrWhiteSpace(LocalName)) {
var from = RemoteUri.LastIndexOfAny(new char[] { '\\', '/' });
if (from <= 0) return;
var name = RemoteUri.Substring(from + 1);
repoName = name.Replace(".git", "");
} else {
repoName = LocalName;
}
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
Task.Run(() => {
var repo = Git.Repository.Clone(RemoteUri, ParentFolder, repoName, msg => Dispatcher.Invoke(() => statusMsg.Content = msg));
if (repo == null) {
PopupManager.Unlock();
Dispatcher.Invoke(() => {
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
});
} else {
Dispatcher.Invoke(() => PopupManager.Close(true));
repo.Open();
}
});
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

408
UI/CommitViewer.xaml Normal file
View file

@ -0,0 +1,408 @@
<UserControl x:Class="SourceGit.UI.CommitViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<TabControl>
<TabItem Header="INFORMATION">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition x:Name="committerRow" Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SHA -->
<Label Grid.Row="0" Grid.Column="0" Content="SHA" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="0" Grid.Column="1"
x:Name="SHA"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- Refs -->
<Label x:Name="lblRefs" Grid.Row="0" Grid.Column="2" Content="REFS" HorizontalAlignment="Right" Opacity=".5"/>
<ItemsControl Grid.Row="0" Grid.Column="3" x:Name="refs" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="BG" Height="16" Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.BG5}">
<Path x:Name="Icon" Width="8" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
</Border>
<Label x:Name="Name" Grid.Column="1" Content="{Binding Name}" FontSize="11" Padding="4,0" Foreground="Black"/>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.Tag}">
<Setter TargetName="BG" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.LocalBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Branch}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.RemoteBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.CurrentBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Icon" Property="Fill" Value="Orange"/>
<Setter TargetName="Name" Property="FontWeight" Value="Bold"/>
<Setter TargetName="Name" Property="Foreground" Value="{StaticResource Brush.FG}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- PARENTS -->
<Label Grid.Row="1" Grid.Column="0" Content="PARENTS" HorizontalAlignment="Right" Opacity=".5"/>
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" x:Name="parents" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Margin="0,0,8,0">
<Hyperlink
RequestNavigate="NavigateParent"
NavigateUri="{Binding .}"
ToolTip="NAVIGATE TO COMMIT">
<Run Text="{Binding .}"/>
</Hyperlink>
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- AUTHOR -->
<Label Grid.Row="2" Grid.Column="0" Content="AUTHOR" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="author"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- AUTHOR TIME -->
<Label Grid.Row="2" Grid.Column="2" Content="AUTHOR TIME" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="2" Grid.Column="3" Grid.ColumnSpan="3" x:Name="authorTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<!-- COMMITTER -->
<Label Grid.Row="3" Grid.Column="0" Content="COMMITTER" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="committer"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- COMMIT TIME -->
<Label Grid.Row="3" Grid.Column="2" Content="COMMIT TIME" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="3" x:Name="committerTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<Rectangle Grid.Row="4" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- SUBJECT -->
<Label Grid.Row="5" Grid.Column="0" Content="SUBJECT" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="subject"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="8,0,16,0"/>
<!-- MESSAGE -->
<Label Grid.Row="6" Grid.Column="0" Content="DESCRIPTION" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".5"/>
<TextBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="message"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
VerticalAlignment="Center"
FontSize="11"
Margin="11,8,0,0"/>
<Rectangle Grid.Row="7" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- CHANGELIST -->
<Label Grid.Row="8" Grid.Column="0" Content="CHANGED" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".5"/>
<DataGrid
Grid.Row="8"
Grid.Column="1"
Grid.ColumnSpan="3"
x:Name="changeList1"
RowHeight="20"
Margin="11,2,0,2">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</TabItem>
<!-- CHANGES -->
<TabItem Header="CHANGES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="2,0">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
<converters:InverseBoolToCollapsed x:Key="InverseBoolToCollapsed"/>
</Grid.Resources>
<Grid Grid.Row="0" Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" Background="{StaticResource Brush.BG3}"/>
<Path Grid.Column="0" Width="14" Height="14" Fill="{StaticResource Brush.FG2}" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<TextBox Grid.Column="1" x:Name="txtChangeFilter" BorderThickness="0" helpers:TextBoxHelper.Placeholder="Search File ..." TextChanged="SearchChangeFileTextChanged"/>
<ToggleButton
Grid.Column="2"
x:Name="toggleSwitchMode"
Margin="4,0,0,0"
ToolTip="SWITCH TO LIST/TREE VIEW"
Style="{StaticResource Style.ToggleButton.ListOrTree}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIUseListInChanges, Mode=TwoWay}"/>
</Grid>
<TreeView
Grid.Row="1"
x:Name="changeTree"
FontFamily="Consolas"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource InverseBoolToCollapsed}}"
Background="{StaticResource Brush.BG2}"
SelectedItemChanged="ChangeTreeItemSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Border x:Name="status" Width="14" Height="14" Visibility="Collapsed" Background="{Binding Change, Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="0,0,4,0">
<TextBlock Text="{Binding Change, Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="10" RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="status" Property="Visibility" Value="Visible"/>
<Setter TargetName="icon" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<DataGrid
Grid.Row="1"
x:Name="changeList2"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
RowHeight="24"
SelectionChanged="ChangeListSelectionChanged"
SelectionMode="Single"
SelectionUnit="FullRow"
Background="{StaticResource Brush.BG2}">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<local:DiffViewer Grid.Column="2" x:Name="diffViewer" Background="{StaticResource Brush.BG3}"/>
</Grid>
</TabItem>
<!-- FILE TREE -->
<TabItem Header="FILES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="2" Background="{StaticResource Brush.BG2}">
<TreeView x:Name="fileTree" SelectedItemChanged="FileTreeItemSelected" FontFamily="Consolas" PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="6,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.File}"/>
<Setter TargetName="icon" Property="Fill" Value="{StaticResource Brush.FG}"/>
<Setter TargetName="icon" Property="Opacity" Value=".75"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderThickness="1" Margin="2,0" BorderBrush="{StaticResource Brush.Border2}">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock FontSize="10pt"
FontFamily="Consolas"
Padding="8"
Opacity="0.8"
Background="{StaticResource Brush.BG2}"
Foreground="{StaticResource Brush.FG}"
x:Name="filePreview"/>
</ScrollViewer>
</Border>
</Grid>
</TabItem>
</TabControl>
</UserControl>

468
UI/CommitViewer.xaml.cs Normal file
View file

@ -0,0 +1,468 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// Commit detail viewer
/// </summary>
public partial class CommitViewer : UserControl {
private Git.Repository repo = null;
private Git.Commit commit = null;
private List<Git.Change> cachedChanges = new List<Git.Change>();
private List<Git.Change> displayChanges = new List<Git.Change>();
private string changeFilter = null;
/// <summary>
/// Node for file tree.
/// </summary>
public class Node {
public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = "";
public string Name { get; set; } = "";
public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true;
public Git.Change Change { get; set; } = null;
public List<Node> Children { get; set; } = new List<Node>();
}
/// <summary>
/// Constructor.
/// </summary>
public CommitViewer() {
InitializeComponent();
}
#region DATA
public void SetData(Git.Repository opened, Git.Commit selected) {
repo = opened;
commit = selected;
SetBaseInfo(commit);
Task.Run(() => {
cachedChanges.Clear();
cachedChanges = commit.GetChanges(repo);
Dispatcher.Invoke(() => {
changeList1.ItemsSource = null;
changeList1.ItemsSource = cachedChanges;
});
LayoutChanges();
SetRevisionFiles(commit.GetFiles(repo));
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
fileTree.ItemsSource = null;
changeList1.ItemsSource = null;
changeList2.ItemsSource = null;
displayChanges.Clear();
cachedChanges.Clear();
diffViewer.Reset();
}
#endregion
#region BASE_INFO
private void SetBaseInfo(Git.Commit commit) {
var parentIds = new List<string>();
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
SHA.Text = commit.SHA;
refs.ItemsSource = commit.Decorators;
parents.ItemsSource = parentIds;
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
authorTime.Text = commit.Author.Time;
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
committerTime.Text = commit.Committer.Time;
subject.Text = commit.Subject;
message.Text = commit.Message.Trim();
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
else lblRefs.Visibility = Visibility.Visible;
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
committerRow.Height = new GridLength(0);
} else {
committerRow.Height = GridLength.Auto;
}
}
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true;
}
#endregion
#region CHANGES
private void LayoutChanges() {
displayChanges.Clear();
if (string.IsNullOrEmpty(changeFilter)) {
displayChanges.AddRange(cachedChanges);
} else {
foreach (var c in cachedChanges) {
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
}
}
List<Node> changeTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
bool isDefaultExpanded = displayChanges.Count < 50;
foreach (var c in displayChanges) {
var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = c.Path;
node.IsFile = true;
node.Name = c.Path;
node.Change = c;
node.IsNodeExpanded = isDefaultExpanded;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
changeTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = isDefaultExpanded;
changeTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = isDefaultExpanded;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = c.Path;
node.Name = c.Path.Substring(start);
node.IsFile = true;
node.Change = c;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(changeTreeSource);
Dispatcher.Invoke(() => {
changeList2.ItemsSource = null;
changeList2.ItemsSource = displayChanges;
changeTree.ItemsSource = changeTreeSource;
diffViewer.Reset();
});
}
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
changeFilter = txtChangeFilter.Text.ToUpper();
Task.Run(() => LayoutChanges());
}
private async void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
diffViewer.Reset();
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, node.FilePath, node.OriginalPath);
});
diffViewer.SetData(data, node.FilePath, node.OriginalPath);
}
private async void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var change = e.AddedItems[0] as Git.Change;
if (change == null) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, change.Path, change.OriginalPath);
});
diffViewer.SetData(data, change.Path, change.OriginalPath);
}
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var path = change.Path;
var menu = new ContextMenu();
if (change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, path);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, path, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
Process.Start("explorer", $"/select,{absPath}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = change.Path;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region FILES
private void SetRevisionFiles(List<string> files) {
List<Node> fileTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
foreach (var path in files) {
var sepIdx = path.IndexOf("/");
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = path;
node.Name = path;
node.IsFile = true;
node.IsNodeExpanded = false;
fileTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = false;
fileTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = false;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = path;
node.Name = path.Substring(start);
node.IsFile = true;
node.IsNodeExpanded = false;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(fileTreeSource);
Dispatcher.Invoke(() => {
fileTree.ItemsSource = fileTreeSource;
filePreview.Text = "";
});
}
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
filePreview.Text = "";
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
await Task.Run(() => {
var data = commit.GetTextFileContent(repo, node.FilePath);
Dispatcher.Invoke(() => filePreview.Text = data);
});
}
#endregion
#region TREE_COMMON
private void SortTreeNodes(List<Node> list) {
list.Sort((l, r) => {
if (l.IsFile) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
} else {
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
}
});
foreach (var sub in list) {
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
}
}
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;
}
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as TreeViewItem;
if (item == null) return;
var node = item.DataContext as Node;
if (node == null || !node.IsFile) return;
item.IsSelected = true;
ContextMenu menu = new ContextMenu();
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, node.FilePath);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = node.FilePath;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var path = Path.Combine(dialog.SelectedPath, node.Name);
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.FilePath);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
}
}

92
UI/CreateBranch.xaml Normal file
View file

@ -0,0 +1,92 @@
<UserControl x:Class="SourceGit.UI.CreateBranch"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="224" Width="500">
<UserControl.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Local Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Based On :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="basedOnType" Width="12" Style="{StaticResource Style.Icon}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="master"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Branch Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="txtName"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Enter branch name.">
<TextBox.Text>
<Binding ElementName="me" Path="BranchName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:BranchNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Changes :"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal">
<RadioButton Content="Stash &amp; Reapply" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me}"/>
<RadioButton Content="Discard" Margin="8,0,0,0" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me, Mode=OneWay, Converter={StaticResource InverseBool}}"/>
</StackPanel>
<CheckBox Grid.Row="5" Grid.Column="1"
x:Name="chkCheckout"
VerticalAlignment="Center"
IsChecked="True"
Content="Check out after created"/>
<Grid Grid.Row="7" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="8" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

143
UI/CreateBranch.xaml.cs Normal file
View file

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Create branch dialog
/// </summary>
public partial class CreateBranch : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// New branch name.
/// </summary>
public string BranchName {
get;
set;
}
/// <summary>
/// Auto Stash
/// </summary>
public bool AutoStash { get; set; } = false;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
public CreateBranch(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create branch based on current head.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
var current = repo.CurrentBranch();
if (current != null) Show(repo, current);
}
/// <summary>
/// Create branch base on existed one.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
var dialog = new CreateBranch(repo);
dialog.based = branch.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
if (!branch.IsLocal) dialog.txtName.Text = branch.Name.Substring(branch.Remote.Length + 1);
PopupManager.Show(dialog);
}
/// <summary>
/// Create branch based on tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
var dialog = new CreateBranch(repo);
dialog.based = tag.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Tag") as Geometry;
dialog.basedOnDesc.Content = tag.Name;
PopupManager.Show(dialog);
}
/// <summary>
/// Create branch based on commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateBranch(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
PopupManager.Show(dialog);
}
/// <summary>
/// Start create branch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
bool checkout = chkCheckout.IsChecked == true;
await Task.Run(() => {
if (checkout) {
bool stashed = false;
if (repo.LocalChanges().Count > 0 && AutoStash) {
Git.Stash.Push(repo, true, "CREATE BRANCH AUTO STASH", new List<string>());
stashed = true;
}
repo.Checkout($"-b {BranchName} {based}");
if (stashed) {
var stashes = repo.Stashes();
if (stashes.Count > 0) stashes[0].Pop(repo);
}
} else {
Git.Branch.Create(repo, BranchName, based);
}
});
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

68
UI/CreateTag.xaml Normal file
View file

@ -0,0 +1,68 @@
<UserControl x:Class="SourceGit.UI.CreateTag"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="224" d:DesignWidth="500" Width="500" Height="224">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="64"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Tag At :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" x:Name="basedOnType" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="xxx"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Tag Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="tagName"
Height="24"
helpers:TextBoxHelper.Placeholder="Recommanded format v1.0.0-alpha">
<TextBox.Text>
<Binding ElementName="me" Path="TagName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:TagNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,4" Content="Tag Message :"/>
<TextBox Grid.Row="4" Grid.Column="1"
x:Name="tagMessage"
Height="56"
AcceptsReturn="True"
helpers:TextBoxHelper.Placeholder="Optional"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

92
UI/CreateTag.xaml.cs Normal file
View file

@ -0,0 +1,92 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Create tag dialog
/// </summary>
public partial class CreateTag : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// Tag name
/// </summary>
public string TagName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
public CreateTag(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create tag using current branch.
/// </summary>
/// <param name="repo">Opened repository.</param>
public static void Show(Git.Repository repo) {
Show(repo, repo.Branches().First(b => b.IsCurrent));
}
/// <summary>
/// Create tag using branch
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
if (branch == null) {
App.RaiseError("Empty repository!");
return;
}
var dialog = new CreateTag(repo);
dialog.based = branch.Head;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
PopupManager.Show(dialog);
}
/// <summary>
/// Create tag using commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateTag(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
PopupManager.Show(dialog);
}
/// <summary>
/// Start to create tag.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
tagName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(tagName)) return;
Git.Tag.Add(repo, TagName, based, tagMessage.Text);
PopupManager.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

450
UI/Dashboard.xaml Normal file
View file

@ -0,0 +1,450 @@
<UserControl x:Class="SourceGit.UI.Dashboard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<UserControl.Resources>
<RoutedUICommand x:Key="OpenSearchBarCommand" Text="OpenSearchBar"/>
<RoutedUICommand x:Key="HideSearchBarCommand" Text="HideSearchBar"/>
</UserControl.Resources>
<UserControl.InputBindings>
<KeyBinding Key="F" Modifiers="Ctrl" Command="{StaticResource OpenSearchBarCommand}"/>
<KeyBinding Key="ESC" Command="{StaticResource HideSearchBarCommand}"/>
</UserControl.InputBindings>
<UserControl.CommandBindings>
<CommandBinding Command="{StaticResource OpenSearchBarCommand}" Executed="OpenSearchBar"/>
<CommandBinding Command="{StaticResource HideSearchBarCommand}" Executed="HideSearchBar"/>
</UserControl.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- TitleBar -->
<Grid Grid.Row="0" Panel.ZIndex="9999">
<Border Background="{StaticResource Brush.BG1}">
<Border.Effect>
<DropShadowEffect ShadowDepth="2" Direction="270" Opacity=".3"/>
</Border.Effect>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Navigation -->
<StackPanel Grid.Column="0" Margin="8,0,0,0" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Click="Close" ToolTip="Back To Welcome" Padding="0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Manager}"/>
<Label Content="Repositories"/>
</StackPanel>
</Button>
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Navigator}"/>
<Path Margin="6,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="repoName"/>
</StackPanel>
<!-- Common Git Options -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Click="OpenFetch" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Fetch}"/>
<Label Content="Fetch"/>
</StackPanel>
</Button>
<Button Click="OpenPull" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Pull}"/>
<Label Content="Pull"/>
</StackPanel>
</Button>
<Button Click="OpenPush" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Push}"/>
<Label Content="Push"/>
</StackPanel>
</Button>
<Button Click="OpenStash" Margin="2,0,0,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.SaveStash}"/>
<Label Content="Stash"/>
</StackPanel>
</Button>
<Button Click="OpenApply" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Apply}"/>
<Label Content="Apply"/>
</StackPanel>
</Button>
</StackPanel>
<!-- External Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Click="OpenSearch" Margin="4,0" ToolTip="Search Commit">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<Label Content="Search"/>
</StackPanel>
</Button>
<Button Click="OpenExplorer" Margin="4,0" ToolTip="Open In File Browser">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder.Open}"/>
<Label Content="Explore"/>
</StackPanel>
</Button>
<Button Click="OpenTerminal" Margin="4,0" ToolTip="Open Git Bash">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Terminal}"/>
<Label Content="Terminal"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
<!-- Main body -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left panel -->
<Grid Grid.Column="0" x:Name="main" Background="{StaticResource Brush.BG4}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="Bool2Collapsed"/>
</Grid.Resources>
<!-- WORKSPACE -->
<Label Grid.Row="0" Margin="4,0,0,0" Content="WORKSPACE" Style="{StaticResource Style.Label.GroupHeader}" />
<ListView
Grid.Row="1"
x:Name="workspace"
Background="{StaticResource Brush.BG3}"
Style="{StaticResource Style.ListView.Borderless}">
<ListViewItem x:Name="historiesSwitch" Selected="SwitchHistories" IsSelected="True">
<StackPanel Margin="16,0,0,0" Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Histories}"/>
<Label Margin="4,0,0,0" Content="Histories"/>
</StackPanel>
</ListViewItem>
<ListViewItem x:Name="workingCopySwitch" Selected="SwitchWorkingCopy">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.WorkingCopy}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Commit"/>
<Border Grid.Column="2" x:Name="localChangesBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="localChangesCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
<ListViewItem x:Name="stashesSwitch" Selected="SwitchStashes">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Stashes}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Stashes"/>
<Border Grid.Column="2" x:Name="stashBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="stashCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
</ListView>
<!-- LOCAL BRANCHES -->
<Grid Grid.Row="2" Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="LOCAL BRANCHES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenGitFlow" Background="Transparent" ToolTip="GIT FLOW">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Flow}"/>
</Button>
<Button Grid.Column="3" Click="OpenNewBranch" Background="Transparent" ToolTip="NEW BRANCH">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Branch.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="3"
x:Name="localBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
LostFocus="TreeLostFocus"
SelectedItemChanged="LocalBranchSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="LocalBranchContextMenuOpening"/>
<EventSetter Event="MouseDoubleClick" Handler="LocalBranchMouseDoubleClick"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<Label Grid.Column="1" x:Name="name" Content="{Binding Name}" Padding="4,0,0,0"/>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<Border Style="{StaticResource Style.Border.Badge}" Visibility="{Binding TrackVisibility}">
<Label Margin="4,-2,4,-2" Content="{Binding Branch.UpstreamTrack}" FontSize="10"/>
</Border>
<ToggleButton
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</StackPanel>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter TargetName="name" Property="FontWeight" Value="ExtraBold"/>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Check}"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- REMOTES -->
<Grid Grid.Row="4" Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="REMOTES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenRemote" ToolTip="ADD REMOTE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Remote.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="5"
x:Name="remoteBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectedItemChanged="RemoteBranchSelected"
LostFocus="TreeLostFocus"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="RemoteContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:RemoteNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
</Grid>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:BranchNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
<ToggleButton
Grid.Column="2"
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<!-- TAGS -->
<ToggleButton
x:Name="tagListToggle"
Grid.Row="6"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIShowTags, Mode=TwoWay}">
<Grid Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="tagCount" Content="TAGS" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenNewTag" ToolTip="NEW TAG">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag.Add}"/>
</Button>
</Grid>
</ToggleButton>
<ListView
Grid.Row="7"
x:Name="tagList"
Visibility="{Binding ElementName=tagListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
Height="200"
LostFocus="TagLostFocus"
SelectionChanged="TagSelectionChanged"
ContextMenuOpening="TagContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Style="{StaticResource Style.ListView.Borderless}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type git:Tag}">
<Grid Margin="16, 0, 0, 0" Height="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="{Binding Name}" Padding="4,0,0,0"/>
<ToggleButton
Grid.Column="2"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<!-- Right -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Abort panel -->
<Grid x:Name="abortPanel" Grid.Row="0" Background="LightGoldenrodYellow" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="txtMergeProcessing" FontWeight="DemiBold" Foreground="{StaticResource Brush.BG4}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="btnResolve" Click="Resolve" Content="RESOLVE" Margin="4">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.Bordered}">
<Setter Property="Background" Value="{StaticResource Brush.BG1}"/>
<Setter Property="Margin" Value="2"/>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnContinue" Click="Continue" Content="CONTINUE" Style="{StaticResource Style.Button.AccentBordered}" Margin="4"/>
<Button Grid.Column="3" Click="Abort" Content="ABORT" Style="{StaticResource Style.Button.Bordered}" Foreground="{StaticResource Brush.BG1}" Margin="4"/>
</StackPanel>
</Grid>
<!-- Others -->
<local:Histories Grid.Row="1" x:Name="histories" Visibility="Visible"/>
<local:WorkingCopy Grid.Row="1" x:Name="commits" Visibility="Collapsed"/>
<local:Stashes Grid.Row="1" x:Name="stashes" Visibility="Collapsed"/>
</Grid>
</Grid>
<!-- Popups -->
<local:PopupManager Grid.Row="1"/>
</Grid>
</UserControl>

984
UI/Dashboard.xaml.cs Normal file
View file

@ -0,0 +1,984 @@
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.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<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) {
Task.Run(() => {
var branches = repo.Branches(force);
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");
} 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<BranchNode>(),
};
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<string>());
}
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<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 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
}
}

51
UI/DeleteBranch.xaml Normal file
View file

@ -0,0 +1,51 @@
<UserControl x:Class="SourceGit.UI.DeleteBranch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Branch :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}" Margin="4,0"/>
<Label x:Name="branchName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

66
UI/DeleteBranch.xaml.cs Normal file
View file

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete branch
/// </summary>
public partial class DeleteBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch branch = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository.</param>
/// <param name="target">Branch to be deleted.</param>
public DeleteBranch(Git.Repository opened, Git.Branch target) {
InitializeComponent();
repo = opened;
branch = target;
branchName.Content = target.Name;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository opened, Git.Branch branch) {
PopupManager.Show(new DeleteBranch(opened, branch));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => branch.Delete(repo));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

50
UI/DeleteRemote.xaml Normal file
View file

@ -0,0 +1,50 @@
<UserControl x:Class="SourceGit.UI.DeleteRemote"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Remote"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}" Margin="4,0"/>
<Label x:Name="remoteName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

67
UI/DeleteRemote.xaml.cs Normal file
View file

@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete a remote
/// </summary>
public partial class DeleteRemote : UserControl {
private Git.Repository repo = null;
private string remote = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
/// <param name="target">Remote to be deleted</param>
public DeleteRemote(Git.Repository opened, string target) {
InitializeComponent();
repo = opened;
remote = target;
remoteName.Content = target;
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="remote"></param>
public static void Show(Git.Repository opened, string remote) {
PopupManager.Show(new DeleteRemote(opened, remote));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => Git.Remote.Delete(repo, remote));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

53
UI/DeleteTag.xaml Normal file
View file

@ -0,0 +1,53 @@
<UserControl x:Class="SourceGit.UI.DeleteTag"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Tag :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}" Margin="4,0"/>
<Label x:Name="tagName"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkWithRemote" Content="Delete from remote repositories"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

69
UI/DeleteTag.xaml.cs Normal file
View file

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Delete tag dialog.
/// </summary>
public partial class DeleteTag : UserControl {
private Git.Repository repo = null;
private Git.Tag tag = null;
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo">Opened repo</param>
/// <param name="tag">Delete tag</param>
public DeleteTag(Git.Repository repo, Git.Tag tag) {
this.repo = repo;
this.tag = tag;
InitializeComponent();
tagName.Content = tag.Name;
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
PopupManager.Show(new DeleteTag(repo, tag));
}
/// <summary>
/// Start request.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
var push = chkWithRemote.IsChecked == true;
await Task.Run(() => Git.Tag.Delete(repo, tag.Name, push));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

139
UI/DiffViewer.xaml Normal file
View file

@ -0,0 +1,139 @@
<UserControl x:Class="SourceGit.UI.DiffViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
FontFamily="Consolas">
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
<StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="orgFileName" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
<TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
</StackPanel>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
</StackPanel>
</Border>
<Grid Grid.Row="1" ClipToBounds="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="leftLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="leftText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="LeftSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
</Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="rightLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="rightText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="RightSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
</Grid>
</Grid>
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/>
<Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
</UserControl>

294
UI/DiffViewer.xaml.cs Normal file
View file

@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Viewer for git diff
/// </summary>
public partial class DiffViewer : UserControl {
private double minWidth = 0;
/// <summary>
/// Line mode.
/// </summary>
public enum LineMode {
Normal,
Indicator,
Empty,
Added,
Deleted,
}
/// <summary>
/// Constructor
/// </summary>
public DiffViewer() {
InitializeComponent();
Reset();
}
/// <summary>
///
/// </summary>
/// <param name="lines"></param>
/// <param name="file"></param>
/// <param name="orgFile"></param>
public void SetData(List<string> lines, string file, string orgFile = null) {
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
fileName.Text = file;
if (!string.IsNullOrEmpty(orgFile)) {
orgFileNamePanel.Visibility = Visibility.Visible;
orgFileName.Text = orgFile;
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed;
}
leftText.Document.Blocks.Clear();
rightText.Document.Blocks.Clear();
leftLineNumber.Text = "";
rightLineNumber.Text = "";
Regex regex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
bool started = false;
List<Paragraph> leftData = new List<Paragraph>();
List<Paragraph> rightData = new List<Paragraph>();
List<string> leftNumbers = new List<string>();
List<string> rightNumbers = new List<string>();
int leftLine = 0;
int rightLine = 0;
bool bLastLeft = true;
foreach (var line in lines) {
if (!started) {
var match = regex.Match(line);
if (!match.Success) continue;
MakeParagraph(leftData, line, LineMode.Indicator);
MakeParagraph(rightData, line, LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
leftLine = int.Parse(match.Groups[1].Value);
rightLine = int.Parse(match.Groups[2].Value);
started = true;
continue;
}
if (line[0] == '-') {
MakeParagraph(leftData, line.Substring(1), LineMode.Deleted);
leftNumbers.Add(leftLine.ToString());
leftLine++;
bLastLeft = true;
} else if (line[0] == '+') {
MakeParagraph(rightData, line.Substring(1), LineMode.Added);
rightNumbers.Add(rightLine.ToString());
rightLine++;
bLastLeft = false;
} else if (line[0] == '\\') {
if (bLastLeft) {
MakeParagraph(leftData, line.Substring(1), LineMode.Indicator);
leftNumbers.Add("");
} else {
MakeParagraph(rightData, line.Substring(1), LineMode.Indicator);
rightNumbers.Add("");
}
} else {
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
bLastLeft = true;
var match = regex.Match(line);
if (match.Success) {
MakeParagraph(leftData, line, LineMode.Indicator);
MakeParagraph(rightData, line, LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
leftLine = int.Parse(match.Groups[1].Value);
rightLine = int.Parse(match.Groups[2].Value);
} else {
var data = line.Substring(1);
MakeParagraph(leftData, data, LineMode.Normal);
MakeParagraph(rightData, data, LineMode.Normal);
leftNumbers.Add(leftLine.ToString());
rightNumbers.Add(rightLine.ToString());
leftLine++;
rightLine++;
}
}
}
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
if (leftData.Count == 0) {
MakeParagraph(leftData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
MakeParagraph(rightData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
}
leftLineNumber.Text = string.Join("\n", leftNumbers);
rightLineNumber.Text = string.Join("\n", rightNumbers);
leftText.Document.PageWidth = minWidth + 16;
rightText.Document.PageWidth = minWidth + 16;
leftText.Document.Blocks.AddRange(leftData);
rightText.Document.Blocks.AddRange(rightData);
leftText.ScrollToHome();
mask.Visibility = Visibility.Collapsed;
}
/// <summary>
/// Reset data.
/// </summary>
public void Reset() {
mask.Visibility = Visibility.Visible;
}
/// <summary>
/// Make paragraph.
/// </summary>
/// <param name="collection"></param>
/// <param name="content"></param>
/// <param name="mode"></param>
private void MakeParagraph(List<Paragraph> collection, string content, LineMode mode) {
Paragraph p = new Paragraph(new Run(content));
p.Margin = new Thickness(0);
p.Padding = new Thickness();
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
switch (mode) {
case LineMode.Normal:
break;
case LineMode.Indicator:
p.Foreground = Brushes.Gray;
p.FontStyle = FontStyles.Italic;
break;
case LineMode.Empty:
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
break;
case LineMode.Added:
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
break;
case LineMode.Deleted:
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
break;
}
var formatter = new FormattedText(
content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
leftText.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) minWidth = formatter.Width;
collection.Add(p);
}
/// <summary>
/// Fit both side with empty lines.
/// </summary>
/// <param name="left"></param>
/// <param name="leftNumbers"></param>
/// <param name="right"></param>
/// <param name="rightNumbers"></param>
private void FitBothSide(List<Paragraph> left, List<string> leftNumbers, List<Paragraph> right, List<string> rightNumbers) {
int leftCount = left.Count;
int rightCount = right.Count;
int diff = 0;
List<Paragraph> fitContent = null;
List<string> fitNumber = null;
if (leftCount > rightCount) {
diff = leftCount - rightCount;
fitContent = right;
fitNumber = rightNumbers;
} else if (rightCount > leftCount) {
diff = rightCount - leftCount;
fitContent = left;
fitNumber = leftNumbers;
}
for (int i = 0; i < diff; i++) {
MakeParagraph(fitContent, "", LineMode.Empty);
fitNumber.Add("");
}
}
/// <summary>
/// Sync scroll both sides.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
if (leftText.VerticalOffset != e.VerticalOffset) {
leftText.ScrollToVerticalOffset(e.VerticalOffset);
}
if (rightText.VerticalOffset != e.VerticalOffset) {
rightText.ScrollToVerticalOffset(e.VerticalOffset);
}
leftLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
rightLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
} else {
if (leftText.HorizontalOffset != e.HorizontalOffset) {
leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
if (rightText.HorizontalOffset != e.HorizontalOffset) {
rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
/// <summary>
/// Scroll using mouse wheel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
var text = sender as RichTextBox;
if (text == null) return;
if (e.Delta > 0) {
text.LineUp();
} else {
text.LineDown();
}
e.Handled = true;
}
private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
if (leftText.Document.PageWidth < leftText.ActualWidth) {
leftText.Document.PageWidth = leftText.ActualWidth;
}
}
private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
if (rightText.Document.PageWidth < rightText.ActualWidth) {
rightText.Document.PageWidth = rightText.ActualWidth;
}
}
}
}

55
UI/Discard.xaml Normal file
View file

@ -0,0 +1,55 @@
<UserControl x:Class="SourceGit.UI.Discard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Discard Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Changes :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="icon" Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}" Fill="{StaticResource Brush.FG2}" Margin="4,0"/>
<Label x:Name="txtPath"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="1" Content="You can't undo this action!!!" Foreground="{StaticResource Brush.FG2}"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

67
UI/Discard.xaml.cs Normal file
View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to discard changes dialog.
/// </summary>
public partial class Discard : UserControl {
private Git.Repository repo = null;
private List<Git.Change> changes = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public Discard(Git.Repository opened, List<Git.Change> targets) {
repo = opened;
changes = targets;
InitializeComponent();
if (changes == null || changes.Count == 0) {
txtPath.Content = "All local changes in working copy.";
icon.Data = FindResource("Icon.Folder") as Geometry;
} else if (changes.Count == 1) {
txtPath.Content = changes[0].Path;
} else {
txtPath.Content = $"Total {changes.Count} changes ...";
}
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public static void Show(Git.Repository opened, List<Git.Change> targets) {
PopupManager.Show(new Discard(opened, targets));
}
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.Discard(changes));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

78
UI/Fetch.xaml Normal file
View file

@ -0,0 +1,78 @@
<UserControl x:Class="SourceGit.UI.Fetch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</Grid.Resources>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Fetch Remote Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<ComboBox x:Name="combRemotes" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" IsEnabled="{Binding ElementName=chkFetchAll, Path=IsChecked, Converter={StaticResource InverseBool}}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type git:Remote}">
<StackPanel Orientation="Horizontal" Height="20">
<Path Margin="4,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<Label Content="{Binding Name}" Padding="8,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="3" Grid.Column="1"
x:Name="chkFetchAll"
IsChecked="True"
Content="Fetch all remotes"/>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkPrune"
IsChecked="True"
Content="Prune remote dead branches"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="7" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

85
UI/Fetch.xaml.cs Normal file
View file

@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Fetch dialog.
/// </summary>
public partial class Fetch : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
/// <param name="preferRemote">Prefer selected remote.</param>
public Fetch(Git.Repository opened, string preferRemote) {
repo = opened;
InitializeComponent();
Task.Run(() => {
var remotes = repo.Remotes();
Dispatcher.Invoke(() => {
combRemotes.ItemsSource = remotes;
if (preferRemote != null) {
combRemotes.SelectedIndex = remotes.FindIndex(r => r.Name == preferRemote);
chkFetchAll.IsChecked = false;
} else {
combRemotes.SelectedIndex = 0;
chkFetchAll.IsChecked = true;
}
});
});
}
/// <summary>
/// Show fetch dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="preferRemote"></param>
public static void Show(Git.Repository repo, string preferRemote = null) {
PopupManager.Show(new Fetch(repo, preferRemote));
}
/// <summary>
/// Start fetch
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
bool prune = chkPrune.IsChecked == true;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
if (chkFetchAll.IsChecked == true) {
await Task.Run(() => repo.Fetch(null, prune, msg => Dispatcher.Invoke(() => statusMsg.Content = msg)));
} else {
var remote = combRemotes.SelectedItem as Git.Remote;
await Task.Run(() => repo.Fetch(remote, prune, msg => Dispatcher.Invoke(() => statusMsg.Content = msg)));
}
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

169
UI/FileHistories.xaml Normal file
View file

@ -0,0 +1,169 @@
<Window x:Class="SourceGit.UI.FileHistories"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
mc:Ignorable="d"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Layout Window -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LOGO & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - FILE HISTORIES" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Body -->
<Border Grid.Row="1" ClipToBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid
x:Name="commitList"
Margin="2,0,0,0"
Grid.Column="0"
Background="{StaticResource Brush.BG2}"
BorderThickness="0"
SelectionMode="Single"
SelectionChanged="CommitSelectionChanged">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource Brush.BG4}" BorderThickness="0,0,0,1" Padding="4">
<StackPanel Orientation="Vertical" Margin="2" MaxWidth="290">
<Grid TextBlock.FontSize="11" TextBlock.Foreground="{StaticResource Brush.FG2}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="72"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Padding="0">
<Hyperlink RequestNavigate="NavigateToCommit" NavigateUri="{Binding SHA}" Foreground="DarkOrange" ToolTip="GOTO COMMIT">
<Run Text="{Binding ShortSHA, Mode=OneWay}"/>
</Hyperlink>
</Label>
<TextBlock Grid.Column="1" Text="{Binding Author.Name}"/>
<TextBlock Grid.Column="2" Text="{Binding Author.Time}"/>
</Grid>
<TextBlock MaxWidth="280" Foreground="{StaticResource Brush.FG}" Text="{Binding Subject}" TextAlignment="Left" Padding="0" Margin="0,8,2,0"/>
</StackPanel>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Loading tip -->
<Path x:Name="loading" Grid.Column="0" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.BG4}"/>
<local:DiffViewer x:Name="diff" Grid.Column="2" Margin="2,0"/>
</Grid>
</Border>
</Grid>
</Border>
</Window>

122
UI/FileHistories.xaml.cs Normal file
View file

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// File histories panel.
/// </summary>
public partial class FileHistories : Window {
private Git.Repository repo = null;
private string file = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
public FileHistories(Git.Repository repo, string file) {
this.repo = repo;
this.file = file;
InitializeComponent();
// Move to center
var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible;
// Load commits
Task.Run(() => {
var commits = repo.Commits($"-n 10000 -- \"{file}\"");
Dispatcher.Invoke(() => {
commitList.ItemsSource = commits;
commitList.SelectedIndex = 0;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
});
});
}
/// <summary>
/// Logo click
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
/// <summary>
/// Commit selection change event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var commit = e.AddedItems[0] as Git.Commit;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, file);
});
diff.SetData(data, $"{file} @ {commit.ShortSHA}");
}
/// <summary>
/// Navigate to given string
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NavigateToCommit(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true;
}
}
}

View file

@ -0,0 +1,50 @@
<UserControl x:Class="SourceGit.UI.GitFlowFinishBranch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" x:Name="txtTitle" FontWeight="DemiBold" FontSize="18"/>
<Label Grid.Row="2" Grid.Column="0" x:Name="txtBranchType" HorizontalAlignment="Right"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}" Margin="4,0"/>
<Label x:Name="txtBranchName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm finish git-flow branch dialog
/// </summary>
public partial class GitFlowFinishBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch branch = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public GitFlowFinishBranch(Git.Repository repo, Git.Branch branch) {
this.repo = repo;
this.branch = branch;
InitializeComponent();
switch (branch.Kind) {
case Git.Branch.Type.Feature:
txtTitle.Content = "Git Flow - Finish Feature";
txtBranchType.Content = "Feature :";
break;
case Git.Branch.Type.Release:
txtTitle.Content = "Git Flow - Finish Release";
txtBranchType.Content = "Release :";
break;
case Git.Branch.Type.Hotfix:
txtTitle.Content = "Git Flow - Finish Hotfix";
txtBranchType.Content = "Hotfix :";
break;
default:
PopupManager.Close();
return;
}
txtBranchName.Content = branch.Name;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
PopupManager.Show(new GitFlowFinishBranch(repo, branch));
}
/// <summary>
/// Do finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.FinishGitFlowBranch(branch));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

68
UI/GitFlowSetup.xaml Normal file
View file

@ -0,0 +1,68 @@
<UserControl x:Class="SourceGit.UI.GitFlowSetup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="304" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Git Flow - Initialize"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Production Branch :"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtMaster" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="master" Height="24" Text="master"/>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Development Branch :"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="txtDevelop" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="develop" Height="24" Text="develop"/>
<Label Grid.Row="5" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Feature Prefix :"/>
<TextBox Grid.Row="5" Grid.Column="1" x:Name="txtFeature" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="feature/" Height="24" Text="feature/"/>
<Label Grid.Row="6" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Release Prefix :"/>
<TextBox Grid.Row="6" Grid.Column="1" x:Name="txtRelease" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="release/" Height="24" Text="release/"/>
<Label Grid.Row="7" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Hotfix Prefix :"/>
<TextBox Grid.Row="7" Grid.Column="1" x:Name="txtHotfix" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="hotfix/" Height="24" Text="hotfix/"/>
<Label Grid.Row="8" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Version Tag Prefix :"/>
<TextBox Grid.Row="8" Grid.Column="1" x:Name="txtVersion" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="Optional." Height="24" Text=""/>
<Grid Grid.Row="10" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="txtValidation" Foreground="Red"/>
<Button Grid.Column="1" x:Name="btnSure" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="11" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

134
UI/GitFlowSetup.xaml.cs Normal file
View file

@ -0,0 +1,134 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Dialog to initialize git flow.
/// </summary>
public partial class GitFlowSetup : UserControl {
private Git.Repository repo = null;
private Regex regex = new Regex(@"^[\w\-/\.]+$");
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
public GitFlowSetup(Git.Repository opened) {
repo = opened;
InitializeComponent();
}
/// <summary>
/// Open this dialog.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
PopupManager.Show(new GitFlowSetup(repo));
}
/// <summary>
/// Start to initialize git-flow.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
var master = txtMaster.Text;
var dev = txtDevelop.Text;
var feature = txtFeature.Text;
var release = txtRelease.Text;
var hotfix = txtHotfix.Text;
var version = txtVersion.Text;
await Task.Run(() => repo.EnableGitFlow(master, dev, feature, release, hotfix, version));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
/// <summary>
/// Validate input names.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ValidateNames(object sender, TextChangedEventArgs e) {
if (!IsLoaded) return;
var master = txtMaster.Text;
var dev = txtDevelop.Text;
var feature = txtFeature.Text;
var release = txtRelease.Text;
var hotfix = txtHotfix.Text;
if (!ValidateBranch("Production", master)) return;
if (!ValidateBranch("Development", dev)) return;
if (dev == master) {
txtValidation.Content = "Development branch is same with production!";
btnSure.IsEnabled = false;
return;
}
if (!ValidatePrefix("Feature", feature)) return;
if (!ValidatePrefix("Release", release)) return;
if (!ValidatePrefix("Hotfix", hotfix)) return;
txtValidation.Content = "";
btnSure.IsEnabled = true;
}
private bool ValidateBranch(string type, string name) {
if (string.IsNullOrEmpty(name)) {
txtValidation.Content = $"{type} branch name can't be empty";
btnSure.IsEnabled = false;
return false;
}
if (!regex.IsMatch(name)) {
txtValidation.Content = $"{type} branch name contains invalid characters.";
btnSure.IsEnabled = false;
return false;
}
return true;
}
private bool ValidatePrefix(string type, string prefix) {
if (string.IsNullOrEmpty(prefix)) {
txtValidation.Content = $"{type} prefix is required!";
btnSure.IsEnabled = false;
return false;
}
if (!regex.IsMatch(prefix)) {
txtValidation.Content = $"{type} prefix contains invalid characters.";
btnSure.IsEnabled = false;
return false;
}
return true;
}
}
}

View file

@ -0,0 +1,57 @@
<UserControl x:Class="SourceGit.UI.GitFlowStartBranch"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" x:Name="txtTitle" FontWeight="DemiBold" FontSize="18"/>
<Label Grid.Row="2" Grid.Column="0" x:Name="txtPrefix" HorizontalAlignment="Right" FontSize="16" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtName" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="Enter name" Height="24">
<TextBox.Text>
<Binding ElementName="me" Path="SubName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:BranchNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,103 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Start git-flow branch dialog.
/// </summary>
public partial class GitFlowStartBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch.Type type = Git.Branch.Type.Feature;
/// <summary>
/// Sub-name for this git-flow branch.
/// </summary>
public string SubName {
get;
set;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="type"></param>
public GitFlowStartBranch(Git.Repository repo, Git.Branch.Type type) {
this.repo = repo;
this.type = type;
InitializeComponent();
nameValidator.Repo = repo;
switch (type) {
case Git.Branch.Type.Feature:
var featurePrefix = repo.GetFeaturePrefix();
txtTitle.Content = "Git Flow - Start Feature";
txtPrefix.Content = featurePrefix;
nameValidator.Prefix = featurePrefix;
break;
case Git.Branch.Type.Release:
var releasePrefix = repo.GetReleasePrefix();
txtTitle.Content = "Git Flow - Start Release";
txtPrefix.Content = releasePrefix;
nameValidator.Prefix = releasePrefix;
break;
case Git.Branch.Type.Hotfix:
var hotfixPrefix = repo.GetHotfixPrefix();
txtTitle.Content = "Git Flow - Start Hotfix";
txtPrefix.Content = hotfixPrefix;
nameValidator.Prefix = hotfixPrefix;
break;
default:
PopupManager.Close();
return;
}
}
/// <summary>
/// Display this dialog
/// </summary>
/// <param name="repo"></param>
/// <param name="type"></param>
public static void Show(Git.Repository repo, Git.Branch.Type type) {
PopupManager.Show(new GitFlowStartBranch(repo, type));
}
/// <summary>
/// Start git-flow branch
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.StartGitFlowBranch(type, SubName));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

Some files were not shown because too many files have changed in this diff Show more