Files
sourcegit/src/Views/BranchTree.axaml.cs
2026-04-03 16:10:55 +08:00

1341 lines
50 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public class BranchTreeNodeIcon : UserControl
{
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
UpdateContent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
UpdateContent();
}
private void UpdateContent()
{
if (DataContext is not ViewModels.BranchTreeNode node)
{
Content = null;
return;
}
if (node.Backend is Models.Remote)
{
CreateContent(new Thickness(0, 0, 0, 0), "Icons.Remote");
}
else if (node.Backend is Models.Branch branch)
{
if (branch.IsCurrent)
CreateContent(new Thickness(0, 0, 0, 0), "Icons.CheckCircled", Brushes.Green);
else if (branch.IsLocal && !string.IsNullOrEmpty(branch.WorktreePath))
CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch", Brushes.DarkCyan);
else
CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch");
}
else
{
if (node.IsExpanded)
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open");
else
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder");
}
}
private void CreateContent(Thickness margin, string iconKey, IBrush fill = null)
{
if (this.FindResource(iconKey) is not StreamGeometry geo)
return;
var path = new Path()
{
Width = 12,
Height = 12,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = margin,
Data = geo,
};
if (fill != null)
path.Fill = fill;
Content = path;
}
}
public class BranchTreeNodeToggleButton : ToggleButton
{
protected override Type StyleKeyOverride => typeof(ToggleButton);
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
DataContext is ViewModels.BranchTreeNode { IsBranch: false } node)
{
var tree = this.FindAncestorOfType<BranchTree>();
tree?.ToggleNodeIsExpanded(node);
}
e.Handled = true;
}
}
public class BranchTreeNodeTrackStatusPresenter : Control
{
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
TextBlock.FontFamilyProperty.AddOwner<BranchTreeNodeTrackStatusPresenter>();
public FontFamily FontFamily
{
get => GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
public static readonly StyledProperty<double> FontSizeProperty =
TextBlock.FontSizeProperty.AddOwner<BranchTreeNodeTrackStatusPresenter>();
public double FontSize
{
get => GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<BranchTreeNodeTrackStatusPresenter, IBrush>(nameof(Foreground), Brushes.White);
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<BranchTreeNodeTrackStatusPresenter, IBrush>(nameof(Background), Brushes.White);
public IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
static BranchTreeNodeTrackStatusPresenter()
{
AffectsMeasure<BranchTreeNodeTrackStatusPresenter>(
FontSizeProperty,
FontFamilyProperty,
ForegroundProperty);
AffectsRender<BranchTreeNodeTrackStatusPresenter>(
ForegroundProperty,
BackgroundProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (_label != null)
{
context.DrawRectangle(Background, null, new RoundedRect(new Rect(8, 0, _label.Width + 18, 18), new CornerRadius(9)));
context.DrawText(_label, new Point(17, 9 - _label.Height * 0.5));
}
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
InvalidateMeasure();
InvalidateVisual();
}
protected override Size MeasureOverride(Size availableSize)
{
_label = null;
if (DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch })
{
var desc = branch.TrackStatusDescription;
if (!string.IsNullOrEmpty(desc))
{
_label = new FormattedText(
desc,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily),
FontSize,
Foreground);
}
}
return _label != null ? new Size(_label.Width + 18 /* Padding */ + 16 /* Margin */, 18) : new Size(0, 0);
}
private FormattedText _label = null;
}
public class BranchTreeNodeTrackStatusTooltip : TextBlock
{
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
Text = string.Empty;
if (DataContext is not Models.Branch { IsTrackStatusVisible: true } branch)
{
SetCurrentValue(IsVisibleProperty, false);
return;
}
var ahead = branch.Ahead.Count;
var behind = branch.Behind.Count;
if (ahead > 0)
Text = behind > 0 ? App.Text("BranchTree.AheadBehind", ahead, behind) : App.Text("BranchTree.Ahead", ahead);
else
Text = App.Text("BranchTree.Behind", behind);
SetCurrentValue(IsVisibleProperty, true);
}
}
public class BranchTreeNodeDescription : TextBlock
{
protected override Type StyleKeyOverride => typeof(TextBlock);
public BranchTreeNodeDescription()
{
IsVisible = false;
}
protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var visible = false;
do
{
if (DataContext is not Models.Branch branch)
break;
if (e.Root is not PopupRoot { Parent: Popup { Parent: Border owner } })
break;
var tree = owner.FindAncestorOfType<BranchTree>();
if (tree is not { DataContext: ViewModels.Repository repo })
break;
var description = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description");
if (string.IsNullOrEmpty(description))
break;
Text = description;
visible = true;
} while (false);
SetCurrentValue(IsVisibleProperty, visible);
}
}
public partial class BranchTree : UserControl
{
public static readonly StyledProperty<List<ViewModels.BranchTreeNode>> NodesProperty =
AvaloniaProperty.Register<BranchTree, List<ViewModels.BranchTreeNode>>(nameof(Nodes));
public List<ViewModels.BranchTreeNode> Nodes
{
get => GetValue(NodesProperty);
set => SetValue(NodesProperty, value);
}
public AvaloniaList<ViewModels.BranchTreeNode> Rows
{
get;
private set;
} = new AvaloniaList<ViewModels.BranchTreeNode>();
public static readonly RoutedEvent<RoutedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
public static readonly RoutedEvent<RoutedEventArgs> RowsChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> RowsChanged
{
add { AddHandler(RowsChangedEvent, value); }
remove { RemoveHandler(RowsChangedEvent, value); }
}
public static readonly RoutedEvent<RoutedEventArgs> SearchRequestedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(SearchRequested), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> SearchRequested
{
add { AddHandler(SearchRequestedEvent, value); }
remove { RemoveHandler(SearchRequestedEvent, value); }
}
public BranchTree()
{
InitializeComponent();
}
public void Select(Models.Branch branch)
{
if (branch == null)
return;
var treePath = new List<ViewModels.BranchTreeNode>();
FindTreePath(treePath, Nodes, branch.Name, 0);
if (treePath.Count == 0)
return;
var oldRowCount = Rows.Count;
var rows = Rows;
for (var i = 0; i < treePath.Count - 1; i++)
{
var node = treePath[i];
if (!node.IsExpanded)
{
node.IsExpanded = true;
var idx = rows.IndexOf(node);
var subtree = new List<ViewModels.BranchTreeNode>();
MakeRows(subtree, node.Children, node.Depth + 1);
rows.InsertRange(idx + 1, subtree);
}
}
var target = treePath[^1];
BranchesPresenter.SelectedItem = target;
BranchesPresenter.ScrollIntoView(target);
if (oldRowCount != rows.Count)
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
public void UnselectAll()
{
BranchesPresenter.SelectedItem = null;
}
public void ToggleNodeIsExpanded(ViewModels.BranchTreeNode node)
{
_disableSelectionChangingEvent = true;
node.IsExpanded = !node.IsExpanded;
var rows = Rows;
var depth = node.Depth;
var idx = rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{
var subtree = new List<ViewModels.BranchTreeNode>();
MakeRows(subtree, node.Children, depth + 1);
rows.InsertRange(idx + 1, subtree);
}
else
{
var removeCount = 0;
for (int i = idx + 1; i < rows.Count; i++)
{
var row = rows[i];
if (row.Depth <= depth)
break;
row.IsSelected = false;
removeCount++;
}
rows.RemoveRange(idx + 1, removeCount);
}
var repo = DataContext as ViewModels.Repository;
repo?.UpdateBranchNodeIsExpanded(node);
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
_disableSelectionChangingEvent = false;
}
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (Bounds.Height >= 23.0)
BranchesPresenter.Height = Bounds.Height;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == NodesProperty)
{
Rows.Clear();
if (Nodes is { Count: > 0 })
{
var rows = new List<ViewModels.BranchTreeNode>();
MakeRows(rows, Nodes, 0);
Rows.AddRange(rows);
}
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
else if (change.Property == IsVisibleProperty)
{
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
private void OnNodePointerPressed(object sender, PointerPressedEventArgs e)
{
var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift))
return;
var p = e.GetCurrentPoint(this);
if (!p.Properties.IsLeftButtonPressed)
return;
if (DataContext is not ViewModels.Repository repo)
return;
if (sender is not Border { DataContext: ViewModels.BranchTreeNode node })
return;
if (node.Backend is not Models.Branch branch)
return;
repo.NavigateToCommit(branch.Head);
}
private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e)
{
if (_disableSelectionChangingEvent)
return;
var repo = DataContext as ViewModels.Repository;
if (repo?.Settings == null)
return;
foreach (var item in e.AddedItems)
{
if (item is ViewModels.BranchTreeNode node)
node.IsSelected = true;
}
foreach (var item in e.RemovedItems)
{
if (item is ViewModels.BranchTreeNode node)
node.IsSelected = false;
}
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
ViewModels.BranchTreeNode prev = null;
foreach (var row in Rows)
{
if (row.IsSelected)
{
if (prev is { IsSelected: true })
{
var prevTop = prev.CornerRadius.TopLeft;
prev.CornerRadius = new CornerRadius(prevTop, 0);
row.CornerRadius = new CornerRadius(0, 4);
}
else
{
row.CornerRadius = new CornerRadius(4);
}
}
prev = row;
}
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2)
{
var repo = DataContext as ViewModels.Repository;
if (repo?.Settings == null)
return;
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote })
{
CreateContextMenuForRemote(repo, remote).Open(this);
return;
}
var branches = new List<Models.Branch>();
foreach (var item in selected)
{
if (item is ViewModels.BranchTreeNode node)
CollectBranchesInNode(branches, node);
}
if (branches.Count == 1)
{
var branch = branches[0];
var menu = branch.IsLocal ? CreateContextMenuForLocalBranch(repo, branch) : CreateContextMenuForRemoteBranch(repo, branch);
menu.Open(this);
}
else
{
var menu = new ContextMenu();
if (branches.Count == 2)
{
var compare = new MenuItem();
compare.Header = App.Text("BranchCM.CompareTwo");
compare.Icon = this.CreateMenuIcon("Icons.Compare");
compare.Click += (_, ev) =>
{
App.ShowWindow(new ViewModels.Compare(repo, branches[0], branches[1]));
ev.Handled = true;
};
menu.Items.Add(compare);
}
if (branches.Find(x => x.IsCurrent) == null)
{
var mergeMulti = new MenuItem();
mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count);
mergeMulti.Icon = this.CreateMenuIcon("Icons.Merge");
mergeMulti.Click += (_, ev) =>
{
repo.MergeMultipleBranches(branches);
ev.Handled = true;
};
var deleteMulti = new MenuItem();
deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count);
deleteMulti.Icon = this.CreateMenuIcon("Icons.Clear");
deleteMulti.Click += (_, ev) =>
{
repo.DeleteMultipleBranches(branches, branches[0].IsLocal);
ev.Handled = true;
};
menu.Items.Add(mergeMulti);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(deleteMulti);
}
if (menu.Items.Count > 0)
menu.Open(this);
}
}
private void OnTreeKeyDown(object _, KeyEventArgs e)
{
if (e.Key == Key.F && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control))
{
RaiseEvent(new RoutedEventArgs(SearchRequestedEvent));
e.Handled = true;
return;
}
if (e.Key is not (Key.Delete or Key.Back))
return;
var repo = DataContext as ViewModels.Repository;
if (repo?.Settings == null)
return;
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote })
{
repo.DeleteRemote(remote);
e.Handled = true;
return;
}
var branches = new List<Models.Branch>();
foreach (var item in selected)
{
if (item is ViewModels.BranchTreeNode node)
CollectBranchesInNode(branches, node);
}
if (branches.Find(x => x.IsCurrent) != null)
return;
if (branches.Count == 1)
repo.DeleteBranch(branches[0]);
else
repo.DeleteMultipleBranches(branches, branches[0].IsLocal);
e.Handled = true;
}
private async void OnDoubleTappedBranchNode(object sender, TappedEventArgs _)
{
if (sender is Grid { DataContext: ViewModels.BranchTreeNode node })
{
if (node.Backend is Models.Branch branch)
{
if (branch.IsCurrent)
return;
if (DataContext is ViewModels.Repository { Settings: not null } repo)
await repo.CheckoutBranchAsync(branch);
}
else
{
ToggleNodeIsExpanded(node);
}
}
}
private void MakeRows(List<ViewModels.BranchTreeNode> rows, List<ViewModels.BranchTreeNode> nodes, int depth)
{
foreach (var node in nodes)
{
node.Depth = depth;
node.IsSelected = false;
rows.Add(node);
if (!node.IsExpanded || node.Backend is Models.Branch)
continue;
MakeRows(rows, node.Children, depth + 1);
}
}
private void CollectBranchesInNode(List<Models.Branch> outs, ViewModels.BranchTreeNode node)
{
if (node.Backend is Models.Branch branch && !outs.Contains(branch))
{
outs.Add(branch);
return;
}
foreach (var sub in node.Children)
CollectBranchesInNode(outs, sub);
}
private void FindTreePath(List<ViewModels.BranchTreeNode> outPath, List<ViewModels.BranchTreeNode> collection, string path, int start)
{
if (start >= path.Length - 1)
return;
var sepIdx = path.IndexOf('/', start);
var name = sepIdx < 0 ? path.Substring(start) : path.Substring(start, sepIdx - start);
foreach (var node in collection)
{
if (node.Name.Equals(name, StringComparison.Ordinal))
{
outPath.Add(node);
FindTreePath(outPath, node.Children, path, sepIdx + 1);
}
}
}
private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, Models.Branch branch)
{
var current = repo.CurrentBranch;
var menu = new ContextMenu();
var upstream = repo.Branches.Find(x => x.FullName.Equals(branch.Upstream, StringComparison.Ordinal));
var push = new MenuItem();
push.Header = App.Text("BranchCM.Push", branch.Name);
push.Icon = this.CreateMenuIcon("Icons.Push");
push.IsEnabled = repo.Remotes.Count > 0;
push.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Push(repo, branch));
e.Handled = true;
};
if (branch.IsCurrent)
{
if (!repo.IsBare)
{
if (upstream != null)
{
var fastForward = new MenuItem();
fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName);
fastForward.Icon = this.CreateMenuIcon("Icons.FastForward");
fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0;
fastForward.Click += async (_, e) =>
{
if (repo.CanCreatePopup())
await repo.ShowAndStartPopupAsync(new ViewModels.Merge(repo, upstream, branch.Name, true));
e.Handled = true;
};
var pull = new MenuItem();
pull.Header = App.Text("BranchCM.Pull", upstream.FriendlyName);
pull.Icon = this.CreateMenuIcon("Icons.Pull");
pull.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Pull(repo, null));
e.Handled = true;
};
menu.Items.Add(fastForward);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(pull);
}
}
menu.Items.Add(push);
var compareWith = new MenuItem();
compareWith.Header = App.Text("BranchCM.CompareWith");
compareWith.Icon = this.CreateMenuIcon("Icons.Compare");
compareWith.Click += (_, _) =>
{
new ViewModels.CompareCommandPalette(repo, branch).Open();
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compareWith);
}
else
{
var hasNoWorktree = string.IsNullOrEmpty(branch.WorktreePath);
var checkout = new MenuItem();
checkout.Header = App.Text(hasNoWorktree ? "BranchCM.Checkout" : "BranchCM.SwitchToWorktree", branch.Name);
checkout.Icon = this.CreateMenuIcon("Icons.Check");
checkout.IsEnabled = !repo.IsBare || !hasNoWorktree;
checkout.Click += async (_, e) =>
{
await repo.CheckoutBranchAsync(branch);
e.Handled = true;
};
menu.Items.Add(checkout);
menu.Items.Add(new MenuItem() { Header = "-" });
if (upstream != null && hasNoWorktree)
{
var fastForward = new MenuItem();
fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName);
fastForward.Icon = this.CreateMenuIcon("Icons.FastForward");
fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0;
fastForward.Click += async (_, e) =>
{
if (repo.CanCreatePopup())
await repo.ShowAndStartPopupAsync(new ViewModels.ResetWithoutCheckout(repo, branch, upstream));
e.Handled = true;
};
menu.Items.Add(fastForward);
var fetchInto = new MenuItem();
fetchInto.Header = App.Text("BranchCM.FetchInto", upstream.FriendlyName, branch.Name);
fetchInto.Icon = this.CreateMenuIcon("Icons.Fetch");
fetchInto.IsEnabled = branch.Ahead.Count == 0;
fetchInto.Click += async (_, e) =>
{
if (repo.CanCreatePopup())
await repo.ShowAndStartPopupAsync(new ViewModels.FetchInto(repo, branch, upstream));
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(fetchInto);
}
menu.Items.Add(push);
if (!repo.IsBare)
{
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name);
merge.Icon = this.CreateMenuIcon("Icons.Merge");
merge.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false));
e.Handled = true;
};
var rebase = new MenuItem();
rebase.Header = App.Text("BranchCM.Rebase", current.Name, branch.Name);
rebase.Icon = this.CreateMenuIcon("Icons.Rebase");
rebase.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Rebase(repo, current, branch));
e.Handled = true;
};
var interactiveRebase = new MenuItem();
interactiveRebase.Header = App.Text("BranchCM.InteractiveRebase.Manually", current.Name, branch.Name);
interactiveRebase.Icon = this.CreateMenuIcon("Icons.InteractiveRebase");
interactiveRebase.IsEnabled = !current.Head.Equals(branch.Head, StringComparison.Ordinal);
interactiveRebase.Click += async (_, e) =>
{
var commit = await new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResultAsync();
await App.ShowDialog(new ViewModels.InteractiveRebase(repo, commit));
e.Handled = true;
};
menu.Items.Add(merge);
menu.Items.Add(rebase);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(interactiveRebase);
}
if (hasNoWorktree)
{
var selectedCommit = repo.GetSelectedCommitInHistory();
if (selectedCommit != null && !selectedCommit.SHA.Equals(branch.Head, StringComparison.Ordinal))
{
var move = new MenuItem();
move.Header = App.Text("BranchCM.ResetToSelectedCommit", branch.Name, selectedCommit.SHA.Substring(0, 10));
move.Icon = this.CreateMenuIcon("Icons.Reset");
move.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.ResetWithoutCheckout(repo, branch, selectedCommit));
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(move);
}
}
var compareWithCurrent = new MenuItem();
compareWithCurrent.Header = App.Text("BranchCM.CompareWithHead");
compareWithCurrent.Icon = this.CreateMenuIcon("Icons.Compare");
compareWithCurrent.Click += (_, _) =>
{
App.ShowWindow(new ViewModels.Compare(repo, branch, current));
};
var compareWith = new MenuItem();
compareWith.Header = App.Text("BranchCM.CompareWith");
compareWith.Icon = this.CreateMenuIcon("Icons.Compare");
compareWith.Click += (_, _) =>
{
new ViewModels.CompareCommandPalette(repo, branch).Open();
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compareWithCurrent);
menu.Items.Add(compareWith);
}
if (!repo.IsBare)
{
var type = repo.GetGitFlowType(branch);
if (type != Models.GitFlowBranchType.None)
{
var finish = new MenuItem();
finish.Header = App.Text("BranchCM.Finish", branch.Name);
finish.Icon = this.CreateMenuIcon("Icons.GitFlow");
finish.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.GitFlowFinish(repo, branch, type));
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(finish);
}
}
if (!branch.IsDetachedHead)
{
var editDescription = new MenuItem();
editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name);
editDescription.Icon = this.CreateMenuIcon("Icons.Edit");
editDescription.Click += async (_, e) =>
{
var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description");
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc));
e.Handled = true;
};
var rename = new MenuItem();
rename.Header = App.Text("BranchCM.Rename", branch.Name);
rename.Icon = this.CreateMenuIcon("Icons.Rename");
rename.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.RenameBranch(repo, branch));
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", branch.Name);
delete.Icon = this.CreateMenuIcon("Icons.Clear");
delete.IsEnabled = !branch.IsCurrent;
delete.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch));
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(editDescription);
menu.Items.Add(rename);
menu.Items.Add(delete);
}
var createBranch = new MenuItem();
createBranch.Icon = this.CreateMenuIcon("Icons.Branch.Add");
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.CreateBranch(repo, branch));
e.Handled = true;
};
var createTag = new MenuItem();
createTag.Icon = this.CreateMenuIcon("Icons.Tag.Add");
createTag.Header = App.Text("CreateTag");
createTag.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.CreateTag(repo, branch));
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(createBranch);
menu.Items.Add(createTag);
if (upstream != null)
{
var remote = repo.Remotes.Find(x => x.Name.Equals(upstream.Remote, StringComparison.Ordinal));
if (remote != null && remote.TryGetCreatePullRequestURL(out var prURL, upstream.Name))
{
var createPR = new MenuItem();
createPR.Header = App.Text("BranchCM.CreatePRForUpstream", upstream.FriendlyName);
createPR.Icon = this.CreateMenuIcon("Icons.CreatePR");
createPR.Click += (_, e) =>
{
Native.OS.OpenBrowser(prURL);
e.Handled = true;
};
menu.Items.Add(createPR);
}
}
menu.Items.Add(new MenuItem() { Header = "-" });
TryToAddCustomActionsToBranchContextMenu(repo, menu, branch);
if (!repo.IsBare)
{
var remoteBranches = new List<Models.Branch>();
foreach (var b in repo.Branches)
{
if (!b.IsLocal)
remoteBranches.Add(b);
}
if (remoteBranches.Count > 0)
{
var tracking = new MenuItem();
tracking.Header = App.Text("BranchCM.Tracking");
tracking.Icon = this.CreateMenuIcon("Icons.Track");
tracking.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.SetUpstream(repo, branch, remoteBranches));
e.Handled = true;
};
menu.Items.Add(tracking);
}
}
var archive = new MenuItem();
archive.Icon = this.CreateMenuIcon("Icons.Archive");
archive.Header = App.Text("Archive");
archive.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Archive(repo, branch));
e.Handled = true;
};
menu.Items.Add(archive);
menu.Items.Add(new MenuItem() { Header = "-" });
var copy = new MenuItem();
copy.Header = App.Text("BranchCM.CopyName");
copy.Icon = this.CreateMenuIcon("Icons.Copy");
copy.Click += async (_, e) =>
{
await this.CopyTextAsync(branch.Name);
e.Handled = true;
};
menu.Items.Add(copy);
return menu;
}
private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Models.Remote remote)
{
var menu = new ContextMenu();
if (remote.TryGetVisitURL(out string visitURL))
{
var visit = new MenuItem();
visit.Header = App.Text("RemoteCM.OpenInBrowser");
visit.Icon = this.CreateMenuIcon("Icons.OpenWith");
visit.Click += (_, e) =>
{
Native.OS.OpenBrowser(visitURL);
e.Handled = true;
};
menu.Items.Add(visit);
menu.Items.Add(new MenuItem() { Header = "-" });
}
var fetch = new MenuItem();
fetch.Header = App.Text("RemoteCM.Fetch");
fetch.Icon = this.CreateMenuIcon("Icons.Fetch");
fetch.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Fetch(repo, remote));
e.Handled = true;
};
var prune = new MenuItem();
prune.Header = App.Text("RemoteCM.Prune");
prune.Icon = this.CreateMenuIcon("Icons.Clean");
prune.Click += async (_, e) =>
{
if (repo.CanCreatePopup())
await repo.ShowAndStartPopupAsync(new ViewModels.PruneRemote(repo, remote));
e.Handled = true;
};
var edit = new MenuItem();
edit.Header = App.Text("RemoteCM.Edit");
edit.Icon = this.CreateMenuIcon("Icons.Edit");
edit.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.EditRemote(repo, remote));
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("RemoteCM.Delete");
delete.Icon = this.CreateMenuIcon("Icons.Clear");
delete.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.DeleteRemote(repo, remote));
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("RemoteCM.CopyURL");
copy.Icon = this.CreateMenuIcon("Icons.Copy");
copy.Click += async (_, e) =>
{
await this.CopyTextAsync(remote.URL);
e.Handled = true;
};
menu.Items.Add(fetch);
menu.Items.Add(prune);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(edit);
menu.Items.Add(delete);
menu.Items.Add(new MenuItem() { Header = "-" });
TryToAddCustomActionsToRemoteContextMenu(repo, menu, remote);
menu.Items.Add(copy);
return menu;
}
public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, Models.Branch branch)
{
var menu = new ContextMenu();
var name = branch.FriendlyName;
var checkout = new MenuItem();
checkout.Header = App.Text("BranchCM.Checkout", name);
checkout.Icon = this.CreateMenuIcon("Icons.Check");
checkout.Click += async (_, e) =>
{
await repo.CheckoutBranchAsync(branch);
e.Handled = true;
};
menu.Items.Add(checkout);
menu.Items.Add(new MenuItem() { Header = "-" });
if (repo.CurrentBranch is { } current)
{
var pull = new MenuItem();
pull.Header = App.Text("BranchCM.PullInto", name, current.Name);
pull.Icon = this.CreateMenuIcon("Icons.Pull");
pull.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Pull(repo, branch));
e.Handled = true;
};
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", name, current.Name);
merge.Icon = this.CreateMenuIcon("Icons.Merge");
merge.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false));
e.Handled = true;
};
var rebase = new MenuItem();
rebase.Header = App.Text("BranchCM.Rebase", current.Name, name);
rebase.Icon = this.CreateMenuIcon("Icons.Rebase");
rebase.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Rebase(repo, current, branch));
e.Handled = true;
};
var interactiveRebase = new MenuItem();
interactiveRebase.Header = App.Text("BranchCM.InteractiveRebase.Manually", current.Name, name);
interactiveRebase.Icon = this.CreateMenuIcon("Icons.InteractiveRebase");
interactiveRebase.IsEnabled = !current.Head.Equals(branch.Head, StringComparison.Ordinal);
interactiveRebase.Click += async (_, e) =>
{
var commit = await new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResultAsync();
await App.ShowDialog(new ViewModels.InteractiveRebase(repo, commit));
e.Handled = true;
};
var compareWithHead = new MenuItem();
compareWithHead.Header = App.Text("BranchCM.CompareWithHead");
compareWithHead.Icon = this.CreateMenuIcon("Icons.Compare");
compareWithHead.Click += (_, _) =>
{
App.ShowWindow(new ViewModels.Compare(repo, branch, current));
};
var compareWith = new MenuItem();
compareWith.Header = App.Text("BranchCM.CompareWith");
compareWith.Icon = this.CreateMenuIcon("Icons.Compare");
compareWith.Click += (_, _) =>
{
new ViewModels.CompareCommandPalette(repo, branch).Open();
};
menu.Items.Add(pull);
menu.Items.Add(merge);
menu.Items.Add(rebase);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(interactiveRebase);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compareWithHead);
menu.Items.Add(compareWith);
}
menu.Items.Add(new MenuItem() { Header = "-" });
var editDescription = new MenuItem();
editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name);
editDescription.Icon = this.CreateMenuIcon("Icons.Edit");
editDescription.Click += async (_, e) =>
{
var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description");
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc));
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", name);
delete.Icon = this.CreateMenuIcon("Icons.Clear");
delete.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch));
e.Handled = true;
};
menu.Items.Add(editDescription);
menu.Items.Add(delete);
menu.Items.Add(new MenuItem() { Header = "-" });
var createBranch = new MenuItem();
createBranch.Icon = this.CreateMenuIcon("Icons.Branch.Add");
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.CreateBranch(repo, branch));
e.Handled = true;
};
var createTag = new MenuItem();
createTag.Icon = this.CreateMenuIcon("Icons.Tag.Add");
createTag.Header = App.Text("CreateTag");
createTag.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.CreateTag(repo, branch));
e.Handled = true;
};
menu.Items.Add(createBranch);
menu.Items.Add(createTag);
var remote = repo.Remotes.Find(x => x.Name.Equals(branch.Remote, StringComparison.Ordinal));
if (remote != null && remote.TryGetCreatePullRequestURL(out var prURL, branch.Name))
{
var createPR = new MenuItem();
createPR.Header = App.Text("BranchCM.CreatePR");
createPR.Icon = this.CreateMenuIcon("Icons.CreatePR");
createPR.Click += (_, e) =>
{
Native.OS.OpenBrowser(prURL);
e.Handled = true;
};
menu.Items.Add(createPR);
}
menu.Items.Add(new MenuItem() { Header = "-" });
var archive = new MenuItem();
archive.Icon = this.CreateMenuIcon("Icons.Archive");
archive.Header = App.Text("Archive");
archive.Click += (_, e) =>
{
if (repo.CanCreatePopup())
repo.ShowPopup(new ViewModels.Archive(repo, branch));
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("BranchCM.CopyName");
copy.Icon = this.CreateMenuIcon("Icons.Copy");
copy.Click += async (_, e) =>
{
await this.CopyTextAsync(name);
e.Handled = true;
};
menu.Items.Add(archive);
menu.Items.Add(new MenuItem() { Header = "-" });
TryToAddCustomActionsToBranchContextMenu(repo, menu, branch);
menu.Items.Add(copy);
return menu;
}
private void TryToAddCustomActionsToBranchContextMenu(ViewModels.Repository repo, ContextMenu menu, Models.Branch branch)
{
var actions = repo.GetCustomActions(Models.CustomActionScope.Branch);
if (actions.Count == 0)
return;
var custom = new MenuItem();
custom.Header = App.Text("BranchCM.CustomAction");
custom.Icon = this.CreateMenuIcon("Icons.Action");
foreach (var action in actions)
{
var (dup, label) = action;
var item = new MenuItem();
item.Icon = this.CreateMenuIcon("Icons.Action");
item.Header = label;
item.Click += async (_, e) =>
{
await repo.ExecCustomActionAsync(dup, branch);
e.Handled = true;
};
custom.Items.Add(item);
}
menu.Items.Add(custom);
menu.Items.Add(new MenuItem() { Header = "-" });
}
private void TryToAddCustomActionsToRemoteContextMenu(ViewModels.Repository repo, ContextMenu menu, Models.Remote remote)
{
var actions = repo.GetCustomActions(Models.CustomActionScope.Remote);
if (actions.Count == 0)
return;
var custom = new MenuItem();
custom.Header = App.Text("RemoteCM.CustomAction");
custom.Icon = this.CreateMenuIcon("Icons.Action");
foreach (var action in actions)
{
var (dup, label) = action;
var item = new MenuItem();
item.Icon = this.CreateMenuIcon("Icons.Action");
item.Header = label;
item.Click += async (_, e) =>
{
await repo.ExecCustomActionAsync(dup, remote);
e.Handled = true;
};
custom.Items.Add(item);
}
menu.Items.Add(custom);
menu.Items.Add(new MenuItem() { Header = "-" });
}
private bool _disableSelectionChangingEvent = false;
}
}