diff --git a/VERSION b/VERSION index b9d71048..d9316e8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.14 \ No newline at end of file +8.15 \ No newline at end of file diff --git a/src/App.axaml b/src/App.axaml index 7129fd7e..fff2ca0d 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -19,6 +19,7 @@ + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 151c6e92..df491dd0 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -62,9 +62,7 @@ namespace SourceGit builder.Append(ex.StackTrace); var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var file = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SourceGit", - $"crash_{time}.log"); + var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); File.WriteAllText(file, builder.ToString()); } } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 618ff014..6788884c 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -5,8 +5,8 @@ namespace SourceGit.Commands { public class QueryCommits : Command { - private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; - private const string GPGSIG_END = " -----END PGP SIGNATURE-----"; + private const string GPGSIG_START = "gpgsig -----BEGIN "; + private const string GPGSIG_END = " -----END "; private readonly List commits = new List(); private Models.Commit current = null; @@ -17,6 +17,7 @@ namespace SourceGit.Commands public QueryCommits(string repo, string limits, bool needFindHead = true) { WorkingDirectory = repo; + Context = repo; Args = "log --date-order --decorate=full --pretty=raw " + limits; findFirstMerged = needFindHead; } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs new file mode 100644 index 00000000..5c0fd760 --- /dev/null +++ b/src/Commands/QuerySingleCommit.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Commands +{ + public class QuerySingleCommit : Command + { + private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; + private const string GPGSIG_END = " -----END PGP SIGNATURE-----"; + + public QuerySingleCommit(string repo, string sha) { + WorkingDirectory = repo; + Context = repo; + Args = $"show --pretty=raw --decorate=full -s {sha}"; + } + + public Models.Commit Result() + { + var succ = Exec(); + if (!succ) + return null; + + _commit.Message.Trim(); + return _commit; + } + + protected override void OnReadline(string line) + { + if (isSkipingGpgsig) + { + if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) + isSkipingGpgsig = false; + return; + } + else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) + { + isSkipingGpgsig = true; + return; + } + + if (line.StartsWith("commit ", StringComparison.Ordinal)) + { + line = line.Substring(7); + + var decoratorStart = line.IndexOf('(', StringComparison.Ordinal); + if (decoratorStart < 0) + { + _commit.SHA = line.Trim(); + } + else + { + _commit.SHA = line.Substring(0, decoratorStart).Trim(); + ParseDecorators(_commit.Decorators, line.Substring(decoratorStart + 1)); + } + + return; + } + + if (line.StartsWith("tree ", StringComparison.Ordinal)) + { + return; + } + else if (line.StartsWith("parent ", StringComparison.Ordinal)) + { + _commit.Parents.Add(line.Substring("parent ".Length)); + } + else if (line.StartsWith("author ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); + _commit.Author = user; + _commit.AuthorTime = time; + } + else if (line.StartsWith("committer ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); + _commit.Committer = user; + _commit.CommitterTime = time; + } + else if (string.IsNullOrEmpty(_commit.Subject)) + { + _commit.Subject = line.Trim(); + } + else + { + _commit.Message += (line.Trim() + "\n"); + } + } + + private bool ParseDecorators(List decorators, string data) + { + bool isHeadOfCurrent = false; + + var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var sub in subs) + { + var d = sub.Trim(); + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.Tag, + Name = d.Substring(15).Trim(), + }); + } + else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + { + continue; + } + else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentBranchHead, + Name = d.Substring(19).Trim(), + }); + } + else if (d.Equals("HEAD")) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentCommitHead, + Name = d.Trim(), + }); + } + else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.LocalBranchHead, + Name = d.Substring(11).Trim(), + }); + } + else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.RemoteBranchHead, + Name = d.Substring(13).Trim(), + }); + } + } + + decorators.Sort((l, r) => + { + if (l.Type != r.Type) + { + return (int)l.Type - (int)r.Type; + } + else + { + return l.Name.CompareTo(r.Name); + } + }); + + return isHeadOfCurrent; + } + + private Models.Commit _commit = new Models.Commit(); + private bool isSkipingGpgsig = false; + } +} diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index c860a1a6..2eb8c60a 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -1,18 +1,17 @@ -using Avalonia.Controls; -using Avalonia.Data.Converters; +using Avalonia.Data.Converters; using Avalonia.Media; namespace SourceGit.Converters { public static class BoolConverters { + public static readonly FuncValueConverter ToPageTabWidth = + new FuncValueConverter(x => x ? 200 : double.NaN); + public static readonly FuncValueConverter HalfIfFalse = new FuncValueConverter(x => x ? 1 : 0.5); public static readonly FuncValueConverter BoldIfTrue = new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Regular); - - public static readonly FuncValueConverter ToStarOrAutoGridLength = - new(value => value ? new GridLength(1, GridUnitType.Star) : new GridLength(1, GridUnitType.Auto)); } } diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index f743f69f..aa687f23 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -1,12 +1,13 @@ using System; using System.Globalization; +using System.Text.RegularExpressions; using Avalonia.Data.Converters; using Avalonia.Styling; namespace SourceGit.Converters { - public static class StringConverters + public static partial class StringConverters { public class ToLocaleConverter : IValueConverter { @@ -68,6 +69,27 @@ namespace SourceGit.Converters public static readonly FormatByResourceKeyConverter FormatByResourceKey = new FormatByResourceKeyConverter(); public static readonly FuncValueConverter ToShortSHA = - new FuncValueConverter(v => v.Length > 10 ? v.Substring(0, 10) : v); + new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); + + public static readonly FuncValueConverter UnderRecommendGitVersion = + new(v => + { + var match = REG_GIT_VERSION().Match(v ?? ""); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var build = int.Parse(match.Groups[3].Value); + + return new Version(major, minor, build) < MINIMAL_GIT_VERSION; + } + + return true; + }); + + [GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] + private static partial Regex REG_GIT_VERSION(); + + private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0); } } diff --git a/src/Converters/WindowStateConverters.cs b/src/Converters/WindowStateConverters.cs index 2c3b2ac6..7122dc1f 100644 --- a/src/Converters/WindowStateConverters.cs +++ b/src/Converters/WindowStateConverters.cs @@ -3,7 +3,6 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Data.Converters; -using Avalonia.Media; namespace SourceGit.Converters { @@ -39,19 +38,6 @@ namespace SourceGit.Converters } }); - public static readonly FuncValueConverter ToMaxOrRestoreIcon = - new FuncValueConverter(state => - { - if (state == WindowState.Maximized) - { - return Application.Current?.FindResource("Icons.Window.Restore") as StreamGeometry; - } - else - { - return Application.Current?.FindResource("Icons.Window.Maximize") as StreamGeometry; - } - }); - public static readonly FuncValueConverter IsNormal = new FuncValueConverter(state => state == WindowState.Normal); } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index fded94e3..50d890e1 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -25,7 +25,7 @@ namespace SourceGit.Models static AvatarManager() { - _storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit", "avatars"); + _storePath = Path.Combine(Native.OS.DataDir, "avatars"); if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath); diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs new file mode 100644 index 00000000..82609642 --- /dev/null +++ b/src/Models/DealWithLocalChanges.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models +{ + public enum DealWithLocalChanges + { + StashAndReaply, + Discard, + DoNothing, + } +} diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 8a72b35d..d9d21031 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -582,6 +582,12 @@ namespace SourceGit.Models public string New { get; set; } = string.Empty; } + public class SubmoduleDiff + { + public Commit Old { get; set; } = null; + public Commit New { get; set; } = null; + } + public class DiffResult { public bool IsBinary { get; set; } = false; diff --git a/src/ViewModels/FileTreeNode.cs b/src/Models/FileTreeNode.cs similarity index 83% rename from src/ViewModels/FileTreeNode.cs rename to src/Models/FileTreeNode.cs index ca6d850f..ad1298c9 100644 --- a/src/ViewModels/FileTreeNode.cs +++ b/src/Models/FileTreeNode.cs @@ -1,28 +1,20 @@ using System; using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace SourceGit.ViewModels +namespace SourceGit.Models { - public class FileTreeNode : ObservableObject + public class FileTreeNode { public string FullPath { get; set; } = string.Empty; public bool IsFolder { get; set; } = false; + public bool IsExpanded { get; set; } = false; public object Backend { get; set; } = null; public List Children { get; set; } = new List(); - public bool IsExpanded - { - get => _isExpanded; - set => SetProperty(ref _isExpanded, value); - } - - public static List Build(List changes) + public static List Build(List changes, bool expanded) { var nodes = new List(); var folders = new Dictionary(); - var expanded = changes.Count <= 50; foreach (var c in changes) { @@ -94,11 +86,10 @@ namespace SourceGit.ViewModels return nodes; } - public static List Build(List files) + public static List Build(List files, bool expanded) { var nodes = new List(); var folders = new Dictionary(); - var expanded = files.Count <= 50; foreach (var f in files) { @@ -170,27 +161,6 @@ namespace SourceGit.ViewModels return nodes; } - public static FileTreeNode SelectByPath(List nodes, string path) - { - foreach (var node in nodes) - { - if (node.FullPath == path) - return node; - - if (node.IsFolder && path.StartsWith(node.FullPath + "/", StringComparison.Ordinal)) - { - var foundInChildren = SelectByPath(node.Children, path); - if (foundInChildren != null) - { - node.IsExpanded = true; - } - return foundInChildren; - } - } - - return null; - } - private static void Sort(List nodes) { nodes.Sort((l, r) => @@ -211,7 +181,5 @@ namespace SourceGit.ViewModels Sort(node.Children); } } - - private bool _isExpanded = true; } } diff --git a/src/Models/GPGFormat.cs b/src/Models/GPGFormat.cs new file mode 100644 index 00000000..bf3b3678 --- /dev/null +++ b/src/Models/GPGFormat.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class GPGFormat(string name, string value, string desc) + { + public string Name { get; set; } = name; + public string Value { get; set; } = value; + public string Desc { get; set; } = desc; + + public static readonly GPGFormat OPENPGP = new GPGFormat("OPENPGP", "openpgp", "DEFAULT"); + + public static readonly GPGFormat SSH = new GPGFormat("SSH", "ssh", "Git >= 2.34.0"); + + public static readonly List Supported = new List() { + OPENPGP, + SSH, + }; + + public bool Equals(GPGFormat other) + { + return Value == other.Value; + } + } +} diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index c1cce340..75ca961f 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -4,11 +4,11 @@ namespace SourceGit.Models { public partial class Remote { - [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+(\.git)?$")] private static partial Regex REG_HTTPS(); - [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/]+/[\w\-\.]+(\.git)?$")] private static partial Regex REG_SSH1(); - [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+(\.git)?$")] private static partial Regex REG_SSH2(); private static readonly Regex[] URL_FORMATS = [ diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs new file mode 100644 index 00000000..b016d739 --- /dev/null +++ b/src/Models/TreeDataGridSelectionModel.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.Input; + +namespace SourceGit.Models +{ + public class TreeDataGridSelectionModel : TreeSelectionModelBase, + ITreeDataGridRowSelectionModel, + ITreeDataGridSelectionInteraction + where TModel : class + { + private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); + + private readonly ITreeDataGridSource _source; + private EventHandler _viewSelectionChanged; + private EventHandler _rowDoubleTapped; + private Point _pressedPoint = s_InvalidPoint; + private bool _raiseViewSelectionChanged; + private Func> _childrenGetter; + + public TreeDataGridSelectionModel(ITreeDataGridSource source, Func> childrenGetter) + : base(source.Items) + { + _source = source; + _childrenGetter = childrenGetter; + + SelectionChanged += (s, e) => + { + if (!IsSourceCollectionChanging) + _viewSelectionChanged?.Invoke(this, e); + else + _raiseViewSelectionChanged = true; + }; + } + + public void Select(IEnumerable items) + { + var sets = new HashSet(); + foreach (var item in items) + sets.Add(item); + + using (BatchUpdate()) + { + Clear(); + + int num = _source.Rows.Count; + for (int i = 0; i < num; ++i) + { + var m = _source.Rows[i].Model as TModel; + if (m != null && sets.Contains(m)) + { + var idx = _source.Rows.RowIndexToModelIndex(i); + Select(idx); + } + } + } + } + + event EventHandler ITreeDataGridSelectionInteraction.SelectionChanged + { + add => _viewSelectionChanged += value; + remove => _viewSelectionChanged -= value; + } + + public event EventHandler RowDoubleTapped + { + add => _rowDoubleTapped += value; + remove => _rowDoubleTapped -= value; + } + + IEnumerable ITreeDataGridSelection.Source + { + get => Source; + set => Source = value; + } + + bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel) + { + if (rowModel is IModelIndexableRow indexable) + return IsSelected(indexable.ModelIndexPath); + return false; + } + + bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex) + { + if (rowIndex >= 0 && rowIndex < _source.Rows.Count) + { + if (_source.Rows[rowIndex] is IModelIndexableRow indexable) + return IsSelected(indexable.ModelIndexPath); + } + + return false; + } + + void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e) + { + if (sender.RowsPresenter is null) + return; + + if (!e.Handled) + { + var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); + if (e.Key == Key.A && ctrl && !SingleSelect) + { + using (BatchUpdate()) + { + Clear(); + + int num = _source.Rows.Count; + for (int i = 0; i < num; ++i) + { + var m = _source.Rows.RowIndexToModelIndex(i); + Select(m); + } + } + e.Handled = true; + } + + var direction = e.Key.ToNavigationDirection(); + var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (direction.HasValue) + { + var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(AnchorIndex); + sender.RowsPresenter.BringIntoView(anchorRowIndex); + + var anchor = sender.TryGetRow(anchorRowIndex); + if (anchor is not null && !ctrl) + { + e.Handled = TryKeyExpandCollapse(sender, direction.Value, anchor); + } + + if (!e.Handled && (!ctrl || shift)) + { + e.Handled = MoveSelection(sender, direction.Value, shift, anchor); + } + + if (!e.Handled && direction == NavigationDirection.Left + && anchor?.Rows is HierarchicalRows hierarchicalRows && anchorRowIndex > 0) + { + var newIndex = hierarchicalRows.GetParentRowIndex(AnchorIndex); + UpdateSelection(sender, newIndex, true); + FocusRow(sender, sender.RowsPresenter.BringIntoView(newIndex)); + } + + if (!e.Handled && direction == NavigationDirection.Right + && anchor?.Rows is HierarchicalRows hierarchicalRows2 && hierarchicalRows2[anchorRowIndex].IsExpanded) + { + var newIndex = anchorRowIndex + 1; + UpdateSelection(sender, newIndex, true); + sender.RowsPresenter.BringIntoView(newIndex); + } + } + } + } + + void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) + { + if (!e.Handled && + e.Pointer.Type == PointerType.Mouse && + e.Source is Control source && + sender.TryGetRow(source, out var row) && + _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) + { + if (!IsSelected(modelIndex)) + { + PointerSelect(sender, row, e); + _pressedPoint = s_InvalidPoint; + } + else + { + var point = e.GetCurrentPoint(sender); + if (point.Properties.IsRightButtonPressed) + { + _pressedPoint = s_InvalidPoint; + return; + } + + if (e.KeyModifiers == KeyModifiers.Control) + { + Deselect(modelIndex); + } + else if (e.ClickCount % 2 == 0) + { + var focus = _source.Rows[row.RowIndex]; + if (focus is IExpander expander && HasChildren(focus)) + expander.IsExpanded = !expander.IsExpanded; + else + _rowDoubleTapped?.Invoke(this, e); + + e.Handled = true; + } + else if (sender.RowSelection.Count > 1) + { + using (BatchUpdate()) + { + Clear(); + Select(modelIndex); + } + } + + _pressedPoint = s_InvalidPoint; + } + } + else + { + if (!sender.TryGetRow(e.Source as Control, out var test)) + Clear(); + + _pressedPoint = e.GetPosition(sender); + } + } + + void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e) + { + if (!e.Handled && + _pressedPoint != s_InvalidPoint && + e.Source is Control source && + sender.TryGetRow(source, out var row) && + _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) + { + if (!IsSelected(modelIndex)) + { + var p = e.GetPosition(sender); + if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3) + PointerSelect(sender, row, e); + } + } + } + + protected override void OnSourceCollectionChangeFinished() + { + if (_raiseViewSelectionChanged) + { + _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + _raiseViewSelectionChanged = false; + } + } + + private void PointerSelect(TreeDataGrid sender, TreeDataGridRow row, PointerEventArgs e) + { + var point = e.GetCurrentPoint(sender); + + var commandModifiers = TopLevel.GetTopLevel(sender)?.PlatformSettings?.HotkeyConfiguration.CommandModifiers; + var toggleModifier = commandModifiers is not null && e.KeyModifiers.HasFlag(commandModifiers); + var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or + PointerUpdateKind.RightButtonReleased; + + UpdateSelection( + sender, + row.RowIndex, + select: true, + rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift), + toggleModifier: toggleModifier, + rightButton: isRightButton); + e.Handled = true; + } + + private void UpdateSelection(TreeDataGrid treeDataGrid, int rowIndex, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) + { + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + if (modelIndex == default) + return; + + var mode = SingleSelect ? SelectionMode.Single : SelectionMode.Multiple; + var multi = (mode & SelectionMode.Multiple) != 0; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; + + if (!select) + { + if (IsSelected(modelIndex) && !treeDataGrid.QueryCancelSelection()) + Deselect(modelIndex); + } + else if (rightButton) + { + if (IsSelected(modelIndex) == false && !treeDataGrid.QueryCancelSelection()) + SelectedIndex = modelIndex; + } + else if (range) + { + if (!treeDataGrid.QueryCancelSelection()) + { + var anchor = RangeAnchorIndex; + var i = Math.Max(_source.Rows.ModelIndexToRowIndex(anchor), 0); + var step = i < rowIndex ? 1 : -1; + + using (BatchUpdate()) + { + Clear(); + + while (true) + { + var m = _source.Rows.RowIndexToModelIndex(i); + Select(m); + anchor = m; + if (i == rowIndex) + break; + i += step; + } + } + } + } + else if (multi && toggle) + { + if (!treeDataGrid.QueryCancelSelection()) + { + if (IsSelected(modelIndex) == true) + Deselect(modelIndex); + else + Select(modelIndex); + } + } + else if (toggle) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectedIndex = (SelectedIndex == modelIndex) ? -1 : modelIndex; + } + else if (SelectedIndex != modelIndex || Count > 1) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectedIndex = modelIndex; + } + } + + private bool TryKeyExpandCollapse(TreeDataGrid treeDataGrid, NavigationDirection direction, TreeDataGridRow focused) + { + if (treeDataGrid.RowsPresenter is null || focused.RowIndex < 0) + return false; + + var row = _source.Rows[focused.RowIndex]; + + if (row is IExpander expander) + { + if (direction == NavigationDirection.Right && !expander.IsExpanded) + { + expander.IsExpanded = true; + return true; + } + else if (direction == NavigationDirection.Left && expander.IsExpanded) + { + expander.IsExpanded = false; + return true; + } + } + + return false; + } + + private bool MoveSelection(TreeDataGrid treeDataGrid, NavigationDirection direction, bool rangeModifier, TreeDataGridRow focused) + { + if (treeDataGrid.RowsPresenter is null || _source.Columns.Count == 0 || _source.Rows.Count == 0) + return false; + + var currentRowIndex = focused?.RowIndex ?? _source.Rows.ModelIndexToRowIndex(SelectedIndex); + int newRowIndex; + + if (direction == NavigationDirection.First || direction == NavigationDirection.Last) + { + newRowIndex = direction == NavigationDirection.First ? 0 : _source.Rows.Count - 1; + } + else + { + (var x, var y) = direction switch + { + NavigationDirection.Up => (0, -1), + NavigationDirection.Down => (0, 1), + NavigationDirection.Left => (-1, 0), + NavigationDirection.Right => (1, 0), + _ => (0, 0) + }; + + newRowIndex = Math.Max(0, Math.Min(currentRowIndex + y, _source.Rows.Count - 1)); + } + + if (newRowIndex != currentRowIndex) + UpdateSelection(treeDataGrid, newRowIndex, true, rangeModifier); + + if (newRowIndex != currentRowIndex) + { + treeDataGrid.RowsPresenter?.BringIntoView(newRowIndex); + FocusRow(treeDataGrid, treeDataGrid.TryGetRow(newRowIndex)); + return true; + } + else + { + return false; + } + } + + private static void FocusRow(TreeDataGrid owner, Control control) + { + if (!owner.TryGetRow(control, out var row) || row.CellsPresenter is null) + return; + + // Get the column index of the currently focused cell if possible: we'll try to focus the + // same column in the new row. + if (TopLevel.GetTopLevel(owner)?.FocusManager is { } focusManager && + focusManager.GetFocusedElement() is Control currentFocus && + owner.TryGetCell(currentFocus, out var currentCell) && + row.TryGetCell(currentCell.ColumnIndex) is { } newCell && + newCell.Focusable) + { + newCell.Focus(); + } + else + { + // Otherwise, just focus the first focusable cell in the row. + foreach (var cell in row.CellsPresenter.GetRealizedElements()) + { + if (cell.Focusable) + { + cell.Focus(); + break; + } + } + } + } + + protected override IEnumerable GetChildren(TModel node) + { + if (node == null) + return null; + + return _childrenGetter?.Invoke(node); + } + + private bool HasChildren(IRow row) + { + var children = GetChildren(row.Model as TModel); + if (children != null) + { + foreach (var c in children) + return true; + } + + return false; + } + } +} diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 46a49e6a..451362ca 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Avalonia; @@ -20,6 +21,7 @@ namespace SourceGit.Native void OpenWithDefaultEditor(string file); } + public static readonly string DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit"); public static string GitExecutable { get; set; } = string.Empty; public static List ExternalTools { get; set; } = new List(); @@ -72,6 +74,9 @@ namespace SourceGit.Native public static void SetupApp(AppBuilder builder) { + if (!Directory.Exists(DataDir)) + Directory.CreateDirectory(DataDir); + _backend.SetupApp(builder); } diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index b61e6839..b6369398 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -97,4 +97,5 @@ M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z + M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 685cb1be..edf3fe1a 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -33,6 +33,8 @@ Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout${0}$ + Compare with HEAD + Compare with Worktree Copy Branch Name Delete${0}$ Delete selected {0} branches @@ -50,17 +52,18 @@ Bytes CANCEL CHANGE DISPLAY MODE - Show as Grid - Show as List - Show as Tree + Show as File and Dir List + Show as Path List + Show as Filesystem Tree Checkout Branch Checkout Commit Warning: By doing a commit checkout, your Head will be detached Commit : Branch : Local Changes : - Stash & Reapply Discard + Do Nothing + Stash & Reapply Cherry-Pick This Commit Commit : Commit all changes @@ -76,8 +79,10 @@ Repository URL : CLOSE Cherry-Pick This Commit - Copy SHA Checkout Commit + Compare with HEAD + Compare with Worktree + Copy SHA Rebase${0}$to Here Reset${0}$to Here Revert Commit @@ -109,13 +114,14 @@ User name for this repository Copy Copy Path + Copy File Name Create Branch Based On : Check out after created Local Changes : Discard - Stash & Reapply Do Nothing + Stash & Reapply New Branch Name : Enter branch name. Create Local Branch @@ -157,6 +163,8 @@ NO CHANGES OR ONLY EOL CHANGES Previous Difference Side-By-Side Diff + SUBMODULE + NEW Syntax Highlighting Open In Merge Tool Decrease Number of Visible Lines @@ -301,8 +309,11 @@ User Name Global git user name Git version + Git (>= 2.23.0) is required by this app GPG SIGNING - Commit GPG signing + Commit GPG signing + Tag GPG signing + GPG Format Install Path Input path for installed gpg program User Signing Key @@ -314,9 +325,12 @@ Input path for merge tool Merger Pull - Stash & reapply local changes Branch : Into : + Local Changes : + Discard + Do Nothing + Stash & Reapply Remote : Pull (Fetch & Merge) Use rebase instead of merge @@ -326,7 +340,7 @@ Remote : Push Changes To Remote Remote Branch : - Tracking remote branch(--set-upstream) + Tracking remote branch Push all tags Push Tag To Remote Push to all remotes @@ -476,4 +490,5 @@ STAGE ALL VIEW ASSUME UNCHANGED Right-click the selected file(s), and make your choice to resolve conflicts. + WORKTREE diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ac48aec9..015cb6cc 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1,4 +1,7 @@ + + + 关于软件 关于本软件 • 项目依赖于 @@ -33,6 +36,8 @@ 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout)${0}$ + 与当前HEAD比较 + 与本地工作树比较 复制分支名 删除${0}$ 删除选中的 {0} 个分支 @@ -50,17 +55,18 @@ 字节 取 消 切换变更显示模式 - 网格模式 - 列表模式 - 树形模式 + 文件名+路径列表模式 + 全路径列表模式 + 文件目录树形结构模式 检出(checkout)分支 检出(checkout)提交 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 提交 : 目标分支 : 未提交更改 : - 贮藏(stash)并自动恢复 - 忽略 + 丢弃更改 + 不做处理 + 贮藏并自动恢复 挑选(cherry-pick)此提交 提交ID : 提交变化 @@ -77,6 +83,8 @@ 关闭 挑选(cherry-pick)此提交 检出此提交 + 与当前HEAD比较 + 与本地工作树比较 复制提交指纹 变基(rebase)${0}$到此处 重置(reset)${0}$到此处 @@ -109,13 +117,14 @@ 应用于本仓库的用户名 复制 复制路径 + 复制文件名 新建分支 新分支基于 : 完成后切换到新分支 未提交更改 : - 放弃所有 + 丢弃更改 + 不做处理 贮藏并自动恢复 - GIT默认 新分支名 : 填写分支名称。 创建本地分支 @@ -157,6 +166,8 @@ 没有变更或仅有换行符差异 上一个差异 分列对比 + 子模块 + 新增 语法高亮 使用外部合并工具查看 减少可见的行数 @@ -301,8 +312,11 @@ 用户名 默认GIT用户名 Git 版本 + 本软件要求GIT最低版本为2.23.0 GPG签名 - 启用提交签名 + 启用提交签名 + 启用标签签名 + GPG签名格式 可执行文件位置 gpg.exe所在路径 用户签名KEY @@ -314,9 +328,12 @@ 填写工具可执行文件所在位置 工具 拉回(pull) - 自动贮藏并恢复本地变更 拉取分支 : 本地分支 : + 未提交更改 : + 丢弃更改 + 不做处理 + 贮藏并自动恢复 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -326,7 +343,7 @@ 远程仓库 : 推送到远程仓库 远程分支 : - 跟踪远程分支(--set-upstream) + 跟踪远程分支 同时推送标签 推送标签到远程仓库 推送到所有远程仓库 @@ -476,4 +493,5 @@ 暂存所有 查看忽略变更文件 请选中冲突文件,打开右键菜单,选择合适的解决方式 + 本地工作树 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 376f9424..c373d891 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -60,6 +60,18 @@ + + + + - - + + + + + + + + + + + + diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index c1dab89b..f102b5f7 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -48,7 +48,7 @@ #FF333333 #FF3A3A3A #FF404040 - #FFF1F1F1 + #FFDDDDDD #40F1F1F1 #FF252525 diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 657b49f3..a3d38391 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -33,6 +33,7 @@ + diff --git a/src/ViewModels/AssumeUnchangedManager.cs b/src/ViewModels/AssumeUnchangedManager.cs index a8208937..0da16552 100644 --- a/src/ViewModels/AssumeUnchangedManager.cs +++ b/src/ViewModels/AssumeUnchangedManager.cs @@ -24,9 +24,9 @@ namespace SourceGit.ViewModels }); } - public void Remove(object param) + public void Remove(string file) { - if (param is string file) + if (!string.IsNullOrEmpty(file)) { new Commands.AssumeUnchanged(_repo).Remove(file); Files.Remove(file); diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 82f97c96..ade6d2f8 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -10,10 +10,10 @@ namespace SourceGit.ViewModels private set; } - public bool AutoStash + public Models.DealWithLocalChanges PreAction { - get => _autoStash; - set => SetProperty(ref _autoStash, value); + get => _preAction; + set => SetProperty(ref _preAction, value); } public Checkout(Repository repo, string branch) @@ -34,7 +34,7 @@ namespace SourceGit.ViewModels var needPopStash = false; if (hasLocalChanges) { - if (AutoStash) + if (_preAction == Models.DealWithLocalChanges.StashAndReaply) { SetProgressDescription("Adding untracked changes ..."); var succ = new Commands.Add(_repo.FullPath).Exec(); @@ -52,7 +52,7 @@ namespace SourceGit.ViewModels needPopStash = true; } - else + else if (_preAction == Models.DealWithLocalChanges.Discard) { SetProgressDescription("Discard local changes ..."); Commands.Discard.All(_repo.FullPath); @@ -78,6 +78,6 @@ namespace SourceGit.ViewModels } private readonly Repository _repo = null; - private bool _autoStash = true; + private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.StashAndReaply; } } diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 1e9bcb63..7db82081 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -50,10 +50,10 @@ namespace SourceGit.ViewModels set => SetProperty(ref _extraArgs, value); } - public Clone(Launcher launcher, LauncherPage page) + public Clone(Launcher launcher) { _launcher = launcher; - _page = page; + _page = launcher.ActivePage; View = new Views.Clone() { DataContext = this }; } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 6fdcd93c..9f79f81d 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -48,48 +49,17 @@ namespace SourceGit.ViewModels set => SetProperty(ref _visibleChanges, value); } - public List ChangeTree + public List SelectedChanges { - get => _changeTree; - set => SetProperty(ref _changeTree, value); - } - - public Models.Change SelectedChange - { - get => _selectedChange; + get => _selectedChanges; set { - if (SetProperty(ref _selectedChange, value)) + if (SetProperty(ref _selectedChanges, value)) { - if (value == null) - { - SelectedChangeNode = null; + if (value == null || value.Count != 1) DiffContext = null; - } else - { - SelectedChangeNode = FileTreeNode.SelectByPath(_changeTree, value.Path); - DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value), _diffContext); - } - } - } - } - - public FileTreeNode SelectedChangeNode - { - get => _selectedChangeNode; - set - { - if (SetProperty(ref _selectedChangeNode, value)) - { - if (value == null) - { - SelectedChange = null; - } - else - { - SelectedChange = value.Backend as Models.Change; - } + DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value[0]), _diffContext); } } } @@ -106,26 +76,10 @@ namespace SourceGit.ViewModels } } - public List RevisionFilesTree + public HierarchicalTreeDataGridSource RevisionFiles { - get => _revisionFilesTree; - set => SetProperty(ref _revisionFilesTree, value); - } - - public FileTreeNode SelectedRevisionFileNode - { - get => _selectedRevisionFileNode; - set - { - if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder) - { - RefreshViewRevisionFile(value.Backend as Models.Object); - } - else - { - ViewRevisionFileContent = null; - } - } + get => _revisionFiles; + private set => SetProperty(ref _revisionFiles, value); } public string SearchFileFilter @@ -159,17 +113,14 @@ namespace SourceGit.ViewModels _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); - if (_changeTree != null) - _changeTree.Clear(); - _selectedChange = null; - _selectedChangeNode = null; + if (_selectedChanges != null) + _selectedChanges.Clear(); _searchChangeFilter = null; _diffContext = null; + if (_revisionFilesBackup != null) + _revisionFilesBackup.Clear(); if (_revisionFiles != null) - _revisionFiles.Clear(); - if (_revisionFilesTree != null) - _revisionFilesTree.Clear(); - _selectedRevisionFileNode = null; + _revisionFiles.Dispose(); _searchFileFilter = null; _viewRevisionFileContent = null; _cancelToken = null; @@ -268,6 +219,16 @@ namespace SourceGit.ViewModels }; menu.Items.Add(copyPath); + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(change.Path)); + e.Handled = true; + }; + menu.Items.Add(copyFileName); + return menu; } @@ -333,12 +294,22 @@ namespace SourceGit.ViewModels ev.Handled = true; }; + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(file.Path)); + e.Handled = true; + }; + var menu = new ContextMenu(); menu.Items.Add(history); menu.Items.Add(blame); menu.Items.Add(explore); menu.Items.Add(saveAs); menu.Items.Add(copyPath); + menu.Items.Add(copyFileName); return menu; } @@ -346,9 +317,14 @@ namespace SourceGit.ViewModels { _changes = null; VisibleChanges = null; - SelectedChange = null; - RevisionFilesTree = null; - SelectedRevisionFileNode = null; + SelectedChanges = null; + + if (_revisionFiles != null) + { + _revisionFiles.Dispose(); + _revisionFiles = null; + } + if (_commit == null) return; if (_cancelToken != null) @@ -379,40 +355,33 @@ namespace SourceGit.ViewModels } } - var tree = FileTreeNode.Build(visible); Dispatcher.UIThread.Invoke(() => { Changes = changes; VisibleChanges = visible; - ChangeTree = tree; }); }); Task.Run(() => { - var files = cmdRevisionFiles.Result(); + _revisionFilesBackup = cmdRevisionFiles.Result(); if (cmdRevisionFiles.Cancel.Requested) return; - var visible = files; - if (!string.IsNullOrWhiteSpace(_searchFileFilter)) + var visible = _revisionFilesBackup; + var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter); + if (isSearching) { visible = new List(); - foreach (var f in files) + foreach (var f in _revisionFilesBackup) { if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(f); - } } } - var tree = FileTreeNode.Build(visible); - Dispatcher.UIThread.Invoke(() => - { - _revisionFiles = files; - RevisionFilesTree = tree; - }); + var tree = Models.FileTreeNode.Build(visible, isSearching || visible.Count <= 100); + Dispatcher.UIThread.Invoke(() => BuildRevisionFilesSource(tree)); }); } @@ -431,15 +400,11 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } VisibleChanges = visible; } - - ChangeTree = FileTreeNode.Build(_visibleChanges); } private void RefreshVisibleFiles() @@ -447,24 +412,29 @@ namespace SourceGit.ViewModels if (_revisionFiles == null) return; - var visible = _revisionFiles; - if (!string.IsNullOrWhiteSpace(_searchFileFilter)) + var visible = _revisionFilesBackup; + var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter); + if (isSearching) { visible = new List(); - foreach (var f in _revisionFiles) + foreach (var f in _revisionFilesBackup) { if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(f); - } } } - RevisionFilesTree = FileTreeNode.Build(visible); + BuildRevisionFilesSource(Models.FileTreeNode.Build(visible, isSearching || visible.Count < 100)); } private void RefreshViewRevisionFile(Models.Object file) { + if (file == null) + { + ViewRevisionFileContent = null; + return; + } + switch (file.Type) { case Models.ObjectType.Blob: @@ -541,6 +511,36 @@ namespace SourceGit.ViewModels } } + private void BuildRevisionFilesSource(List tree) + { + var source = new HierarchicalTreeDataGridSource(tree) + { + Columns = + { + new HierarchicalExpanderColumn( + new TemplateColumn("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto), + x => x.Children, + x => x.Children.Count > 0, + x => x.IsExpanded), + new TextColumn( + null, + x => string.Empty, + GridLength.Star) + } + }; + + var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); + selection.SingleSelect = true; + selection.SelectionChanged += (s, _) => + { + if (s is Models.TreeDataGridSelectionModel selection) + RefreshViewRevisionFile(selection.SelectedItem?.Backend as Models.Object); + }; + + source.Selection = selection; + RevisionFiles = source; + } + private static readonly HashSet IMG_EXTS = new HashSet() { ".ico", ".bmp", ".jpg", ".png", ".jpeg" @@ -551,14 +551,11 @@ namespace SourceGit.ViewModels private Models.Commit _commit = null; private List _changes = null; private List _visibleChanges = null; - private List _changeTree = null; - private Models.Change _selectedChange = null; - private FileTreeNode _selectedChangeNode = null; + private List _selectedChanges = null; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; - private List _revisionFiles = null; - private List _revisionFilesTree = null; - private FileTreeNode _selectedRevisionFileNode = null; + private List _revisionFilesBackup = null; + private HierarchicalTreeDataGridSource _revisionFiles = null; private string _searchFileFilter = string.Empty; private object _viewRevisionFileContent = null; private Commands.Command.CancelToken _cancelToken = null; diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index c6bc43c9..ad270809 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -2,14 +2,7 @@ using System.Threading.Tasks; namespace SourceGit.ViewModels -{ - public enum BeforeCreateBranchAction - { - StashAndReaply, - Discard, - DoNothing, - } - +{ public class CreateBranch : Popup { [Required(ErrorMessage = "Branch name is required!")] @@ -27,7 +20,7 @@ namespace SourceGit.ViewModels private set; } - public BeforeCreateBranchAction PreAction + public Models.DealWithLocalChanges PreAction { get => _preAction; set => SetProperty(ref _preAction, value); @@ -97,7 +90,7 @@ namespace SourceGit.ViewModels bool needPopStash = false; if (_repo.WorkingCopyChangesCount > 0) { - if (_preAction == BeforeCreateBranchAction.StashAndReaply) + if (_preAction == Models.DealWithLocalChanges.StashAndReaply) { SetProgressDescription("Adding untracked changes..."); var succ = new Commands.Add(_repo.FullPath).Exec(); @@ -115,7 +108,7 @@ namespace SourceGit.ViewModels needPopStash = true; } - else if (_preAction == BeforeCreateBranchAction.Discard) + else if (_preAction == Models.DealWithLocalChanges.Discard) { SetProgressDescription("Discard local changes..."); Commands.Discard.All(_repo.FullPath); @@ -133,6 +126,7 @@ namespace SourceGit.ViewModels } else { + SetProgressDescription($"Create new branch '{_name}'"); Commands.Branch.Create(_repo.FullPath, _name, _baseOnRevision); } @@ -144,6 +138,6 @@ namespace SourceGit.ViewModels private readonly Repository _repo = null; private string _name = null; private readonly string _baseOnRevision = null; - private BeforeCreateBranchAction _preAction = BeforeCreateBranchAction.StashAndReaply; + private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.StashAndReaply; } } diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 3aad22ea..a9bfb804 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -85,6 +85,11 @@ namespace SourceGit.ViewModels _content = previous._content; } + if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") + _title = _option.Path; + else + _title = $"{_option.OrgPath} → {_option.Path}"; + LoadDiffContent(); } @@ -123,8 +128,31 @@ namespace SourceGit.ViewModels if (latest.TextDiff != null) { - latest.TextDiff.File = _option.Path; - rs = latest.TextDiff; + var repo = Preference.FindRepository(_repo); + if (repo != null && repo.Submodules.Contains(_option.Path)) + { + var submoduleDiff = new Models.SubmoduleDiff(); + var submoduleRoot = $"{_repo}/{_option.Path}".Replace("\\", "/"); + foreach (var line in latest.TextDiff.Lines) + { + if (line.Type == Models.TextDiffLineType.Added) + { + var sha = line.Content.Substring("Subproject commit ".Length); + submoduleDiff.New = new Commands.QuerySingleCommit(submoduleRoot, sha).Result(); + } + else if (line.Type == Models.TextDiffLineType.Deleted) + { + var sha = line.Content.Substring("Subproject commit ".Length); + submoduleDiff.Old = new Commands.QuerySingleCommit(submoduleRoot, sha).Result(); + } + } + rs = submoduleDiff; + } + else + { + latest.TextDiff.File = _option.Path; + rs = latest.TextDiff; + } } else if (latest.IsBinary) { @@ -175,14 +203,9 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Post(() => { - if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") - Title = _option.Path; - else - Title = $"{_option.OrgPath} → {_option.Path}"; - FileModeChange = latest.FileModeChange; Content = rs; - IsTextDiff = latest.TextDiff != null; + IsTextDiff = rs is Models.TextDiff; IsLoading = false; }); }); diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index 1520285a..1fad99ae 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -42,6 +42,7 @@ namespace SourceGit.ViewModels break; } + SetProgressDescription($"Git Flow - finishing {_branch.Name} ..."); var succ = new Commands.GitFlow(_repo.FullPath).Finish(_type, branch, KeepBranch); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index ddcec725..33fa3cf4 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -65,6 +65,7 @@ namespace SourceGit.ViewModels _repo.SetWatcherEnabled(false); return Task.Run(() => { + SetProgressDescription($"Git Flow - starting {_prefix}{_name} ..."); var succ = new Commands.GitFlow(_repo.FullPath).Start(_type, _name); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c28b4ade..bba8f901 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -69,7 +69,7 @@ namespace SourceGit.ViewModels public Models.Commit AutoSelectedCommit { get => _autoSelectedCommit; - private set => SetProperty(ref _autoSelectedCommit, value); + set => SetProperty(ref _autoSelectedCommit, value); } public long NavigationId @@ -81,7 +81,7 @@ namespace SourceGit.ViewModels public object DetailContext { get => _detailContext; - private set => SetProperty(ref _detailContext, value); + set => SetProperty(ref _detailContext, value); } public Histories(Repository repo) @@ -171,17 +171,16 @@ namespace SourceGit.ViewModels } } - public ContextMenu MakeContextMenu() + public ContextMenu MakeContextMenu(DataGrid datagrid) { - var detail = _detailContext as CommitDetail; - if (detail == null) + if (datagrid.SelectedItems.Count != 1) return null; var current = _repo.Branches.Find(x => x.IsCurrent); if (current == null) return null; - var commit = detail.Commit; + var commit = datagrid.SelectedItem as Models.Commit; var menu = new ContextMenu(); var tags = new List(); @@ -317,6 +316,46 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); + if (current.Head != commit.SHA) + { + var compareWithHead = new MenuItem(); + compareWithHead.Header = App.Text("CommitCM.CompareWithHead"); + compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithHead.Click += (o, e) => + { + var head = _commits.Find(x => x.SHA == current.Head); + if (head == null) + { + _repo.SearchResultSelectedCommit = null; + head = new Commands.QuerySingleCommit(_repo.FullPath, current.Head).Result(); + if (head != null) + DetailContext = new RevisionCompare(_repo.FullPath, commit, head); + } + else + { + datagrid.SelectedItems.Add(head); + } + + e.Handled = true; + }; + menu.Items.Add(compareWithHead); + + if (_repo.WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("CommitCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + DetailContext = new RevisionCompare(_repo.FullPath, commit, null); + e.Handled = true; + }; + menu.Items.Add(compareWithWorktree); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + } + var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); @@ -474,7 +513,7 @@ namespace SourceGit.ViewModels checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (o, e) => { - _repo.CheckoutLocalBranch(branch.Name); + _repo.CheckoutBranch(branch); e.Handled = true; }; submenu.Items.Add(checkout); @@ -546,20 +585,7 @@ namespace SourceGit.ViewModels checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (o, e) => { - foreach (var b in _repo.Branches) - { - if (b.IsLocal && b.Upstream == branch.FullName) - { - if (!b.IsCurrent) - _repo.CheckoutLocalBranch(b.Name); - - return; - } - } - - if (PopupHost.CanCreatePopup()) - PopupHost.ShowPopup(new CreateBranch(_repo, branch)); - + _repo.CheckoutBranch(branch); e.Handled = true; }; submenu.Items.Add(checkout); diff --git a/src/ViewModels/PopupHost.cs b/src/ViewModels/PopupHost.cs index b6351fcb..1a4c8bb2 100644 --- a/src/ViewModels/PopupHost.cs +++ b/src/ViewModels/PopupHost.cs @@ -52,17 +52,13 @@ namespace SourceGit.ViewModels if (task != null) { var finished = await task; + _popup.InProgress = false; if (finished) - { Popup = null; - } - else - { - _popup.InProgress = false; - } } else { + _popup.InProgress = false; Popup = null; } } diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index ee387e35..68dd3532 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -459,10 +459,6 @@ namespace SourceGit.ViewModels public static void Save() { - var dir = Path.GetDirectoryName(_savePath); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - var data = JsonSerializer.Serialize(_instance, JsonCodeGen.Default.Preference); File.WriteAllText(_savePath, data); } @@ -515,10 +511,7 @@ namespace SourceGit.ViewModels } private static Preference _instance = null; - private static readonly string _savePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SourceGit", - "preference.json"); + private static readonly string _savePath = Path.Combine(Native.OS.DataDir, "preference.json"); private string _locale = "en_US"; private string _theme = "Default"; diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 6461e1b3..86987895 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -47,13 +47,13 @@ namespace SourceGit.ViewModels set => SetProperty(ref _selectedBranch, value); } - public bool UseRebase + public Models.DealWithLocalChanges PreAction { - get; - set; - } = true; + get => _preAction; + set => SetProperty(ref _preAction, value); + } - public bool AutoStash + public bool UseRebase { get; set; @@ -117,23 +117,31 @@ namespace SourceGit.ViewModels return Task.Run(() => { var needPopStash = false; - if (AutoStash && _repo.WorkingCopyChangesCount > 0) + if (_repo.WorkingCopyChangesCount > 0) { - SetProgressDescription("Adding untracked changes..."); - var succ = new Commands.Add(_repo.FullPath).Exec(); - if (succ) + if (_preAction == Models.DealWithLocalChanges.StashAndReaply) { - SetProgressDescription("Stash local changes..."); - succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH"); - } + SetProgressDescription("Adding untracked changes..."); + var succ = new Commands.Add(_repo.FullPath).Exec(); + if (succ) + { + SetProgressDescription("Stash local changes..."); + succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH"); + } - if (!succ) + if (!succ) + { + CallUIThread(() => _repo.SetWatcherEnabled(true)); + return false; + } + + needPopStash = true; + } + else if (_preAction == Models.DealWithLocalChanges.Discard) { - CallUIThread(() => _repo.SetWatcherEnabled(true)); - return false; + SetProgressDescription("Discard local changes ..."); + Commands.Discard.All(_repo.FullPath); } - - needPopStash = true; } SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}..."); @@ -154,5 +162,6 @@ namespace SourceGit.ViewModels private Models.Remote _selectedRemote = null; private List _remoteBranches = null; private Models.Branch _selectedBranch = null; + private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.StashAndReaply; } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 94a04172..cef2dddc 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -158,13 +158,6 @@ namespace SourceGit.ViewModels get => _stashesPage == null ? 0 : _stashesPage.Count; } - [JsonIgnore] - public bool CanCommitWithPush - { - get => _canCommitWithPush; - private set => SetProperty(ref _canCommitWithPush, value); - } - [JsonIgnore] public bool IncludeUntracked { @@ -172,9 +165,7 @@ namespace SourceGit.ViewModels set { if (SetProperty(ref _includeUntracked, value)) - { Task.Run(RefreshWorkingCopyChanges); - } } } @@ -385,7 +376,11 @@ namespace SourceGit.ViewModels } if (Branches.Find(x => x.IsCurrent) == null) + { App.RaiseException(_fullpath, "Can NOT found current branch!!!"); + return; + } + PopupHost.ShowPopup(new Push(this, null)); } @@ -563,8 +558,11 @@ namespace SourceGit.ViewModels LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; - var cur = Branches.Find(x => x.IsCurrent); - CanCommitWithPush = cur != null && !string.IsNullOrEmpty(cur.Upstream); + if (_workingCopy != null) + { + var cur = Branches.Find(x => x.IsCurrent); + _workingCopy.CanCommitWithPush = cur != null && !string.IsNullOrEmpty(cur.Upstream); + } }); } @@ -711,17 +709,35 @@ namespace SourceGit.ViewModels PopupHost.ShowPopup(new CreateBranch(this, current)); } - public void CheckoutLocalBranch(string branch) + public void CheckoutBranch(Models.Branch branch) { if (!PopupHost.CanCreatePopup()) return; - if (WorkingCopyChangesCount > 0) - PopupHost.ShowPopup(new Checkout(this, branch)); + if (branch.IsLocal) + { + if (WorkingCopyChangesCount > 0) + PopupHost.ShowPopup(new Checkout(this, branch.Name)); + else + PopupHost.ShowAndStartPopup(new Checkout(this, branch.Name)); + } else - PopupHost.ShowAndStartPopup(new Checkout(this, branch)); + { + foreach (var b in Branches) + { + if (b.IsLocal && b.Upstream == branch.FullName) + { + if (!b.IsCurrent) + CheckoutBranch(b); + + return; + } + } + + PopupHost.ShowPopup(new CreateBranch(this, branch)); + } } - + public void DeleteMultipleBranches(List branches, bool isLocal) { if (PopupHost.CanCreatePopup()) @@ -880,7 +896,7 @@ namespace SourceGit.ViewModels checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (o, e) => { - CheckoutLocalBranch(branch.Name); + CheckoutBranch(branch); e.Handled = true; }; menu.Items.Add(checkout); @@ -928,6 +944,46 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); + + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareWithHead"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + compare.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, head); + } + + e.Handled = true; + }; + + if (WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + }; + menu.Items.Add(compareWithWorktree); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(compare); } var type = GitFlow.GetBranchType(branch.Name); @@ -1142,20 +1198,7 @@ namespace SourceGit.ViewModels checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (o, e) => { - foreach (var b in Branches) - { - if (b.IsLocal && b.Upstream == branch.FullName) - { - if (b.IsCurrent) - return; - if (PopupHost.CanCreatePopup()) - PopupHost.ShowAndStartPopup(new Checkout(this, b.Name)); - return; - } - } - - if (PopupHost.CanCreatePopup()) - PopupHost.ShowPopup(new CreateBranch(this, branch)); + CheckoutBranch(branch); e.Handled = true; }; menu.Items.Add(checkout); @@ -1197,6 +1240,49 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); menu.Items.Add(new MenuItem() { Header = "-" }); + + if (current.Head != branch.Head) + { + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareWithHead"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + compare.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, head); + } + + e.Handled = true; + }; + menu.Items.Add(compare); + + if (WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + }; + menu.Items.Add(compareWithWorktree); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + } } var delete = new MenuItem(); @@ -1428,7 +1514,6 @@ namespace SourceGit.ViewModels private List _remoteBranchTrees = new List(); private List _tags = new List(); private List _submodules = new List(); - private bool _canCommitWithPush = false; private bool _includeUntracked = true; private InProgressContext _inProgressContext = null; diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index 7af0c83f..c1bbdcf5 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -17,7 +17,19 @@ namespace SourceGit.ViewModels set; } - public bool GPGSigningEnabled + public Models.GPGFormat GPGFormat + { + get; + set; + } + + public bool GPGCommitSigningEnabled + { + get; + set; + } + + public bool GPGTagSigningEnabled { get; set; @@ -44,8 +56,14 @@ namespace SourceGit.ViewModels UserName = name; if (_cached.TryGetValue("user.email", out var email)) UserEmail = email; - if (_cached.TryGetValue("commit.gpgsign", out var gpgsign)) - GPGSigningEnabled = gpgsign == "true"; + if (_cached.TryGetValue("commit.gpgsign", out var gpgCommitSign)) + GPGCommitSigningEnabled = gpgCommitSign == "true"; + if (_cached.TryGetValue("tag.gpgSign", out var gpgTagSign)) + GPGTagSigningEnabled = gpgTagSign == "true"; + if (_cached.TryGetValue("gpg.format", out var gpgFormat)) + GPGFormat = Models.GPGFormat.Supported.Find(x => x.Value == gpgFormat); + else + GPGFormat = Models.GPGFormat.OPENPGP; if (_cached.TryGetValue("user.signingkey", out var signingKey)) GPGUserSigningKey = signingKey; if (_cached.TryGetValue("http.proxy", out var proxy)) @@ -58,20 +76,22 @@ namespace SourceGit.ViewModels { SetIfChanged("user.name", UserName); SetIfChanged("user.email", UserEmail); - SetIfChanged("commit.gpgsign", GPGSigningEnabled ? "true" : "false"); + SetIfChanged("commit.gpgsign", GPGCommitSigningEnabled ? "true" : "false"); + SetIfChanged("tag.gpgSign", GPGTagSigningEnabled ? "true" : "false"); + SetIfChanged("gpg.format", GPGFormat?.Value, Models.GPGFormat.OPENPGP.Value); SetIfChanged("user.signingkey", GPGUserSigningKey); SetIfChanged("http.proxy", HttpProxy); return null; } - private void SetIfChanged(string key, string value) + private void SetIfChanged(string key, string value, string defaultValue = null) { bool changed = false; if (_cached.TryGetValue(key, out var old)) { changed = old != value; } - else if (!string.IsNullOrEmpty(value)) + else if (!string.IsNullOrEmpty(value) && value != defaultValue) { changed = true; } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 1ed85c9e..1fdec102 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -10,6 +10,11 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { + public class CompareTargetWorktree + { + public string SHA => string.Empty; + } + public class RevisionCompare : ObservableObject { public Models.Commit StartPoint @@ -18,7 +23,7 @@ namespace SourceGit.ViewModels private set; } - public Models.Commit EndPoint + public object EndPoint { get; private set; @@ -30,48 +35,17 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _visibleChanges, value); } - public List ChangeTree + public List SelectedChanges { - get => _changeTree; - private set => SetProperty(ref _changeTree, value); - } - - public Models.Change SelectedChange - { - get => _selectedChange; + get => _selectedChanges; set { - if (SetProperty(ref _selectedChange, value)) + if (SetProperty(ref _selectedChanges, value)) { - if (value == null) - { - SelectedNode = null; + if (value != null && value.Count == 1) + DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value[0]), _diffContext); + else DiffContext = null; - } - else - { - SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path); - DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, value), _diffContext); - } - } - } - } - - public FileTreeNode SelectedNode - { - get => _selectedNode; - set - { - if (SetProperty(ref _selectedNode, value)) - { - if (value == null) - { - SelectedChange = null; - } - else - { - SelectedChange = value.Backend as Models.Change; - } } } } @@ -98,11 +72,21 @@ namespace SourceGit.ViewModels { _repo = repo; StartPoint = startPoint; - EndPoint = endPoint; + + if (endPoint == null) + { + EndPoint = new CompareTargetWorktree(); + _endPoint = string.Empty; + } + else + { + EndPoint = endPoint; + _endPoint = endPoint.SHA; + } Task.Run(() => { - _changes = new Commands.CompareRevisions(_repo, startPoint.SHA, endPoint.SHA).Result(); + _changes = new Commands.CompareRevisions(_repo, startPoint.SHA, _endPoint).Result(); var visible = _changes; if (!string.IsNullOrWhiteSpace(_searchFilter)) @@ -111,18 +95,11 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } } - var tree = FileTreeNode.Build(visible); - Dispatcher.UIThread.Invoke(() => - { - VisibleChanges = visible; - ChangeTree = tree; - }); + Dispatcher.UIThread.Invoke(() => VisibleChanges = visible); }); } @@ -133,10 +110,8 @@ namespace SourceGit.ViewModels _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); - if (_changeTree != null) - _changeTree.Clear(); - _selectedChange = null; - _selectedNode = null; + if (_selectedChanges != null) + _selectedChanges.Clear(); _searchFilter = null; _diffContext = null; } @@ -153,8 +128,12 @@ namespace SourceGit.ViewModels SearchFilter = string.Empty; } - public ContextMenu CreateChangeContextMenu(Models.Change change) + public ContextMenu CreateChangeContextMenu() { + if (_selectedChanges == null || _selectedChanges.Count != 1) + return null; + + var change = _selectedChanges[0]; var menu = new ContextMenu(); var diffWithMerger = new MenuItem(); @@ -162,7 +141,7 @@ namespace SourceGit.ViewModels diffWithMerger.Icon = App.CreateMenuIcon("Icons.Diff"); diffWithMerger.Click += (_, ev) => { - var opt = new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, change); + var opt = new Models.DiffOption(StartPoint.SHA, _endPoint, change); var type = Preference.Instance.ExternalMergeToolType; var exec = Preference.Instance.ExternalMergeToolPath; @@ -202,8 +181,18 @@ namespace SourceGit.ViewModels App.CopyText(change.Path); ev.Handled = true; }; - menu.Items.Add(copyPath); + + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(change.Path)); + e.Handled = true; + }; + menu.Items.Add(copyFileName); + return menu; } @@ -222,23 +211,18 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } VisibleChanges = visible; } - - ChangeTree = FileTreeNode.Build(_visibleChanges); } private string _repo = string.Empty; + private string _endPoint = string.Empty; private List _changes = null; private List _visibleChanges = null; - private List _changeTree = null; - private Models.Change _selectedChange = null; - private FileTreeNode _selectedNode = null; + private List _selectedChanges = null; private string _searchFilter = string.Empty; private DiffContext _diffContext = null; } diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index bae81012..8276e654 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; - +using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -106,34 +106,42 @@ namespace SourceGit.ViewModels _diffContext = null; } - public void Apply(object param) + public ContextMenu MakeContextMenu(Models.Stash stash) { - if (param is Models.Stash stash) - { - Task.Run(() => - { - new Commands.Stash(_repo.FullPath).Apply(stash.Name); - }); - } - } + if (stash == null) + return null; - public void Pop(object param) - { - if (param is Models.Stash stash) + var apply = new MenuItem(); + apply.Header = App.Text("StashCM.Apply"); + apply.Click += (o, ev) => { - Task.Run(() => - { - new Commands.Stash(_repo.FullPath).Pop(stash.Name); - }); - } - } + Task.Run(() => new Commands.Stash(_repo.FullPath).Apply(stash.Name)); + ev.Handled = true; + }; - public void Drop(object param) - { - if (param is Models.Stash stash && PopupHost.CanCreatePopup()) + var pop = new MenuItem(); + pop.Header = App.Text("StashCM.Pop"); + pop.Click += (o, ev) => { - PopupHost.ShowPopup(new DropStash(_repo.FullPath, stash)); - } + Task.Run(() => new Commands.Stash(_repo.FullPath).Pop(stash.Name)); + ev.Handled = true; + }; + + var drop = new MenuItem(); + drop.Header = App.Text("StashCM.Drop"); + drop.Click += (o, ev) => + { + if (PopupHost.CanCreatePopup()) + PopupHost.ShowPopup(new DropStash(_repo.FullPath, stash)); + + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(apply); + menu.Items.Add(pop); + menu.Items.Add(drop); + return menu; } public void Clear() diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 45bf7614..d077bfb7 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -2,6 +2,7 @@ using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using CommunityToolkit.Mvvm.ComponentModel; @@ -40,20 +41,21 @@ namespace SourceGit.ViewModels } } - public void Clone(object param) + public void Clone() { - var launcher = param as Launcher; - var page = launcher.ActivePage; - if (!Preference.Instance.IsGitConfigured) { - App.RaiseException(page.GetId(), App.Text("NotConfigured")); + App.RaiseException(string.Empty, App.Text("NotConfigured")); return; } if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new Clone(launcher, page)); + if (App.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var launcher = desktop.MainWindow.DataContext as Launcher; + PopupHost.ShowPopup(new Clone(launcher)); + } } } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 76c3be50..4167c244 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -37,6 +37,29 @@ namespace SourceGit.ViewModels public class WorkingCopy : ObservableObject { + public bool IncludeUntracked + { + get => _repo.IncludeUntracked; + set + { + if (_repo.IncludeUntracked != value) + { + _repo.IncludeUntracked = value; + OnPropertyChanged(nameof(IncludeUntracked)); + } + } + } + + public bool CanCommitWithPush + { + get => _canCommitWithPush; + set + { + if (SetProperty(ref _canCommitWithPush, value)) + OnPropertyChanged(nameof(IsCommitWithPushVisible)); + } + } + public bool IsStaging { get => _isStaging; @@ -58,7 +81,30 @@ namespace SourceGit.ViewModels public bool UseAmend { get => _useAmend; - set => SetProperty(ref _useAmend, value); + set + { + if (SetProperty(ref _useAmend, value) && value) + { + var commits = new Commands.QueryCommits(_repo.FullPath, "-n 1", false).Result(); + if (commits.Count == 0) + { + App.RaiseException(_repo.FullPath, "No commits to amend!!!"); + _useAmend = false; + OnPropertyChanged(); + } + else + { + CommitMessage = commits[0].FullMessage; + } + } + + OnPropertyChanged(nameof(IsCommitWithPushVisible)); + } + } + + public bool IsCommitWithPushVisible + { + get => !UseAmend && CanCommitWithPush; } public List Unstaged @@ -73,103 +119,58 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _staged, value); } - public int Count + public List SelectedUnstaged { - get => _count; - } - - public Models.Change SelectedUnstagedChange - { - get => _selectedUnstagedChange; + get => _selectedUnstaged; set { - if (SetProperty(ref _selectedUnstagedChange, value) && value != null) + if (SetProperty(ref _selectedUnstaged, value)) { - SelectedStagedChange = null; - SelectedStagedTreeNode = null; - SetDetail(value, true); - } - } - } - - public Models.Change SelectedStagedChange - { - get => _selectedStagedChange; - set - { - if (SetProperty(ref _selectedStagedChange, value) && value != null) - { - SelectedUnstagedChange = null; - SelectedUnstagedTreeNode = null; - SetDetail(value, false); - } - } - } - - public List UnstagedTree - { - get => _unstagedTree; - private set => SetProperty(ref _unstagedTree, value); - } - - public List StagedTree - { - get => _stagedTree; - private set => SetProperty(ref _stagedTree, value); - } - - public FileTreeNode SelectedUnstagedTreeNode - { - get => _selectedUnstagedTreeNode; - set - { - if (SetProperty(ref _selectedUnstagedTreeNode, value)) - { - if (value == null) + if (value == null || value.Count == 0) { - SelectedUnstagedChange = null; + if (_selectedStaged == null || _selectedStaged.Count == 0) + SetDetail(null); } else { - SelectedUnstagedChange = value.Backend as Models.Change; - SelectedStagedTreeNode = null; - SelectedStagedChange = null; + SelectedStaged = null; - if (value.IsFolder) - { - SetDetail(null, true); - } + if (value.Count == 1) + SetDetail(value[0]); + else + SetDetail(null); } } } } - public FileTreeNode SelectedStagedTreeNode + public List SelectedStaged { - get => _selectedStagedTreeNode; + get => _selectedStaged; set { - if (SetProperty(ref _selectedStagedTreeNode, value)) + if (SetProperty(ref _selectedStaged, value)) { - if (value == null) + if (value == null || value.Count == 0) { - SelectedStagedChange = null; + if (_selectedUnstaged == null || _selectedUnstaged.Count == 0) + SetDetail(null); } else { - SelectedStagedChange = value.Backend as Models.Change; - SelectedUnstagedTreeNode = null; - SelectedUnstagedChange = null; + SelectedUnstaged = null; - if (value.IsFolder) - { - SetDetail(null, false); - } + if (value.Count == 1) + SetDetail(value[0]); + else + SetDetail(null); } } } } + public int Count => _count; + public object DetailContext { get => _detailContext; @@ -190,18 +191,31 @@ namespace SourceGit.ViewModels public void Cleanup() { _repo = null; + + if (_selectedUnstaged != null) + { + _selectedUnstaged.Clear(); + OnPropertyChanged(nameof(SelectedUnstaged)); + } + + if (_selectedStaged != null) + { + _selectedStaged.Clear(); + OnPropertyChanged(nameof(SelectedStaged)); + } + if (_unstaged != null) + { _unstaged.Clear(); + OnPropertyChanged(nameof(Unstaged)); + } + if (_staged != null) + { _staged.Clear(); - if (_unstagedTree != null) - _unstagedTree.Clear(); - if (_stagedTree != null) - _stagedTree.Clear(); - _selectedUnstagedChange = null; - _selectedStagedChange = null; - _selectedUnstagedTreeNode = null; - _selectedStagedTreeNode = null; + OnPropertyChanged(nameof(Staged)); + } + _detailContext = null; _commitMessage = string.Empty; } @@ -210,20 +224,22 @@ namespace SourceGit.ViewModels { var unstaged = new List(); var staged = new List(); + var selectedUnstaged = new List(); + var selectedStaged = new List(); - var viewFile = string.Empty; - var lastSelectedIsUnstaged = false; - if (_selectedUnstagedChange != null) + var lastSelectedUnstaged = new HashSet(); + var lastSelectedStaged = new HashSet(); + if (_selectedUnstaged != null) { - viewFile = _selectedUnstagedChange.Path; - lastSelectedIsUnstaged = true; + foreach (var c in _selectedUnstaged) + lastSelectedUnstaged.Add(c.Path); } - else if (_selectedStagedChange != null) + else if (_selectedStaged != null) { - viewFile = _selectedStagedChange.Path; + foreach (var c in _selectedStaged) + lastSelectedStaged.Add(c.Path); } - var viewChange = null as Models.Change; var hasConflict = false; foreach (var c in changes) { @@ -233,65 +249,43 @@ namespace SourceGit.ViewModels || c.Index == Models.ChangeState.Renamed) { staged.Add(c); - if (!lastSelectedIsUnstaged && c.Path == viewFile) - { - viewChange = c; - } + + if (lastSelectedStaged.Contains(c.Path)) + selectedStaged.Add(c); } if (c.WorkTree != Models.ChangeState.None) { unstaged.Add(c); hasConflict |= c.IsConflit; - if (lastSelectedIsUnstaged && c.Path == viewFile) - { - viewChange = c; - } + + if (lastSelectedUnstaged.Contains(c.Path)) + selectedUnstaged.Add(c); } } _count = changes.Count; - var unstagedTree = FileTreeNode.Build(unstaged); - var stagedTree = FileTreeNode.Build(staged); Dispatcher.UIThread.Invoke(() => { _isLoadingData = true; Unstaged = unstaged; Staged = staged; - UnstagedTree = unstagedTree; - StagedTree = stagedTree; _isLoadingData = false; - // Restore last selection states. - if (viewChange != null) - { - var scrollOffset = Vector.Zero; - if (_detailContext is DiffContext old) - scrollOffset = old.SyncScrollOffset; + var scrollOffset = Vector.Zero; + if (_detailContext is DiffContext old) + scrollOffset = old.SyncScrollOffset; - if (lastSelectedIsUnstaged) - { - SelectedUnstagedChange = viewChange; - SelectedUnstagedTreeNode = FileTreeNode.SelectByPath(_unstagedTree, viewFile); - } - else - { - SelectedStagedChange = viewChange; - SelectedStagedTreeNode = FileTreeNode.SelectByPath(_stagedTree, viewFile); - } - - if (_detailContext is DiffContext cur) - cur.SyncScrollOffset = scrollOffset; - } + if (selectedUnstaged.Count > 0) + SelectedUnstaged = selectedUnstaged; + else if (selectedStaged.Count > 0) + SelectedStaged = selectedStaged; else - { - SelectedUnstagedChange = null; - SelectedUnstagedTreeNode = null; - SelectedStagedChange = null; - SelectedStagedTreeNode = null; - SetDetail(null, false); - } + SetDetail(null); + + if (_detailContext is DiffContext cur) + cur.SyncScrollOffset = scrollOffset; // Try to load merge message from MERGE_MSG if (string.IsNullOrEmpty(_commitMessage)) @@ -305,30 +299,24 @@ namespace SourceGit.ViewModels return hasConflict; } - public void SetDetail(Models.Change change, bool isUnstaged) + public void OpenAssumeUnchanged() { - if (_isLoadingData) - return; + var dialog = new Views.AssumeUnchangedManager() + { + DataContext = new AssumeUnchangedManager(_repo.FullPath) + }; - if (change == null) - { - DetailContext = null; - } - else if (change.IsConflit && isUnstaged) - { - DetailContext = new ConflictContext(_repo.FullPath, change); - } - else - { - if (_detailContext is DiffContext previous) - { - DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous); - } - else - { - DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged)); - } - } + dialog.ShowDialog(App.GetTopLevel() as Window); + } + + public void StageSelected() + { + StageChanges(_selectedUnstaged); + } + + public void StageAll() + { + StageChanges(_unstaged); } public async void StageChanges(List changes) @@ -336,7 +324,7 @@ namespace SourceGit.ViewModels if (_unstaged.Count == 0 || changes.Count == 0) return; - SetDetail(null, true); + SetDetail(null); IsStaging = true; _repo.SetWatcherEnabled(false); if (changes.Count == _unstaged.Count) @@ -357,12 +345,22 @@ namespace SourceGit.ViewModels IsStaging = false; } + public void UnstageSelected() + { + UnstageChanges(_selectedStaged); + } + + public void UnstageAll() + { + UnstageChanges(_staged); + } + public async void UnstageChanges(List changes) { if (_staged.Count == 0 || changes.Count == 0) return; - SetDetail(null, false); + SetDetail(null); IsUnstaging = true; _repo.SetWatcherEnabled(false); if (changes.Count == _staged.Count) @@ -412,113 +410,25 @@ namespace SourceGit.ViewModels } } - public async void UseTheirs(List changes) + public void Commit() { - var files = new List(); - foreach (var change in changes) - { - if (change.IsConflit) - files.Add(change.Path); - } - - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files)); - if (succ) - { - await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); + DoCommit(false); } - public async void UseMine(List changes) + public void CommitWithPush() { - var files = new List(); - foreach (var change in changes) - { - if (change.IsConflit) - files.Add(change.Path); - } - - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files)); - if (succ) - { - await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); + DoCommit(true); } - public async void UseExternalMergeTool(Models.Change change) + public ContextMenu CreateContextMenuForUnstagedChanges() { - var type = Preference.Instance.ExternalMergeToolType; - var exec = Preference.Instance.ExternalMergeToolPath; - - var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type); - if (tool == null) - { - App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!"); - return; - } - - var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd; - - _repo.SetWatcherEnabled(false); - await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path)); - _repo.SetWatcherEnabled(true); - } - - public async void DoCommit(bool autoPush) - { - if (!PopupHost.CanCreatePopup()) - { - App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!"); - return; - } - - if (_staged.Count == 0) - { - App.RaiseException(_repo.FullPath, "No files added to commit!"); - return; - } - - if (string.IsNullOrWhiteSpace(_commitMessage)) - { - App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!"); - return; - } - - PushCommitMessage(); - - SetDetail(null, false); - IsCommitting = true; - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec()); - if (succ) - { - CommitMessage = string.Empty; - UseAmend = false; - - if (autoPush) - { - PopupHost.ShowAndStartPopup(new Push(_repo, null)); - } - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsCommitting = false; - } - - public ContextMenu CreateContextMenuForUnstagedChanges(List changes) - { - if (changes.Count == 0) + if (_selectedUnstaged.Count == 0) return null; var menu = new ContextMenu(); - if (changes.Count == 1) + if (_selectedUnstaged.Count == 1) { - var change = changes[0]; + var change = _selectedUnstaged[0]; var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path)); var explore = new MenuItem(); @@ -551,7 +461,7 @@ namespace SourceGit.ViewModels useTheirs.Header = App.Text("FileCM.UseTheirs"); useTheirs.Click += (_, e) => { - UseTheirs(changes); + UseTheirs(_selectedUnstaged); e.Handled = true; }; @@ -560,7 +470,7 @@ namespace SourceGit.ViewModels useMine.Header = App.Text("FileCM.UseMine"); useMine.Click += (_, e) => { - UseMine(changes); + UseMine(_selectedUnstaged); e.Handled = true; }; @@ -585,7 +495,7 @@ namespace SourceGit.ViewModels stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Click += (_, e) => { - StageChanges(changes); + StageChanges(_selectedUnstaged); e.Handled = true; }; @@ -594,7 +504,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, true); + Discard(_selectedUnstaged, true); e.Handled = true; }; @@ -605,7 +515,7 @@ namespace SourceGit.ViewModels { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false)); } e.Handled = true; }; @@ -627,7 +537,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -674,12 +584,22 @@ namespace SourceGit.ViewModels e.Handled = true; }; menu.Items.Add(copy); + + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(change.Path)); + e.Handled = true; + }; + menu.Items.Add(copyFileName); } else { var hasConflicts = false; var hasNoneConflicts = false; - foreach (var change in changes) + foreach (var change in _selectedUnstaged) { if (change.IsConflit) { @@ -704,7 +624,7 @@ namespace SourceGit.ViewModels useTheirs.Header = App.Text("FileCM.UseTheirs"); useTheirs.Click += (_, e) => { - UseTheirs(changes); + UseTheirs(_selectedUnstaged); e.Handled = true; }; @@ -713,7 +633,7 @@ namespace SourceGit.ViewModels useMine.Header = App.Text("FileCM.UseMine"); useMine.Click += (_, e) => { - UseMine(changes); + UseMine(_selectedUnstaged); e.Handled = true; }; @@ -723,31 +643,31 @@ namespace SourceGit.ViewModels } var stage = new MenuItem(); - stage.Header = App.Text("FileCM.StageMulti", changes.Count); + stage.Header = App.Text("FileCM.StageMulti", _selectedUnstaged.Count); stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Click += (_, e) => { - StageChanges(changes); + StageChanges(_selectedUnstaged); e.Handled = true; }; var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", changes.Count); + discard.Header = App.Text("FileCM.DiscardMulti", _selectedUnstaged.Count); discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, true); + Discard(_selectedUnstaged, true); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", changes.Count); + stash.Header = App.Text("FileCM.StashMulti", _selectedUnstaged.Count); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Click += (_, e) => { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false)); } e.Handled = true; }; @@ -769,7 +689,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -786,15 +706,15 @@ namespace SourceGit.ViewModels return menu; } - public ContextMenu CreateContextMenuForStagedChanges(List changes) + public ContextMenu CreateContextMenuForStagedChanges() { - if (changes.Count == 0) + if (_selectedStaged.Count == 0) return null; var menu = new ContextMenu(); - if (changes.Count == 1) + if (_selectedStaged.Count == 1) { - var change = changes[0]; + var change = _selectedStaged[0]; var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path)); var explore = new MenuItem(); @@ -822,7 +742,7 @@ namespace SourceGit.ViewModels unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Click += (o, e) => { - UnstageChanges(changes); + UnstageChanges(_selectedStaged); e.Handled = true; }; @@ -831,7 +751,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, false); + Discard(_selectedStaged, false); e.Handled = true; }; @@ -842,7 +762,7 @@ namespace SourceGit.ViewModels { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false)); } e.Handled = true; }; @@ -864,7 +784,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -880,6 +800,15 @@ namespace SourceGit.ViewModels App.CopyText(change.Path); e.Handled = true; }; + + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(change.Path)); + e.Handled = true; + }; menu.Items.Add(explore); menu.Items.Add(openWith); @@ -890,35 +819,36 @@ namespace SourceGit.ViewModels menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); + menu.Items.Add(copyFileName); } else { var unstage = new MenuItem(); - unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count); + unstage.Header = App.Text("FileCM.UnstageMulti", _selectedStaged.Count); unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Click += (o, e) => { - UnstageChanges(changes); + UnstageChanges(_selectedStaged); e.Handled = true; }; var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", changes.Count); + discard.Header = App.Text("FileCM.DiscardMulti", _selectedStaged.Count); discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, false); + Discard(_selectedStaged, false); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", changes.Count); + stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Click += (_, e) => { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false)); } e.Handled = true; }; @@ -940,7 +870,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -993,6 +923,139 @@ namespace SourceGit.ViewModels return menu; } + private void SetDetail(Models.Change change) + { + if (_isLoadingData) + return; + + var isUnstaged = _selectedUnstaged != null && _selectedUnstaged.Count > 0; + if (change == null) + { + DetailContext = null; + } + else if (change.IsConflit && isUnstaged) + { + DetailContext = new ConflictContext(_repo.FullPath, change); + } + else + { + if (_detailContext is DiffContext previous) + { + DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous); + } + else + { + DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged)); + } + } + } + + private async void UseTheirs(List changes) + { + var files = new List(); + foreach (var change in changes) + { + if (change.IsConflit) + files.Add(change.Path); + } + + _repo.SetWatcherEnabled(false); + var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files)); + if (succ) + { + await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + } + + private async void UseMine(List changes) + { + var files = new List(); + foreach (var change in changes) + { + if (change.IsConflit) + files.Add(change.Path); + } + + _repo.SetWatcherEnabled(false); + var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files)); + if (succ) + { + await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + } + + private async void UseExternalMergeTool(Models.Change change) + { + var type = Preference.Instance.ExternalMergeToolType; + var exec = Preference.Instance.ExternalMergeToolPath; + + var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type); + if (tool == null) + { + App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!"); + return; + } + + var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd; + + _repo.SetWatcherEnabled(false); + await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path)); + _repo.SetWatcherEnabled(true); + } + + private void DoCommit(bool autoPush) + { + if (!PopupHost.CanCreatePopup()) + { + App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!"); + return; + } + + if (_staged.Count == 0) + { + App.RaiseException(_repo.FullPath, "No files added to commit!"); + return; + } + + if (string.IsNullOrWhiteSpace(_commitMessage)) + { + App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!"); + return; + } + + PushCommitMessage(); + + SetDetail(null); + IsCommitting = true; + _repo.SetWatcherEnabled(false); + + Task.Run(() => + { + var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); + Dispatcher.UIThread.Post(() => + { + if (succ) + { + CommitMessage = string.Empty; + UseAmend = false; + + if (autoPush) + { + PopupHost.ShowAndStartPopup(new Push(_repo, null)); + } + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + + IsCommitting = false; + }); + }); + } + private void PushCommitMessage() { var existIdx = _repo.CommitMessages.IndexOf(CommitMessage); @@ -1020,15 +1083,12 @@ namespace SourceGit.ViewModels private bool _isUnstaging = false; private bool _isCommitting = false; private bool _useAmend = false; + private bool _canCommitWithPush = false; private List _unstaged = null; private List _staged = null; - private Models.Change _selectedUnstagedChange = null; - private Models.Change _selectedStagedChange = null; + private List _selectedUnstaged = null; + private List _selectedStaged = null; private int _count = 0; - private List _unstagedTree = null; - private List _stagedTree = null; - private FileTreeNode _selectedUnstagedTreeNode = null; - private FileTreeNode _selectedStagedTreeNode = null; private object _detailContext = null; private string _commitMessage = string.Empty; } diff --git a/src/Views/AssumeUnchangedManager.axaml b/src/Views/AssumeUnchangedManager.axaml index 7289f0b4..6d8b2dc1 100644 --- a/src/Views/AssumeUnchangedManager.axaml +++ b/src/Views/AssumeUnchangedManager.axaml @@ -92,9 +92,7 @@ - diff --git a/src/Views/AssumeUnchangedManager.axaml.cs b/src/Views/AssumeUnchangedManager.axaml.cs index 7efc06a5..3035ab77 100644 --- a/src/Views/AssumeUnchangedManager.axaml.cs +++ b/src/Views/AssumeUnchangedManager.axaml.cs @@ -20,5 +20,12 @@ namespace SourceGit.Views { Close(); } + + private void OnRemoveButtonClicked(object sender, RoutedEventArgs e) { + if (DataContext is ViewModels.AssumeUnchangedManager vm && sender is Button button) + vm.Remove(button.DataContext as string); + + e.Handled = true; + } } } diff --git a/src/Views/Blame.axaml b/src/Views/Blame.axaml index 06bd43e0..3befbadb 100644 --- a/src/Views/Blame.axaml +++ b/src/Views/Blame.axaml @@ -84,10 +84,7 @@ - + diff --git a/src/Views/CaptionButtons.axaml b/src/Views/CaptionButtons.axaml index 53436340..7b778aff 100644 --- a/src/Views/CaptionButtons.axaml +++ b/src/Views/CaptionButtons.axaml @@ -11,8 +11,8 @@ -