diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index cf2e285f..a524af05 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -4,13 +4,15 @@ namespace SourceGit.Commands { public class Merge : Command { - public Merge(string repo, string source, string mode, Action outputHandler) + public Merge(string repo, string source, string mode, string strategy, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; 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) diff --git a/src/Models/MergeStrategy.cs b/src/Models/MergeStrategy.cs new file mode 100644 index 00000000..a79bcf59 --- /dev/null +++ b/src/Models/MergeStrategy.cs @@ -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 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; + } + } +} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index a5767355..c50f3c4f 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -58,6 +58,7 @@ Fetch ${0}$ into ${1}$... Git Flow - Finish ${0}$ Merge ${0}$ into ${1}$... + Merge selected {0} branches Pull ${0}$ Pull ${0}$ into ${1}$... Push ${0}$ @@ -110,6 +111,7 @@ Copy SHA Custom Action Interactive Rebase ${0}$ to Here + Merge ... Rebase ${0}$ to Here Reset ${0}$ to Here Revert Commit @@ -404,6 +406,10 @@ Into: Merge Option: Source Branch: + Merge commits + Commit(s): + Commit all changes + Strategy: Move Repository Node Select parent node for: Name: diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 18ca2e51..b38b4afc 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -228,22 +228,28 @@ namespace SourceGit.ViewModels { var selected = new List(); var canCherryPick = true; + var canMerge = true; + foreach (var item in list.SelectedItems) { if (item is Models.Commit c) { selected.Add(c); - if (c.IsMerged || c.Parents.Count > 1) + if (c.IsMerged) + { + canMerge = false; canCherryPick = false; + } + else if (c.Parents.Count > 1) + { + canCherryPick = false; + } } } // Sort selected commits in order. - selected.Sort((l, r) => - { - return _commits.IndexOf(r) - _commits.IndexOf(l); - }); + selected.Sort((l, r) => _commits.IndexOf(r) - _commits.IndexOf(l)); var multipleMenu = new ContextMenu(); @@ -259,9 +265,25 @@ namespace SourceGit.ViewModels e.Handled = true; }; 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(); saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff"); saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch"); diff --git a/src/ViewModels/Merge.cs b/src/ViewModels/Merge.cs index b7630101..116797a8 100644 --- a/src/ViewModels/Merge.cs +++ b/src/ViewModels/Merge.cs @@ -37,7 +37,7 @@ namespace SourceGit.ViewModels 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)); return succ; }); diff --git a/src/ViewModels/MergeMultiple.cs b/src/ViewModels/MergeMultiple.cs new file mode 100644 index 00000000..03d24773 --- /dev/null +++ b/src/ViewModels/MergeMultiple.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using SourceGit.Models; + +namespace SourceGit.ViewModels +{ + public class MergeMultiple : Popup + { + public List Strategies = ["octopus", "ours"]; + + public List Targets + { + get; + private set; + } + + public bool AutoCommit + { + get; + set; + } + + public MergeStrategy Strategy + { + get; + set; + } + + public MergeMultiple(Repository repo, List targets) + { + _repo = repo; + Targets = targets; + AutoCommit = true; + Strategy = MergeStrategy.ForMultiple.Find(s => s.Arg == null); + View = new Views.MergeMultiple() { DataContext = this }; + } + + public override Task 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; + } +} diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index e7c62980..2d0e8da2 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -172,7 +172,7 @@ namespace SourceGit.ViewModels else { 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 diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 38855fef..d593171e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -13,6 +13,7 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using SourceGit.Models; namespace SourceGit.ViewModels { @@ -950,6 +951,12 @@ namespace SourceGit.ViewModels PopupHost.ShowPopup(new DeleteMultipleBranches(this, branches, isLocal)); } + public void MergeMultipleBranches(List branches) + { + if (PopupHost.CanCreatePopup()) + PopupHost.ShowPopup(new MergeMultiple(this, branches.ConvertAll(b => _histories?.Commits?.Find(c => c.SHA == b.Head)))); + } + public void CreateNewTag() { if (_currentBranch == null) diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index 92c2b043..d9588a0f 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -405,6 +405,17 @@ namespace SourceGit.Views ev.Handled = true; }; 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); } } diff --git a/src/Views/MergeMultiple.axaml b/src/Views/MergeMultiple.axaml new file mode 100644 index 00000000..01a6b976 --- /dev/null +++ b/src/Views/MergeMultiple.axaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/MergeMultiple.axaml.cs b/src/Views/MergeMultiple.axaml.cs new file mode 100644 index 00000000..c0997067 --- /dev/null +++ b/src/Views/MergeMultiple.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class MergeMultiple : UserControl + { + public MergeMultiple() + { + InitializeComponent(); + } + } +}