feature: allow merging multiple heads

This commit is contained in:
Dmitrij D. Czarkoff 2024-12-08 14:27:25 +01:00
parent 0c04cccd52
commit 2518f34b86
No known key found for this signature in database
9 changed files with 213 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

@ -110,6 +110,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>
@ -403,6 +404,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

@ -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();
}
}
}