Skip to content

Commit dce33fd

Browse files
authored
feature: merge multiple heads (#793)
* feature: allow merging multiple heads * feature: allow merging multiple branches from branch tree
1 parent c9c7fb5 commit dce33fd

File tree

11 files changed

+232
-10
lines changed

11 files changed

+232
-10
lines changed

src/Commands/Merge.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ namespace SourceGit.Commands
44
{
55
public class Merge : Command
66
{
7-
public Merge(string repo, string source, string mode, Action<string> outputHandler)
7+
public Merge(string repo, string source, string mode, string strategy, Action<string> outputHandler)
88
{
99
_outputHandler = outputHandler;
1010
WorkingDirectory = repo;
1111
Context = repo;
1212
TraitErrorAsOutput = true;
13-
Args = $"merge --progress {source} {mode}";
13+
if (strategy != null)
14+
strategy = string.Concat("--strategy=", strategy);
15+
Args = $"merge --progress {strategy} {source} {mode}";
1416
}
1517

1618
protected override void OnReadline(string line)

src/Models/MergeStrategy.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Collections.Generic;
2+
3+
namespace SourceGit.Models
4+
{
5+
public class MergeStrategy
6+
{
7+
public string Name { get; internal set; }
8+
public string Desc { get; internal set; }
9+
public string Arg { get; internal set; }
10+
11+
public static List<MergeStrategy> ForMultiple { get; private set; } = [
12+
new MergeStrategy(string.Empty, "Let Git automatically select a strategy", null),
13+
new MergeStrategy("Octopus", "Attempt merging multiple heads", "octopus"),
14+
new MergeStrategy("Ours", "Record the merge without modifying the tree", "ours"),
15+
];
16+
17+
public MergeStrategy(string n, string d, string a)
18+
{
19+
Name = n;
20+
Desc = d;
21+
Arg = a;
22+
}
23+
}
24+
}

src/Resources/Locales/en_US.axaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<x:String x:Key="Text.BranchCM.FetchInto" xml:space="preserve">Fetch ${0}$ into ${1}$...</x:String>
5959
<x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">Git Flow - Finish ${0}$</x:String>
6060
<x:String x:Key="Text.BranchCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String>
61+
<x:String x:Key="Text.BranchCM.MergeMultiBranches" xml:space="preserve">Merge selected {0} branches</x:String>
6162
<x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">Pull ${0}$</x:String>
6263
<x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">Pull ${0}$ into ${1}$...</x:String>
6364
<x:String x:Key="Text.BranchCM.Push" xml:space="preserve">Push ${0}$</x:String>
@@ -110,6 +111,7 @@
110111
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String>
111112
<x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Custom Action</x:String>
112113
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Interactive Rebase ${0}$ to Here</x:String>
114+
<x:String x:Key="Text.CommitCM.MergeMultiple" xml:space="preserve">Merge ...</x:String>
113115
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">Rebase ${0}$ to Here</x:String>
114116
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Reset ${0}$ to Here</x:String>
115117
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Revert Commit</x:String>
@@ -404,6 +406,10 @@
404406
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
405407
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
406408
<x:String x:Key="Text.Merge.Source" xml:space="preserve">Source Branch:</x:String>
409+
<x:String x:Key="Text.MergeMultiple" xml:space="preserve">Merge commits</x:String>
410+
<x:String x:Key="Text.MergeMultiple.Commit" xml:space="preserve">Commit(s):</x:String>
411+
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">Commit all changes</x:String>
412+
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">Strategy:</x:String>
407413
<x:String x:Key="Text.MoveRepositoryNode" xml:space="preserve">Move Repository Node</x:String>
408414
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">Select parent node for:</x:String>
409415
<x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String>

src/ViewModels/Histories.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,22 +228,28 @@ public ContextMenu MakeContextMenu(ListBox list)
228228
{
229229
var selected = new List<Models.Commit>();
230230
var canCherryPick = true;
231+
var canMerge = true;
232+
231233
foreach (var item in list.SelectedItems)
232234
{
233235
if (item is Models.Commit c)
234236
{
235237
selected.Add(c);
236238

237-
if (c.IsMerged || c.Parents.Count > 1)
239+
if (c.IsMerged)
240+
{
241+
canMerge = false;
242+
canCherryPick = false;
243+
}
244+
else if (c.Parents.Count > 1)
245+
{
238246
canCherryPick = false;
247+
}
239248
}
240249
}
241250

242251
// Sort selected commits in order.
243-
selected.Sort((l, r) =>
244-
{
245-
return _commits.IndexOf(r) - _commits.IndexOf(l);
246-
});
252+
selected.Sort((l, r) => _commits.IndexOf(r) - _commits.IndexOf(l));
247253

248254
var multipleMenu = new ContextMenu();
249255

@@ -259,9 +265,25 @@ public ContextMenu MakeContextMenu(ListBox list)
259265
e.Handled = true;
260266
};
261267
multipleMenu.Items.Add(cherryPickMultiple);
262-
multipleMenu.Items.Add(new MenuItem() { Header = "-" });
263268
}
264269

270+
if (canMerge)
271+
{
272+
var mergeMultiple = new MenuItem();
273+
mergeMultiple.Header = App.Text("CommitCM.MergeMultiple");
274+
mergeMultiple.Icon = App.CreateMenuIcon("Icons.Merge");
275+
mergeMultiple.Click += (_, e) =>
276+
{
277+
if (PopupHost.CanCreatePopup())
278+
PopupHost.ShowPopup(new MergeMultiple(_repo, selected));
279+
e.Handled = true;
280+
};
281+
multipleMenu.Items.Add(mergeMultiple);
282+
}
283+
284+
if (canCherryPick || canMerge)
285+
multipleMenu.Items.Add(new MenuItem() { Header = "-" });
286+
265287
var saveToPatchMultiple = new MenuItem();
266288
saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff");
267289
saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch");

src/ViewModels/Merge.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public override Task<bool> Sure()
3737

3838
return Task.Run(() =>
3939
{
40-
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, SetProgressDescription).Exec();
40+
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, null, SetProgressDescription).Exec();
4141
CallUIThread(() => _repo.SetWatcherEnabled(true));
4242
return succ;
4343
});

src/ViewModels/MergeMultiple.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using SourceGit.Models;
4+
5+
namespace SourceGit.ViewModels
6+
{
7+
public class MergeMultiple : Popup
8+
{
9+
public List<string> Strategies = ["octopus", "ours"];
10+
11+
public List<Commit> Targets
12+
{
13+
get;
14+
private set;
15+
}
16+
17+
public bool AutoCommit
18+
{
19+
get;
20+
set;
21+
}
22+
23+
public MergeStrategy Strategy
24+
{
25+
get;
26+
set;
27+
}
28+
29+
public MergeMultiple(Repository repo, List<Commit> targets)
30+
{
31+
_repo = repo;
32+
Targets = targets;
33+
AutoCommit = true;
34+
Strategy = MergeStrategy.ForMultiple.Find(s => s.Arg == null);
35+
View = new Views.MergeMultiple() { DataContext = this };
36+
}
37+
38+
public override Task<bool> Sure()
39+
{
40+
_repo.SetWatcherEnabled(false);
41+
ProgressDescription = "Merge head(s) ...";
42+
43+
return Task.Run(() =>
44+
{
45+
var succ = new Commands.Merge(
46+
_repo.FullPath,
47+
string.Join(" ", Targets.ConvertAll(c => c.Decorators.Find(d => d.Type == DecoratorType.RemoteBranchHead || d.Type == DecoratorType.LocalBranchHead)?.Name ?? c.Decorators.Find(d => d.Type == DecoratorType.Tag)?.Name ?? c.SHA)),
48+
AutoCommit ? string.Empty : "--no-commit",
49+
Strategy?.Arg,
50+
SetProgressDescription).Exec();
51+
52+
CallUIThread(() => _repo.SetWatcherEnabled(true));
53+
return succ;
54+
});
55+
}
56+
57+
private readonly Repository _repo = null;
58+
}
59+
}

src/ViewModels/Pull.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public override Task<bool> Sure()
172172
else
173173
{
174174
SetProgressDescription($"Merge {_selectedBranch.FriendlyName} into {_current.Name} ...");
175-
rs = new Commands.Merge(_repo.FullPath, _selectedBranch.FriendlyName, "", SetProgressDescription).Exec();
175+
rs = new Commands.Merge(_repo.FullPath, _selectedBranch.FriendlyName, "", null, SetProgressDescription).Exec();
176176
}
177177
}
178178
else

src/ViewModels/Repository.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Avalonia.Threading;
1414

1515
using CommunityToolkit.Mvvm.ComponentModel;
16+
using SourceGit.Models;
1617

1718
namespace SourceGit.ViewModels
1819
{
@@ -950,6 +951,12 @@ public void DeleteMultipleBranches(List<Models.Branch> branches, bool isLocal)
950951
PopupHost.ShowPopup(new DeleteMultipleBranches(this, branches, isLocal));
951952
}
952953

954+
public void MergeMultipleBranches(List<Models.Branch> branches)
955+
{
956+
if (PopupHost.CanCreatePopup())
957+
PopupHost.ShowPopup(new MergeMultiple(this, branches.ConvertAll(b => _histories?.Commits?.Find(c => c.SHA == b.Head))));
958+
}
959+
953960
public void CreateNewTag()
954961
{
955962
if (_currentBranch == null)

src/Views/BranchTree.axaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,17 @@ private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2)
405405
ev.Handled = true;
406406
};
407407
menu.Items.Add(deleteMulti);
408+
409+
var mergeMulti = new MenuItem();
410+
mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count);
411+
mergeMulti.Icon = App.CreateMenuIcon("Icons.Merge");
412+
mergeMulti.Click += (_, ev) =>
413+
{
414+
repo.MergeMultipleBranches(branches);
415+
ev.Handled = true;
416+
};
417+
menu.Items.Add(mergeMulti);
418+
408419
menu?.Open(this);
409420
}
410421
}

src/Views/MergeMultiple.axaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:m="using:SourceGit.Models"
6+
xmlns:vm="using:SourceGit.ViewModels"
7+
xmlns:c="using:SourceGit.Converters"
8+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
9+
x:Class="SourceGit.Views.MergeMultiple"
10+
x:DataType="vm:MergeMultiple">
11+
<StackPanel Orientation="Vertical" Margin="8,0">
12+
<TextBlock FontSize="18"
13+
Classes="bold"
14+
Text="{DynamicResource Text.MergeMultiple}"/>
15+
<Grid Margin="0,16,0,0" RowDefinitions="Auto,32,32" ColumnDefinitions="100,*">
16+
<TextBlock Grid.Row="0" Grid.Column="0"
17+
HorizontalAlignment="Right" VerticalAlignment="Center"
18+
Margin="0,0,8,0"
19+
Text="{DynamicResource Text.MergeMultiple.Commit}"/>
20+
<ListBox Grid.Row="0" Grid.Column="1"
21+
MinHeight="32" MaxHeight="100"
22+
ItemsSource="{Binding Targets}"
23+
Background="{DynamicResource Brush.Contents}"
24+
BorderThickness="1"
25+
BorderBrush="{DynamicResource Brush.Border2}"
26+
Padding="4"
27+
CornerRadius="4"
28+
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
29+
ScrollViewer.VerticalScrollBarVisibility="Auto">
30+
<ListBox.Styles>
31+
<Style Selector="ListBoxItem">
32+
<Setter Property="Padding" Value="4,0"/>
33+
<Setter Property="Height" Value="26"/>
34+
<Setter Property="CornerRadius" Value="4"/>
35+
</Style>
36+
</ListBox.Styles>
37+
38+
<ListBox.ItemsPanel>
39+
<ItemsPanelTemplate>
40+
<StackPanel Orientation="Vertical"/>
41+
</ItemsPanelTemplate>
42+
</ListBox.ItemsPanel>
43+
44+
<ListBox.ItemTemplate>
45+
<DataTemplate DataType="m:Commit">
46+
<Grid ColumnDefinitions="14,Auto,*">
47+
<Path Grid.Column="0" Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
48+
<TextBlock Grid.Column="1" FontFamily="{DynamicResource Fonts.Monospace}" VerticalAlignment="Center" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="6,0,4,0"/>
49+
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding Subject}" TextTrimming="CharacterEllipsis"/>
50+
</Grid>
51+
</DataTemplate>
52+
</ListBox.ItemTemplate>
53+
</ListBox>
54+
55+
<CheckBox Grid.Row="1" Grid.Column="1"
56+
Content="{DynamicResource Text.MergeMultiple.CommitChanges}"
57+
IsChecked="{Binding AutoCommit, Mode=TwoWay}"/>
58+
59+
<TextBlock Grid.Row="2" Grid.Column="0"
60+
HorizontalAlignment="Right" VerticalAlignment="Center"
61+
Margin="0,0,8,0"
62+
Text="{DynamicResource Text.MergeMultiple.Strategy}"/>
63+
<ComboBox Grid.Row="2" Grid.Column="1"
64+
Height="28" Padding="8,0"
65+
VerticalAlignment="Center" HorizontalAlignment="Stretch"
66+
ItemsSource="{Binding Source={x:Static m:MergeStrategy.ForMultiple}}"
67+
SelectedItem="{Binding Strategy, Mode=TwoWay}">
68+
<ComboBox.ItemTemplate>
69+
<DataTemplate DataType="m:MergeStrategy">
70+
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
71+
<TextBlock Text="{Binding Name}"/>
72+
<TextBlock Text="{Binding Desc}" Margin="8,0,0,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
73+
</StackPanel>
74+
</DataTemplate>
75+
</ComboBox.ItemTemplate>
76+
</ComboBox>
77+
</Grid>
78+
</StackPanel>
79+
</UserControl>

0 commit comments

Comments
 (0)