commit 38227b1d57081cd1e0103100063f9111ee9dce67 Author: leo Date: Fri Jul 3 15:24:31 2020 +0800 v1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4b82ccd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs/ +bin/ +obj/ diff --git a/App.config b/App.config new file mode 100644 index 00000000..3699f9dd --- /dev/null +++ b/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/App.ico b/App.ico new file mode 100644 index 00000000..9063ffee Binary files /dev/null and b/App.ico differ diff --git a/App.xaml b/App.xaml new file mode 100644 index 00000000..875ead33 --- /dev/null +++ b/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 00000000..a5f6b39c --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,134 @@ +using Microsoft.Win32; +using System; +using System.IO; +using System.Windows; + +namespace SourceGit { + + /// + /// Application. + /// + public partial class App : Application { + + /// + /// Getter/Setter for Git preference. + /// + public static Git.Preference Preference { + get { return Git.Preference.Instance; } + set { Git.Preference.Instance = value; } + } + + /// + /// Check if GIT has been configured. + /// + public static bool IsGitConfigured { + get { + return !string.IsNullOrEmpty(Preference.GitExecutable) + && File.Exists(Preference.GitExecutable); + } + } + + /// + /// Interactive rebase sequence file. + /// + public static string InteractiveRebaseScript { + get; + private set; + } + + /// + /// TODO file for interactive rebase. + /// + public static string InteractiveRebaseTodo { + get; + private set; + } + + /// + /// Error handler. + /// + public static Action OnError { + get; + set; + } + + /// + /// Raise error message. + /// + /// + public static void RaiseError(string message) { + OnError?.Invoke(message); + } + + /// + /// Startup event. + /// + /// + /// + 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(); + } + + /// + /// Deactivated event. + /// + /// + /// + private void OnAppDeactivated(object sender, EventArgs e) { + GC.Collect(); + } + + /// + /// Quit event. + /// + /// + /// + private void OnAppExit(object sender, ExitEventArgs e) { + Git.Preference.Save(); + } + } +} diff --git a/Converters/BoolToCollapsed.cs b/Converters/BoolToCollapsed.cs new file mode 100644 index 00000000..47ce3c73 --- /dev/null +++ b/Converters/BoolToCollapsed.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Same as BoolToVisibilityConverter. + /// + public class BoolToCollapsed : IValueConverter { + + /// + /// Implement IValueConverter.Convert + /// + /// + /// + /// + /// + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + return (bool)value ? Visibility.Visible : Visibility.Collapsed; + } + + /// + /// Implement IValueConverter.ConvertBack + /// + /// + /// + /// + /// + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + throw new NotImplementedException(); + } + } +} diff --git a/Converters/FileStatusToColor.cs b/Converters/FileStatusToColor.cs new file mode 100644 index 00000000..aba855f9 --- /dev/null +++ b/Converters/FileStatusToColor.cs @@ -0,0 +1,59 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace SourceGit.Converters { + + /// + /// Convert file status to brush + /// + public class FileStatusToColor : IValueConverter { + + /// + /// Is only test local changes. + /// + 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(); + } + } +} diff --git a/Converters/FileStatusToIcon.cs b/Converters/FileStatusToIcon.cs new file mode 100644 index 00000000..85447770 --- /dev/null +++ b/Converters/FileStatusToIcon.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Convert file status to icon. + /// + public class FileStatusToIcon : IValueConverter { + + /// + /// Is only test local changes. + /// + 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(); + } + } +} diff --git a/Converters/IndentToMargin.cs b/Converters/IndentToMargin.cs new file mode 100644 index 00000000..5214396b --- /dev/null +++ b/Converters/IndentToMargin.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Convert indent(horizontal offset) to Margin property + /// + public class IndentToMargin : IValueConverter { + + /// + /// Implement IValueConverter.Convert + /// + /// + /// + /// + /// + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + return new Thickness((double)value, 0, 0, 0); + } + + /// + /// Implement IValueConverter.ConvertBack + /// + /// + /// + /// + /// + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + return ((Thickness)value).Left; + } + } +} diff --git a/Converters/InverseBool.cs b/Converters/InverseBool.cs new file mode 100644 index 00000000..932ae4ef --- /dev/null +++ b/Converters/InverseBool.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Inverse bool converter. + /// + 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(); + } + } +} diff --git a/Converters/InverseBoolToCollapsed.cs b/Converters/InverseBoolToCollapsed.cs new file mode 100644 index 00000000..862bf913 --- /dev/null +++ b/Converters/InverseBoolToCollapsed.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Inverse BoolToCollapsed. + /// + public class InverseBoolToCollapsed : IValueConverter { + + /// + /// Implement IValueConverter.Convert + /// + /// + /// + /// + /// + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + return (bool)value ? Visibility.Collapsed : Visibility.Visible; + } + + /// + /// Implement IValueConverter.ConvertBack + /// + /// + /// + /// + /// + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + throw new NotImplementedException(); + } + } +} diff --git a/Converters/PercentToDouble.cs b/Converters/PercentToDouble.cs new file mode 100644 index 00000000..a76c9b92 --- /dev/null +++ b/Converters/PercentToDouble.cs @@ -0,0 +1,41 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace SourceGit.Converters { + + /// + /// Convert percent to double. + /// + public class PercentToDouble : IValueConverter { + + /// + /// Percentage. + /// + public double Percent { get; set; } + + /// + /// Implement IValueConverter.Convert + /// + /// + /// + /// + /// + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + return (double)value * Percent; + } + + /// + /// Implement IValueConverter.ConvertBack + /// + /// + /// + /// + /// + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + throw new NotImplementedException(); + } + } +} diff --git a/Converters/TreeViewItemDepthToMargin.cs b/Converters/TreeViewItemDepthToMargin.cs new file mode 100644 index 00000000..8c7eb856 --- /dev/null +++ b/Converters/TreeViewItemDepthToMargin.cs @@ -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 { + + /// + /// Convert depth of a TreeViewItem to Margin property. + /// + public class TreeViewItemDepthToMargin : IValueConverter { + + /// + /// Indent length + /// + public double Indent { get; set; } = 19; + + /// + /// Implement IValueConverter.Convert + /// + /// + /// + /// + /// + /// + 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); + } + + /// + /// Implement IValueConvert.ConvertBack + /// + /// + /// + /// + /// + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + throw new NotImplementedException(); + } + + /// + /// Get parent item. + /// + /// + /// + 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; + } + } +} diff --git a/Git/Blame.cs b/Git/Blame.cs new file mode 100644 index 00000000..bf597274 --- /dev/null +++ b/Git/Blame.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace SourceGit.Git { + + /// + /// Blame + /// + public class Blame { + + /// + /// Block content. + /// + public class Block { + public string CommitSHA { get; set; } + public string Author { get; set; } + public string Time { get; set; } + public string Content { get; set; } + } + + /// + /// Blocks + /// + public List Blocks { get; set; } = new List(); + + /// + /// Is binary file? + /// + public bool IsBinary { get; set; } = false; + + /// + /// Line count. + /// + public int LineCount { get; set; } = 0; + } +} diff --git a/Git/Branch.cs b/Git/Branch.cs new file mode 100644 index 00000000..05375b95 --- /dev/null +++ b/Git/Branch.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git branch + /// + 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+)"); + + /// + /// Branch type. + /// + public enum Type { + Normal, + Feature, + Release, + Hotfix, + } + + /// + /// Branch name + /// + public string Name { get; set; } + + /// + /// Full name. + /// + public string FullName { get; set; } + + /// + /// Head ref + /// + public string Head { get; set; } + + /// + /// Subject for head ref. + /// + public string HeadSubject { get; set; } + + /// + /// Is local branch + /// + public bool IsLocal { get; set; } + + /// + /// Branch type. + /// + public Type Kind { get; set; } = Type.Normal; + + /// + /// Remote name. Only used for remote branch + /// + public string Remote { get; set; } + + /// + /// Upstream. Only used for local branches. + /// + public string Upstream { get; set; } + + /// + /// Track information for upstream. Only used for local branches. + /// + public string UpstreamTrack { get; set; } + + /// + /// Is current branch. Only used for local branches. + /// + public bool IsCurrent { get; set; } + + /// + /// Is this branch's HEAD same with upstream? + /// + public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack); + + /// + /// Enable filter in log histories. + /// + public bool IsFiltered { get; set; } + + /// + /// Load branches. + /// + /// + public static List Load(Repository repo) { + var localPrefix = "refs/heads/"; + var remotePrefix = "refs/remotes/"; + var branches = new List(); + var remoteBranches = new List(); + + 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; + } + + /// + /// Create new branch. + /// + /// + /// + /// + public static void Create(Repository repo, string name, string startPoint) { + var errs = repo.RunCommand($"branch {name} {startPoint}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Rename branch + /// + /// + /// + public void Rename(Repository repo, string name) { + var errs = repo.RunCommand($"branch -M {Name} {name}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Delete branch. + /// + /// + 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(); + } + } +} diff --git a/Git/Change.cs b/Git/Change.cs new file mode 100644 index 00000000..c5f9ca4a --- /dev/null +++ b/Git/Change.cs @@ -0,0 +1,147 @@ +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Changed file status. + /// + public class Change { + private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); + + /// + /// Status Code + /// + public enum Status { + None, + Modified, + Added, + Deleted, + Renamed, + Copied, + Unmerged, + Untracked, + } + + /// + /// Index status + /// + public Status Index { get; set; } + + /// + /// Work tree status. + /// + public Status WorkTree { get; set; } + + /// + /// Current file path. + /// + public string Path { get; set; } + + /// + /// Original file path before this revision. + /// + public string OriginalPath { get; set; } + + /// + /// Staged(added) in index? + /// + public bool IsAddedToIndex { + get { + if (Index == Status.None || Index == Status.Untracked) return false; + return true; + } + } + + /// + /// Is conflict? + /// + 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; + } + } + + /// + /// Parse change for `--name-status` data. + /// + /// Raw data. + /// Read from commit? + /// Parsed change instance. + 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); + } + } + } + } + } +} diff --git a/Git/Commit.cs b/Git/Commit.cs new file mode 100644 index 00000000..7cbafad4 --- /dev/null +++ b/Git/Commit.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git commit information. + /// + public class Commit { + private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; + private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----"; + + /// + /// SHA + /// + public string SHA { get; set; } + + /// + /// Short SHA. + /// + public string ShortSHA => SHA.Substring(0, 8); + + /// + /// Parent commit SHAs. + /// + public List Parents { get; set; } = new List(); + + /// + /// Author + /// + public User Author { get; set; } = new User(); + + /// + /// Committer. + /// + public User Committer { get; set; } = new User(); + + /// + /// Subject + /// + public string Subject { get; set; } = ""; + + /// + /// Extra message. + /// + public string Message { get; set; } = ""; + + /// + /// HEAD commit? + /// + public bool IsHEAD { get; set; } = false; + + /// + /// Merged in current branch? + /// + public bool IsMerged { get; set; } = false; + + /// + /// X offset in graph + /// + public double GraphOffset { get; set; } = 0; + + /// + /// Has decorators. + /// + public bool HasDecorators => Decorators.Count > 0; + + /// + /// Decorators. + /// + public List Decorators { get; set; } = new List(); + + /// + /// Read commits. + /// + /// Repository + /// Limitations + /// Parsed commits. + public static List Load(Repository repo, string limit) { + List commits = new List(); + 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; + } + + /// + /// Get changed file list. + /// + /// + /// + public List GetChanges(Repository repo) { + var changes = new List(); + 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; + } + + /// + /// Get revision files. + /// + /// + /// + public List GetFiles(Repository repo) { + var files = new List(); + + var errs = repo.RunCommand($"ls-tree --name-only -r {SHA}", line => { + files.Add(line); + }); + + if (errs != null) App.RaiseError(errs); + return files; + } + + /// + /// Get file content. + /// + /// + /// + /// + public string GetTextFileContent(Repository repo, string file) { + var data = new List(); + 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() + }); + } + } + } + } +} diff --git a/Git/Decorator.cs b/Git/Decorator.cs new file mode 100644 index 00000000..d9131712 --- /dev/null +++ b/Git/Decorator.cs @@ -0,0 +1,21 @@ +namespace SourceGit.Git { + + /// + /// Decorator type. + /// + public enum DecoratorType { + None, + CurrentBranchHead, + LocalBranchHead, + RemoteBranchHead, + Tag, + } + + /// + /// Commit decorator. + /// + public class Decorator { + public DecoratorType Type { get; set; } + public string Name { get; set; } + } +} diff --git a/Git/MergeTool.cs b/Git/MergeTool.cs new file mode 100644 index 00000000..8fda9492 --- /dev/null +++ b/Git/MergeTool.cs @@ -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 { + + /// + /// External merge tool + /// + public class MergeTool { + + /// + /// Display name + /// + public string Name { get; set; } + + /// + /// Executable file name. + /// + public string ExecutableName { get; set; } + + /// + /// Command line parameter. + /// + public string Parameter { get; set; } + + /// + /// Auto finder. + /// + public Func Finder { get; set; } + + /// + /// Is this merge tool configured. + /// + public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName); + + /// + /// Supported merge tools. + /// + public static List Supported = new List() { + 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), + }; + + /// + /// Finder for invalid merge tool. + /// + /// + public static string FindInvalid() { + return "--"; + } + + /// + /// Find araxis merge tool install path. + /// + /// + public static string FindAraxisMerge() { + var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe"; + if (File.Exists(path)) return path; + return ""; + } + + /// + /// Find kdiff3.exe by registry. + /// + /// + 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; + } + + /// + /// Finder for p4merge + /// + /// + public static string FindP4Merge() { + var path = @"C:\Program Files\Perforce\p4merge.exe"; + if (File.Exists(path)) return path; + return ""; + } + + /// + /// Find BComp.exe by registry. + /// + /// + 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"; + } + + /// + /// Find TortoiseMerge.exe by registry. + /// + /// + 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; + } + + /// + /// Find vsDiffMerge.exe. + /// + /// + 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"; + } + + /// + /// Find VSCode executable file path. + /// + /// + 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 ""; + } + + /// + /// Constructor. + /// + /// + /// + /// + /// + public MergeTool(string name, string exe, string param, Func finder) { + Name = name; + ExecutableName = exe; + Parameter = param; + Finder = finder; + } + } +} diff --git a/Git/Preference.cs b/Git/Preference.cs new file mode 100644 index 00000000..2d223b53 --- /dev/null +++ b/Git/Preference.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Serialization; + +namespace SourceGit.Git { + + /// + /// User's preference settings. Serialized to + /// + public class Preference { + + /// + /// Group(Virtual folder) for watched repositories. + /// + public class Group { + /// + /// Unique ID of this group. + /// + public string Id { get; set; } + /// + /// Display name. + /// + public string Name { get; set; } + /// + /// Parent ID. + /// + public string ParentId { get; set; } + /// + /// Cache UI IsExpended status. + /// + public bool IsExpended { get; set; } + } + + #region STATICS + /// + /// Storage path for Preference. + /// + private static readonly string SAVE_PATH = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit", + "preference.xml"); + /// + /// Runtime singleton instance. + /// + private static Preference instance = null; + public static Preference Instance { + get { + if (instance == null) Load(); + return instance; + } + set { + instance = value; + } + } + #endregion + + #region SETTING_GIT + /// + /// Git executable file path. + /// + public string GitExecutable { get; set; } + /// + /// Default clone directory. + /// + public string GitDefaultCloneDir { get; set; } + #endregion + + #region SETTING_MERGE_TOOL + /// + /// Selected merge tool. + /// + public int MergeTool { get; set; } = 0; + /// + /// Executable file path for merge tool. + /// + public string MergeExecutable { get; set; } = "--"; + #endregion + + #region SETTING_UI + /// + /// Main window's width + /// + public double UIMainWindowWidth { get; set; } + /// + /// Main window's height + /// + public double UIMainWindowHeight { get; set; } + /// + /// Use light color theme. + /// + public bool UIUseLightTheme { get; set; } + /// + /// Show/Hide tags' list view. + /// + public bool UIShowTags { get; set; } = true; + /// + /// Use horizontal layout for histories. + /// + public bool UIUseHorizontalLayout { get; set; } + /// + /// Use list instead of tree in unstaged view + /// + public bool UIUseListInUnstaged { get; set; } + /// + /// Use list instead of tree in staged view. + /// + public bool UIUseListInStaged { get; set; } + /// + /// Use list instead of tree in change view. + /// + public bool UIUseListInChanges { get; set; } + #endregion + + #region SETTING_REPOS + /// + /// Groups for repositories. + /// + public List Groups { get; set; } = new List(); + /// + /// Watched repositories. + /// + public List Repositories { get; set; } = new List(); + #endregion + + #region METHODS_LOAD_SAVE + /// + /// Load preference from disk. + /// + /// Loaded preference instance. + 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(); + } + + /// + /// Save current preference into disk. + /// + 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 + /// + /// Add new group(virtual folder). + /// + /// Display name. + /// Parent group ID. + /// Added group instance. + 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; + } + + /// + /// Find group by ID. + /// + /// Unique ID + /// Founded group's instance. + public Group FindGroup(string id) { + foreach (var group in Groups) { + if (group.Id == id) return group; + } + return null; + } + + /// + /// Rename group. + /// + /// Unique ID + /// New name. + 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)); + } + + /// + /// Remove a group. + /// + /// Unique ID + 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 + /// + /// Add repository. + /// + /// Local storage path. + /// Group's ID + /// Added repository instance. + 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; + } + + /// + /// Find repository by path. + /// + /// Local storage path. + /// Founded repository instance. + public Repository FindRepository(string path) { + var dir = new DirectoryInfo(path); + foreach (var repo in Repositories) { + if (repo.Path == dir.FullName) return repo; + } + return null; + } + + /// + /// Change a repository's display name in RepositoryManager. + /// + /// Local storage path. + /// New name + 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)); + } + + /// + /// Remove a repository in RepositoryManager. + /// + /// Local storage path. + 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 + } +} diff --git a/Git/Remote.cs b/Git/Remote.cs new file mode 100644 index 00000000..4b411ff5 --- /dev/null +++ b/Git/Remote.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git remote + /// + public class Remote { + private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); + + /// + /// Name of this remote + /// + public string Name { get; set; } + + /// + /// URL + /// + public string URL { get; set; } + + /// + /// Parsing remote + /// + /// Repository + /// + public static List Load(Repository repo) { + var remotes = new List(); + var added = new List(); + + repo.RunCommand("remote -v", data => { + var match = FORMAT.Match(data); + if (!match.Success) return; + + var remote = new Remote() { + Name = match.Groups[1].Value, + URL = match.Groups[2].Value, + }; + + if (added.Contains(remote.Name)) return; + + added.Add(remote.Name); + remotes.Add(remote); + }); + + return remotes; + } + + /// + /// Add new remote + /// + /// + /// + /// + public static void Add(Repository repo, string name, string url) { + var errs = repo.RunCommand($"remote add {name} {url}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Delete remote. + /// + /// + /// + public static void Delete(Repository repo, string remote) { + var errs = repo.RunCommand($"remote remove {remote}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Edit remote. + /// + /// + /// + /// + public void Edit(Repository repo, string name, string url) { + string errs = null; + + if (name != Name) { + errs = repo.RunCommand($"remote rename {Name} {name}", null); + if (errs != null) { + App.RaiseError(errs); + return; + } + } + + if (url != URL) { + errs = repo.RunCommand($"remote set-url {name} {url}", null); + if (errs != null) App.RaiseError(errs); + } + } + } +} diff --git a/Git/Repository.cs b/Git/Repository.cs new file mode 100644 index 00000000..3d364b5b --- /dev/null +++ b/Git/Repository.cs @@ -0,0 +1,1059 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Threading; +using System.Xml.Serialization; + +namespace SourceGit.Git { + + /// + /// Git repository + /// + public class Repository { + + #region HOOKS + public static Action OnOpen = null; + public static Action OnClose = null; + [XmlIgnore] public Action OnNavigateCommit = null; + [XmlIgnore] public Action OnWorkingCopyChanged = null; + [XmlIgnore] public Action OnTagChanged = null; + [XmlIgnore] public Action OnStashChanged = null; + [XmlIgnore] public Action OnBranchChanged = null; + [XmlIgnore] public Action OnCommitsChanged = null; + #endregion + + #region PROPERTIES_SAVED + /// + /// Storage path. + /// + public string Path { get; set; } + /// + /// Display name. + /// + public string Name { get; set; } + /// + /// Owner group. + /// + public string GroupId { get; set; } + /// + /// Last open time(File time format). + /// + public long LastOpenTime { get; set; } + /// + /// Filters for logs. + /// + public List LogFilters { get; set; } = new List(); + /// + /// Last 10 Commit message. + /// + public List CommitMsgRecords { get; set; } = new List(); + #endregion + + #region PROPERTIES_RUNTIME + private List cachedRemotes = new List(); + private List cachedBranches = new List(); + private List cachedTags = new List(); + private FileSystemWatcher watcher = null; + private DispatcherTimer timer = null; + private bool isWatcherDisabled = false; + private long nextUpdateTags = 0; + private long nextUpdateLocalChanges = 0; + private long nextUpdateStashes = 0; + private long nextUpdateTree = 0; + + private string featurePrefix = null; + private string releasePrefix = null; + private string hotfixPrefix = null; + #endregion + + #region METHOD_PROCESS + /// + /// Read git config + /// + /// + /// + public string GetConfig(string key) { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = Preference.Instance.GitExecutable; + startInfo.Arguments = $"config {key}"; + startInfo.WorkingDirectory = Path; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardOutput = true; + startInfo.StandardOutputEncoding = Encoding.UTF8; + + var proc = new Process() { StartInfo = startInfo }; + proc.Start(); + var output = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + proc.Close(); + + return output.Trim(); + } + + /// + /// Configure git. + /// + /// + /// + public void SetConfig(string key, string value) { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = Preference.Instance.GitExecutable; + startInfo.Arguments = $"config {key} \"{value}\""; + startInfo.WorkingDirectory = Path; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + var proc = new Process() { StartInfo = startInfo }; + proc.Start(); + proc.WaitForExit(); + proc.Close(); + } + + /// + /// Run git command without repository. + /// + /// Working directory. + /// Arguments for running git command. + /// Handler for output. + /// Handle error as output. + /// Errors if exists. + public static string RunCommand(string cwd, string args, Action outputHandler, bool includeError = false) { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = Preference.Instance.GitExecutable; + startInfo.Arguments = "--no-pager -c core.quotepath=off " + args; + startInfo.WorkingDirectory = cwd; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.StandardOutputEncoding = Encoding.UTF8; + startInfo.StandardErrorEncoding = Encoding.UTF8; + + var progressFilter = new Regex(@"\d+\%"); + var errs = new List(); + var proc = new Process() { StartInfo = startInfo }; + + proc.OutputDataReceived += (o, e) => { + if (e.Data == null) return; + outputHandler?.Invoke(e.Data); + }; + proc.ErrorDataReceived += (o, e) => { + if (e.Data == null) return; + if (includeError) outputHandler?.Invoke(e.Data); + if (string.IsNullOrEmpty(e.Data)) return; + if (progressFilter.IsMatch(e.Data)) return; + if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return; + errs.Add(e.Data); + }; + + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + proc.WaitForExit(); + + int exitCode = proc.ExitCode; + proc.Close(); + + if (exitCode != 0 && errs.Count > 0) { + return string.Join("\n", errs); + } else { + return null; + } + } + + /// + /// Create process for reading outputs/errors using git.exe + /// + /// Arguments for running git command. + /// Handler for output. + /// Handle error as output. + /// Errors if exists. + public string RunCommand(string args, Action outputHandler, bool includeError = false) { + return RunCommand(Path, args, outputHandler, includeError); + } + + /// + /// Create process and redirect output to file. + /// + /// Git command arguments. + /// File path to redirect output into. + public void RunAndRedirect(string args, string redirectTo) { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = Preference.Instance.GitExecutable; + startInfo.Arguments = "--no-pager " + args; + startInfo.WorkingDirectory = Path; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + + var proc = new Process() { StartInfo = startInfo }; + proc.Start(); + + using (var writer = new FileStream(redirectTo, FileMode.OpenOrCreate)) { + proc.StandardOutput.BaseStream.CopyTo(writer); + } + + proc.WaitForExit(); + proc.Close(); + } + + /// + /// Assert command result and then update branches and commits. + /// + /// + public void AssertCommand(string err) { + if (!string.IsNullOrEmpty(err)) { + App.RaiseError(err); + } else { + Branches(true); + OnBranchChanged?.Invoke(); + OnCommitsChanged?.Invoke(); + OnWorkingCopyChanged?.Invoke(); + } + + isWatcherDisabled = false; + } + #endregion + + #region METHOD_VALIDATIONS + /// + /// Is valid git directory. + /// + /// Local path. + /// + public static bool IsValid(string path) { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = Preference.Instance.GitExecutable; + startInfo.Arguments = "rev-parse --git-dir"; + startInfo.WorkingDirectory = path; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + try { + var proc = new Process() { StartInfo = startInfo }; + proc.Start(); + proc.WaitForExit(); + + var test = proc.ExitCode == 0; + proc.Close(); + return test; + } catch { + return false; + } + } + + /// + /// Is remote url valid. + /// + /// + /// + public static bool IsValidUrl(string url) { + return !string.IsNullOrEmpty(url) + && (url.StartsWith("http://", StringComparison.Ordinal) + || url.StartsWith("https://", StringComparison.Ordinal) + || url.StartsWith("git://", StringComparison.Ordinal) + || url.StartsWith("ssh://", StringComparison.Ordinal) + || url.StartsWith("file://", StringComparison.Ordinal)); + } + #endregion + + #region METHOD_OPEN_CLOSE + /// + /// Open repository. + /// + public void Open() { + LastOpenTime = DateTime.Now.ToFileTime(); + isWatcherDisabled = false; + + watcher = new FileSystemWatcher(); + watcher.Path = Path; + watcher.Filter = "*"; + watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; + watcher.IncludeSubdirectories = true; + watcher.Created += OnFSChanged; + watcher.Renamed += OnFSChanged; + watcher.Changed += OnFSChanged; + watcher.Deleted += OnFSChanged; + watcher.EnableRaisingEvents = true; + + timer = new DispatcherTimer(); + timer.Tick += Tick; + timer.Interval = TimeSpan.FromSeconds(.1); + timer.Start(); + + featurePrefix = GetConfig("gitflow.prefix.feature"); + releasePrefix = GetConfig("gitflow.prefix.release"); + hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); + + OnOpen?.Invoke(this); + } + + /// + /// Close repository. + /// + public void Close() { + OnBranchChanged = null; + OnCommitsChanged = null; + OnTagChanged = null; + OnStashChanged = null; + OnWorkingCopyChanged = null; + OnNavigateCommit = null; + + cachedBranches.Clear(); + cachedRemotes.Clear(); + cachedTags.Clear(); + + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + timer.Stop(); + + watcher = null; + timer = null; + featurePrefix = null; + releasePrefix = null; + hotfixPrefix = null; + + OnClose?.Invoke(); + } + #endregion + + #region METHOD_WATCHER + public void SetWatcherEnabled(bool enabled) { + isWatcherDisabled = !enabled; + } + + private void Tick(object sender, EventArgs e) { + if (isWatcherDisabled) { + nextUpdateLocalChanges = 0; + nextUpdateStashes = 0; + nextUpdateTags = 0; + nextUpdateTree = 0; + return; + } + + var now = DateTime.Now.ToFileTime(); + if (nextUpdateLocalChanges > 0 && now >= nextUpdateLocalChanges) { + nextUpdateLocalChanges = 0; + OnWorkingCopyChanged?.Invoke(); + } + + if (nextUpdateTags > 0 && now >= nextUpdateTags) { + nextUpdateTags = 0; + OnTagChanged?.Invoke(); + } + + if (nextUpdateStashes > 0 && now >= nextUpdateStashes) { + nextUpdateStashes = 0; + OnStashChanged?.Invoke(); + } + + if (nextUpdateTree > 0 && now >= nextUpdateTree) { + nextUpdateTree = 0; + Branches(true); + OnBranchChanged?.Invoke(); + OnCommitsChanged?.Invoke(); + } + } + + private void OnFSChanged(object sender, FileSystemEventArgs e) { + if (e.Name.StartsWith(".git", StringComparison.Ordinal)) { + if (e.Name.Equals(".git") || e.Name.StartsWith(".git\\index")) return; + + if (e.Name.Equals(".gitignore") || e.Name.Equals(".gitattributes")) { + nextUpdateLocalChanges = DateTime.Now.AddSeconds(1.5).ToFileTime(); + } else if (e.Name.StartsWith(".git\\refs\\tags", StringComparison.Ordinal)) { + nextUpdateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); + } else if (e.Name.StartsWith(".git\\refs\\stash", StringComparison.Ordinal)) { + nextUpdateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); + } else if (e.Name.EndsWith("_HEAD", StringComparison.Ordinal) || + e.Name.StartsWith(".git\\refs\\heads", StringComparison.Ordinal) || + e.Name.StartsWith(".git\\refs\\remotes", StringComparison.Ordinal)) { + nextUpdateTree = DateTime.Now.AddSeconds(.5).ToFileTime(); + } + } else { + nextUpdateLocalChanges = DateTime.Now.AddSeconds(1.5).ToFileTime(); + } + } + #endregion + + #region METHOD_GITCOMMANDS + /// + /// Clone repository. + /// + /// Remote repository URL + /// Folder to clone into + /// Local name + /// + /// + public static Repository Clone(string url, string folder, string name, Action onProgress) { + var errs = RunCommand(folder, $"-c credential.helper=manager clone --progress --verbose --recurse-submodules {url} {name}", line => { + if (line != null) onProgress?.Invoke(line); + }, true); + + if (errs != null) { + App.RaiseError(errs); + return null; + } + + var path = new DirectoryInfo(folder + "/" + name).FullName; + var repo = Preference.Instance.AddRepository(path, ""); + return repo; + } + + /// + /// Fetch remote changes + /// + /// + /// + /// + public void Fetch(Remote remote, bool prune, Action onProgress) { + isWatcherDisabled = true; + + var args = "-c credential.helper=manager fetch --progress --verbose "; + + if (prune) args += "--prune "; + + if (remote == null) { + args += "--all"; + } else { + args += remote.Name; + } + + var errs = RunCommand(args, line => { + if (line != null) onProgress?.Invoke(line); + }, true); + + AssertCommand(errs); + } + + /// + /// Pull remote changes. + /// + /// remote + /// branch + /// Progress message handler. + /// Use rebase instead of merge. + /// Auto stash local changes. + /// Progress message handler. + public void Pull(string remote, string branch, Action onProgress, bool rebase = false, bool autostash = false) { + isWatcherDisabled = true; + + var args = "-c credential.helper=manager pull --verbose --progress "; + var needPopStash = false; + + if (rebase) args += "--rebase "; + if (autostash) { + if (rebase) { + args += "--autostash "; + } else { + var changes = LocalChanges(); + if (changes.Count > 0) { + var fatal = RunCommand("stash push -u -m \"PULL_AUTO_STASH\"", null); + if (fatal != null) { + App.RaiseError(fatal); + isWatcherDisabled = false; + return; + } + needPopStash = true; + } + } + } + + var errs = RunCommand(args + remote + " " + branch, line => { + if (line != null) onProgress?.Invoke(line); + }, true); + + AssertCommand(errs); + + if (needPopStash) RunCommand("stash pop -q stash@{0}", null); + } + + /// + /// Push local branch to remote. + /// + /// Remote + /// Local branch name + /// Remote branch name + /// Progress message handler. + /// Push tags + /// Create track reference + /// Force push + public void Push(string remote, string localBranch, string remoteBranch, Action onProgress, bool withTags = false, bool track = false, bool force = false) { + isWatcherDisabled = true; + + var args = "-c credential.helper=manager push --progress --verbose "; + + if (withTags) args += "--tags "; + if (track) args += "-u "; + if (force) args += "--force-with-lease "; + + var errs = RunCommand(args + remote + " " + localBranch + ":" + remoteBranch, line => { + if (line != null) onProgress?.Invoke(line); + }, true); + + AssertCommand(errs); + } + + /// + /// Apply patch. + /// + /// + /// + public void Apply(string patch, string whitespaceMode) { + isWatcherDisabled = true; + + var errs = RunCommand($"apply --whitespace={whitespaceMode} \"{patch}\"", null); + if (errs != null) { + App.RaiseError(errs); + } else { + OnWorkingCopyChanged?.Invoke(); + } + + isWatcherDisabled = false; + } + + /// + /// Revert given commit. + /// + /// + /// + public void Revert(string commit, bool autoCommit) { + isWatcherDisabled = true; + + var errs = RunCommand($"revert {commit} --no-edit" + (autoCommit ? "" : " --no-commit"), null); + AssertCommand(errs); + } + + /// + /// Checkout + /// + /// Options. + public void Checkout(string option) { + isWatcherDisabled = true; + + var errs = RunCommand($"checkout {option}", null); + AssertCommand(errs); + } + + /// + /// Merge given branch into current. + /// + /// + /// + public void Merge(string branch, string option) { + isWatcherDisabled = true; + + var errs = RunCommand($"merge {branch} {option}", null); + AssertCommand(errs); + } + + /// + /// Rebase current branch to revision + /// + /// + /// + public void Rebase(string revision, bool autoStash) { + isWatcherDisabled = true; + + var args = $"rebase "; + if (autoStash) args += "--autostash "; + args += revision; + + var errs = RunCommand(args, null); + AssertCommand(errs); + } + + /// + /// Reset. + /// + /// + /// + public void Reset(string revision, string mode = "") { + isWatcherDisabled = true; + + var errs = RunCommand($"reset {mode} {revision}", null); + AssertCommand(errs); + } + + /// + /// Cherry pick commit. + /// + /// + /// + public void CherryPick(string commit, bool noCommit) { + isWatcherDisabled = true; + + var args = "cherry-pick "; + args += noCommit ? "-n " : "--ff "; + args += commit; + + var errs = RunCommand(args, null); + AssertCommand(errs); + } + + /// + /// Stage(add) files to index. + /// + /// + public void Stage(params string[] files) { + isWatcherDisabled = true; + + var args = "add"; + if (files == null || files.Length == 0) { + args += " ."; + } else { + args += " --"; + foreach (var file in files) args += $" \"{file}\""; + } + + var errs = RunCommand(args, null); + if (errs != null) App.RaiseError(errs); + + OnWorkingCopyChanged?.Invoke(); + isWatcherDisabled = false; + } + + /// + /// Unstage files from index + /// + /// + public void Unstage(params string[] files) { + isWatcherDisabled = true; + + var args = "reset"; + if (files != null && files.Length > 0) { + args += " --"; + foreach (var file in files) args += $" \"{file}\""; + } + + var errs = RunCommand(args, null); + if (errs != null) App.RaiseError(errs); + + OnWorkingCopyChanged?.Invoke(); + isWatcherDisabled = false; + } + + /// + /// Discard changes. + /// + /// + public void Discard(List changes) { + isWatcherDisabled = true; + + if (changes == null || changes.Count == 0) { + var errs = RunCommand("reset --hard HEAD", null); + if (errs != null) { + App.RaiseError(errs); + isWatcherDisabled = false; + return; + } + + RunCommand("clean -qfd", null); + } else { + foreach (var change in changes) { + if (change.WorkTree == Change.Status.Untracked || change.WorkTree == Change.Status.Added) { + RunCommand($"clean -qfd -- \"{change.Path}\"", null); + } else { + RunCommand($"restore -- \"{change.Path}\"", null); + } + } + } + + OnWorkingCopyChanged?.Invoke(); + isWatcherDisabled = false; + } + + /// + /// Commit + /// + /// + /// + public bool DoCommit(string message, bool amend) { + isWatcherDisabled = true; + + var file = System.IO.Path.Combine(Path, ".git", "COMMITMESSAGE"); + File.WriteAllText(file, message); + + var args = $"commit --file=\"{file}\""; + if (amend) args += " --amend --no-edit"; + var errs = RunCommand(args, null); + AssertCommand(errs); + + var branch = CurrentBranch(); + OnNavigateCommit?.Invoke(branch.Head); + return string.IsNullOrEmpty(errs); + } + + /// + /// Get all remotes of this repository. + /// + /// Force reload + /// Remote collection + public List Remotes(bool bForceReload = false) { + if (cachedRemotes.Count == 0 || bForceReload) { + cachedRemotes = Remote.Load(this); + } + + return cachedRemotes; + } + + /// + /// Local changes in working copy. + /// + /// Changes. + public List LocalChanges() { + List changes = new List(); + RunCommand("status -uall --porcelain", line => { + if (!string.IsNullOrEmpty(line)) { + var change = Change.Parse(line); + if (change != null) changes.Add(change); + } + }); + return changes; + } + + /// + /// Get total commit count. + /// + /// Number of total commits. + public int TotalCommits() { + int count = 0; + RunCommand("rev-list --all --count", line => { + if (!string.IsNullOrEmpty(line)) count = int.Parse(line.Trim()); + }); + return count; + } + + /// + /// Load commits. + /// + /// Extra limit arguments for `git log` + /// Commit collection + public List Commits(string limit = null) { + return Commit.Load(this, (limit == null ? "" : limit)); ; + } + + /// + /// Load all branches. + /// + /// Force reload. + /// Branches collection. + public List Branches(bool bForceReload = false) { + if (cachedBranches.Count == 0 || bForceReload) { + cachedBranches = Branch.Load(this); + } + + if (IsGitFlowEnabled()) { + foreach (var b in cachedBranches) { + if (b.IsLocal) { + if (b.Name.StartsWith(featurePrefix)) { + b.Kind = Branch.Type.Feature; + } else if (b.Name.StartsWith(releasePrefix)) { + b.Kind = Branch.Type.Release; + } else if (b.Name.StartsWith(hotfixPrefix)) { + b.Kind = Branch.Type.Hotfix; + } + } + } + } + + return cachedBranches; + } + + /// + /// Get current branch + /// + /// + public Branch CurrentBranch() { + foreach (var b in cachedBranches) { + if (b.IsCurrent) return b; + } + + return null; + } + + /// + /// Load all tags. + /// + /// + /// + public List Tags(bool bForceReload = false) { + if (cachedTags.Count == 0 || bForceReload) { + cachedTags = Tag.Load(this); + } + + return cachedTags; + } + + /// + /// Get all stashes + /// + /// + public List Stashes() { + var reflog = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$"); + var stashes = new List(); + var current = null as Stash; + + var errs = RunCommand("stash list --pretty=raw", line => { + if (line.StartsWith("commit ")) { + if (current != null && !string.IsNullOrEmpty(current.Name)) stashes.Add(current); + current = new Stash() { SHA = line.Substring(7, 8) }; + return; + } + + if (current == null) return; + + if (line.StartsWith("Reflog: refs/stash@")) { + var match = reflog.Match(line); + if (match.Success) current.Name = match.Groups[1].Value; + } else if (line.StartsWith("Reflog message: ")) { + current.Message = line.Substring(16); + } else if (line.StartsWith("author ")) { + current.Author.Parse(line); + } + }); + + if (current != null) stashes.Add(current); + if (errs != null) App.RaiseError(errs); + return stashes; + } + + /// + /// Diff + /// + /// + /// + /// + /// + /// + public List Diff(string startRevision, string endRevision, string file, string orgFile = null) { + var args = $"diff --ignore-cr-at-eol {startRevision} {endRevision} -- "; + if (!string.IsNullOrEmpty(orgFile)) args += $"\"{orgFile}\" "; + args += $"\"{file}\""; + + var data = new List(); + var errs = RunCommand(args, line => data.Add(line)); + + if (errs != null) App.RaiseError(errs); + return data; + } + + /// + /// Blame file. + /// + /// + /// + /// + public Blame BlameFile(string file, string revision) { + var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); + var blame = new Blame(); + var current = null as Blame.Block; + + var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => { + if (blame.IsBinary) return; + if (string.IsNullOrEmpty(line)) return; + + if (line.IndexOf('\0') >= 0) { + blame.IsBinary = true; + blame.Blocks.Clear(); + return; + } + + var match = regex.Match(line); + if (!match.Success) return; + + var commit = match.Groups[1].Value; + var data = match.Groups[4].Value; + if (current != null && current.CommitSHA == commit) { + current.Content = current.Content + "\n" + data; + } else { + var timestamp = int.Parse(match.Groups[3].Value); + var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + + current = new Blame.Block() { + CommitSHA = commit, + Author = match.Groups[2].Value, + Time = when, + Content = data, + }; + + if (current.Author == null) current.Author = ""; + blame.Blocks.Add(current); + } + + blame.LineCount++; + }); + + if (errs != null) App.RaiseError(errs); + return blame; + } + #endregion + + #region METHOD_GITFLOW + /// + /// Check if git-flow feature enabled + /// + /// + public bool IsGitFlowEnabled() { + return !string.IsNullOrEmpty(featurePrefix) + && !string.IsNullOrEmpty(releasePrefix) + && !string.IsNullOrEmpty(hotfixPrefix); + } + + /// + /// Get git-flow branch prefix. + /// + /// + public string GetFeaturePrefix() { return featurePrefix; } + public string GetReleasePrefix() { return releasePrefix; } + public string GetHotfixPrefix() { return hotfixPrefix; } + + /// + /// Enable git-flow + /// + /// + /// + /// + /// + /// + /// + public void EnableGitFlow(string master, string develop, string feature, string release, string hotfix, string version = "") { + isWatcherDisabled = true; + + var branches = Branches(); + var masterBranch = branches.Find(b => b.Name == master); + var devBranch = branches.Find(b => b.Name == develop); + var refreshBranches = false; + + if (masterBranch == null) { + var errs = RunCommand($"branch --no-track {master}", null); + if (errs != null) { + App.RaiseError(errs); + isWatcherDisabled = false; + return; + } + + refreshBranches = true; + } + + if (devBranch == null) { + var errs = RunCommand($"branch --no-track {develop}", null); + if (errs != null) { + App.RaiseError(errs); + if (refreshBranches) { + Branches(true); + OnBranchChanged?.Invoke(); + OnCommitsChanged?.Invoke(); + OnWorkingCopyChanged?.Invoke(); + } + isWatcherDisabled = false; + return; + } + + refreshBranches = true; + } + + SetConfig("gitflow.branch.master", master); + SetConfig("gitflow.branch.develop", develop); + SetConfig("gitflow.prefix.feature", feature); + SetConfig("gitflow.prefix.bugfix", "bugfix"); + SetConfig("gitflow.prefix.release", release); + SetConfig("gitflow.prefix.hotfix", hotfix); + SetConfig("gitflow.prefix.support", "support"); + SetConfig("gitflow.prefix.versiontag", version); + + RunCommand("flow init -d", null); + + featurePrefix = GetConfig("gitflow.prefix.feature"); + releasePrefix = GetConfig("gitflow.prefix.release"); + hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); + + if (!IsGitFlowEnabled()) App.RaiseError("Initialize Git-flow failed!"); + + if (refreshBranches) { + Branches(true); + OnBranchChanged?.Invoke(); + OnCommitsChanged?.Invoke(); + OnWorkingCopyChanged?.Invoke(); + } + + isWatcherDisabled = false; + } + + /// + /// Start git-flow branch + /// + /// + /// + public void StartGitFlowBranch(Branch.Type type, string name) { + isWatcherDisabled = true; + + string args; + switch (type) { + case Branch.Type.Feature: args = $"flow feature start {name}"; break; + case Branch.Type.Release: args = $"flow release start {name}"; break; + case Branch.Type.Hotfix: args = $"flow hotfix start {name}"; break; + default: + App.RaiseError("Bad git-flow branch type!"); + return; + } + + var errs = RunCommand(args, null); + AssertCommand(errs); + } + + /// + /// Finish git-flow branch + /// + /// + public void FinishGitFlowBranch(Branch branch) { + isWatcherDisabled = true; + + string args; + switch (branch.Kind) { + case Branch.Type.Feature: + args = $"flow feature finish {branch.Name.Substring(featurePrefix.Length)}"; + break; + case Branch.Type.Release: + var releaseName = branch.Name.Substring(releasePrefix.Length); + args = $"flow release finish {releaseName} -m \"Release done\""; + break; + case Branch.Type.Hotfix: + var hotfixName = branch.Name.Substring(hotfixPrefix.Length); + args = $"flow hotfix finish {hotfixName} -m \"Hotfix done\""; + break; + default: + App.RaiseError("Bad git-flow branch type!"); + return; + } + + var errs = RunCommand(args, null); + AssertCommand(errs); + OnTagChanged?.Invoke(); + } + #endregion + + #region METHOD_COMMITMSG + public void RecordCommitMessage(string message) { + if (string.IsNullOrEmpty(message)) return; + + int exists = CommitMsgRecords.Count; + if (exists > 0) { + var last = CommitMsgRecords[0]; + if (last == message) return; + } + + if (exists >= 10) { + CommitMsgRecords.RemoveRange(9, exists - 9); + } + + CommitMsgRecords.Insert(0, message); + } + #endregion + } +} diff --git a/Git/Stash.cs b/Git/Stash.cs new file mode 100644 index 00000000..d83f85f0 --- /dev/null +++ b/Git/Stash.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SourceGit.Git { + + /// + /// Git stash + /// + public class Stash { + + /// + /// SHA for this stash + /// + public string SHA { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Author + /// + public User Author { get; set; } = new User(); + + /// + /// Message + /// + public string Message { get; set; } + + /// + /// Stash push. + /// + /// + /// + /// + /// + public static void Push(Repository repo, bool includeUntracked, string message, List 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); + } + + /// + /// Get changed file list in this stash. + /// + /// + /// + public List GetChanges(Repository repo) { + List changes = new List(); + + 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; + } + + /// + /// Apply stash. + /// + /// + public void Apply(Repository repo) { + var errs = repo.RunCommand($"stash apply -q {Name}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Pop stash + /// + /// + public void Pop(Repository repo) { + var errs = repo.RunCommand($"stash pop -q {Name}", null); + if (errs != null) App.RaiseError(errs); + } + + /// + /// Drop stash + /// + /// + public void Drop(Repository repo) { + var errs = repo.RunCommand($"stash drop -q {Name}", null); + if (errs != null) App.RaiseError(errs); + } + } +} diff --git a/Git/Tag.cs b/Git/Tag.cs new file mode 100644 index 00000000..f329c4d4 --- /dev/null +++ b/Git/Tag.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git tag. + /// + public class Tag { + private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)"); + + /// + /// SHA + /// + public string SHA { get; set; } + + /// + /// Display name. + /// + public string Name { get; set; } + + /// + /// Enable filter in log histories. + /// + public bool IsFiltered { get; set; } + + /// + /// Load all tags + /// + /// + /// + public static List Load(Repository repo) { + var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags"; + var tags = new List(); + + 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; + } + + /// + /// Add new tag. + /// + /// + /// + /// + /// + 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(); + } + + /// + /// Delete tag. + /// + /// + /// + /// + 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(); + } + + /// + /// Push tag to remote. + /// + /// + /// + /// + 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); + } + } +} diff --git a/Git/User.cs b/Git/User.cs new file mode 100644 index 00000000..0e1b4120 --- /dev/null +++ b/Git/User.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Git user. + /// + public class User { + private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+"); + + /// + /// Name. + /// + public string Name { get; set; } = ""; + + /// + /// Email. + /// + public string Email { get; set; } = ""; + + /// + /// Operation time. + /// + public string Time { get; set; } = ""; + + /// + /// Parse user from raw string. + /// + /// Raw string + 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"); + } + } +} diff --git a/Helpers/CommitGraph.cs b/Helpers/CommitGraph.cs new file mode 100644 index 00000000..82c37f0d --- /dev/null +++ b/Helpers/CommitGraph.cs @@ -0,0 +1,274 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Media; + +namespace SourceGit.Helpers { + + /// + /// Tools to parse commit graph. + /// + public class CommitGraphMaker { + /// + /// Sizes + /// + 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; + + /// + /// Colors + /// + public static Brush[] Colors = new Brush[] { + Brushes.Orange, + Brushes.ForestGreen, + Brushes.Gold, + Brushes.Magenta, + Brushes.Red, + Brushes.Gray, + Brushes.Turquoise, + Brushes.Olive, + }; + + /// + /// Helpers to draw lines. + /// + public class LineHelper { + private double lastX = 0; + private double lastY = 0; + + /// + /// Parent commit id. + /// + public string Next { get; set; } + + /// + /// Is merged into this tree. + /// + public bool IsMerged { get; set; } + + /// + /// Points in line + /// + public List Points { get; set; } + + /// + /// Brush to draw line + /// + public Brush Brush { get; set; } + + /// + /// Current horizontal offset. + /// + public double HorizontalOffset => lastX; + + /// + /// Constructor. + /// + /// Parent commit id + /// Is merged in tree + /// Color index + /// Start point + public LineHelper(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) { + Next = nextCommitId; + IsMerged = isMerged; + Points = new List() { startPoint }; + Brush = Colors[colorIdx % Colors.Length]; + + lastX = startPoint.X; + lastY = startPoint.Y; + } + + /// + /// Line to. + /// + /// + /// + /// + 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)); + } + } + } + + /// + /// Short link between two commits. + /// + public struct ShortLink { + public Point Start; + public Point Control; + public Point End; + public Brush Brush; + } + + /// + /// Dot + /// + public struct Dot { + public double X; + public double Y; + public Brush Color; + } + + /// + /// Independent lines in graph + /// + public List Lines { get; set; } = new List(); + + /// + /// Short links. + /// + public List Links { get; set; } = new List(); + + /// + /// All dots. + /// + public List Dots { get; set; } = new List(); + + /// + /// Highlight commit id. + /// + public string Highlight { get; set; } + + /// + /// Parse commits. + /// + /// + /// + public static CommitGraphMaker Parse(List commits) { + CommitGraphMaker maker = new CommitGraphMaker(); + + List unsolved = new List(); + List ended = new List(); + Dictionary currentMap = new Dictionary(); + 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; + } + } +} diff --git a/Helpers/TextBoxHelper.cs b/Helpers/TextBoxHelper.cs new file mode 100644 index 00000000..3f34aed8 --- /dev/null +++ b/Helpers/TextBoxHelper.cs @@ -0,0 +1,143 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace SourceGit.Helpers { + + /// + /// Attached properties to TextBox. + /// + public static class TextBoxHelper { + + /// + /// Placeholder property + /// + public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached( + "Placeholder", + typeof(string), + typeof(TextBoxHelper), + new PropertyMetadata(string.Empty, OnPlaceholderChanged)); + + /// + /// Vertical alignment for placeholder. + /// + public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached( + "PlaceholderBaseline", + typeof(AlignmentY), + typeof(TextBoxHelper), + new PropertyMetadata(AlignmentY.Center)); + + /// + /// Property to store generated placeholder brush. + /// + public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached( + "PlaceholderBrush", + typeof(Brush), + typeof(TextBoxHelper), + new PropertyMetadata(Brushes.Transparent)); + + /// + /// Triggered when placeholder changed. + /// + /// + /// + private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + var textBox = d as TextBox; + if (textBox != null) textBox.Loaded += OnTextLoaded; + } + + /// + /// Setter for Placeholder property + /// + /// + /// + public static void SetPlaceholder(UIElement element, string value) { + element.SetValue(PlaceholderProperty, value); + } + + /// + /// Getter for Placeholder property + /// + /// + /// + public static string GetPlaceholder(UIElement element) { + return (string)element.GetValue(PlaceholderProperty); + } + + /// + /// Setter for PlaceholderBaseline property + /// + /// + /// + public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) { + element.SetValue(PlaceholderBaselineProperty, align); + } + + /// + /// Setter for PlaceholderBaseline property. + /// + /// + /// + public static AlignmentY GetPlaceholderBaseline(UIElement element) { + return (AlignmentY)element.GetValue(PlaceholderBaselineProperty); + } + + /// + /// Setter for PlaceholderBrush property. + /// + /// + /// + public static void SetPlaceholderBrush(UIElement element, Brush value) { + element.SetValue(PlaceholderBrushProperty, value); + } + + /// + /// Getter for PlaceholderBrush property. + /// + /// + /// + public static Brush GetPlaceholderBrush(UIElement element) { + return (Brush)element.GetValue(PlaceholderBrushProperty); + } + + /// + /// Set placeholder as background when TextBox was loaded. + /// + /// + /// + 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); + } + + /// + /// Dynamically hide/show placeholder. + /// + /// + /// + 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; + } + } + } +} diff --git a/Helpers/TreeViewHelper.cs b/Helpers/TreeViewHelper.cs new file mode 100644 index 00000000..d1b74bf2 --- /dev/null +++ b/Helpers/TreeViewHelper.cs @@ -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 { + + /// + /// Helper class to enable multi-selection of TreeView + /// + public static class TreeViewHelper { + + /// + /// Definition of EnableMultiSelection property. + /// + public static readonly DependencyProperty EnableMultiSelectionProperty = + DependencyProperty.RegisterAttached( + "EnableMultiSelection", + typeof(bool), + typeof(TreeViewHelper), + new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged)); + + /// + /// Getter of EnableMultiSelection + /// + /// + /// + public static bool GetEnableMultiSelection(DependencyObject obj) { + return (bool)obj.GetValue(EnableMultiSelectionProperty); + } + + /// + /// Setter of EnableMultiSelection + /// + /// + /// + public static void SetEnableMultiSelection(DependencyObject obj, bool value) { + obj.SetValue(EnableMultiSelectionProperty, value); + } + + /// + /// Definition of SelectedItems + /// + public static readonly DependencyProperty SelectedItemsProperty = + DependencyProperty.RegisterAttached( + "SelectedItems", + typeof(ObservableCollection), + typeof(TreeViewHelper), + new FrameworkPropertyMetadata(null)); + + /// + /// Getter of SelectedItems + /// + /// + /// + public static ObservableCollection GetSelectedItems(DependencyObject obj) { + return (ObservableCollection)obj.GetValue(SelectedItemsProperty); + } + + /// + /// Setter of SelectedItems + /// + /// + /// + public static void SetSelectedItems(DependencyObject obj, ObservableCollection value) { + obj.SetValue(SelectedItemsProperty, value); + } + + /// + /// Definition of IsChecked property. + /// + public static readonly DependencyProperty IsCheckedProperty = + DependencyProperty.RegisterAttached( + "IsChecked", + typeof(bool), + typeof(TreeViewHelper), + new FrameworkPropertyMetadata(false)); + + /// + /// Getter of IsChecked Property. + /// + /// + /// + public static bool GetIsChecked(DependencyObject obj) { + return (bool)obj.GetValue(IsCheckedProperty); + } + + /// + /// Setter of IsChecked property + /// + /// + /// + public static void SetIsChecked(DependencyObject obj, bool value) { + obj.SetValue(IsCheckedProperty, value); + } + + /// + /// Definition of MultiSelectionChangedEvent + /// + public static readonly RoutedEvent MultiSelectionChangedEvent = + EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper)); + + /// + /// Add handler for MultiSelectionChanged event. + /// + /// + /// + public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) { + var tree = d as TreeView; + if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler); + } + + /// + /// Remove handler for MultiSelectionChanged event. + /// + /// + /// + public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) { + var tree = d as TreeView; + if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler); + } + + /// + /// Select all items in tree. + /// + /// + public static void SelectWholeTree(TreeView tree) { + var selected = GetSelectedItems(tree); + selected.Clear(); + SelectAll(selected, tree); + tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent)); + } + + /// + /// Selected one item by DataContext + /// + /// + /// + 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)); + } + } + + /// + /// Unselect the whole tree. + /// + /// + 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)); + } + + /// + /// Hooks when EnableMultiSelection changed. + /// + /// + /// + private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + var tree = d as TreeView; + if (tree != null && (bool)e.NewValue) { + tree.SetValue(SelectedItemsProperty, new ObservableCollection()); + tree.PreviewMouseDown += OnTreeMouseDown; + } + } + + /// + /// Preview mouse button select. + /// + /// + /// + 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)); + } + + /// + /// Find TreeViewItem by child element. + /// + /// + /// + /// + 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)); + } + + /// + /// Find TreeViewItem by DataContext + /// + /// + /// + /// + 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; + } + + /// + /// Select all items. + /// + /// + /// + private static void SelectAll(ObservableCollection 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); + } + } + + /// + /// Select range items between given. + /// + /// + /// + /// + /// + /// + private static int SelectRange(ObservableCollection 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 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; + } + } +} \ No newline at end of file diff --git a/Helpers/Validations.cs b/Helpers/Validations.cs new file mode 100644 index 00000000..ceaf48fc --- /dev/null +++ b/Helpers/Validations.cs @@ -0,0 +1,124 @@ +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using System.Windows.Controls; + +namespace SourceGit.Helpers { + + /// + /// Validate clone folder. + /// + 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); + } + } + + /// + /// Validate git remote URL + /// + 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); + } + } + + /// + /// Validate tag name. + /// + 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; + } + } + + /// + /// Validate branch name. + /// + 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; + } + } + + /// + /// Validate tag name. + /// + 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; + } + } + + /// + /// Required for commit subject. + /// + 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; + } + } + + /// + /// Required for patch file. + /// + 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; + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..05cd0620 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/Preview_Dark.png b/Preview_Dark.png new file mode 100644 index 00000000..64f70af0 Binary files /dev/null and b/Preview_Dark.png differ diff --git a/Preview_Light.png b/Preview_Light.png new file mode 100644 index 00000000..c4b41672 Binary files /dev/null and b/Preview_Light.png differ diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3c73e67f --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -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 +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the 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")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 00000000..9b5d30b8 --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace SourceGit.Properties { + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 00000000..af7dbebb --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 00000000..86e4a784 --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +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; + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 00000000..033d7a5e --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..fee0e5d7 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# SourceGit + +开源的Git客户端,仅用于Windows 10。单文件,无需安装,< 500KB。 + +* DarkTheme + +![Preview_Dark](./Preview_Dark.png) + +* LightTheme + +![Preview_Light](./Preview_Light.png) + diff --git a/Resources/Controls.xaml b/Resources/Controls.xaml new file mode 100644 index 00000000..03450990 --- /dev/null +++ b/Resources/Controls.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Icons.xaml b/Resources/Icons.xaml new file mode 100644 index 00000000..262eab32 --- /dev/null +++ b/Resources/Icons.xaml @@ -0,0 +1,59 @@ + + 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 + + F1M0,6L0,9 9,9 9,6 0,6z + F1M0,0L0,9 9,9 9,0 0,0 0,3 8,3 8,8 1,8 1,3z + 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 + 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 + 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 + 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 + 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 + 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 + + 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 + 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 + + 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 + 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 + + 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 + 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 + M169.5 831l342.8-341.9L855.1 831l105.3-105.3-448.1-448.1L64.2 725.7 169.5 831z + M768 800V684.8L512 480 256 684.8V800l256-204.8L768 800zM512 339.2L768 544V428.8L512 224 256 428.8V544l256-204.8z + + 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 + 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 + 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 + M448 64l128 128h448v768H0V64z + M832 960l192-512H192L0 960zM128 384L0 960V128h288l128 128h416v128z + 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 + 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 + 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 + M989.866667 512L689.493333 802.133333 614.4 729.6 839.68 512 614.4 294.4 689.493333 221.866667z + 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 + 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 + M599.22969 424.769286 599.22969 657.383158 424.769286 831.844585 424.769286 424.769286 192.155415 192.155415 831.844585 192.155415Z + + M1024 1024H0V0h1024v1024z m-64-64V320H320V256h640V64H64v896h192V64h64v896z + 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 + + M1024 896v128H0V704h128v192h768V704h128v192zM576 554.688L810.688 320 896 405.312l-384 384-384-384L213.312 320 448 554.688V0h128v554.688z + 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 + 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 + 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 + 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 + 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 + 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 + + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + \ No newline at end of file diff --git a/Resources/Styles/Border.xaml b/Resources/Styles/Border.xaml new file mode 100644 index 00000000..a6f9bcd5 --- /dev/null +++ b/Resources/Styles/Border.xaml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/Resources/Styles/Button.xaml b/Resources/Styles/Button.xaml new file mode 100644 index 00000000..49c6169b --- /dev/null +++ b/Resources/Styles/Button.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/CheckBox.xaml b/Resources/Styles/CheckBox.xaml new file mode 100644 index 00000000..b271d993 --- /dev/null +++ b/Resources/Styles/CheckBox.xaml @@ -0,0 +1,38 @@ + + + + \ No newline at end of file diff --git a/Resources/Styles/ComboBox.xaml b/Resources/Styles/ComboBox.xaml new file mode 100644 index 00000000..5fd3e228 --- /dev/null +++ b/Resources/Styles/ComboBox.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/ContextMenu.xaml b/Resources/Styles/ContextMenu.xaml new file mode 100644 index 00000000..c82d6fee --- /dev/null +++ b/Resources/Styles/ContextMenu.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/DataGrid.xaml b/Resources/Styles/DataGrid.xaml new file mode 100644 index 00000000..88bc5450 --- /dev/null +++ b/Resources/Styles/DataGrid.xaml @@ -0,0 +1,44 @@ + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/HyperLink.xaml b/Resources/Styles/HyperLink.xaml new file mode 100644 index 00000000..db1e0330 --- /dev/null +++ b/Resources/Styles/HyperLink.xaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/Resources/Styles/Label.xaml b/Resources/Styles/Label.xaml new file mode 100644 index 00000000..7fecb8de --- /dev/null +++ b/Resources/Styles/Label.xaml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/Resources/Styles/ListView.xaml b/Resources/Styles/ListView.xaml new file mode 100644 index 00000000..72ab8ba5 --- /dev/null +++ b/Resources/Styles/ListView.xaml @@ -0,0 +1,66 @@ + + + + + + \ No newline at end of file diff --git a/Resources/Styles/Path.xaml b/Resources/Styles/Path.xaml new file mode 100644 index 00000000..d3f643e6 --- /dev/null +++ b/Resources/Styles/Path.xaml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/Resources/Styles/RadioButton.xaml b/Resources/Styles/RadioButton.xaml new file mode 100644 index 00000000..d7b65747 --- /dev/null +++ b/Resources/Styles/RadioButton.xaml @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/Resources/Styles/ScrollBar.xaml b/Resources/Styles/ScrollBar.xaml new file mode 100644 index 00000000..25c06ecf --- /dev/null +++ b/Resources/Styles/ScrollBar.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/ScrollViewer.xaml b/Resources/Styles/ScrollViewer.xaml new file mode 100644 index 00000000..971545e7 --- /dev/null +++ b/Resources/Styles/ScrollViewer.xaml @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/Resources/Styles/TabControl.xaml b/Resources/Styles/TabControl.xaml new file mode 100644 index 00000000..1a449039 --- /dev/null +++ b/Resources/Styles/TabControl.xaml @@ -0,0 +1,62 @@ + + + + + \ No newline at end of file diff --git a/Resources/Styles/TextBox.xaml b/Resources/Styles/TextBox.xaml new file mode 100644 index 00000000..728587fd --- /dev/null +++ b/Resources/Styles/TextBox.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/ToggleButton.xaml b/Resources/Styles/ToggleButton.xaml new file mode 100644 index 00000000..3920885e --- /dev/null +++ b/Resources/Styles/ToggleButton.xaml @@ -0,0 +1,111 @@ + + + + + + + + + \ No newline at end of file diff --git a/Resources/Styles/Tooltip.xaml b/Resources/Styles/Tooltip.xaml new file mode 100644 index 00000000..0e7f9d3e --- /dev/null +++ b/Resources/Styles/Tooltip.xaml @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/Resources/Styles/TreeView.xaml b/Resources/Styles/TreeView.xaml new file mode 100644 index 00000000..f8b9b802 --- /dev/null +++ b/Resources/Styles/TreeView.xaml @@ -0,0 +1,201 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Themes/Dark.xaml b/Resources/Themes/Dark.xaml new file mode 100644 index 00000000..12896ffb --- /dev/null +++ b/Resources/Themes/Dark.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/Themes/Light.xaml b/Resources/Themes/Light.xaml new file mode 100644 index 00000000..f2b3ae76 --- /dev/null +++ b/Resources/Themes/Light.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SourceGit.csproj b/SourceGit.csproj new file mode 100644 index 00000000..99e44228 --- /dev/null +++ b/SourceGit.csproj @@ -0,0 +1,515 @@ + + + + + Debug + AnyCPU + {6B38FAF0-57D6-44E6-9B21-9BEAC481EF9E} + WinExe + SourceGit + SourceGit + v4.6 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + On + + + SourceGit.App + + + App.ico + + + + Always + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + + + + + + + + + + + Apply.xaml + + + Blame.xaml + + + CherryPick.xaml + + + Clone.xaml + + + CommitViewer.xaml + + + CreateBranch.xaml + + + CreateTag.xaml + + + Dashboard.xaml + + + DeleteBranch.xaml + + + DeleteRemote.xaml + + + DeleteTag.xaml + + + DiffViewer.xaml + + + Discard.xaml + + + Fetch.xaml + + + FileHistories.xaml + + + GitFlowFinishBranch.xaml + + + GitFlowSetup.xaml + + + GitFlowStartBranch.xaml + + + Histories.xaml + + + Init.xaml + + + InteractiveRebase.xaml + + + Manager.xaml + + + Merge.xaml + + + PopupManager.xaml + + + Preference.xaml + + + Pull.xaml + + + Push.xaml + + + PushTag.xaml + + + Rebase.xaml + + + Remote.xaml + + + RenameBranch.xaml + + + Reset.xaml + + + Revert.xaml + + + Stash.xaml + + + Stashes.xaml + + + WorkingCopy.xaml + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + + + + + + + About.xaml + + + Launcher.xaml + Code + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + @echo off &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 > VERSION +set /P version=<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! +))>"%25temp%25" +del /f %25file%25 +del /f VERSION +move %25temp%25 %25file%25 +endlocal + + + del /f $(SolutionDir)Properties\AssemblyInfo.cs +move $(SolutionDir)Properties\AssemblyInfo.cs.bk $(SolutionDir)Properties\AssemblyInfo.cs + + \ No newline at end of file diff --git a/SourceGit.sln b/SourceGit.sln new file mode 100644 index 00000000..6ab4db41 --- /dev/null +++ b/SourceGit.sln @@ -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 diff --git a/UI/About.xaml b/UI/About.xaml new file mode 100644 index 00000000..35c8e273 --- /dev/null +++ b/UI/About.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/About.xaml.cs b/UI/About.xaml.cs new file mode 100644 index 00000000..d3080e3d --- /dev/null +++ b/UI/About.xaml.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using System.Reflection; +using System.Windows; +using System.Windows.Navigation; + +namespace SourceGit.UI { + + /// + /// About dialog + /// + public partial class About : Window { + + /// + /// Current app version + /// + public string Version { + get { + return FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion; + } + } + + /// + /// Constructor + /// + public About() { + InitializeComponent(); + } + + /// + /// Open source code link + /// + /// + /// + private void OpenSource(object sender, RequestNavigateEventArgs e) { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); + e.Handled = true; + } + + /// + /// Close this dialog + /// + private void Quit(object sender, RoutedEventArgs e) { + Close(); + } + } +} diff --git a/UI/Apply.xaml b/UI/Apply.xaml new file mode 100644 index 00000000..1533930f --- /dev/null +++ b/UI/Apply.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + diff --git a/UI/Blame.xaml.cs b/UI/Blame.xaml.cs new file mode 100644 index 00000000..8f4bc3c0 --- /dev/null +++ b/UI/Blame.xaml.cs @@ -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 { + + /// + /// Viewer to show git-blame + /// + public partial class Blame : Window { + + /// + /// Background color for blocks. + /// + public static Brush[] BG = new Brush[] { + Brushes.Transparent, + new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) + }; + + /// + /// Constructor + /// + /// + /// + /// + 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 numbers = new List(); + 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; + }); + }); + } + + /// + /// Click logo + /// + /// + /// + 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); + } + + /// + /// Minimize + /// + private void Minimize(object sender, RoutedEventArgs e) { + SystemCommands.MinimizeWindow(this); + } + + /// + /// Maximize/Restore + /// + private void MaximizeOrRestore(object sender, RoutedEventArgs e) { + if (WindowState == WindowState.Normal) { + SystemCommands.MaximizeWindow(this); + } else { + SystemCommands.RestoreWindow(this); + } + } + + /// + /// Quit + /// + private void Quit(object sender, RoutedEventArgs e) { + Close(); + } + + /// + /// Sync scroll + /// + /// + /// + private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) { + if (e.VerticalChange != 0) { + var margin = new Thickness(4, -e.VerticalOffset, 4, 0); + lineNumber.Margin = margin; + } + } + + /// + /// Mouse wheel + /// + /// + /// + private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) { + if (e.Delta > 0) { + content.LineUp(); + } else { + content.LineDown(); + } + + e.Handled = true; + } + + /// + /// Content size changed. + /// + /// + /// + private void ContentSizeChanged(object sender, SizeChangedEventArgs e) { + if (content.Document.PageWidth < content.ActualWidth) { + content.Document.PageWidth = content.ActualWidth; + } + } + } +} diff --git a/UI/CherryPick.xaml b/UI/CherryPick.xaml new file mode 100644 index 00000000..c22fd9b7 --- /dev/null +++ b/UI/CherryPick.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + +