feature: saving as patch supports multiple commits (#658)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2024-11-06 12:35:55 +08:00
parent c72506d939
commit 9cb85081ab
No known key found for this signature in database
9 changed files with 110 additions and 8 deletions

View file

@ -9,7 +9,7 @@ namespace SourceGit.Commands
{ {
public static class SaveChangesAsPatch public static class SaveChangesAsPatch
{ {
public static bool Exec(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo) public static bool ProcessLocalChanges(string repo, List<Models.Change> changes, bool isUnstaged, string saveTo)
{ {
using (var sw = File.Create(saveTo)) using (var sw = File.Create(saveTo))
{ {
@ -23,6 +23,20 @@ namespace SourceGit.Commands
return true; return true;
} }
public static bool ProcessRevisionCompareChanges(string repo, List<Models.Change> changes, string baseRevision, string targetRevision, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var change in changes)
{
if (!ProcessSingleChange(repo, new Models.DiffOption(baseRevision, targetRevision, change), sw))
return false;
}
}
return true;
}
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{ {
var starter = new ProcessStartInfo(); var starter = new ProcessStartInfo();

View file

@ -238,6 +238,7 @@
<x:String x:Key="Text.Diff.Next" xml:space="preserve">Next Difference</x:String> <x:String x:Key="Text.Diff.Next" xml:space="preserve">Next Difference</x:String>
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">NO CHANGES OR ONLY EOL CHANGES</x:String> <x:String x:Key="Text.Diff.NoChange" xml:space="preserve">NO CHANGES OR ONLY EOL CHANGES</x:String>
<x:String x:Key="Text.Diff.Prev" xml:space="preserve">Previous Difference</x:String> <x:String x:Key="Text.Diff.Prev" xml:space="preserve">Previous Difference</x:String>
<x:String x:Key="Text.Diff.SaveAsPatch" xml:space="preserve">Save as Patch</x:String>
<x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">Show hidden symbols</x:String> <x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">Show hidden symbols</x:String>
<x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">Side-By-Side Diff</x:String> <x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">Side-By-Side Diff</x:String>
<x:String x:Key="Text.Diff.Submodule" xml:space="preserve">SUBMODULE</x:String> <x:String x:Key="Text.Diff.Submodule" xml:space="preserve">SUBMODULE</x:String>

View file

@ -241,6 +241,7 @@
<x:String x:Key="Text.Diff.Next" xml:space="preserve">下一个差异</x:String> <x:String x:Key="Text.Diff.Next" xml:space="preserve">下一个差异</x:String>
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">没有变更或仅有换行符差异</x:String> <x:String x:Key="Text.Diff.NoChange" xml:space="preserve">没有变更或仅有换行符差异</x:String>
<x:String x:Key="Text.Diff.Prev" xml:space="preserve">上一个差异</x:String> <x:String x:Key="Text.Diff.Prev" xml:space="preserve">上一个差异</x:String>
<x:String x:Key="Text.Diff.SaveAsPatch" xml:space="preserve">保存为补丁文件</x:String>
<x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">显示隐藏符号</x:String> <x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">显示隐藏符号</x:String>
<x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">分列对比</x:String> <x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">分列对比</x:String>
<x:String x:Key="Text.Diff.Submodule" xml:space="preserve">子模块</x:String> <x:String x:Key="Text.Diff.Submodule" xml:space="preserve">子模块</x:String>

View file

@ -241,6 +241,7 @@
<x:String x:Key="Text.Diff.Next" xml:space="preserve">下一個差異</x:String> <x:String x:Key="Text.Diff.Next" xml:space="preserve">下一個差異</x:String>
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">沒有變更或僅有換行字元差異</x:String> <x:String x:Key="Text.Diff.NoChange" xml:space="preserve">沒有變更或僅有換行字元差異</x:String>
<x:String x:Key="Text.Diff.Prev" xml:space="preserve">上一個差異</x:String> <x:String x:Key="Text.Diff.Prev" xml:space="preserve">上一個差異</x:String>
<x:String x:Key="Text.Diff.SaveAsPatch" xml:space="preserve">另存為修補檔</x:String>
<x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">顯示隱藏符號</x:String> <x:String x:Key="Text.Diff.ShowHiddenSymbols" xml:space="preserve">顯示隱藏符號</x:String>
<x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">並排對比</x:String> <x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">並排對比</x:String>
<x:String x:Key="Text.Diff.Submodule" xml:space="preserve">子模組</x:String> <x:String x:Key="Text.Diff.Submodule" xml:space="preserve">子模組</x:String>

View file

@ -2,6 +2,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@ -258,6 +259,44 @@ namespace SourceGit.ViewModels
multipleMenu.Items.Add(new MenuItem() { Header = "-" }); multipleMenu.Items.Add(new MenuItem() { Header = "-" });
} }
var saveToPatchMultiple = new MenuItem();
saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff");
saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch");
saveToPatchMultiple.Click += async (_, e) =>
{
var storageProvider = App.GetStorageProvider();
if (storageProvider == null)
return;
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
try
{
var picker = await storageProvider.OpenFolderPickerAsync(options);
if (picker.Count == 1)
{
var saveTo = $"{picker[0].Path.LocalPath}/patches";
var succ = false;
foreach (var c in selected)
{
succ = await Task.Run(() => new Commands.FormatPatch(_repo.FullPath, c.SHA, saveTo).Exec());
if (!succ)
break;
}
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
}
catch (Exception exception)
{
App.RaiseException(_repo.FullPath, $"Failed to save as patch: {exception.Message}");
}
e.Handled = true;
};
multipleMenu.Items.Add(saveToPatchMultiple);
multipleMenu.Items.Add(new MenuItem() { Header = "-" });
var copyMultipleSHAs = new MenuItem(); var copyMultipleSHAs = new MenuItem();
copyMultipleSHAs.Header = App.Text("CommitCM.CopySHA"); copyMultipleSHAs.Header = App.Text("CommitCM.CopySHA");
copyMultipleSHAs.Icon = App.CreateMenuIcon("Icons.Copy"); copyMultipleSHAs.Icon = App.CreateMenuIcon("Icons.Copy");

View file

@ -24,6 +24,11 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _endPoint, value); private set => SetProperty(ref _endPoint, value);
} }
public bool CanSaveAsPatch
{
get => _canSaveAsPatch;
}
public List<Models.Change> VisibleChanges public List<Models.Change> VisibleChanges
{ {
get => _visibleChanges; get => _visibleChanges;
@ -73,6 +78,7 @@ namespace SourceGit.ViewModels
_repo = repo; _repo = repo;
_startPoint = (object)startPoint ?? new Models.Null(); _startPoint = (object)startPoint ?? new Models.Null();
_endPoint = (object)endPoint ?? new Models.Null(); _endPoint = (object)endPoint ?? new Models.Null();
_canSaveAsPatch = startPoint != null && endPoint != null;
Task.Run(Refresh); Task.Run(Refresh);
} }
@ -105,6 +111,16 @@ namespace SourceGit.ViewModels
Task.Run(Refresh); Task.Run(Refresh);
} }
public void SaveAsPatch(string saveTo)
{
Task.Run(() =>
{
var succ = Commands.SaveChangesAsPatch.ProcessRevisionCompareChanges(_repo, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo);
if (succ)
Dispatcher.UIThread.Invoke(() => App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")));
});
}
public void ClearSearchFilter() public void ClearSearchFilter()
{ {
SearchFilter = string.Empty; SearchFilter = string.Empty;
@ -218,6 +234,7 @@ namespace SourceGit.ViewModels
private string _repo; private string _repo;
private object _startPoint = null; private object _startPoint = null;
private object _endPoint = null; private object _endPoint = null;
private bool _canSaveAsPatch = false;
private List<Models.Change> _changes = null; private List<Models.Change> _changes = null;
private List<Models.Change> _visibleChanges = null; private List<Models.Change> _visibleChanges = null;
private List<Models.Change> _selectedChanges = null; private List<Models.Change> _selectedChanges = null;

View file

@ -539,7 +539,7 @@ namespace SourceGit.ViewModels
var storageFile = await storageProvider.SaveFilePickerAsync(options); var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null) if (storageFile != null)
{ {
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ) if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
} }
@ -858,7 +858,7 @@ namespace SourceGit.ViewModels
var storageFile = await storageProvider.SaveFilePickerAsync(options); var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null) if (storageFile != null)
{ {
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ) if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
} }
@ -981,7 +981,7 @@ namespace SourceGit.ViewModels
var storageFile = await storageProvider.SaveFilePickerAsync(options); var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null) if (storageFile != null)
{ {
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ) if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
} }
@ -1156,7 +1156,7 @@ namespace SourceGit.ViewModels
var storageFile = await storageProvider.SaveFilePickerAsync(options); var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null) if (storageFile != null)
{ {
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ) if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
} }

View file

@ -35,21 +35,26 @@
<Grid RowDefinitions="50,*" Margin="4"> <Grid RowDefinitions="50,*" Margin="4">
<!-- Compare Revision Info --> <!-- Compare Revision Info -->
<Grid Grid.Row="0" Margin="48,0,48,4" ColumnDefinitions="*,48,*"> <Grid Grid.Row="0" Margin="0,0,0,6" ColumnDefinitions="*,32,*,Auto">
<!-- Base Revision --> <!-- Base Revision -->
<Border Grid.Column="0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}" CornerRadius="4" Padding="4"> <Border Grid.Column="0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}" CornerRadius="4" Padding="4">
<ContentControl Content="{Binding StartPoint}"/> <ContentControl Content="{Binding StartPoint}"/>
</Border> </Border>
<!-- Swap Button --> <!-- Swap Buttons -->
<Button Grid.Column="1" Classes="icon_button" Command="{Binding Swap}" HorizontalAlignment="Center" ToolTip.Tip="{DynamicResource Text.Diff.SwapCommits}"> <Button Grid.Column="1" Classes="icon_button" Command="{Binding Swap}" HorizontalAlignment="Center" ToolTip.Tip="{DynamicResource Text.Diff.SwapCommits}">
<Path Width="16" Height="16" Fill="{DynamicResource Brush.FG2}" Data="{DynamicResource Icons.Compare}"/> <Path Width="16" Height="16" Data="{DynamicResource Icons.Compare}"/>
</Button> </Button>
<!-- Right Revision --> <!-- Right Revision -->
<Border Grid.Column="2" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}" CornerRadius="4" Padding="4"> <Border Grid.Column="2" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}" CornerRadius="4" Padding="4">
<ContentControl Content="{Binding EndPoint}"/> <ContentControl Content="{Binding EndPoint}"/>
</Border> </Border>
<!-- Save As Patch Button -->
<Button Grid.Column="3" Classes="icon_button" Width="32" Click="OnSaveAsPatch" IsVisible="{Binding CanSaveAsPatch}" ToolTip.Tip="{DynamicResource Text.Diff.SaveAsPatch}">
<Path Width="16" Height="16" Data="{DynamicResource Icons.Diff}"/>
</Button>
</Grid> </Grid>
<!-- Changes View --> <!-- Changes View -->

View file

@ -1,5 +1,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views namespace SourceGit.Views
{ {
@ -28,5 +30,27 @@ namespace SourceGit.Views
e.Handled = true; 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.RevisionCompare;
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)
vm.SaveAsPatch(storageFile.Path.LocalPath);
e.Handled = true;
}
} }
} }