Skip to content

Commit fe471ac

Browse files
committed
feature: enable multiple-selection for changes collection view in Changes Detail/Stash Changes/Branch Compare/Revision Compare (#1826)
Signed-off-by: leo <longshuang@msn.cn>
1 parent a70182e commit fe471ac

14 files changed

+585
-201
lines changed

src/Commands/Checkout.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,20 @@ public async Task<bool> FileWithRevisionAsync(string file, string revision)
7272
Args = $"checkout --no-overlay {revision} -- {file.Quoted()}";
7373
return await ExecAsync().ConfigureAwait(false);
7474
}
75+
76+
public async Task<bool> MultipleFilesWithRevisionAsync(List<string> files, string revision)
77+
{
78+
var builder = new StringBuilder();
79+
builder
80+
.Append("checkout --no-overlay ")
81+
.Append(revision)
82+
.Append(" --");
83+
84+
foreach (var f in files)
85+
builder.Append(' ').Append(f.Quoted());
86+
87+
Args = builder.ToString();
88+
return await ExecAsync().ConfigureAwait(false);
89+
}
7590
}
7691
}

src/ViewModels/BranchCompare.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ public string GetAbsPath(string path)
128128
return Native.OS.GetAbsPath(_repo, path);
129129
}
130130

131+
public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string saveTo)
132+
{
133+
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based.Head, _to.Head, saveTo);
134+
if (succ)
135+
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
136+
}
137+
131138
private void Refresh()
132139
{
133140
IsLoading = true;

src/ViewModels/CommitDetail.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string sa
239239
public async Task ResetToThisRevisionAsync(string path)
240240
{
241241
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'");
242-
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, $"{_commit.SHA}");
242+
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA);
243243
log.Complete();
244244
}
245245

@@ -254,6 +254,41 @@ public async Task ResetToParentRevisionAsync(Models.Change change)
254254
log.Complete();
255255
}
256256

257+
public async Task ResetMultipleToThisRevisionAsync(List<Models.Change> changes)
258+
{
259+
var files = new List<string>();
260+
foreach (var c in changes)
261+
files.Add(c.Path);
262+
263+
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'");
264+
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA);
265+
log.Complete();
266+
}
267+
268+
public async Task ResetMultipleToParentRevisionAsync(List<Models.Change> changes)
269+
{
270+
var renamed = new List<string>();
271+
var modified = new List<string>();
272+
273+
foreach (var c in changes)
274+
{
275+
if (c.Index == Models.ChangeState.Renamed)
276+
renamed.Add(c.OriginalPath);
277+
else
278+
modified.Add(c.Path);
279+
}
280+
281+
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'");
282+
283+
if (modified.Count > 0)
284+
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1");
285+
286+
if (renamed.Count > 0)
287+
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1");
288+
289+
log.Complete();
290+
}
291+
257292
public async Task<List<Models.Object>> GetRevisionFilesUnderFolderAsync(string parentFolder)
258293
{
259294
return await new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder)

src/ViewModels/RevisionCompare.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,11 @@ public string GetAbsPath(string path)
127127
return Native.OS.GetAbsPath(_repo, path);
128128
}
129129

130-
public void SaveAsPatch(string saveTo)
130+
public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string saveTo)
131131
{
132-
Task.Run(async () =>
133-
{
134-
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo);
135-
if (succ)
136-
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
137-
});
132+
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo);
133+
if (succ)
134+
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
138135
}
139136

140137
public void ClearSearchFilter()

src/ViewModels/StashesPage.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public void Drop(Models.Stash stash)
158158
_repo.ShowPopup(new DropStash(_repo, stash));
159159
}
160160

161-
public async Task SaveStashAsPathAsync(Models.Stash stash, string saveTo)
161+
public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo)
162162
{
163163
var opts = new List<Models.DiffOption>();
164164
var changes = await new Commands.CompareRevisions(_repo.FullPath, $"{stash.SHA}^", stash.SHA)
@@ -211,6 +211,42 @@ public async Task CheckoutSingleFileAsync(Models.Change change)
211211
log.Complete();
212212
}
213213

214+
public async Task CheckoutMultipleFileAsync(List<Models.Change> changes)
215+
{
216+
var untracked = new List<string>();
217+
var added = new List<string>();
218+
var modified = new List<string>();
219+
220+
foreach (var c in changes)
221+
{
222+
if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3)
223+
untracked.Add(c.Path);
224+
else if (c.Index == Models.ChangeState.Added && _selectedStash.Parents.Count > 1)
225+
added.Add(c.Path);
226+
else
227+
modified.Add(c.Path);
228+
}
229+
230+
var log = _repo.CreateLog($"Reset File to '{_selectedStash.Name}'");
231+
232+
if (untracked.Count > 0)
233+
await new Commands.Checkout(_repo.FullPath)
234+
.Use(log)
235+
.MultipleFilesWithRevisionAsync(untracked, _selectedStash.Parents[2]);
236+
237+
if (added.Count > 0)
238+
await new Commands.Checkout(_repo.FullPath)
239+
.Use(log)
240+
.MultipleFilesWithRevisionAsync(added, _selectedStash.Parents[1]);
241+
242+
if (modified.Count > 0)
243+
await new Commands.Checkout(_repo.FullPath)
244+
.Use(log)
245+
.MultipleFilesWithRevisionAsync(modified, _selectedStash.SHA);
246+
247+
log.Complete();
248+
}
249+
214250
private void RefreshVisible()
215251
{
216252
if (string.IsNullOrEmpty(_searchFilter))

src/Views/BranchCompare.axaml.cs

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
22
using System.IO;
3+
using System.Text;
34

45
using Avalonia.Controls;
56
using Avalonia.Input;
7+
using Avalonia.Platform.Storage;
68

79
namespace SourceGit.Views
810
{
@@ -15,61 +17,127 @@ public BranchCompare()
1517

1618
private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e)
1719
{
18-
if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: 1 } selected } vm &&
20+
if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: > 0 } selected } vm &&
1921
sender is ChangeCollectionView view)
2022
{
21-
var repo = vm.RepositoryPath;
22-
var change = selected[0];
2323
var menu = new ContextMenu();
24+
var repo = vm.RepositoryPath;
2425

25-
var openWithMerger = new MenuItem();
26-
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
27-
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
28-
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
29-
openWithMerger.Click += (_, ev) =>
26+
var patch = new MenuItem();
27+
patch.Header = App.Text("FileCM.SaveAsPatch");
28+
patch.Icon = App.CreateMenuIcon("Icons.Diff");
29+
patch.Click += async (_, e) =>
3030
{
31-
new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open();
32-
ev.Handled = true;
31+
var storageProvider = this.StorageProvider;
32+
if (storageProvider == null)
33+
return;
34+
35+
var options = new FilePickerSaveOptions();
36+
options.Title = App.Text("FileCM.SaveAsPatch");
37+
options.DefaultExtension = ".patch";
38+
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
39+
40+
var storageFile = await storageProvider.SaveFilePickerAsync(options);
41+
if (storageFile != null)
42+
{
43+
var saveTo = storageFile.Path.LocalPath;
44+
await vm.SaveChangesAsPatchAsync(selected, saveTo);
45+
}
46+
47+
e.Handled = true;
3348
};
34-
menu.Items.Add(openWithMerger);
3549

36-
if (change.Index != Models.ChangeState.Deleted)
50+
if (selected.Count == 1)
3751
{
38-
var full = Path.GetFullPath(Path.Combine(repo, change.Path));
39-
var explore = new MenuItem();
40-
explore.Header = App.Text("RevealFile");
41-
explore.Icon = App.CreateMenuIcon("Icons.Explore");
42-
explore.IsEnabled = File.Exists(full);
43-
explore.Click += (_, ev) =>
52+
var change = selected[0];
53+
var openWithMerger = new MenuItem();
54+
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
55+
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
56+
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
57+
openWithMerger.Click += (_, ev) =>
4458
{
45-
Native.OS.OpenInFileManager(full, true);
59+
new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open();
4660
ev.Handled = true;
4761
};
48-
menu.Items.Add(explore);
49-
}
62+
menu.Items.Add(openWithMerger);
5063

51-
var copyPath = new MenuItem();
52-
copyPath.Header = App.Text("CopyPath");
53-
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
54-
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
55-
copyPath.Click += async (_, ev) =>
56-
{
57-
await App.CopyTextAsync(change.Path);
58-
ev.Handled = true;
59-
};
60-
menu.Items.Add(new MenuItem() { Header = "-" });
61-
menu.Items.Add(copyPath);
62-
63-
var copyFullPath = new MenuItem();
64-
copyFullPath.Header = App.Text("CopyFullPath");
65-
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
66-
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
67-
copyFullPath.Click += async (_, ev) =>
64+
if (change.Index != Models.ChangeState.Deleted)
65+
{
66+
var full = Path.GetFullPath(Path.Combine(repo, change.Path));
67+
var explore = new MenuItem();
68+
explore.Header = App.Text("RevealFile");
69+
explore.Icon = App.CreateMenuIcon("Icons.Explore");
70+
explore.IsEnabled = File.Exists(full);
71+
explore.Click += (_, ev) =>
72+
{
73+
Native.OS.OpenInFileManager(full, true);
74+
ev.Handled = true;
75+
};
76+
menu.Items.Add(explore);
77+
}
78+
79+
var copyPath = new MenuItem();
80+
copyPath.Header = App.Text("CopyPath");
81+
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
82+
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
83+
copyPath.Click += async (_, ev) =>
84+
{
85+
await App.CopyTextAsync(change.Path);
86+
ev.Handled = true;
87+
};
88+
89+
var copyFullPath = new MenuItem();
90+
copyFullPath.Header = App.Text("CopyFullPath");
91+
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
92+
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
93+
copyFullPath.Click += async (_, ev) =>
94+
{
95+
await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path));
96+
ev.Handled = true;
97+
};
98+
99+
menu.Items.Add(new MenuItem() { Header = "-" });
100+
menu.Items.Add(patch);
101+
menu.Items.Add(new MenuItem() { Header = "-" });
102+
menu.Items.Add(copyPath);
103+
menu.Items.Add(copyFullPath);
104+
}
105+
else
68106
{
69-
await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path));
70-
ev.Handled = true;
71-
};
72-
menu.Items.Add(copyFullPath);
107+
var copyPath = new MenuItem();
108+
copyPath.Header = App.Text("CopyPath");
109+
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
110+
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
111+
copyPath.Click += async (_, ev) =>
112+
{
113+
var builder = new StringBuilder();
114+
foreach (var c in selected)
115+
builder.AppendLine(c.Path);
116+
117+
await App.CopyTextAsync(builder.ToString());
118+
ev.Handled = true;
119+
};
120+
121+
var copyFullPath = new MenuItem();
122+
copyFullPath.Header = App.Text("CopyFullPath");
123+
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
124+
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
125+
copyFullPath.Click += async (_, ev) =>
126+
{
127+
var builder = new StringBuilder();
128+
foreach (var c in selected)
129+
builder.AppendLine(Native.OS.GetAbsPath(repo, c.Path));
130+
131+
await App.CopyTextAsync(builder.ToString());
132+
ev.Handled = true;
133+
};
134+
135+
menu.Items.Add(patch);
136+
menu.Items.Add(new MenuItem() { Header = "-" });
137+
menu.Items.Add(copyPath);
138+
menu.Items.Add(copyFullPath);
139+
}
140+
73141
menu.Open(view);
74142
}
75143

@@ -89,17 +157,24 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e)
89157
if (DataContext is not ViewModels.BranchCompare vm)
90158
return;
91159

92-
if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges })
160+
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges })
93161
return;
94162

95-
var change = selectedChanges[0];
96163
if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C)
97164
{
98-
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
99-
await App.CopyTextAsync(vm.GetAbsPath(change.Path));
165+
var builder = new StringBuilder();
166+
var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
167+
if (selectedChanges.Count == 1)
168+
{
169+
builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path);
170+
}
100171
else
101-
await App.CopyTextAsync(change.Path);
172+
{
173+
foreach (var c in selectedChanges)
174+
builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path);
175+
}
102176

177+
await App.CopyTextAsync(builder.ToString());
103178
e.Handled = true;
104179
}
105180
}

src/Views/ChangeCollectionView.axaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<v:ChangeCollectionContainer Focusable="True"
3434
ItemsSource="{Binding Rows}"
3535
SelectedItems="{Binding SelectedRows, Mode=TwoWay}"
36-
SelectionMode="{Binding #ThisControl.SelectionMode}"
36+
SelectionMode="Multiple"
3737
SelectionChanged="OnRowSelectionChanged">
3838
<ListBox.ItemTemplate>
3939
<DataTemplate DataType="vm:ChangeTreeNode">
@@ -78,7 +78,7 @@
7878
<v:ChangeCollectionContainer Focusable="True"
7979
ItemsSource="{Binding Changes}"
8080
SelectedItems="{Binding SelectedChanges, Mode=TwoWay}"
81-
SelectionMode="{Binding #ThisControl.SelectionMode}"
81+
SelectionMode="Multiple"
8282
SelectionChanged="OnRowSelectionChanged">
8383
<ListBox.ItemTemplate>
8484
<DataTemplate DataType="m:Change">
@@ -110,7 +110,7 @@
110110
<v:ChangeCollectionContainer Focusable="True"
111111
ItemsSource="{Binding Changes}"
112112
SelectedItems="{Binding SelectedChanges, Mode=TwoWay}"
113-
SelectionMode="{Binding #ThisControl.SelectionMode}"
113+
SelectionMode="Multiple"
114114
SelectionChanged="OnRowSelectionChanged">
115115
<ListBox.ItemTemplate>
116116
<DataTemplate DataType="m:Change">

0 commit comments

Comments
 (0)