diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index c4674c8e..b09face9 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -18,6 +18,15 @@ namespace SourceGit.Commands Args = $"diff --name-status {based} {end}"; } + public CompareRevisions(string repo, string start, string end, string path) + { + WorkingDirectory = repo; + Context = repo; + + var based = string.IsNullOrEmpty(start) ? "-R" : start; + Args = $"diff --name-status {based} {end} -- {path}"; + } + public List Result() { Exec(); diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index f21c5d24..1634f94e 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -22,6 +22,9 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsNotOne = new FuncValueConverter(v => v != 1); + public static readonly FuncValueConverter IsTwo = + new FuncValueConverter(v => v == 2); + public static readonly FuncValueConverter IsSubjectLengthBad = new FuncValueConverter(v => v > ViewModels.Preferences.Instance.SubjectGuideLength); diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index ede73cd1..40b3cbea 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -31,12 +31,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _commits, value); } - public Models.Commit SelectedCommit + public List SelectedCommits { - get => _selectedCommit; + get => _selectedCommits; set { - if (SetProperty(ref _selectedCommit, value)) + if (SetProperty(ref _selectedCommits, value)) RefreshViewContent(); } } @@ -51,6 +51,18 @@ namespace SourceGit.ViewModels } } + public Models.Commit StartPoint + { + get => _startPoint; + set => SetProperty(ref _startPoint, value); + } + + public Models.Commit EndPoint + { + get => _endPoint; + set => SetProperty(ref _endPoint, value); + } + public object ViewContent { get => _viewContent; @@ -71,7 +83,7 @@ namespace SourceGit.ViewModels IsLoading = false; Commits = commits; if (commits.Count > 0) - SelectedCommit = commits[0]; + SelectedCommits = [commits[0]]; }); }); } @@ -83,18 +95,40 @@ namespace SourceGit.ViewModels public void ResetToSelectedRevision() { - new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_selectedCommit.SHA}"); + if (_selectedCommits is not { Count: 1 }) + return; + new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_selectedCommits[0].SHA}"); + } + + public void Swap() + { + if (_selectedCommits is not { Count: 2 }) + return; + + (_selectedCommits[0], _selectedCommits[1]) = (_selectedCommits[1], _selectedCommits[0]); + RefreshViewContent(); + } + + public Task SaveAsPatch(string saveTo) + { + return Task.Run(() => + { + Commands.SaveChangesAsPatch.ProcessRevisionCompareChanges(_repo.FullPath, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); + return true; + }); } private void RefreshViewContent() { - if (_selectedCommit == null) + if (_selectedCommits == null || _selectedCommits.Count == 0) { - ViewContent = null; + StartPoint = null; + EndPoint = null; + ViewContent = 0; return; } - if (_isViewContent) + if (_isViewContent && _selectedCommits.Count == 1) SetViewContentAsRevisionFile(); else SetViewContentAsDiff(); @@ -102,7 +136,10 @@ namespace SourceGit.ViewModels private void SetViewContentAsRevisionFile() { - var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _selectedCommit.SHA, _file).Result(); + StartPoint = null; + EndPoint = null; + var selectedCommit = _selectedCommits[0]; + var objs = new Commands.QueryRevisionObjects(_repo.FullPath, selectedCommit.SHA, _file).Result(); if (objs.Count == 0) { ViewContent = new FileHistoriesRevisionFile(_file, null); @@ -115,13 +152,13 @@ namespace SourceGit.ViewModels case Models.ObjectType.Blob: Task.Run(() => { - var isBinary = new Commands.IsBinary(_repo.FullPath, _selectedCommit.SHA, _file).Result(); + var isBinary = new Commands.IsBinary(_repo.FullPath, selectedCommit.SHA, _file).Result(); if (isBinary) { var ext = Path.GetExtension(_file); if (IMG_EXTS.Contains(ext)) { - var stream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file); + var stream = Commands.QueryFileContent.Run(_repo.FullPath, selectedCommit.SHA, _file); var fileSize = stream.Length; var bitmap = fileSize > 0 ? new Bitmap(stream) : null; var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture); @@ -130,7 +167,7 @@ namespace SourceGit.ViewModels } else { - var size = new Commands.QueryFileSize(_repo.FullPath, _file, _selectedCommit.SHA).Result(); + var size = new Commands.QueryFileSize(_repo.FullPath, _file, selectedCommit.SHA).Result(); var binaryFile = new Models.RevisionBinaryFile() { Size = size }; Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile)); } @@ -138,7 +175,7 @@ namespace SourceGit.ViewModels return; } - var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file); + var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, selectedCommit.SHA, _file); var content = new StreamReader(contentStream).ReadToEnd(); var matchLFS = REG_LFS_FORMAT().Match(content); if (matchLFS.Success) @@ -181,8 +218,35 @@ namespace SourceGit.ViewModels private void SetViewContentAsDiff() { - var option = new Models.DiffOption(_selectedCommit, _file); - ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext); + if (_selectedCommits is { Count: 1 }) + { + StartPoint = null; + EndPoint = null; + var option = new Models.DiffOption(_selectedCommits[0], _file); + ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext); + } + else if (_selectedCommits is { Count: 2 }) + { + StartPoint = _selectedCommits[0]; + EndPoint = _selectedCommits[1]; + _changes = new Commands.CompareRevisions(_repo.FullPath, GetSHA(_selectedCommits[0]), GetSHA(_selectedCommits[1]), _file).Result(); + if (_changes.Count == 0) + { + ViewContent = null; + return; + } + var option = new Models.DiffOption(GetSHA(_selectedCommits[0]), GetSHA(_selectedCommits[1]), _changes[0]); + ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext); + } + else + { + ViewContent = _selectedCommits.Count; + } + } + + private string GetSHA(object obj) + { + return obj is Models.Commit commit ? commit.SHA : string.Empty; } [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] @@ -197,8 +261,11 @@ namespace SourceGit.ViewModels private readonly string _file = null; private bool _isLoading = true; private List _commits = null; - private Models.Commit _selectedCommit = null; + private List _selectedCommits = []; private bool _isViewContent = false; private object _viewContent = null; + private Models.Commit _startPoint = null; + private Models.Commit _endPoint = null; + private List _changes = null; } } diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index b0706d24..e3bbac9c 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -13,6 +13,30 @@ Icon="/App.ico" Title="{DynamicResource Text.FileHistory}" MinWidth="1280" MinHeight="720"> + + + + + + + + + + + + + + + + + + + + + + + + @@ -56,8 +80,8 @@ Margin="8,4,4,8" BorderBrush="{DynamicResource Brush.Border2}" ItemsSource="{Binding Commits}" - SelectedItem="{Binding SelectedCommit, Mode=TwoWay}" - SelectionMode="Single" + SelectionMode="Multiple" + SelectionChanged="OnRowSelectionChanged" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto"> @@ -113,7 +137,7 @@ IsVisible="{Binding IsLoading}"/> - + @@ -126,6 +150,29 @@ + + + + + + + + + + + + + + + + + + + @@ -154,6 +201,21 @@ + + + + + + + + @@ -162,7 +224,8 @@ Margin="0,0,0,8" HorizontalAlignment="Center" Content="{DynamicResource Text.ChangeCM.CheckoutThisRevision}" - Click="OnResetToSelectedRevision"/> + Click="OnResetToSelectedRevision" + IsVisible="{Binding SelectedCommits.Count, Converter={x:Static c:IntConverters.IsOne}}"/> diff --git a/src/Views/FileHistories.axaml.cs b/src/Views/FileHistories.axaml.cs index 9d74892b..2318cb6c 100644 --- a/src/Views/FileHistories.axaml.cs +++ b/src/Views/FileHistories.axaml.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using SourceGit.Models; namespace SourceGit.Views { @@ -38,5 +41,46 @@ namespace SourceGit.Views NotifyDonePanel.IsVisible = false; e.Handled = true; } + + private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (DataContext is ViewModels.FileHistories vm && sender is ListBox { SelectedItems: IList commits }) + { + var selectedCommits = new List(); + foreach (var commit in commits) + { + if (commit is Models.Commit modelCommit) + { + selectedCommits.Add(modelCommit); + } + } + vm.SelectedCommits = selectedCommits; + } + + e.Handled = true; + } + + private async void OnSaveAsPatch(object sender, RoutedEventArgs e) + { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + return; + + var vm = DataContext as ViewModels.FileHistories; + if (vm == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + await vm.SaveAsPatch(storageFile.Path.LocalPath); + NotifyDonePanel.IsVisible = true; + + e.Handled = true; + } } }