feature: merge multiple heads (#793)

* feature: allow merging multiple heads
* feature: allow merging multiple branches from branch tree
This commit is contained in:
Dmitrij D. Czarkoff 2024-12-09 13:04:25 +00:00 committed by GitHub
parent c9c7fb5d5b
commit dce33fdf60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 232 additions and 10 deletions

View file

@ -4,13 +4,15 @@ namespace SourceGit.Commands
{ {
public class Merge : Command public class Merge : Command
{ {
public Merge(string repo, string source, string mode, Action<string> outputHandler) public Merge(string repo, string source, string mode, string strategy, Action<string> outputHandler)
{ {
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
TraitErrorAsOutput = true; TraitErrorAsOutput = true;
Args = $"merge --progress {source} {mode}"; if (strategy != null)
strategy = string.Concat("--strategy=", strategy);
Args = $"merge --progress {strategy} {source} {mode}";
} }
protected override void OnReadline(string line) protected override void OnReadline(string line)

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class MergeStrategy
{
public string Name { get; internal set; }
public string Desc { get; internal set; }
public string Arg { get; internal set; }
public static List<MergeStrategy> ForMultiple { get; private set; } = [
new MergeStrategy(string.Empty, "Let Git automatically select a strategy", null),
new MergeStrategy("Octopus", "Attempt merging multiple heads", "octopus"),
new MergeStrategy("Ours", "Record the merge without modifying the tree", "ours"),
];
public MergeStrategy(string n, string d, string a)
{
Name = n;
Desc = d;
Arg = a;
}
}
}

View file

@ -58,6 +58,7 @@
<x:String x:Key="Text.BranchCM.FetchInto" xml:space="preserve">Fetch ${0}$ into ${1}$...</x:String> <x:String x:Key="Text.BranchCM.FetchInto" xml:space="preserve">Fetch ${0}$ into ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">Git Flow - Finish ${0}$</x:String> <x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">Git Flow - Finish ${0}$</x:String>
<x:String x:Key="Text.BranchCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String> <x:String x:Key="Text.BranchCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.MergeMultiBranches" xml:space="preserve">Merge selected {0} branches</x:String>
<x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">Pull ${0}$</x:String> <x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">Pull ${0}$</x:String>
<x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">Pull ${0}$ into ${1}$...</x:String> <x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">Pull ${0}$ into ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.Push" xml:space="preserve">Push ${0}$</x:String> <x:String x:Key="Text.BranchCM.Push" xml:space="preserve">Push ${0}$</x:String>
@ -110,6 +111,7 @@
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String> <x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">Copy SHA</x:String>
<x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Custom Action</x:String> <x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Custom Action</x:String>
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Interactive Rebase ${0}$ to Here</x:String> <x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Interactive Rebase ${0}$ to Here</x:String>
<x:String x:Key="Text.CommitCM.MergeMultiple" xml:space="preserve">Merge ...</x:String>
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">Rebase ${0}$ to Here</x:String> <x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">Rebase ${0}$ to Here</x:String>
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Reset ${0}$ to Here</x:String> <x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Reset ${0}$ to Here</x:String>
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Revert Commit</x:String> <x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Revert Commit</x:String>
@ -404,6 +406,10 @@
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String> <x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String> <x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">Source Branch:</x:String> <x:String x:Key="Text.Merge.Source" xml:space="preserve">Source Branch:</x:String>
<x:String x:Key="Text.MergeMultiple" xml:space="preserve">Merge commits</x:String>
<x:String x:Key="Text.MergeMultiple.Commit" xml:space="preserve">Commit(s):</x:String>
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">Commit all changes</x:String>
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">Strategy:</x:String>
<x:String x:Key="Text.MoveRepositoryNode" xml:space="preserve">Move Repository Node</x:String> <x:String x:Key="Text.MoveRepositoryNode" xml:space="preserve">Move Repository Node</x:String>
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">Select parent node for:</x:String> <x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">Select parent node for:</x:String>
<x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String> <x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String>

View file

@ -228,22 +228,28 @@ namespace SourceGit.ViewModels
{ {
var selected = new List<Models.Commit>(); var selected = new List<Models.Commit>();
var canCherryPick = true; var canCherryPick = true;
var canMerge = true;
foreach (var item in list.SelectedItems) foreach (var item in list.SelectedItems)
{ {
if (item is Models.Commit c) if (item is Models.Commit c)
{ {
selected.Add(c); selected.Add(c);
if (c.IsMerged || c.Parents.Count > 1) if (c.IsMerged)
{
canMerge = false;
canCherryPick = false; canCherryPick = false;
}
else if (c.Parents.Count > 1)
{
canCherryPick = false;
}
} }
} }
// Sort selected commits in order. // Sort selected commits in order.
selected.Sort((l, r) => selected.Sort((l, r) => _commits.IndexOf(r) - _commits.IndexOf(l));
{
return _commits.IndexOf(r) - _commits.IndexOf(l);
});
var multipleMenu = new ContextMenu(); var multipleMenu = new ContextMenu();
@ -259,9 +265,25 @@ namespace SourceGit.ViewModels
e.Handled = true; e.Handled = true;
}; };
multipleMenu.Items.Add(cherryPickMultiple); multipleMenu.Items.Add(cherryPickMultiple);
multipleMenu.Items.Add(new MenuItem() { Header = "-" });
} }
if (canMerge)
{
var mergeMultiple = new MenuItem();
mergeMultiple.Header = App.Text("CommitCM.MergeMultiple");
mergeMultiple.Icon = App.CreateMenuIcon("Icons.Merge");
mergeMultiple.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new MergeMultiple(_repo, selected));
e.Handled = true;
};
multipleMenu.Items.Add(mergeMultiple);
}
if (canCherryPick || canMerge)
multipleMenu.Items.Add(new MenuItem() { Header = "-" });
var saveToPatchMultiple = new MenuItem(); var saveToPatchMultiple = new MenuItem();
saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff"); saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff");
saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch"); saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch");

View file

@ -37,7 +37,7 @@ namespace SourceGit.ViewModels
return Task.Run(() => return Task.Run(() =>
{ {
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, SetProgressDescription).Exec(); var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, null, SetProgressDescription).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ; return succ;
}); });

View file

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SourceGit.Models;
namespace SourceGit.ViewModels
{
public class MergeMultiple : Popup
{
public List<string> Strategies = ["octopus", "ours"];
public List<Commit> Targets
{
get;
private set;
}
public bool AutoCommit
{
get;
set;
}
public MergeStrategy Strategy
{
get;
set;
}
public MergeMultiple(Repository repo, List<Commit> targets)
{
_repo = repo;
Targets = targets;
AutoCommit = true;
Strategy = MergeStrategy.ForMultiple.Find(s => s.Arg == null);
View = new Views.MergeMultiple() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Merge head(s) ...";
return Task.Run(() =>
{
var succ = new Commands.Merge(
_repo.FullPath,
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)),
AutoCommit ? string.Empty : "--no-commit",
Strategy?.Arg,
SetProgressDescription).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private readonly Repository _repo = null;
}
}

View file

@ -172,7 +172,7 @@ namespace SourceGit.ViewModels
else else
{ {
SetProgressDescription($"Merge {_selectedBranch.FriendlyName} into {_current.Name} ..."); SetProgressDescription($"Merge {_selectedBranch.FriendlyName} into {_current.Name} ...");
rs = new Commands.Merge(_repo.FullPath, _selectedBranch.FriendlyName, "", SetProgressDescription).Exec(); rs = new Commands.Merge(_repo.FullPath, _selectedBranch.FriendlyName, "", null, SetProgressDescription).Exec();
} }
} }
else else

View file

@ -13,6 +13,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using SourceGit.Models;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
@ -950,6 +951,12 @@ namespace SourceGit.ViewModels
PopupHost.ShowPopup(new DeleteMultipleBranches(this, branches, isLocal)); PopupHost.ShowPopup(new DeleteMultipleBranches(this, branches, isLocal));
} }
public void MergeMultipleBranches(List<Models.Branch> branches)
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new MergeMultiple(this, branches.ConvertAll(b => _histories?.Commits?.Find(c => c.SHA == b.Head))));
}
public void CreateNewTag() public void CreateNewTag()
{ {
if (_currentBranch == null) if (_currentBranch == null)

View file

@ -405,6 +405,17 @@ namespace SourceGit.Views
ev.Handled = true; ev.Handled = true;
}; };
menu.Items.Add(deleteMulti); menu.Items.Add(deleteMulti);
var mergeMulti = new MenuItem();
mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count);
mergeMulti.Icon = App.CreateMenuIcon("Icons.Merge");
mergeMulti.Click += (_, ev) =>
{
repo.MergeMultipleBranches(branches);
ev.Handled = true;
};
menu.Items.Add(mergeMulti);
menu?.Open(this); menu?.Open(this);
} }
} }

View file

@ -0,0 +1,79 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.MergeMultiple"
x:DataType="vm:MergeMultiple">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.MergeMultiple}"/>
<Grid Margin="0,16,0,0" RowDefinitions="Auto,32,32" ColumnDefinitions="100,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.MergeMultiple.Commit}"/>
<ListBox Grid.Row="0" Grid.Column="1"
MinHeight="32" MaxHeight="100"
ItemsSource="{Binding Targets}"
Background="{DynamicResource Brush.Contents}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
Padding="4"
CornerRadius="4"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="4,0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Commit">
<Grid ColumnDefinitions="14,Auto,*">
<Path Grid.Column="0" Width="14" Height="14" Margin="0,8,0,0" Data="{StaticResource Icons.Commit}"/>
<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"/>
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding Subject}" TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<CheckBox Grid.Row="1" Grid.Column="1"
Content="{DynamicResource Text.MergeMultiple.CommitChanges}"
IsChecked="{Binding AutoCommit, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.MergeMultiple.Strategy}"/>
<ComboBox Grid.Row="2" Grid.Column="1"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={x:Static m:MergeStrategy.ForMultiple}}"
SelectedItem="{Binding Strategy, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="m:MergeStrategy">
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Desc}" Margin="8,0,0,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace SourceGit.Views
{
public partial class MergeMultiple : UserControl
{
public MergeMultiple()
{
InitializeComponent();
}
}
}