mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-25 21:07:20 -08:00
feature: merge multiple heads (#793)
* feature: allow merging multiple heads * feature: allow merging multiple branches from branch tree
This commit is contained in:
parent
c9c7fb5d5b
commit
dce33fdf60
11 changed files with 232 additions and 10 deletions
|
@ -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)
|
||||||
|
|
24
src/Models/MergeStrategy.cs
Normal file
24
src/Models/MergeStrategy.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
59
src/ViewModels/MergeMultiple.cs
Normal file
59
src/ViewModels/MergeMultiple.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
79
src/Views/MergeMultiple.axaml
Normal file
79
src/Views/MergeMultiple.axaml
Normal 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>
|
12
src/Views/MergeMultiple.axaml.cs
Normal file
12
src/Views/MergeMultiple.axaml.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public partial class MergeMultiple : UserControl
|
||||||
|
{
|
||||||
|
public MergeMultiple()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue